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

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

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

مقدمة

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

Normal interaction between web server, client, and blockchain

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

The interaction with the addition of a server

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

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

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

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

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

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

لتشغيله:

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

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

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

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

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

كيف يعمل؟

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

src/app.ts

الغالبية العظمى من البرنامج موجودة في src/app.ts (يفتح في علامة تبويب جديدة).

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

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

import { privateKeyToAccount } from "viem/accounts"

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

import { holesky } from "viem/chains"

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

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

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

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

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

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

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

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

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

تستخدم الدالة privateKeyToAccount (يفتح في علامة تبويب جديدة) بعد ذلك هذا المفتاح الخاص لإنشاء كائن حساب كامل.

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

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

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

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

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

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

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

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

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

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

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

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

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

    return txHash
}

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

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

تتيح لك الدالة watchEvent (يفتح في علامة تبويب جديدة) تحديد دالة ليتم تشغيلها عند إصدار حدث. إذا كنت تهتم فقط بنوع واحد من الأحداث (في هذه الحالة، 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

يتحكم هذا الملف (يفتح في علامة تبويب جديدة) في تكوين Node.js (يفتح في علامة تبويب جديدة). تشرح هذه المقالة التعريفات المهمة فقط.

{
  "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> (يفتح في علامة تبويب جديدة) هذا الإصدار أو إصدارًا أعلى لا يحتوي على تغييرات جذرية. راجع هنا (يفتح في علامة تبويب جديدة) لمزيد من المعلومات حول معنى أرقام الإصدارات.

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

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

الخاتمة

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

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

انظر هنا للمزيد من أعمالي (يفتح في علامة تبويب جديدة).