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 (opens in a new tab) görebilirsiniz. Bu sunucu, Hardhat'in Greeter'ının değiştirilmiş bir versiyonu olan bu Sözleşmeden (opens in a new tab) 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 (opens in a new tab).
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 (opens in a new tab).PRIVATE_KEY=0x <private key goes here> -
Sunucuyu başlatın.
npm start -
Bir blok gezginine (opens in a new tab) 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 (opens in a new tab) 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 (opens in a new tab) varlıkları, işlevleri ve Address türüdür (opens in a new tab). Bu sunucu, JavaScript'in onu sıkı tipli (strongly typed) (opens in a new tab) yapan bir uzantısı olan TypeScript (opens in a new tab) ile yazılmıştır.
import { privateKeyToAccount } from "viem/accounts"
Bu işlev (opens in a new tab), 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 (opens in a new tab) 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) (opens in a new tab) oluşturun. Genel istemcilerin ekli bir özel anahtarı yoktur ve bu nedenle işlem gönderemezler. view işlevlerini (opens in a new tab) çağırabilir, Hesap bakiyelerini okuyabilir vb.
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)
Ortam değişkenleri process.env (opens in a new tab) 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 (opens in a new tab) 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 (opens in a new tab) 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 (opens in a new tab) 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 (opens in a new tab) ve pure (opens in a new tab)) read altında mevcuttur. Bu durumda, selamlamayı döndüren greet (opens in a new tab) 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 (opens in a new tab) 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 (opens in a new tab), 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 (opens in a new tab) 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 (opens in a new tab), 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 (opens in a new tab) Node.js (opens in a new tab) 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 (opens in a new tab), 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 (opens in a new tab) 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 (opens in a new tab).