تخطي إلى المحتوى الرئيسي

مكونات الخادم والوكلاء لتطبيقات ⁦web3⁩

وكيل
خادم
خارج السلسلة
dapps
مبتدئ
أوري بوميرانتس
15 يوليو 2024
8 دقيقة للقراءة

مقدمة

في معظم الحالات، يستخدم التطبيق اللامركزي (dapp) خادمًا لتوزيع البرنامج، ولكن كل التفاعل الفعلي يحدث بين العميل (عادةً، متصفح الويب) وسلسلة الكتل.

Normal interaction between web server, client, and blockchain

ومع ذلك، هناك بعض الحالات التي قد يستفيد فيها التطبيق من وجود مكون خادم يعمل بشكل مستقل. سيكون مثل هذا الخادم قادرًا على الاستجابة للأحداث، وللطلبات التي تأتي من مصادر أخرى، مثل API، عن طريق إصدار المعاملات.

The interaction with the addition of a server

هناك العديد من المهام المحتملة التي يمكن لمثل هذا الخادم إنجازها.

  • حامل الحالة السرية. في الألعاب، غالبًا ما يكون من المفيد ألا تكون كل المعلومات التي تعرفها اللعبة متاحة للاعبين. ومع ذلك، لا توجد أسرار على سلسلة الكتل، فأي معلومات موجودة في سلسلة الكتل يسهل على أي شخص اكتشافها. لذلك، إذا كان يجب إبقاء جزء من حالة اللعبة سريًا، فيجب تخزينه في مكان آخر (وربما يتم التحقق من تأثيرات تلك الحالة باستخدام براهين المعرفة الصفرية).

  • أوراكل مركزي. إذا كانت المخاطر منخفضة بما فيه الكفاية، فإن الخادم الخارجي الذي يقرأ بعض المعلومات عبر الإنترنت ثم ينشرها على السلسلة قد يكون جيدًا بما يكفي لاستخدامه كـ أوراكل.

  • وكيل. لا يحدث شيء على سلسلة الكتل دون معاملة لتنشيطه. يمكن للخادم أن يتصرف نيابة عن المستخدم لأداء إجراءات مثل المراجحة عندما تسنح الفرصة.

برنامج نموذجي

يمكنك رؤية خادم نموذجي على GitHub (opens in a new tab). يستمع هذا الخادم إلى الأحداث القادمة من هذا العقد (opens in a new tab)، وهو نسخة معدلة من عقد الترحيب الخاص بـ Hardhat. عندما يتم تغيير التحية، فإنه يعيدها إلى ما كانت عليه.

لتشغيله:

  1. استنسخ المستودع.

    git clone https://github.com/qbzzt/20240715-server-component.git
    cd 20240715-server-component
    
  2. قم بتثبيت الحزم اللازمة. إذا لم يكن لديك بالفعل، قم بتثبيت Node أولاً (opens in a new tab).

    npm install
    
  3. قم بتحرير .env لتحديد المفتاح الخاص لحساب يمتلك ETH على شبكة اختبار هوليسكي. إذا لم يكن لديك ETH على هوليسكي، يمكنك استخدام هذا الصنبور (opens in a new tab).

    PRIVATE_KEY=0x <private key goes here>
    
  4. ابدأ تشغيل الخادم.

    npm start
    
  5. انتقل إلى مستكشف الكتل (opens in a new tab)، وباستخدام عنوان مختلف عن العنوان الذي يمتلك المفتاح الخاص، قم بتعديل التحية. لاحظ أن التحية يتم تعديلها تلقائيًا للعودة إلى ما كانت عليه.

كيف يعمل؟

أسهل طريقة لفهم كيفية كتابة مكون خادم هي مراجعة النموذج سطرًا بسطر.

src/app.ts

الغالبية العظمى من البرنامج موجودة في src/app.ts (opens in a new tab).

إنشاء الكائنات الأساسية
import {
  createPublicClient,
  createWalletClient,
  getContract,
  http,
  Address,
} from "viem"

هذه هي كيانات Viem (opens in a new tab) التي نحتاجها، الدوال والنوع Address (opens in a new tab). هذا الخادم مكتوب بلغة TypeScript (opens in a new tab)، وهي امتداد للغة JavaScript يجعلها قوية النوع (opens in a new tab).

import { privateKeyToAccount } from "viem/accounts"

تتيح لنا هذه الدالة (opens in a new tab) إنشاء معلومات المحفظة، بما في ذلك العنوان، المقابلة لمفتاح خاص.

import { holesky } from "viem/chains"

لاستخدام سلسلة الكتل في Viem، تحتاج إلى استيراد تعريفها. في هذه الحالة، نريد الاتصال بسلسلة كتل الاختبار هوليسكي (opens in a new tab).

// هكذا نضيف التعريفات في .env إلى process.env.
import * as dotenv from "dotenv"
dotenv.config()

هذه هي الطريقة التي نقرأ بها .env في البيئة. نحتاجه من أجل المفتاح الخاص (انظر لاحقًا).

لاستخدام عقد، نحتاج إلى عنوانه و الخاص به. نحن نقدم كليهما هنا.

في JavaScript (وبالتالي TypeScript) لا يمكنك تعيين قيمة جديدة لثابت، ولكن يمكنك تعديل الكائن المخزن فيه. باستخدام اللاحقة as const، فإننا نخبر TypeScript أن القائمة نفسها ثابتة ولا يجوز تغييرها.

const publicClient = createPublicClient({
  chain: holesky,
  transport: http(),
})

قم بإنشاء عميل عام (opens in a new tab) في Viem. العملاء العامون ليس لديهم مفتاح خاص مرفق، وبالتالي لا يمكنهم إرسال المعاملات. يمكنهم استدعاء دوال view (opens in a new tab)، وقراءة أرصدة الحسابات، وما إلى ذلك.

const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)

متغيرات البيئة متاحة في process.env (opens in a new tab). ومع ذلك، فإن TypeScript قوية النوع. يمكن أن يكون متغير البيئة أي سلسلة نصية، أو فارغًا، لذا فإن نوع متغير البيئة هو string | undefined. ومع ذلك، يتم تعريف المفتاح في Viem على أنه 0x${string} (0x متبوعًا بسلسلة نصية). هنا نخبر TypeScript أن متغير البيئة PRIVATE_KEY سيكون من هذا النوع. إذا لم يكن كذلك، فسنحصل على خطأ في وقت التشغيل.

تستخدم الدالة privateKeyToAccount (opens in a new tab) بعد ذلك هذا المفتاح الخاص لإنشاء كائن حساب كامل.

const walletClient = createWalletClient({
  account,
  chain: holesky,
  transport: http(),
})

بعد ذلك، نستخدم كائن الحساب لإنشاء عميل محفظة (opens in a new tab). يحتوي هذا العميل على مفتاح خاص وعنوان، لذا يمكن استخدامه لإرسال المعاملات.

const greeter = getContract({
  address: greeterAddress,
  abi: greeterABI,
  client: { public: publicClient, wallet: walletClient },
})

الآن بعد أن أصبح لدينا جميع المتطلبات الأساسية، يمكننا أخيرًا إنشاء نسخة عقد (opens in a new tab). سنستخدم نسخة العقد هذه للتواصل مع العقد على السلسلة.

القراءة من سلسلة الكتل
console.log(`Current greeting:`, await greeter.read.greet())

دوال العقد المخصصة للقراءة فقط (view (opens in a new tab) وpure (opens in a new tab)) متاحة ضمن read. في هذه الحالة، نستخدمها للوصول إلى الدالة greet (opens in a new tab)، والتي تُرجع التحية.

لغة JavaScript أحادية المسار (single-threaded)، لذلك عندما نطلق عملية طويلة الأمد، نحتاج إلى تحديد أننا نقوم بذلك بشكل غير متزامن (opens in a new tab). يتطلب استدعاء سلسلة الكتل، حتى بالنسبة لعملية القراءة فقط، رحلة ذهاب وإياب بين الكمبيوتر وعقدة سلسلة الكتل. هذا هو السبب في أننا نحدد هنا أن الكود يحتاج إلى await (انتظار) النتيجة.

إذا كنت مهتمًا بكيفية عمل ذلك، يمكنك القراءة عنه هنا (opens in a new tab)، ولكن من الناحية العملية، كل ما تحتاج إلى معرفته هو أنك تستخدم await للنتائج إذا بدأت عملية تستغرق وقتًا طويلاً، وأن أي دالة تقوم بذلك يجب الإعلان عنها كـ async.

إصدار المعاملات
const setGreeting = async (greeting: string): Promise<any> => {

هذه هي الدالة التي تستدعيها لإصدار معاملة تغير التحية. نظرًا لأن هذه عملية طويلة، يتم الإعلان عن الدالة كـ async. بسبب التنفيذ الداخلي، تحتاج أي دالة async إلى إرجاع كائن Promise. في هذه الحالة، يعني Promise<any> أننا لا نحدد بالضبط ما سيتم إرجاعه في Promise.

const txHash = await greeter.write.setGreeting([greeting])

يحتوي الحقل write في نسخة العقد على جميع الدوال التي تكتب في حالة سلسلة الكتل (تلك التي تتطلب إرسال معاملة)، مثل setGreeting (opens in a new tab). يتم توفير المعلمات، إن وجدت، كقائمة، وتُرجع الدالة تجزئة المعاملة.

    console.log(`Working on a fix, see https://eth-holesky.blockscout.com/tx/${txHash}`)

    return txHash
}

أبلغ عن تجزئة المعاملة (كجزء من عنوان URL لمستكشف الكتل لعرضها) وقم بإرجاعها.

الاستجابة للأحداث
greeter.watchEvent.SetGreeting({

تتيح لك الدالة watchEvent (opens in a new tab) تحديد دالة ليتم تشغيلها عند إصدار حدث. إذا كنت تهتم فقط بنوع واحد من الأحداث (في هذه الحالة، SetGreeting)، يمكنك استخدام بناء الجملة هذا لقصر نفسك على نوع الحدث هذا.

    onLogs: logs => {

يتم استدعاء الدالة onLogs عندما تكون هناك إدخالات سجل. في إيثيريوم، عادةً ما يكون المصطلحان "سجل" و"حدث" قابلين للتبادل.

console.log(
  `Address ${logs[0].args.sender} changed the greeting to ${logs[0].args.greeting}`
)

قد تكون هناك أحداث متعددة، ولكن للتبسيط نحن نهتم فقط بالحدث الأول. logs[0].args هي وسيطات الحدث، في هذه الحالة sender وgreeting.

        if (logs[0].args.sender != account.address)
            setGreeting(`${account.address} insists on it being Hello!`)
    }
})

إذا لم يكن المرسل هو هذا الخادم، فاستخدم setGreeting لتغيير التحية.

package.json

يتحكم هذا الملف (opens in a new tab) في تكوين Node.js (opens in a new tab). تشرح هذه المقالة التعريفات المهمة فقط.

{
  "main": "dist/index.js",

يحدد هذا التعريف ملف JavaScript الذي سيتم تشغيله.

  "scripts": {
    "start": "tsc && node dist/app.js",
  },

البرامج النصية هي إجراءات تطبيق مختلفة. في هذه الحالة، الإجراء الوحيد الذي لدينا هو start، والذي يقوم بتجميع الخادم ثم تشغيله. الأمر tsc هو جزء من حزمة typescript ويقوم بتجميع TypeScript إلى JavaScript. إذا كنت ترغب في تشغيله يدويًا، فهو موجود في node_modules/.bin. الأمر الثاني يقوم بتشغيل الخادم.

  "type": "module",

هناك أنواع متعددة من تطبيقات عقدة JavaScript. يتيح لنا النوع module استخدام await في كود المستوى الأعلى، وهو أمر مهم عندما تقوم بعمليات بطيئة (وبالتالي غير متزامنة).

  "devDependencies": {
    "@types/node": "^20.14.2",
    "typescript": "^5.4.5"
  },

هذه هي الحزم المطلوبة فقط للتطوير. هنا نحتاج إلى typescript ولأننا نستخدمه مع Node.js، فإننا نحصل أيضًا على الأنواع لمتغيرات وكائنات العقدة، مثل process. يعني الترميز ^<version> (opens in a new tab) هذا الإصدار أو إصدارًا أعلى لا يحتوي على تغييرات جذرية. راجع هنا (opens in a new tab) لمزيد من المعلومات حول معنى أرقام الإصدارات.

  "dependencies": {
    "dotenv": "^16.4.5",
    "viem": "2.14.1"
  }
}

هذه هي الحزم المطلوبة في وقت التشغيل، عند تشغيل dist/app.js.

الخاتمة

الخادم المركزي الذي أنشأناه هنا يقوم بعمله، وهو العمل كوكيل للمستخدم. يمكن لأي شخص آخر يريد أن يستمر التطبيق اللامركزي (dapp) في العمل ومستعد لإنفاق الغاز تشغيل نسخة جديدة من الخادم بعنوانه الخاص.

ومع ذلك، لا ينجح هذا إلا عندما يمكن التحقق من إجراءات الخادم المركزي بسهولة. إذا كان الخادم المركزي يحتوي على أي معلومات حالة سرية، أو يقوم بإجراء حسابات صعبة، فهو كيان مركزي تحتاج إلى الوثوق به لاستخدام التطبيق، وهو بالضبط ما تحاول سلاسل الكتل تجنبه. في مقال مستقبلي، أخطط لإظهار كيفية استخدام براهين المعرفة الصفرية للتغلب على هذه المشكلة.

انظر هنا للمزيد من أعمالي (opens in a new tab).