Skip to main contentA-REX Core

الورقة البيضاء · 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_ROLEregisterHook، removeHookالتأخير الزمني نفسه (فتصبح تغييرات الخطافات مرئية قبل أن تسري)
ENFORCER_ROLEsetFrozenTokens، forcedTransferتوقيع متعدد للإنفاذ (الامتثال والشؤون القانونية)
PAUSER_ROLEpause، unpauseمفتاح الاستجابة للحوادث
ISSUER_ROLEcreateBatchخدمة الإنشاء
STATUS_ROLEsetBatchStatus، 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_ROLEAccessControlUnauthorizedAccount(account, role)
to هو عنوان الصفرARexInvalidRecipient()
quantity == 0ARexInvalidBatch("ZERO_QUANTITY")
commodity == 0 أو unit == 0ARexInvalidBatch("COMMODITY_REQUIRED") / ("UNIT_REQUIRED")
ضُبط التاريخان كلاهما وexpiresAt <= producedAtARexInvalidBatch("EXPIRY_BEFORE_PRODUCTION")
إيقاف مؤقت، أو مستلم غير مؤهل، أو رفض خطاف للعملية OP_MINTEnforcedPause()، ERC7943CannotReceive(to)، ARexHookRejected(hook, code)

أعراف الحقول:

الحقلالعُرفمثال
commoditykeccak256("<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، ومعرّف سجل
metadataHashkeccak256 لمستند البيانات الوصفية القياسي (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:PAUSEDEnforcedPause()
2المُرسِل مؤهل (كل خطاف ELIGIBILITY)AREX:SENDER_INELIGIBLEERC7943CannotSend(from)
3المستلم مؤهلAREX:RECEIVER_INELIGIBLEERC7943CannotReceive(to)
4الدفعة موجودةAREX:BATCH_NOT_FOUNDARexBatchNotFound(id)
5الدفعة غير مسحوبة من التداولAREX:BATCH_RETIREDARexBatchRetired(id)
6amount <= balance − frozenAREX:FROZENERC7943InsufficientUnfrozenBalance(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 أو معتمَد من fromtransform
كل مدخل موجود وغير مسحوب من التداول ولديه رصيد غير مجمّد كافٍ_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
الخطافالأعلامالمُنشئالضبط بواسطة
EligibilityHookCHECK، ELIGIBILITY(admin)setAllowlistEnabled، setAllowlisted، setBlocklisted، setSanctioned
ExpiryHookCHECK(allowPreHarvestTransfers)— (يقرأ سجل الدفعة)
AttestationHookCHECK(owner, attestationRegistry)setRequirement(commodity, topic)
PerishabilityHookCHECK(admin)setCommodityDecay، setBatchDecay
SeasonalHookCHECK(admin)addWindow(commodity, op, start, end)، clearWindows
DeliveryHookCHECK، EXECUTED(admin)schedule، pickUp، markInTransit، recordReading، confirmDelivered، dispute، resolve
TraceabilityHookEXECUTED(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، TrustedIssuerUpdatedARexAttestationsالأدلة

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()، لاختباراتك الخاصة.

الانتقال إلى الإنتاج: قائمة التحقق

  1. بناء قابل لإعادة الإنتاج (npm ci)، وتكامل مستمر ناجح، ومعرّفات واجهات مطابقة للمواصفة.
  2. إعادة توزيع الأدوار كما في الخطوة 3؛ والتخلي عن أدوار الجهة المنشِرة.
  3. تدقيق الخطافات، وتوثيق الترتيب، ونشر getHooks().
  4. نشر إصدار مخطط البيانات الوصفية وجدول الحالات؛ وخلو originRef والبيانات المقيَّدة من البيانات الشخصية.
  5. الإشارة إلى الأدوات القانونية في البيانات الوصفية؛ وحسم تصنيف العرض مع مستشار قانوني.
  6. تدقيق من طرف ثالث للرمز وسجل الإفادات وكل خطاف مسجَّل (الحزمة: docs/AUDIT-READINESS.md).
  7. تجربة الترقية على تفرّع (fork) مع validateUpgrade.