Web3 uygulamaları için sunucu bileşenleri ve temsilciler
Giriş
Çoğu durumda, merkeziyetsiz bir uygulama (dapp) yazılımı dağıtmak için bir sunucu kullanır, ancak asıl etkileşimin tamamı istemci (genellikle web tarayıcısı) ile Blokzincir arasında gerçekleşir.
Ancak, bir uygulamanın bağımsız olarak çalışan bir sunucu bileşenine sahip olmaktan fayda sağlayacağı bazı durumlar vardır. Böyle bir sunucu, olaylara ve bir API gibi diğer kaynaklardan gelen isteklere işlemler göndererek yanıt verebilir.
Böyle bir sunucunun yerine getirebileceği birkaç olası görev vardır.
-
Gizli durum tutucusu. Oyunlarda, oyunun bildiği tüm bilgilerin oyunculara açık olmaması genellikle yararlıdır. Ancak, Blokzincir üzerinde sır yoktur, Blokzincirde bulunan herhangi bir bilgiyi herkesin anlaması kolaydır. Bu nedenle, oyun durumunun bir kısmının gizli tutulması gerekiyorsa, başka bir yerde saklanması (ve muhtemelen bu durumun etkilerinin sıfır bilgi ispatları kullanılarak doğrulanması) gerekir.
-
Merkezi kâhin. Riskler yeterince düşükse, çevrim içi bazı bilgileri okuyan ve ardından bunları Zincire gönderen harici bir sunucu, bir kâhin olarak kullanmak için yeterince iyi olabilir.
-
Temsilci. Blokzincir üzerinde onu etkinleştirecek bir işlem olmadan hiçbir şey olmaz. Bir sunucu, fırsat ortaya çıktığında arbitraj gibi eylemleri gerçekleştirmek için bir kullanıcı adına hareket edebilir.
Örnek program
Örnek bir sunucuyu GitHub'da (yeni sekmede açılır) görebilirsiniz. Bu sunucu, Hardhat'in Greeter'ının değiştirilmiş bir versiyonu olan bu Sözleşmeden (yeni sekmede açılır) gelen olayları dinler. Selamlama değiştirildiğinde, onu eski haline getirir.
Çalıştırmak için:
-
Depoyu klonlayın.
git clone https://github.com/qbzzt/20240715-server-component.git cd 20240715-server-component -
Gerekli paketleri yükleyin. Eğer henüz sahip değilseniz, önce Node'u yükleyin (yeni sekmede açılır).
npm install -
Holesky test ağında ETH'si olan bir Hesabın özel anahtarını belirtmek için
.envdosyasını düzenleyin. Holesky'de ETH'niz yoksa, bu musluğu kullanabilirsiniz (yeni sekmede açılır).PRIVATE_KEY=0x <private key goes here> -
Sunucuyu başlatın.
npm start -
Bir blok gezginine (yeni sekmede açılır) gidin ve özel anahtara sahip olandan farklı bir Adres kullanarak selamlamayı değiştirin. Selamlamanın otomatik olarak eski haline getirildiğini görün.
Nasıl çalışır?
Bir sunucu bileşeninin nasıl yazılacağını anlamanın en kolay yolu, örnek üzerinden satır satır gitmektir.
src/app.ts
Programın büyük çoğunluğu src/app.ts (yeni sekmede açılır) içinde yer almaktadır.
Ön koşul nesnelerini oluşturma
import {
createPublicClient,
createWalletClient,
getContract,
http,
Address,
} from "viem"
Bunlar ihtiyacımız olan Viem (yeni sekmede açılır) varlıkları, işlevleri ve Address türüdür (yeni sekmede açılır). Bu sunucu, JavaScript'in onu sıkı tipli (strongly typed) (yeni sekmede açılır) yapan bir uzantısı olan TypeScript (yeni sekmede açılır) ile yazılmıştır.
import { privateKeyToAccount } from "viem/accounts"
Bu işlev (yeni sekmede açılır), bir özel anahtara karşılık gelen Adres de dahil olmak üzere Cüzdan bilgilerini oluşturmamızı sağlar.
import { holesky } from "viem/chains"
Viem'de bir Blokzincir kullanmak için tanımını içe aktarmanız gerekir. Bu durumda, Holesky (yeni sekmede açılır) test Blokzincirine bağlanmak istiyoruz.
// .env içindeki tanımları process.env'ye bu şekilde ekliyoruz.
import * as dotenv from "dotenv"
dotenv.config()
.env dosyasını ortama bu şekilde okuruz. Buna özel anahtar için ihtiyacımız var (daha sonrasına bakın).
const greeterAddress : Address = "0xB8f6460Dc30c44401Be26B0d6eD250873d8a50A6"
const greeterABI = [
{
"inputs": [
{
"internalType": "string",
"name": "_greeting",
"type": "string"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
.
.
.
{
"inputs": [
{
"internalType": "string",
"name": "_greeting",
"type": "string"
}
],
"name": "setGreeting",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
] as const
Bir Sözleşmeyi kullanmak için Adresine ve onun 'sine ihtiyacımız var. Burada her ikisini de sağlıyoruz.
JavaScript'te (ve dolayısıyla TypeScript'te) bir sabite yeni bir değer atayamazsınız, ancak içinde saklanan nesneyi değiştirebilirsiniz. as const son ekini kullanarak TypeScript'e listenin kendisinin sabit olduğunu ve değiştirilemeyeceğini söylüyoruz.
const publicClient = createPublicClient({
chain: holesky,
transport: http(),
})
Bir Viem genel istemcisi (public client) (yeni sekmede açılır) oluşturun. Genel istemcilerin ekli bir özel anahtarı yoktur ve bu nedenle işlem gönderemezler. view işlevlerini (yeni sekmede açılır) çağırabilir, Hesap bakiyelerini okuyabilir vb.
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)
Ortam değişkenleri process.env (yeni sekmede açılır) içinde mevcuttur. Ancak, TypeScript sıkı tiplidir. Bir ortam değişkeni herhangi bir dize veya boş olabilir, bu nedenle bir ortam değişkeninin türü string | undefined şeklindedir. Ancak, bir anahtar Viem'de 0x${string} (0x ve ardından bir dize) olarak tanımlanır. Burada TypeScript'e PRIVATE_KEY ortam değişkeninin bu türde olacağını söylüyoruz. Eğer değilse, bir çalışma zamanı hatası alırız.
privateKeyToAccount (yeni sekmede açılır) işlevi daha sonra tam bir Hesap nesnesi oluşturmak için bu özel anahtarı kullanır.
const walletClient = createWalletClient({
account,
chain: holesky,
transport: http(),
})
Ardından, bir Cüzdan istemcisi (yeni sekmede açılır) oluşturmak için Hesap nesnesini kullanırız. Bu istemcinin bir özel anahtarı ve bir Adresi vardır, bu nedenle işlem göndermek için kullanılabilir.
const greeter = getContract({
address: greeterAddress,
abi: greeterABI,
client: { public: publicClient, wallet: walletClient },
})
Artık tüm ön koşullara sahip olduğumuza göre, nihayet bir Sözleşme örneği (yeni sekmede açılır) oluşturabiliriz. Zincir içi Sözleşme ile iletişim kurmak için bu Sözleşme örneğini kullanacağız.
Blokzincirden okuma
console.log(`Current greeting:`, await greeter.read.greet())
Salt okunur olan Sözleşme işlevleri (view (yeni sekmede açılır) ve pure (yeni sekmede açılır)) read altında mevcuttur. Bu durumda, selamlamayı döndüren greet (yeni sekmede açılır) işlevine erişmek için kullanırız.
JavaScript tek iş parçacıklıdır, bu nedenle uzun süren bir işlemi başlattığımızda bunu eşzamansız (asenkron) olarak yaptığımızı belirtmemiz (yeni sekmede açılır) gerekir. Blokzinciri çağırmak, salt okunur bir işlem için bile olsa, bilgisayar ile bir Blokzincir Düğümü arasında gidiş-dönüş gerektirir. Bu nedenle burada kodun sonuç için await (beklemesi) gerektiğini belirtiyoruz.
Bunun nasıl çalıştığıyla ilgileniyorsanız buradan okuyabilirsiniz (yeni sekmede açılır), ancak pratik anlamda bilmeniz gereken tek şey, uzun süren bir işlem başlatırsanız sonuçları await ile beklemeniz gerektiği ve bunu yapan herhangi bir işlevin async olarak bildirilmesi gerektiğidir.
İşlem gönderme
const setGreeting = async (greeting: string): Promise<any> => {
Bu, selamlamayı değiştiren bir işlem göndermek için çağırdığınız işlevdir. Bu uzun bir işlem olduğundan, işlev async olarak bildirilir. Dahili uygulama nedeniyle, herhangi bir async işlevinin bir Promise nesnesi döndürmesi gerekir. Bu durumda Promise<any>, Promise içinde tam olarak neyin döndürüleceğini belirtmediğimiz anlamına gelir.
const txHash = await greeter.write.setGreeting([greeting])
Sözleşme örneğinin write alanı, setGreeting (yeni sekmede açılır) gibi Blokzincir durumuna yazan (bir işlem gönderilmesini gerektiren) tüm işlevlere sahiptir. Varsa parametreler bir liste olarak sağlanır ve işlev işlemin hash'ini döndürür.
console.log(`Working on a fix, see https://eth-holesky.blockscout.com/tx/${txHash}`)
return txHash
}
İşlemin hash'ini (görüntülemek için blok gezginine giden bir URL'nin parçası olarak) bildirin ve döndürün.
Olaylara yanıt verme
greeter.watchEvent.SetGreeting({
watchEvent işlevi (yeni sekmede açılır), bir olay yayınlandığında çalıştırılacak bir işlevi belirtmenizi sağlar. Yalnızca bir tür olayla ilgileniyorsanız (bu durumda SetGreeting), kendinizi o olay türüyle sınırlamak için bu sözdizimini kullanabilirsiniz.
onLogs: logs => {
onLogs işlevi, Günlük girişleri olduğunda çağrılır. Ethereum'da "Günlük" ve "olay" genellikle birbirinin yerine kullanılabilir.
console.log(
`Address ${logs[0].args.sender} changed the greeting to ${logs[0].args.greeting}`
)
Birden fazla olay olabilir, ancak basitlik adına yalnızca ilkiyle ilgileniyoruz. logs[0].args, olayın argümanlarıdır, bu durumda sender ve greeting.
if (logs[0].args.sender != account.address)
setGreeting(`${account.address} insists on it being Hello!`)
}
})
Gönderen bu sunucu değilse, selamlamayı değiştirmek için setGreeting kullanın.
package.json
Bu dosya (yeni sekmede açılır) Node.js (yeni sekmede açılır) yapılandırmasını kontrol eder. Bu makale yalnızca önemli tanımları açıklamaktadır.
{
"main": "dist/index.js",
Bu tanım, hangi JavaScript dosyasının çalıştırılacağını belirtir.
"scripts": {
"start": "tsc && node dist/app.js",
},
Betikler çeşitli uygulama eylemleridir. Bu durumda, sahip olduğumuz tek betik, sunucuyu derleyen ve ardından çalıştıran start betiğidir. tsc komutu typescript paketinin bir parçasıdır ve TypeScript'i JavaScript'e derler. Manuel olarak çalıştırmak isterseniz, node_modules/.bin içinde bulunur. İkinci komut sunucuyu çalıştırır.
"type": "module",
Birden fazla JavaScript Node uygulaması türü vardır. module türü, en üst düzey kodda await kullanmamıza izin verir, bu da yavaş (ve dolayısıyla eşzamansız) işlemler yaptığınızda önemlidir.
"devDependencies": {
"@types/node": "^20.14.2",
"typescript": "^5.4.5"
},
Bunlar yalnızca geliştirme için gerekli olan paketlerdir. Burada typescript paketine ihtiyacımız var ve bunu Node.js ile kullandığımız için process gibi node değişkenleri ve nesneleri için türleri de alıyoruz. ^<version> gösterimi (yeni sekmede açılır), o sürüm veya bozucu değişiklikleri olmayan daha yüksek bir sürüm anlamına gelir. Sürüm numaralarının anlamı hakkında daha fazla bilgi için buraya (yeni sekmede açılır) bakın.
"dependencies": {
"dotenv": "^16.4.5",
"viem": "2.14.1"
}
}
Bunlar, dist/app.js çalıştırılırken çalışma zamanında gerekli olan paketlerdir.
Sonuç
Burada oluşturduğumuz merkezi sunucu, bir kullanıcı için temsilci olarak hareket etmek olan işini yapar. Dapp'in çalışmaya devam etmesini isteyen ve Gaz harcamaya istekli olan herkes, kendi Adresiyle sunucunun yeni bir örneğini çalıştırabilir.
Ancak bu, yalnızca merkezi sunucunun eylemleri kolayca doğrulanabildiğinde işe yarar. Merkezi sunucunun herhangi bir gizli durum bilgisi varsa veya zor hesaplamalar çalıştırıyorsa, uygulamayı kullanmak için güvenmeniz gereken merkezi bir varlıktır ki bu tam olarak Blokzincirlerin kaçınmaya çalıştığı şeydir. Gelecekteki bir makalede, bu sorunu aşmak için sıfır bilgi ispatlarının nasıl kullanılacağını göstermeyi planlıyorum.
Çalışmalarımın daha fazlası için buraya bakın (yeni sekmede açılır).