الورقة البيضاء · 11
دليل التنفيذ
من نسخة مستنسخة نظيفة إلى رمز دفعة منشور ومزوَّد بخطافات وإفادات، خطوة بخطوة. كل كتلة JavaScript في هذا الفصل منسوخة من test/standard/ImplementationGuide.test.js وتعمل في التكامل المستمر؛ ولو كانت كتلة هنا خاطئة لفشل البناء.
كيفية استخدام هذا الدليل
تبني الخطوات بعضها على بعض وتتشارك المتغيرات: token وattestations والحسابات الموقّعة والمعرّفات المنشأة على امتداد الطريق. اقرأها بالترتيب في المرة الأولى. وشغّلها بنفسك بالأمر:
npx hardhat test test/standard/ImplementationGuide.test.js
| الخطوة | ما تحصل عليه | من ينفّذ | الاستدعاءات الرئيسية |
|---|---|---|---|
| 1 · الإعداد | نسخة مستنسخة مترجمة ومختبرة | المطوّر | npm ci، npm test |
| 2 · النشر | وكيل الرمز + سجل الإفادات | الجهة المنشِرة | upgrades.deployProxy، ARexAttestations |
| 3 · الأدوار | مشغّلون بأقل الصلاحيات | المسؤول | grantRole |
| 4 · الإنشاء | دفعة مع التزام بالبيانات الوصفية | جهة الإصدار | createBatch |
| 5 · القراءة | سجل الدفعة والمعروض | أي شخص | batchOf، totalSupply(uint256)، parentsOf |
| 6 · التحويل | تحويل مفحوص | الحامل | transferCheck، safeTransferFrom |
| 7 · النسب | التقسيم والدمج والمعالجة | الحامل / المعالِج | transform |
| 8 · الحالة | تحديثات دورة الحياة والبيانات الوصفية | دور الحالة | setBatchStatus، setBatchMetadata |
| 9 · الخطافات | مسار قواعد مرتب | مسؤول الخطافات | registerHook، getHooks |
| 10 · الإفادة | أدلة تفتح التحويلات | جهة إصدار موثوقة | setTrustedIssuer، attest |
| 11 · العقوبات | أهلية مع المصدر ومدة الصلاحية TTL | مدير الأهلية | setSanctioned |
| 12 · الإنفاذ | التجميد والإيقاف والتحويل القسري | جهة الإنفاذ / جهة الإيقاف | setFrozenTokens، forcedTransfer |
| 13 · السحب من التداول | نهاية عمر الدفعة | دور الحالة، الحامل | setBatchStatus(…, 255, …)، burn |
| 14 · الفهرسة | رسم النسب البياني من الأحداث | المفهرس | queryFilter(BatchTransformed) |
| 15 · الترقية | ترقية تنفيذ جرى التحقق من صلاحيتها | القائم بالترقية | validateUpgrade، upgradeProxy |
1 · الإعداد
Node.js 20 أو 22 LTS وnpm. والاعتماديات مثبّتة في package-lock.json (OpenZeppelin 5.3.0)؛ فاستخدم npm ci ليكون بناؤك هو البناء الذي ينتجه التكامل المستمر.
git clone https://github.com/Arex-Corp/A-REX.git && cd A-REX
npm ci
npm run compile
npm test # 46 tests: reference implementation, hooks, extensions, invariants, this guide
npm run interface-ids # prints the ERC-165 ids; they must match the specification
تستخدم المقتطفات أدناه ثلاث دوال مساعدة صغيرة وتسمّي عشرة حسابات. وتحوّل explain كود الرفض إلى اسمه؛ والقائمة هي مجموعة الأكواد المعرّفة في ARexCodes.
const commodity = (scheme, code) => ethers.keccak256(ethers.toUtf8Bytes(`${scheme}:${code}`));
const unit = (u) => ethers.encodeBytes32String(u);
const ref = (text) => ethers.keccak256(ethers.toUtf8Bytes(text));
const REASONS = Object.fromEntries(
["PAUSED", "FROZEN", "SENDER_INELIGIBLE", "RECEIVER_INELIGIBLE", "SANCTIONED", "BATCH_NOT_FOUND",
"BATCH_RETIRED", "BATCH_EXPIRED", "BATCH_NOT_PRODUCED", "ATTESTATION_MISSING", "SEASON_CLOSED",
"IN_TRANSIT", "LIMIT_EXCEEDED", "DOCUMENT_MISSING"].map((n) => [ethers.keccak256(ethers.toUtf8Bytes(`AREX:${n}`)), n]),
);
const explain = (code) => (code === ethers.ZeroHash ? "OK" : REASONS[code] ?? code);
const [admin, issuer, farmer, buyer, coop, mill, warehouse, certifier, enforcer, custodian] = await ethers.getSigners();
2 · النشر
الرمز المرجعي قابل للترقية عبر UUPS، ولذلك يُنشر خلف وكيل ERC-1967 بواسطة إضافة الترقيات الخاصة بـ OpenZeppelin، التي تتحقق أيضًا من تخطيط التخزين. وسجل الإفادات عقد عادي مرتبط بالرمز.
const ARexToken = await ethers.getContractFactory("ARexToken");
const token = await upgrades.deployProxy(ARexToken, [admin.address, "ipfs://<cid>/{id}.json"], { kind: "uups" });
await token.waitForDeployment();
const attestations = await (await ethers.getContractFactory("ARexAttestations"))
.deploy(admin.address, await token.getAddress());
ومن سطر الأوامر تُنشر الحزمة نفسها، مضافًا إليها خطافا الأهلية وانتهاء الصلاحية، بالأوامر:
npx hardhat node # terminal 1
npx hardhat run scripts/deploy-standard.js --network localhost # token, attestations, 2 hooks
npx hardhat run scripts/deploy-full-standard.js --network localhost # every hook in order + extensions
أو باستخدام Hardhat Ignition:
npx hardhat ignition deploy ignition/modules/ARexToken.js --network localhost \
--parameters '{"ARexTokenModule":{"baseURI":"ipfs://<cid>/{id}.json","allowPreHarvestTransfers":true}}'
وبالنسبة إلى شبكة اختبار أو L2، يعرّف hardhat.config.js الشبكات sepolia وpolygon وarbitrum؛ فاضبط عنوان RPC وPRIVATE_KEY في .env (القالب في .env.example). ويُترجَم ARexToken إلى 24,136 بايت مع optimizer.runs = 1، أي أقل من حد EIP-170 البالغ 24,576 بايت.
3 · الأدوار
تمنح initialize(admin, baseURI) كل دور إلى admin، وهذا مناسب لأول عملية نشر وخاطئ في الإنتاج. فامنح كل دور تشغيلي للخدمة أو التوقيع المتعدد الذي ينفّذه:
await token.grantRole(await token.ISSUER_ROLE(), issuer.address);
await token.grantRole(await token.TRANSFORMER_ROLE(), mill.address);
await token.grantRole(await token.STATUS_ROLE(), warehouse.address);
await token.grantRole(await token.ENFORCER_ROLE(), enforcer.address);
| الدور | ما يتيحه | الحامل الموصى به |
|---|---|---|
DEFAULT_ADMIN_ROLE | منح كل دور وإلغاؤه؛ وsetBaseURI | توقيع متعدد للحوكمة خلف تأخير زمني |
UPGRADER_ROLE | استبدال التنفيذ | التأخير الزمني نفسه |
HOOK_ADMIN_ROLE | registerHook، removeHook | التأخير الزمني نفسه (فتصبح تغييرات الخطافات مرئية قبل أن تسري) |
ENFORCER_ROLE | setFrozenTokens، forcedTransfer | توقيع متعدد للإنفاذ (الامتثال والشؤون القانونية) |
PAUSER_ROLE | pause، unpause | مفتاح الاستجابة للحوادث |
ISSUER_ROLE | createBatch | خدمة الإنشاء |
STATUS_ROLE | setBatchStatus، setBatchMetadata | مستودع أو مفتش أو سجل |
TRANSFORMER_ROLE | عمليات التحويل التصنيعي للمعالجة (تغيّر السلعة أو وحدة القياس أو الكمية) | المعالِجون |
ATTEST_ADMIN_ROLE (على ARexAttestations) | setTrustedIssuer، وإبطال أي إفادة | جهة منح الشهادات أو المنصة |
وحين يصبح لكل دور حامله، تتخلى الجهة المنشِرة عمّا لم تعد بحاجة إليه عبر renounceRole(role, deployer).
4 · إنشاء دفعة
الدفعة هي معرّف ERC-1155 واحد. وسجلها على السلسلة صغير وثابت؛ وكل ما هو وصفي يوجد في مستند بيانات وصفية تُلتزَم تجزئته على السلسلة (المخطط: standard/schema/batch-metadata.schema.json).
const metadata = { schemaVersion: "1.0.0", commodity: { scheme: "HS", code: "100199" }, unit: "KGM", quantity: 250000 };
const canonical = JSON.stringify(metadata); // RFC 8785 canonicalization in production
const metadataHash = ethers.keccak256(ethers.toUtf8Bytes(canonical));
const uri = "ipfs://<cid>/batch-1.json"; // wherever you stored `canonical`
const init = {
commodity: commodity("HS", "100199"), // wheat
unit: unit("KGM"),
producedAt: Math.floor(Date.parse("2026-03-18T00:00:00Z") / 1000), // 0 = not yet harvested
expiresAt: 0, // 0 = no expiry
originRef: ref("DDS-2026-AR-000123"), // a reference, never coordinates
metadataHash,
uri,
};
const id = await token.connect(issuer).createBatch.staticCall(init, 250_000, farmer.address, "0x");
await (await token.connect(issuer).createBatch(init, 250_000, farmer.address, "0x")).wait();
تُرجع createBatch.staticCall المعرّف الذي ستسنده المعاملة؛ وتبدأ المعرّفات من 1 وتزداد بمقدار واحد. ويرفض الرمز ما يلي:
| الشرط | الخطأ |
|---|---|
يفتقر المستدعي إلى ISSUER_ROLE | AccessControlUnauthorizedAccount(account, role) |
to هو عنوان الصفر | ARexInvalidRecipient() |
quantity == 0 | ARexInvalidBatch("ZERO_QUANTITY") |
commodity == 0 أو unit == 0 | ARexInvalidBatch("COMMODITY_REQUIRED") / ("UNIT_REQUIRED") |
ضُبط التاريخان كلاهما وexpiresAt <= producedAt | ARexInvalidBatch("EXPIRY_BEFORE_PRODUCTION") |
إيقاف مؤقت، أو مستلم غير مؤهل، أو رفض خطاف للعملية OP_MINT | EnforcedPause()، ERC7943CannotReceive(to)، ARexHookRejected(hook, code) |
أعراف الحقول:
| الحقل | العُرف | مثال |
|---|---|---|
commodity | keccak256("<scheme>:<code>")، والنظام من بين HS أو FAO أو GTIN أو UNSPSC أو نظامك الخاص | keccak256("HS:100199") للقمح |
unit | كود التوصية رقم 20 الصادرة عن UN/CEFACT بصيغة bytes32 | "KGM"، "TNE"، "LTR"، "BX" |
producedAt | ثوانٍ بنظام Unix؛ و0 لدفعة لم تُحصد بعد | بيع آجل، مستحقات محصولية |
expiresAt | ثوانٍ بنظام Unix؛ و0 لعدم انتهاء الصلاحية | منتجات قابلة للتلف |
originRef | تجزئة مرجع إلى أدلة المنشأ؛ وليس إحداثيات أبدًا | مرجع DDS في EUDR، ومعرّف سجل |
metadataHash | keccak256 لمستند البيانات الوصفية القياسي (RFC 8785) | |
uri | مكان وجود المستند؛ أبقِه قصيرًا لأنه يُخزَّن على السلسلة | ipfs://<cid>، وGS1 Digital Link |
5 · قراءة دفعة
const batch = await token.batchOf(id); // reverts ARexBatchNotFound if unknown
await token.batchExists(id); // true / false, never reverts
await token["totalSupply(uint256)"](id); // units in circulation (overloaded name)
await token.parentsOf(id); // [] for an original lot
await token.uri(id); // batch URI, or the base URI if empty
await token.isExpired(id); // reference-implementation helper
الدالة totalSupply متعددة التحميل في ERC-1155 Supply (totalSupply() وtotalSupply(uint256))، ولذلك استدعِها في ethers v6 بحسب التوقيع. وقيمة Batch.status هي 0 (ACTIVE) أو 255 (RETIRED) أو قيمة تحددها جهة الإصدار بينهما.
6 · التحويل
اسأل دائمًا أولًا. تعيد transferCheck تشغيل مسار التحويل بوصفها دالة عرض وتُرجع أول كود رفض؛ وتعني 0x00…00 أن التحويل مسموح. وcanTransfer هي النسخة المنطقية (boolean) من الإجابة نفسها في ERC-7943.
const code = await token.transferCheck(farmer.address, buyer.address, id, 10_000);
if (code !== ethers.ZeroHash) throw new Error(`transfer blocked: ${explain(code)}`);
await token.connect(farmer).safeTransferFrom(farmer.address, buyer.address, id, 10_000, "0x");
ترتيب المسار ثابت، ولذلك يخبرك الكود بأي قاعدة فشلت:
| الترتيب | الفحص | الكود الذي تُرجعه transferCheck | التراجع في المعاملة |
|---|---|---|---|
| 1 | الرمز موقوف مؤقتًا | AREX:PAUSED | EnforcedPause() |
| 2 | المُرسِل مؤهل (كل خطاف ELIGIBILITY) | AREX:SENDER_INELIGIBLE | ERC7943CannotSend(from) |
| 3 | المستلم مؤهل | AREX:RECEIVER_INELIGIBLE | ERC7943CannotReceive(to) |
| 4 | الدفعة موجودة | AREX:BATCH_NOT_FOUND | ARexBatchNotFound(id) |
| 5 | الدفعة غير مسحوبة من التداول | AREX:BATCH_RETIRED | ARexBatchRetired(id) |
| 6 | amount <= balance − frozen | AREX:FROZEN | ERC7943InsufficientUnfrozenBalance(from, id, amount, unfrozen) |
| 7 | كل خطاف CHECK، بترتيب التسجيل | كود الخطاف | ARexHookRejected(hook, code) |
تُقيَّم دالة العرض والمعاملة بالترتيب نفسه، ولذلك يسمّي الكود دائمًا الفحص الذي تفشل فيه المعاملة أولًا.
ثمة تفصيلان مهمان في الممارسة. يُرجَع AREX:FROZEN أيضًا حين لا يحوز المُرسِل ما يكفي من الكميات ببساطة، لأن الفحص يجري على الرصيد غير المجمّد. وتمرّر transferCheck قيمة data فارغة إلى الخطافات؛ ويمكن لخطاف يقرأ بيانات التحويل (مثل تسوية DeliveryHook) أن يجيب بصورة مختلفة في المعاملة الفعلية.
7 · النسب: التقسيم والدمج والمعالجة
transform هي الطريقة الوحيدة لصنع دفعة من دفعات. فهي تحرق المدخلات من from، وتسك المخرجات إلى to، وتسجّل parentsOf(output) = inputIds، وتُصدر BatchTransformed واحدًا يقابل TransformationEvent في EPCIS. وتُرجع transform.staticCall المعرّفات الجديدة.
إعادة تكوين الدفعة تحفظ السلعة ووحدة القياس والكمية الإجمالية، ولا تحتاج إلى دور، بل إلى الملكية أو تفويض ERC-1155 فقط. قسّم دفعة واحدة إلى اثنتين لمشترٍ:
const [lotA, lotB] = await token.connect(farmer).transform.staticCall(
farmer.address, buyer.address, [id], [400], [init, init], [250, 150], ref("SALE-42"));
await token.connect(farmer).transform(
farmer.address, buyer.address, [id], [400], [init, init], [250, 150], ref("SALE-42"));
ادمج دفعتين في دفعة مشتركة واحدة لتعاونية:
const [pooled] = await token.connect(buyer).transform.staticCall(
buyer.address, coop.address, [lotA, lotB], [250, 150], [init], [400], ref("POOL-7"));
await token.connect(buyer).transform(
buyer.address, coop.address, [lotA, lotB], [250, 150], [init], [400], ref("POOL-7"));
المعالجة تغيّر السلعة أو وحدة القياس أو الكمية، ولذلك تحتاج إلى TRANSFORMER_ROLE إضافةً إلى التفويض. وتحوّل المطحنة 1,000 كغ من قمح المزارع إلى 750 كغ من الدقيق:
const now = (await ethers.provider.getBlock("latest")).timestamp;
const flourInit = { ...init, commodity: commodity("HS", "110100"), producedAt: now };
await token.connect(farmer).setApprovalForAll(mill.address, true);
const [flour] = await token.connect(mill).transform.staticCall(
farmer.address, mill.address, [id], [1_000], [flourInit], [750], ref("MILL-RUN-7"));
await token.connect(mill).transform(
farmer.address, mill.address, [id], [1_000], [flourInit], [750], ref("MILL-RUN-7"));
| القاعدة | ما يفرضها |
|---|---|
المستدعي هو from أو معتمَد من from | transform |
| كل مدخل موجود وغير مسحوب من التداول ولديه رصيد غير مجمّد كافٍ | _update |
| إعادة تكوين الدفعة: السلعة ووحدة القياس نفسهما، Σ المدخلات = Σ المخرجات | لا حاجة إلى دور |
| أي شيء آخر | TRANSFORMER_ROLE، وإلا AccessControlUnauthorizedAccount |
| ترى الخطافات كل مرحلة | OP_TRANSFORM للحرق والسك |
النسب الذي يبنيه هذا الدليل:
flowchart LR
B1["#1 قمح 250,000 كغ<br/>مزارع"] -->|"تقسيم · SALE-42"| B2["#2 قمح 250"]
B1 -->|"تقسيم · SALE-42"| B3["#3 قمح 150"]
B2 -->|"دمج · POOL-7"| B4["#4 قمح 400<br/>تعاونية"]
B3 -->|"دمج · POOL-7"| B4
B1 -->|"معالجة · MILL-RUN-7"| B5["#5 دقيق 750<br/>مطحنة"]8 · الحالة والبيانات الوصفية
لا يغيّر الحالة إلا STATUS_ROLE؛ وحيازة الكميات لا تمنح شيئًا. أما القيم 1..254 فلك أن تعرّفها؛ فانشر معناها في مستند البيانات الوصفية (status.table). ويؤدي تحديث البيانات الوصفية إلى استبدال التجزئة وعنوان URI وإصدار BatchMetadataUpdated، ولذلك يظل تاريخ المستندات قابلًا للتحقق.
await token.connect(warehouse).setBatchStatus(flour, 1, ref("IN_STORAGE")); // 1..254 are yours
const labReport = { ...metadata, lab: { moisture: 14.2 } };
await token.connect(warehouse).setBatchMetadata(
flour, ethers.keccak256(ethers.toUtf8Bytes(JSON.stringify(labReport))), "ipfs://<cid>/flour-lab.json");
9 · الخطافات
الخطافات عقود مستقلة يستدعيها الرمز بترتيب التسجيل. وهنا يعمل خطاف الأهلية أولًا، ثم خطاف انتهاء الصلاحية، ثم قاعدة تشترط أن يحتاج البن إلى بيان عناية واجبة وفق EUDR مثبَت بإفادة قبل أن يجوز بيعه.
const eligibility = await (await ethers.getContractFactory("EligibilityHook")).deploy(admin.address);
const expiry = await (await ethers.getContractFactory("ExpiryHook")).deploy(true); // allow pre-harvest
const attestationHook = await (await ethers.getContractFactory("AttestationHook"))
.deploy(admin.address, await attestations.getAddress());
const COFFEE = commodity("HS", "090111");
const EUDR_DDS = ref("AREX:EUDR:DDS");
await attestationHook.setRequirement(COFFEE, EUDR_DDS); // coffee needs a due-diligence statement
await token.registerHook(await eligibility.getAddress()); // evaluated first
await token.registerHook(await expiry.getAddress());
await token.registerHook(await attestationHook.getAddress()); // evaluated last
await token.getHooks(); // the ordered list
| الخطاف | الأعلام | المُنشئ | الضبط بواسطة |
|---|---|---|---|
EligibilityHook | CHECK، ELIGIBILITY | (admin) | setAllowlistEnabled، setAllowlisted، setBlocklisted، setSanctioned |
ExpiryHook | CHECK | (allowPreHarvestTransfers) | — (يقرأ سجل الدفعة) |
AttestationHook | CHECK | (owner, attestationRegistry) | setRequirement(commodity, topic) |
PerishabilityHook | CHECK | (admin) | setCommodityDecay، setBatchDecay |
SeasonalHook | CHECK | (admin) | addWindow(commodity, op, start, end)، clearWindows |
DeliveryHook | CHECK، EXECUTED | (admin) | schedule، pickUp، markInTransit، recordReading، confirmDelivered، dispute، resolve |
TraceabilityHook | EXECUTED | (admin, persist) | recordEvent (RECORDER_ROLE) |
يرفض التسجيل حسابات EOA والعقود التي لا تدعم IARexHook عبر ERC-165 والخطافات التي تكون أعلامها 0. وضع الخطافات الرخيصة التي ترفض كثيرًا أولًا: فالرفض يوقف المسار. وللاطلاع على كتابة خطافك الخاص: docs/HOOK-DEVELOPMENT.md.
10 · الإفادات
الإفادة هي (batch, topic) → (issuer, reference, issuedAt, expiresAt, revoked)، ولا تكون سارية إلا ما دامت جهة إصدارها موثوقة للموضوع. ومع وجود المتطلب الوارد في الخطوة 9، لا تستطيع دفعة البن التحرك حتى يُثبَت البيان بإفادة:
const coffee = await token.connect(issuer).createBatch.staticCall({ ...init, commodity: COFFEE }, 60, farmer.address, "0x");
await token.connect(issuer).createBatch({ ...init, commodity: COFFEE }, 60, farmer.address, "0x");
explain(await token.transferCheck(farmer.address, buyer.address, coffee, 1)); // "ATTESTATION_MISSING"
await attestations.setTrustedIssuer(EUDR_DDS, certifier.address, true); // ATTEST_ADMIN_ROLE
await attestations.connect(certifier).attest(coffee, EUDR_DDS, ref("DDS-2026-AR-000123"), 0);
await attestations.isAttested(coffee, EUDR_DDS); // true
explain(await token.transferCheck(farmer.address, buyer.address, coffee, 1)); // "OK"
تكون isAttested مساوية false عندما لا توجد إفادة، أو عندما تكون مُبطَلة، أو عندما يمر expiresAt، أو عندما لا تعود جهة الإصدار موثوقة للموضوع. ويجوز لجهة الإصدار أو لـ ATTEST_ADMIN_ROLE الإبطال. والمواضيع قيم bytes32 حرة؛ فاستخدم keccak256("<NAMESPACE>:<NAME>")، مثل AREX:EUDR:DDS أو AREX:CERT:ORGANIC_EU.
11 · العقوبات والأهلية
يجيب EligibilityHook عن canSend وcanReceive للرمز. وتسجّل العقوبة مصدرها وانتهاء صلاحية اختياريًا، بحيث يمكن أن ينقضي أثر تحديث القائمة تلقائيًا.
await eligibility.setSanctioned(buyer.address, true, ethers.encodeBytes32String("OFAC-SDN"), 0);
explain(await token.transferCheck(farmer.address, buyer.address, id, 1)); // "RECEIVER_INELIGIBLE"
await eligibility.setSanctioned(buyer.address, false, ethers.encodeBytes32String("OFAC-SDN"), 0);
12 · الإنفاذ
التجميد والتحويلات القسرية هما سطح الإنفاذ في ERC-7943، لحقوق الامتياز والأوامر القضائية وعمليات الحجز. ويجوز أن يتجاوز التجميد الرصيد الحالي، وعندئذ يشمل الإيداعات المستقبلية أيضًا. ويتجاوز التحويل القسري الأهلية وفحص المجمّد، ويعمل أثناء إيقاف الرمز مؤقتًا، ويخفض المقدار المجمّد إلى ما تبقى، ويشغّل الخطافات مع OP_FORCED_TRANSFER.
await token.connect(enforcer).setFrozenTokens(buyer.address, id, 6_000); // may exceed the balance
await token.getFrozenTokens(buyer.address, id); // 6_000
explain(await token.transferCheck(buyer.address, farmer.address, id, 5_000)); // "FROZEN"
await token.pause(); // PAUSER_ROLE
await token.connect(enforcer).forcedTransfer(buyer.address, custodian.address, id, 6_000); // works while paused
await token.unpause();
وثّق الإجراء الذي يعمل بموجبه ENFORCER_ROLE (أمر قضائي، أو اتفاقية رهن، أو إخطار عقوبات) وأبقِ الدور خلف توقيع متعدد. ويُصدر كل إجراء Frozen أو ForcedTransfer.
13 · السحب من التداول
RETIRED (255) حالة نهائية: لا تحويل ولا سك ولا تحويل تصنيعي، ولا طريق للعودة. ويظل الحرق ممكنًا كي تخرج السلع المسلَّمة أو المتلَفة من المعروض.
await token.connect(warehouse).setBatchStatus(flour, 255, ref("DELIVERED")); // RETIRED is terminal
explain(await token.transferCheck(mill.address, buyer.address, flour, 1)); // "BATCH_RETIRED"
await token.connect(mill).burn(mill.address, flour, 750); // burning still works
14 · الفهرسة
الرسم البياني للنسب هو قائمة أحداث BatchTransformed؛ ولا حاجة إلى إعادة بناء أي شيء من التحويلات الفردية.
const transforms = await token.queryFilter(token.filters.BatchTransformed());
const edges = transforms.flatMap((e) =>
e.args.inputIds.flatMap((input) => e.args.outputIds.map((output) => [input, output, e.args.processRef])));
// edges: [1 → 2], [1 → 3], [2 → 4], [3 → 4], [1 → 5]
| الحدث | يُصدره | الاستخدام |
|---|---|---|
BatchCreated(id, issuer, commodity, unit, quantity, to) | createBatch | دفعة جديدة؛ وObjectEvent من نوع ADD في EPCIS |
BatchTransformed(operator, inputIds, inputAmounts, outputIds, outputAmounts, processRef) | transform | حافة في رسم النسب؛ وTransformationEvent في EPCIS |
BatchStatusChanged(id, previousStatus, newStatus, reason) | setBatchStatus | دورة الحياة |
BatchMetadataUpdated(id, metadataHash, uri) | setBatchMetadata | تاريخ المستندات |
TransferSingle، TransferBatch | كل تغيير في الرصيد | الحيازات؛ وObjectEvent من نوع OBSERVE في EPCIS |
Frozen(account, tokenId, amount)، ForcedTransfer(from, to, tokenId, amount) | الإنفاذ | حقوق الامتياز، وعمليات الحجز |
HookRegistered(hook, flags, position)، HookRemoved(hook) | مسؤول الخطافات | تغييرات القواعد |
Attested، AttestationRevoked، TrustedIssuerUpdated | ARexAttestations | الأدلة |
15 · الترقية
التخزين محدد بفضاء أسماء وفق ERC-7201 (erc7201:arex.storage.token)؛ فأضف الحقول إلى TokenStorage بالإلحاق ولا تعِد ترتيبها ولا تحذفها أبدًا. وتحقق قبل كل ترقية؛ فالإضافة ترفض التخطيطات التي من شأنها إفساد التخزين.
await upgrades.validateUpgrade(await token.getAddress(), ARexToken, { kind: "uups" });
await upgrades.upgradeProxy(await token.getAddress(), ARexToken); // UPGRADER_ROLE
وفي الإنتاج يكون UPGRADER_ROLE خلف توقيع متعدد وتأخير زمني، وتُجرَّب الترقية أولًا على تفرّع (fork) من السلسلة المستهدفة.
اختبار تكاملك
npx hardhat test test/standard/ImplementationGuide.test.js # this chapter, executed
npx hardhat test test/standard/ARexToken.test.js # token behaviour and conformance
npx hardhat run scripts/smoke-standard.js # narrated walkthrough of one lot
npx hardhat run scripts/gas-report-standard.js # gas with 0, 1 and 2 hooks, and per hook
يصدّر test/standard/helpers.js الدالتين deployStack() وbatchInit() والثوابت OP وHOOK وSTATUS وCODES والدالة supply()، لاختباراتك الخاصة.
الانتقال إلى الإنتاج: قائمة التحقق
- بناء قابل لإعادة الإنتاج (
npm ci)، وتكامل مستمر ناجح، ومعرّفات واجهات مطابقة للمواصفة. - إعادة توزيع الأدوار كما في الخطوة 3؛ والتخلي عن أدوار الجهة المنشِرة.
- تدقيق الخطافات، وتوثيق الترتيب، ونشر
getHooks(). - نشر إصدار مخطط البيانات الوصفية وجدول الحالات؛ وخلو
originRefوالبيانات المقيَّدة من البيانات الشخصية. - الإشارة إلى الأدوات القانونية في البيانات الوصفية؛ وحسم تصنيف العرض مع مستشار قانوني.
- تدقيق من طرف ثالث للرمز وسجل الإفادات وكل خطاف مسجَّل (الحزمة:
docs/AUDIT-READINESS.md). - تجربة الترقية على تفرّع (fork) مع
validateUpgrade.