البيانات على السلسلة في بيئة الإنتاج: ما لا يخبرك به أحد
بيانات البلوكتشين ليست نظيفة ولا موثوقة ولا سهلة. حدود معدل RPC، إعادة تنظيم السلسلة، أخطاء BigInt، ومقايضات الفهرسة — دروس صعبة من إطلاق منتجات DeFi حقيقية.
هناك وهم بأن البيانات على السلسلة جديرة بالثقة بطبيعتها. سجل غير قابل للتغيير. حالة شفافة. اقرأها فقط وانتهى الأمر.
صدقت ذلك أيضا. ثم أطلقت لوحة تحكم DeFi في بيئة الإنتاج وأمضيت ثلاثة أسابيع أحاول معرفة لماذا أرصدة التوكنات خاطئة، ولماذا سجل الأحداث يحتوي على فجوات، ولماذا قاعدة بياناتنا تحتوي على معاملات من كتل لم تعد موجودة.
البيانات على السلسلة خام وعدائية ومليئة بالحالات الحدية التي ستكسر تطبيقك بطرق لن تلاحظها حتى يقدم مستخدم بلاغ خطأ. تغطي هذه التدوينة كل ما تعلمته بالطريقة الصعبة.
وهم البيانات الموثوقة#
إليك أول شيء لا يخبرك به أحد: البلوكتشين لا يعطيك بيانات. يعطيك تحولات حالة. لا يوجد SELECT * FROM transfers WHERE user = '0x...'. هناك سجلات ووصولات تخزين وتتبعات استدعاءات — كلها مشفرة بتنسيقات تتطلب سياقا لفك تشفيرها.
سجل حدث Transfer يعطيك from و to و value. لا يخبرك باسم التوكن. لا يخبرك بالكسور العشرية. لا يخبرك إذا كان هذا تحويلا مشروعا أم توكن برسوم على التحويل يقتطع 3% من القيمة. لا يخبرك إذا كانت هذه الكتلة ستظل موجودة بعد 30 ثانية.
الجزء "غير القابل للتغيير" صحيح — بمجرد الإنهاء. لكن الإنهاء ليس فوريا. والبيانات التي تحصل عليها من عقدة RPC ليست بالضرورة من كتلة نهائية. معظم المطورين يستعلمون عن latest ويعاملونها كحقيقة. هذا خطأ برمجي، وليس ميزة.
ثم هناك الترميز. كل شيء بالنظام السداسي عشري. العناوين ذات حالة أحرف مختلطة مع تدقيق (أو بدون). مبالغ التوكنات هي أعداد صحيحة مضروبة في 10^decimals. تحويل USDC بقيمة 100 دولار يبدو كـ 100000000 على السلسلة لأن USDC يحتوي على 6 كسور عشرية، وليس 18. رأيت كود إنتاج يفترض 18 كسرا عشريا لكل توكن ERC-20. الأرصدة الناتجة كانت خاطئة بمعامل 10^12.
حدود معدل RPC ستدمر عطلة نهاية أسبوعك#
كل تطبيق Web3 إنتاجي يتحدث إلى نقطة نهاية RPC. وكل نقطة نهاية RPC لها حدود معدل أكثر صرامة مما تتوقع.
إليك الأرقام المهمة:
- Alchemy المجاني: ~30 مليون وحدة حسابية/شهر، 40 طلبا/دقيقة. يبدو سخيا حتى تدرك أن استدعاء
eth_getLogsواحد على نطاق كتل واسع يمكن أن يستهلك مئات الوحدات. ستستنفد حصتك الشهرية في يوم واحد من الفهرسة. - Infura المجاني: 100 ألف طلب/يوم، تقريبا 1.15 طلب/ثانية. حاول تصفح 500 ألف كتلة من سجلات الأحداث بهذا المعدل.
- QuickNode المجاني: مشابه لـ Infura — 100 ألف طلب/يوم.
الخطط المدفوعة تساعد، لكنها لا تقضي على المشكلة. حتى بـ 200 دولار/شهر على خطة Growth من Alchemy، ستصطدم مهمة فهرسة ثقيلة بحدود الإنتاجية. وعندما تصطدم بها، لا تحصل على تدهور رشيق. تحصل على أخطاء 429، أحيانا برسائل غير مفيدة، وأحيانا بدون ترويسة retry-after.
الحل هو مزيج من مزودين احتياطيين ومنطق إعادة المحاولة والتعمد الشديد بشأن الاستدعاءات التي تقوم بها. إليك كيف يبدو إعداد RPC متين مع viem:
import { createPublicClient, fallback, http } from "viem";
import { mainnet } from "viem/chains";
const client = createPublicClient({
chain: mainnet,
transport: fallback(
[
http("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY", {
retryCount: 3,
retryDelay: 1500,
timeout: 15_000,
}),
http("https://mainnet.infura.io/v3/YOUR_KEY", {
retryCount: 3,
retryDelay: 1500,
timeout: 15_000,
}),
http("https://rpc.ankr.com/eth", {
retryCount: 2,
retryDelay: 2000,
timeout: 20_000,
}),
],
{ rank: true }
),
});خيار rank: true حاسم. يخبر viem بقياس زمن الاستجابة ومعدل النجاح لكل ناقل وتفضيل الأسرع والأكثر موثوقية تلقائيا. إذا بدأ Alchemy بتقييد معدلك، ينقل viem حركة المرور إلى Infura. إذا توقف Infura، ينتقل إلى Ankr.
لكن هناك تفصيل دقيق: منطق إعادة المحاولة الافتراضي في viem يستخدم تراجعا أسيا، وهو عادة ما تريده. ومع ذلك، اعتبارا من أوائل 2025، هناك مشكلة معروفة حيث لا يعيد retryCount المحاولة بشكل صحيح لأخطاء مستوى RPC (مثل 429) عند تفعيل وضع الدُفعات. إذا كنت تجمع الطلبات في دفعات، اختبر سلوك إعادة المحاولة بشكل صريح. لا تثق بأنه يعمل.
إعادة التنظيم: الخطأ الذي لن تراه قادما#
إعادة تنظيم السلسلة تحدث عندما تختلف الشبكة مؤقتا حول أي كتلة هي الأصلية. العقدة A ترى الكتلة 1000 بمعاملات [A, B, C]. العقدة B ترى كتلة 1000 مختلفة بمعاملات [A, D]. في النهاية تتوافق الشبكة، وتفوز نسخة واحدة.
على سلاسل إثبات العمل، كان هذا شائعا — إعادة تنظيم من 1-3 كتل كانت تحدث عدة مرات في اليوم. Ethereum بعد الدمج أفضل. هجوم إعادة تنظيم ناجح يتطلب الآن تنسيق ما يقارب 50% من المدققين. لكن "أفضل" لا يعني "مستحيل". كانت هناك إعادة تنظيم ملحوظة من 7 كتل على Beacon Chain في مايو 2022، بسبب تطبيقات غير متسقة لتعزيز المُقترح عبر العملاء.
ولا يهم كم هي نادرة إعادة التنظيم على شبكة Ethereum الرئيسية. إذا كنت تبني على L2 أو سلاسل جانبية — Polygon أو Arbitrum أو Optimism — فإن إعادة التنظيم أكثر تكرارا. Polygon تاريخيا شهدت إعادة تنظيم من 10+ كتل.
إليك المشكلة العملية: فهرست الكتلة 18,000,000. كتبت الأحداث في قاعدة بياناتك. ثم أُعيد تنظيم الكتلة 18,000,000. الآن قاعدة بياناتك تحتوي على أحداث من كتلة لم تعد موجودة على السلسلة الأصلية. تلك الأحداث قد تشير إلى معاملات لم تحدث أبدا. مستخدموك يرون تحويلات وهمية.
الحل يعتمد على معماريتك:
الخيار 1: تأخير التأكيد. لا تفهرس البيانات حتى يمر N كتلة من التأكيدات. لشبكة Ethereum الرئيسية، 64 كتلة (فترتان) تعطيك ضمانات نهائية. لـ L2، تحقق من نموذج النهائية للسلسلة المحددة. هذا بسيط لكنه يضيف تأخيرا — حوالي 13 دقيقة على Ethereum.
الخيار 2: اكتشاف إعادة التنظيم والتراجع. فهرس بقوة لكن تتبع تجزئات الكتل. مع كل كتلة جديدة، تحقق من أن تجزئة الأب تطابق الكتلة السابقة التي فهرستها. إذا لم تتطابق، اكتشفت إعادة تنظيم: احذف كل شيء من الكتل اليتيمة وأعد فهرسة السلسلة الأصلية.
interface IndexedBlock {
number: bigint;
hash: `0x${string}`;
parentHash: `0x${string}`;
}
async function detectReorg(
client: PublicClient,
lastIndexed: IndexedBlock
): Promise<{ reorged: boolean; depth: number }> {
const currentBlock = await client.getBlock({
blockNumber: lastIndexed.number,
});
if (currentBlock.hash === lastIndexed.hash) {
return { reorged: false, depth: 0 };
}
// الرجوع للخلف لإيجاد نقطة انحراف السلسلة
let depth = 1;
let checkNumber = lastIndexed.number - 1n;
while (checkNumber > 0n && depth < 128) {
const onChain = await client.getBlock({ blockNumber: checkNumber });
const inDb = await getIndexedBlock(checkNumber); // البحث في قاعدة بياناتك
if (onChain.hash === inDb?.hash) {
return { reorged: true, depth };
}
depth++;
checkNumber--;
}
return { reorged: true, depth };
}هذا ليس نظريا. كان لدي نظام إنتاجي حيث فهرسنا الأحداث عند رأس السلسلة بدون اكتشاف إعادة التنظيم. لمدة ثلاثة أسابيع عمل بشكل جيد. ثم تسببت إعادة تنظيم من كتلتين على Polygon في حدث سك NFT مكرر في قاعدة بياناتنا. الواجهة الأمامية أظهرت لمستخدم أنه يملك توكن لا يملكه. استغرق تصحيح هذا الخطأ يومين لأن لا أحد كان يبحث عن إعادة التنظيم كسبب جذري.
مشكلة الفهرسة: اختر ألمك#
لديك ثلاثة خيارات حقيقية للحصول على بيانات منظمة من السلسلة إلى تطبيقك.
استدعاءات RPC المباشرة#
استدعِ getLogs و getBlock و getTransaction مباشرة. هذا يعمل للقراءات صغيرة النطاق — التحقق من رصيد مستخدم، جلب أحداث حديثة لعقد واحد. لا يعمل للفهرسة التاريخية أو الاستعلامات المعقدة عبر العقود.
المشكلة تركيبية. تريد جميع مبادلات Uniswap V3 في آخر 30 يوما؟ هذا ~200 ألف كتلة. بحد 2 ألف كتلة لكل استدعاء getLogs من Alchemy، هذا 100 طلب مُقسّم على الأقل. كل واحد يُحسب ضمن حد معدلك. وإذا فشل أي استدعاء، تحتاج منطق إعادة محاولة وتتبع مؤشر وطريقة للاستئناف من حيث توقفت.
The Graph (الرسوم البيانية الفرعية)#
The Graph كان الحل الأصلي. عرّف مخططا، اكتب عمليات التحويل بـ AssemblyScript، انشر، واستعلم بـ GraphQL. تم إيقاف الخدمة المُستضافة — كل شيء الآن على شبكة The Graph اللامركزية، مما يعني أنك تدفع بتوكنات GRT مقابل الاستعلامات.
الجيد: معياري، موثق جيدا، نظام بيئي كبير من الرسوم البيانية الفرعية الموجودة التي يمكنك نسخها.
السيئ: AssemblyScript مؤلم. التصحيح محدود. النشر يستغرق دقائق إلى ساعات. إذا كان رسمك البياني الفرعي يحتوي على خطأ، تُعيد النشر وتنتظر إعادة المزامنة من الصفر. الشبكة اللامركزية تضيف تأخيرا وأحيانا يتأخر المُفهرسون عن رأس السلسلة.
استخدمت The Graph للوحات تحكم كثيفة القراءة حيث تأخر البيانات من 30-60 ثانية مقبول. يعمل جيدا هناك. لن أستخدمه لأي شيء يتطلب بيانات فورية أو منطق أعمال معقد في عمليات التحويل.
مُفهرسات مخصصة (Ponder و Envio)#
هنا نضج النظام البيئي بشكل ملحوظ. Ponder و Envio يتيحان لك كتابة منطق الفهرسة بـ TypeScript (وليس AssemblyScript)، والتشغيل محليا أثناء التطوير، والنشر كخدمات مستقلة.
Ponder يعطيك أقصى تحكم. تُعرّف معالجات الأحداث بـ TypeScript، يدير خط أنابيب الفهرسة، وتحصل على قاعدة بيانات SQL كمخرج. المقايضة: أنت تملك البنية التحتية. التوسع والمراقبة ومعالجة إعادة التنظيم — كلها مسؤوليتك.
Envio يحسّن لسرعة المزامنة. معاييرهم تظهر أوقات مزامنة أولية أسرع بكثير مقارنة بـ The Graph. يتعاملون مع إعادة التنظيم بشكل أصلي ويدعمون HyperSync، بروتوكول متخصص لجلب البيانات أسرع. المقايضة: أنت تشتري بنيتهم التحتية وواجهة API الخاصة بهم.
توصيتي: إذا كنت تبني تطبيق DeFi إنتاجي ولديك قدرة هندسية، استخدم Ponder. إذا كنت تحتاج أسرع مزامنة ممكنة ولا تريد إدارة البنية التحتية، قيّم Envio. إذا كنت تحتاج نموذجا أوليا سريعا أو تريد رسوما بيانية فرعية يديرها المجتمع، The Graph لا يزال مناسبا.
getLogs أخطر مما يبدو#
طريقة eth_getLogs في RPC بسيطة بشكل مخادع. أعطها نطاق كتل وبعض الفلاتر، واحصل على سجلات الأحداث المطابقة. إليك ما يحدث فعلا في بيئة الإنتاج:
حدود نطاق الكتل تختلف حسب المزود. Alchemy يحدد بـ 2 ألف كتلة (سجلات غير محدودة) أو كتل غير محدودة (حد أقصى 10 آلاف سجل). Infura لديه حدود مختلفة. QuickNode لديه حدود مختلفة. RPC عام قد يحدد بـ 1000 كتلة. كودك يجب أن يتعامل مع كل هذا.
حدود حجم الاستجابة موجودة. حتى ضمن نطاق الكتل، إذا كان عقد شائع يُصدر آلاف الأحداث لكل كتلة، يمكن أن تتجاوز استجابتك حد الحمولة لدى المزود (150 ميغابايت على Alchemy). الاستدعاء لا يُرجع نتائج جزئية. يفشل.
النطاقات الفارغة ليست مجانية. حتى لو لم تكن هناك سجلات مطابقة، المزود يظل يمسح نطاق الكتل. هذا يُحسب ضمن وحداتك الحسابية.
إليك أداة تصفح الصفحات التي تتعامل مع هذه القيود:
import type { PublicClient, Log, AbiEvent } from "viem";
async function fetchLogsInChunks<T extends AbiEvent>(
client: PublicClient,
params: {
address: `0x${string}`;
event: T;
fromBlock: bigint;
toBlock: bigint;
maxBlockRange?: bigint;
}
): Promise<Log<bigint, number, false, T, true>[]> {
const { address, event, fromBlock, toBlock, maxBlockRange = 2000n } = params;
const allLogs: Log<bigint, number, false, T, true>[] = [];
let currentFrom = fromBlock;
while (currentFrom <= toBlock) {
const currentTo =
currentFrom + maxBlockRange - 1n > toBlock
? toBlock
: currentFrom + maxBlockRange - 1n;
try {
const logs = await client.getLogs({
address,
event,
fromBlock: currentFrom,
toBlock: currentTo,
});
allLogs.push(...logs);
currentFrom = currentTo + 1n;
} catch (error) {
// إذا كان النطاق كبيرا جدا (نتائج كثيرة)، قسمه إلى نصفين
if (isRangeTooLargeError(error) && currentTo > currentFrom) {
const mid = currentFrom + (currentTo - currentFrom) / 2n;
const firstHalf = await fetchLogsInChunks(client, {
address,
event,
fromBlock: currentFrom,
toBlock: mid,
maxBlockRange,
});
const secondHalf = await fetchLogsInChunks(client, {
address,
event,
fromBlock: mid + 1n,
toBlock: currentTo,
maxBlockRange,
});
allLogs.push(...firstHalf, ...secondHalf);
currentFrom = currentTo + 1n;
} else {
throw error;
}
}
}
return allLogs;
}
function isRangeTooLargeError(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error);
return (
message.includes("Log response size exceeded") ||
message.includes("query returned more than") ||
message.includes("exceed maximum block range")
);
}الفكرة الرئيسية هي التقسيم الثنائي عند الفشل. إذا كان نطاق 2 ألف كتلة يُرجع سجلات كثيرة جدا، قسمه إلى نطاقين من 1000 كتلة. إذا كان 1000 لا يزال كثيرا، قسم مرة أخرى. هذا يتكيف تلقائيا مع العقود عالية النشاط دون أن تحتاج لمعرفة كثافة الأحداث مسبقا.
BigInt ستُذِلّك#
نوع Number في JavaScript هو عدد عشري بـ 64 بت. يمكنه تمثيل أعداد صحيحة حتى 2^53 - 1 — حوالي 9 كوادريليون. يبدو كثيرا حتى تدرك أن مبلغ توكن من 1 ETH بالـ wei هو 1000000000000000000 — رقم بـ 18 صفرا. هذا 10^18، أبعد بكثير من Number.MAX_SAFE_INTEGER.
إذا حوّلت BigInt إلى Number عن طريق الخطأ في أي مكان في خط أنابيبك — JSON.parse أو مشغل قاعدة بيانات أو مكتبة تسجيل — تحصل على فقدان دقة صامت. الرقم يبدو صحيحا تقريبا لكن الأرقام الأخيرة خاطئة. لن تكتشف هذا في الاختبارات لأن مبالغ اختباراتك صغيرة.
إليك الخطأ الذي أطلقته في بيئة الإنتاج:
// الخطأ: يبدو بريئا، لكنه ليس كذلك
function formatTokenAmount(amount: bigint, decimals: number): string {
return (Number(amount) / Math.pow(10, decimals)).toFixed(4);
}
// للمبالغ الصغيرة يعمل بشكل جيد:
formatTokenAmount(1000000n, 6); // "1.0000" -- صحيح
// للمبالغ الكبيرة ينكسر بصمت:
formatTokenAmount(123456789012345678n, 18);
// يُرجع "0.1235" -- خاطئ، فُقدت الدقة الفعلية
// Number(123456789012345678n) === 123456789012345680
// الرقمان الأخيران تم تقريبهما بواسطة IEEE 754الحل: لا تحوّل أبدا إلى Number قبل القسمة. استخدم أدوات viem المدمجة، التي تعمل على النصوص و BigInt:
import { formatUnits, parseUnits } from "viem";
// صحيح: يعمل على BigInt، يُرجع نصا
function formatTokenAmount(
amount: bigint,
decimals: number,
displayDecimals: number = 4
): string {
const formatted = formatUnits(amount, decimals);
// formatUnits يُرجع نص الدقة الكاملة مثل "0.123456789012345678"
// اقتطع (لا تقرّب) إلى دقة العرض المطلوبة
const [whole, fraction = ""] = formatted.split(".");
const truncated = fraction.slice(0, displayDecimals).padEnd(displayDecimals, "0");
return `${whole}.${truncated}`;
}
// مهم أيضا: استخدم parseUnits لمدخلات المستخدم، وليس parseFloat أبدا
function parseTokenInput(input: string, decimals: number): bigint {
// parseUnits يتعامل مع تحويل النص إلى BigInt بشكل صحيح
return parseUnits(input, decimals);
}لاحظ أنني أقتطع بدلا من التقريب. هذا متعمد. في السياقات المالية، عرض "1.0001 ETH" عندما تكون القيمة الحقيقية "1.00009999..." أفضل من عرض "1.0001" عندما تكون القيمة الحقيقية "1.00005001..." وتم تقريبها للأعلى. المستخدمون يتخذون قرارات بناء على المبالغ المعروضة. الاقتطاع هو الخيار المحافظ.
فخ آخر: JSON.stringify لا يعرف كيف يُسلسل BigInt. يرمي خطأ. كل استجابة من واجهة API الخاصة بك تتضمن مبالغ توكنات تحتاج استراتيجية تسلسل. أستخدم التحويل إلى نص عند حدود API:
// مُسلسل استجابة API
function serializeForApi(data: Record<string, unknown>): string {
return JSON.stringify(data, (_, value) =>
typeof value === "bigint" ? value.toString() : value
);
}استراتيجية التخزين المؤقت: ماذا، كم المدة، ومتى تُبطل#
ليس كل البيانات على السلسلة لها نفس متطلبات الحداثة. إليك التسلسل الهرمي الذي أستخدمه:
التخزين المؤقت للأبد (غير قابل للتغيير):
- وصولات المعاملات (بمجرد التعدين، لا تتغير)
- بيانات الكتل النهائية (تجزئة الكتلة، الطابع الزمني، قائمة المعاملات)
- بايتكود العقود
- سجلات الأحداث التاريخية من الكتل النهائية
التخزين المؤقت لدقائق إلى ساعات:
- بيانات التوكن الوصفية (الاسم، الرمز، الكسور العشرية) — غير قابلة للتغيير تقنيا لمعظم التوكنات، لكن ترقيات الوكيل يمكن أن تغير التطبيق
- حلول ENS — TTL مدة 5 دقائق يعمل جيدا
- أسعار التوكنات — تعتمد على متطلبات دقتك، 30 ثانية إلى 5 دقائق
التخزين المؤقت لثوانٍ أو بدون تخزين:
- رقم الكتلة الحالي
- أرصدة الحسابات و nonce
- حالة المعاملات المعلقة
- سجلات أحداث غير نهائية (مشكلة إعادة التنظيم مرة أخرى)
التنفيذ لا يحتاج أن يكون معقدا. ذاكرة مؤقتة بمستويين مع LRU في الذاكرة و Redis تغطي معظم الحالات:
import { LRUCache } from "lru-cache";
const memoryCache = new LRUCache<string, unknown>({
max: 10_000,
ttl: 1000 * 60, // دقيقة واحدة افتراضيا
});
type CacheTier = "immutable" | "short" | "volatile";
const TTL_MAP: Record<CacheTier, number> = {
immutable: 1000 * 60 * 60 * 24, // 24 ساعة في الذاكرة، دائم في Redis
short: 1000 * 60 * 5, // 5 دقائق
volatile: 1000 * 15, // 15 ثانية
};
async function cachedRpcCall<T>(
key: string,
tier: CacheTier,
fetcher: () => Promise<T>
): Promise<T> {
// تحقق من الذاكرة أولا
const cached = memoryCache.get(key) as T | undefined;
if (cached !== undefined) return cached;
// ثم Redis (إذا كان لديك)
// const redisCached = await redis.get(key);
// if (redisCached) { ... }
const result = await fetcher();
memoryCache.set(key, result, { ttl: TTL_MAP[tier] });
return result;
}
// الاستخدام:
const receipt = await cachedRpcCall(
`receipt:${txHash}`,
"immutable",
() => client.getTransactionReceipt({ hash: txHash })
);الدرس غير البديهي: أكبر مكسب في الأداء ليس تخزين استجابات RPC مؤقتا. إنه تجنب استدعاءات RPC بالكامل. في كل مرة توشك على استدعاء getBlock، اسأل نفسك: هل أحتاج فعلا بيانات من السلسلة الآن، أم يمكنني اشتقاقها من بيانات لدي بالفعل؟ هل يمكنني الاستماع للأحداث عبر WebSocket بدلا من الاستقصاء؟ هل يمكنني تجميع قراءات متعددة في multicall واحد؟
TypeScript و ABI العقود: الطريقة الصحيحة#
نظام أنواع viem، المدعوم بـ ABIType، يوفر استدلال أنواع كامل من طرف إلى طرف من ABI العقد إلى كود TypeScript الخاص بك. لكن فقط إذا أعددته بشكل صحيح.
الطريقة الخاطئة:
// لا استدلال أنواع — args هو unknown[]، والعائد unknown
const result = await client.readContract({
address: "0x...",
abi: JSON.parse(abiString), // مُحلّل وقت التشغيل = لا معلومات أنواع
functionName: "balanceOf",
args: ["0x..."],
});الطريقة الصحيحة:
// عرّف ABI كـ const للاستدلال الكامل على الأنواع
const erc20Abi = [
{
name: "balanceOf",
type: "function",
stateMutability: "view",
inputs: [{ name: "account", type: "address" }],
outputs: [{ name: "balance", type: "uint256" }],
},
{
name: "transfer",
type: "function",
stateMutability: "nonpayable",
inputs: [
{ name: "to", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ name: "success", type: "bool" }],
},
] as const;
// الآن TypeScript يعرف:
// - functionName يُكمل تلقائيا إلى "balanceOf" | "transfer"
// - args لـ balanceOf هو [address: `0x${string}`]
// - نوع العائد لـ balanceOf هو bigint
const balance = await client.readContract({
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
abi: erc20Abi,
functionName: "balanceOf",
args: ["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"],
});
// typeof balance = bigint -- مُحدد النوع بالكاملتأكيد as const هو ما يجعلها تعمل. بدونه، يُوسّع TypeScript نوع ABI إلى { name: string, type: string, ... }[] وتنهار كل آلية الاستدلال. هذا هو الخطأ الأكثر شيوعا الذي أراه في قواعد كود TypeScript لـ Web3.
للمشاريع الأكبر، استخدم @wagmi/cli لإنشاء روابط عقود مُحددة الأنواع مباشرة من مشروع Foundry أو Hardhat الخاص بك. يقرأ ملفات ABI المُجمّعة وينتج ملفات TypeScript مع تأكيدات as const مطبقة مسبقا. لا نسخ يدوي لـ ABI، لا انحراف في الأنواع.
الحقيقة غير المريحة#
بيانات البلوكتشين هي مشكلة أنظمة موزعة تتنكر كمشكلة قواعد بيانات. في اللحظة التي تعاملها كـ "مجرد API آخر"، تبدأ بتراكم أخطاء غير مرئية في التطوير ومتقطعة في الإنتاج.
الأدوات تحسنت بشكل كبير. viem تحسين هائل على ethers.js لأمان الأنواع وتجربة المطور. Ponder و Envio جعلا الفهرسة المخصصة متاحة. لكن التحديات الأساسية — إعادة التنظيم وحدود المعدل والترميز والنهائية — هي على مستوى البروتوكول. لا مكتبة تُجرّدها بعيدا.
ابنِ مع افتراض أن RPC الخاص بك سيكذب عليك، وأن كتلك ستُعاد تنظيمها، وأن أرقامك ستتجاوز الحد، وأن ذاكرتك المؤقتة ستقدم بيانات قديمة. ثم تعامل مع كل حالة بشكل صريح.
هذا ما تبدو عليه البيانات على السلسلة بمستوى الإنتاج.