@lawrenceliang-btc/atel-sdk 1.1.43 → 1.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/bin/atel.mjs +710 -36
- package/dist/bitrefill/client.d.ts +6 -0
- package/dist/bitrefill/client.js +45 -0
- package/dist/bitrefill/index.d.ts +32 -0
- package/dist/bitrefill/index.js +88 -0
- package/dist/bitrefill/types.d.ts +62 -0
- package/dist/bitrefill/types.js +30 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/package.json +2 -2
- package/skill/atel-agent/SKILL.md +194 -8
- package/skill/atel-agent/setup.sh +23 -0
- package/skill/references/executor.md +13 -13
- package/skill/references/quickstart.md +16 -7
- package/skill/references/workflows.md +20 -9
- package/skill/SKILL.md +0 -624
package/bin/atel.mjs
CHANGED
|
@@ -603,6 +603,13 @@ async function pushTradeNotification(eventType, payload, body) {
|
|
|
603
603
|
const { targets, enabled } = resolveNotifyTargets(orderId);
|
|
604
604
|
if (enabled.length === 0) return;
|
|
605
605
|
|
|
606
|
+
const chainLabel = (p) => {
|
|
607
|
+
const c = p?.chain || body?.chain || '';
|
|
608
|
+
// NOTE: fast-coop 分支为与合作方集成预留,合作推进中未开放正式路径,保留仅为避免订单链路解析失败
|
|
609
|
+
if (c === 'fast-coop') return ' (Fast)';
|
|
610
|
+
if (c === 'bsc') return ' (BSC)';
|
|
611
|
+
return '';
|
|
612
|
+
};
|
|
606
613
|
const templates = {
|
|
607
614
|
'order_created': (p) => `📥 收到新订单
|
|
608
615
|
订单: ${p.orderId || body?.orderId || '?'}
|
|
@@ -2606,8 +2613,19 @@ async function promptInput(question) {
|
|
|
2606
2613
|
// ─── Anchor Configuration ────────────────────────────────────────
|
|
2607
2614
|
|
|
2608
2615
|
async function configureAnchor() {
|
|
2609
|
-
console.log('\n🔗 Configure On-Chain Anchoring\n');
|
|
2610
|
-
|
|
2616
|
+
console.log('\n🔗 Configure On-Chain Anchoring (legacy V1 mode)\n');
|
|
2617
|
+
console.log('⚠️ In V2 the ATEL Platform anchors on behalf of agents using its own');
|
|
2618
|
+
console.log(' executor wallets — you do NOT need this for paid orders.');
|
|
2619
|
+
console.log(' Your smart wallet addresses (derived from your DID) already receive');
|
|
2620
|
+
console.log(' USDC and the platform pays gas for you. Only continue if you are');
|
|
2621
|
+
console.log(' running a legacy V1 self-anchoring agent.');
|
|
2622
|
+
console.log('');
|
|
2623
|
+
const go = await promptYesNo('Continue anyway?');
|
|
2624
|
+
if (!go) {
|
|
2625
|
+
console.log('Aborted. Your identity is unchanged.');
|
|
2626
|
+
return;
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2611
2629
|
// 1. Select chain
|
|
2612
2630
|
const chain = await promptChoice(
|
|
2613
2631
|
'Select blockchain for anchoring:',
|
|
@@ -2688,7 +2706,7 @@ async function cmdInit(agentId) {
|
|
|
2688
2706
|
const identity = new AgentIdentity({ agent_id: name });
|
|
2689
2707
|
saveIdentity(identity);
|
|
2690
2708
|
savePolicy(DEFAULT_POLICY);
|
|
2691
|
-
|
|
2709
|
+
|
|
2692
2710
|
// Create default agent-context.md for built-in executor
|
|
2693
2711
|
const ctxFile = resolve(ATEL_DIR, 'agent-context.md');
|
|
2694
2712
|
if (!existsSync(ctxFile)) {
|
|
@@ -2720,45 +2738,81 @@ You are an ATEL agent (${name}) processing tasks from other agents via the ATEL
|
|
|
2720
2738
|
}
|
|
2721
2739
|
}
|
|
2722
2740
|
|
|
2723
|
-
|
|
2724
|
-
status: 'created',
|
|
2725
|
-
agent_id: identity.agent_id,
|
|
2726
|
-
did: identity.did,
|
|
2741
|
+
const output = {
|
|
2742
|
+
status: 'created',
|
|
2743
|
+
agent_id: identity.agent_id,
|
|
2744
|
+
did: identity.did,
|
|
2727
2745
|
policy: POLICY_FILE,
|
|
2728
2746
|
anchor: anchorConfigured ? 'configured' : 'disabled',
|
|
2729
|
-
|
|
2730
|
-
|
|
2747
|
+
nextSteps: [
|
|
2748
|
+
'atel start — bring the agent online (network + auto-register)',
|
|
2749
|
+
`atel register ${name} "<capability1>,<capability2>" — advertise what you can do`,
|
|
2750
|
+
'atel info — show your DID, wallets, and policy',
|
|
2751
|
+
'atel balance — check USDC balance on Base/BSC smart wallets',
|
|
2752
|
+
],
|
|
2753
|
+
note: 'Paid orders work out of the box in V2 — no on-chain private key needed. The Platform anchors on your behalf using its own executor wallets. If you specifically need legacy self-anchoring, run: atel anchor config',
|
|
2754
|
+
};
|
|
2755
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2731
2756
|
|
|
2732
2757
|
try {
|
|
2733
|
-
const
|
|
2734
|
-
if (
|
|
2735
|
-
const
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
join(home, '.openclaw', 'workspace', 'skills', 'atel-agent'),
|
|
2739
|
-
];
|
|
2740
|
-
let installed = false;
|
|
2741
|
-
for (const dir of candidates) {
|
|
2742
|
-
if (existsSync(dirname(dir))) {
|
|
2743
|
-
mkdirSync(dir, { recursive: true });
|
|
2744
|
-
copyFileSync(sdkSkillPath, join(dir, 'SKILL.md'));
|
|
2745
|
-
console.log(`\n✅ ATEL Skill auto-installed to ${dir}`);
|
|
2746
|
-
console.log(' Your AI agent will automatically know how to use ATEL.');
|
|
2747
|
-
console.log('');
|
|
2748
|
-
console.log('📌 Tell your agent this message to get started:');
|
|
2749
|
-
console.log(' "Read the atel-agent skill at ~/.openclaw/skills/atel-agent/SKILL.md carefully, then help me set up ATEL and start earning."');
|
|
2750
|
-
installed = true;
|
|
2751
|
-
break;
|
|
2752
|
-
}
|
|
2753
|
-
}
|
|
2754
|
-
if (!installed) {
|
|
2755
|
-
console.log('\n📋 To teach your AI agent about ATEL, send it this message:');
|
|
2756
|
-
console.log(` "Read ${sdkSkillPath} carefully, then help me set up ATEL and start earning."`);
|
|
2757
|
-
}
|
|
2758
|
+
const installed = syncAtelSkillToOpenClaw({ verbose: true });
|
|
2759
|
+
if (!installed.length) {
|
|
2760
|
+
const sdkSkillPath = resolveSdkSkillPath();
|
|
2761
|
+
console.log('\n📋 To teach your AI agent about ATEL, send it this message:');
|
|
2762
|
+
console.log(` "Read ${sdkSkillPath} carefully, then help me set up ATEL and start earning."`);
|
|
2758
2763
|
}
|
|
2759
2764
|
} catch (e) { /* skill install is best-effort */ }
|
|
2760
2765
|
}
|
|
2761
2766
|
|
|
2767
|
+
// Path to the SKILL.md shipped inside the currently-installed SDK package.
|
|
2768
|
+
// Used both at `atel init` time and on every `atel start` to keep openclaw's
|
|
2769
|
+
// workspace copy in sync with the latest published skill content.
|
|
2770
|
+
function resolveSdkSkillPath() {
|
|
2771
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), '..', 'skill', 'atel-agent', 'SKILL.md');
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
// Sync the SDK's SKILL.md into every openclaw skills dir that already hosts
|
|
2775
|
+
// an atel-agent skill. Written to fix a subtle version-drift bug: `npm i -g`
|
|
2776
|
+
// upgrades the SDK package on disk, but openclaw's session `skillsSnapshot`
|
|
2777
|
+
// reads from `~/.openclaw/workspace/skills/atel-agent/SKILL.md`, which is a
|
|
2778
|
+
// physical copy made at init time and never refreshed. After an upgrade the
|
|
2779
|
+
// agent keeps seeing stale skill content indefinitely. See incident
|
|
2780
|
+
// 2026-04-09, order ord-4c12a03d-ea8, where the workspace copy was ~12 days
|
|
2781
|
+
// older than the npm copy and missing the "--desc required" guidance.
|
|
2782
|
+
//
|
|
2783
|
+
// We intentionally sync to *every* candidate dir where an `atel-agent` sibling
|
|
2784
|
+
// is present (not just the first one), because different openclaw versions
|
|
2785
|
+
// read from different roots and we want all of them to end up coherent.
|
|
2786
|
+
// Returns the list of destinations actually written.
|
|
2787
|
+
function syncAtelSkillToOpenClaw({ verbose = false } = {}) {
|
|
2788
|
+
const sdkSkillPath = resolveSdkSkillPath();
|
|
2789
|
+
if (!existsSync(sdkSkillPath)) return [];
|
|
2790
|
+
const home = process.env.HOME || '';
|
|
2791
|
+
// Candidate parent dirs, in the order we want to probe. We sync to any
|
|
2792
|
+
// that exist, creating the leaf `atel-agent/` dir if its parent is present.
|
|
2793
|
+
const parents = [
|
|
2794
|
+
join(home, '.openclaw', 'workspace', 'skills'),
|
|
2795
|
+
join(home, '.openclaw', 'skills'),
|
|
2796
|
+
];
|
|
2797
|
+
const written = [];
|
|
2798
|
+
for (const parent of parents) {
|
|
2799
|
+
if (!existsSync(parent)) continue;
|
|
2800
|
+
const dir = join(parent, 'atel-agent');
|
|
2801
|
+
try {
|
|
2802
|
+
mkdirSync(dir, { recursive: true });
|
|
2803
|
+
const dest = join(dir, 'SKILL.md');
|
|
2804
|
+
copyFileSync(sdkSkillPath, dest);
|
|
2805
|
+
written.push(dest);
|
|
2806
|
+
if (verbose) {
|
|
2807
|
+
console.log(`✅ ATEL skill synced → ${dest}`);
|
|
2808
|
+
}
|
|
2809
|
+
} catch (e) {
|
|
2810
|
+
if (verbose) console.error(` (skipped ${dir}: ${e.message})`);
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
return written;
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2762
2816
|
async function cmdAnchor(subcommand) {
|
|
2763
2817
|
if (subcommand === 'config') {
|
|
2764
2818
|
await configureAnchor();
|
|
@@ -3102,6 +3156,21 @@ async function cmdStart(port) {
|
|
|
3102
3156
|
log({ event: 'openclaw_lock_cleanup_error', error: e.message });
|
|
3103
3157
|
}
|
|
3104
3158
|
|
|
3159
|
+
// Refresh workspace SKILL.md from the currently-installed SDK package.
|
|
3160
|
+
// Without this, `npm i -g` upgrades leave a stale copy at
|
|
3161
|
+
// ~/.openclaw/workspace/skills/atel-agent/SKILL.md that openclaw keeps
|
|
3162
|
+
// reading — the documented behaviour of the upgrade then silently lags
|
|
3163
|
+
// behind the code. Fixes incident 2026-04-09 where a ~12-day-old workspace
|
|
3164
|
+
// skill copy was missing "--desc is required" guidance.
|
|
3165
|
+
try {
|
|
3166
|
+
const synced = syncAtelSkillToOpenClaw();
|
|
3167
|
+
if (synced.length > 0) {
|
|
3168
|
+
log({ event: 'atel_skill_synced', count: synced.length, destinations: synced });
|
|
3169
|
+
}
|
|
3170
|
+
} catch (e) {
|
|
3171
|
+
log({ event: 'atel_skill_sync_error', error: e.message });
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3105
3174
|
// Initialize Ollama only if explicitly enabled (optional local AI audit)
|
|
3106
3175
|
if (process.env.ATEL_OLLAMA_ENABLED === 'true') {
|
|
3107
3176
|
await initializeOllama().catch(err => {
|
|
@@ -4388,6 +4457,33 @@ Advance the current milestone strictly based on these approved results. Do not i
|
|
|
4388
4457
|
dedupeKey: body.dedupeKey,
|
|
4389
4458
|
});
|
|
4390
4459
|
|
|
4460
|
+
// ⚠️ Fast-coop 待合作推进(2026-04):此分支代码为与合作方集成预留,
|
|
4461
|
+
// Platform 端 fast-coop 路径尚未全部打通。本块只在 chain === 'fast-coop' 时
|
|
4462
|
+
// 触发,正式订单不会走到这里,合作方就绪后再启用。
|
|
4463
|
+
//
|
|
4464
|
+
// ── Fast-coop: auto-submit deliverable when all milestones verified ──
|
|
4465
|
+
// When executor receives the final milestone_verified for a fast-coop order,
|
|
4466
|
+
// sign and submit Escrow::Submit on Fast staging so the Platform can Complete.
|
|
4467
|
+
if (event === 'milestone_verified' && payload.allComplete === true) {
|
|
4468
|
+
const fastOrderId = body.orderId || payload.orderId || '';
|
|
4469
|
+
if (fastOrderId) {
|
|
4470
|
+
// Payload may not include chain — fetch order to check
|
|
4471
|
+
try {
|
|
4472
|
+
const _resp = await fetch(`${PLATFORM_URL}/trade/v1/order/${fastOrderId}`, { signal: AbortSignal.timeout(10000) });
|
|
4473
|
+
const orderInfo = _resp.ok ? await _resp.json() : null;
|
|
4474
|
+
const orderChain = orderInfo?.chain || orderInfo?.Chain || '';
|
|
4475
|
+
if (orderChain === 'fast-coop') {
|
|
4476
|
+
log({ event: 'fast_submit_triggered', orderId: fastOrderId, chain: orderChain });
|
|
4477
|
+
submitFastDeliverable(fastOrderId, id).catch(e => {
|
|
4478
|
+
log({ event: 'fast_submit_error', orderId: fastOrderId, error: e.message });
|
|
4479
|
+
});
|
|
4480
|
+
}
|
|
4481
|
+
} catch (e) {
|
|
4482
|
+
log({ event: 'fast_submit_check_error', orderId: fastOrderId, error: e.message });
|
|
4483
|
+
}
|
|
4484
|
+
}
|
|
4485
|
+
}
|
|
4486
|
+
|
|
4391
4487
|
const dedupeKey = body.dedupeKey || `${event}:${body.orderId || payload.orderId || ''}`;
|
|
4392
4488
|
const orderIdForCwd = body.orderId || payload.orderId || '';
|
|
4393
4489
|
let workspace = getOrderWorkspace(orderIdForCwd, {
|
|
@@ -6825,11 +6921,19 @@ async function cmdTransactions() {
|
|
|
6825
6921
|
// ─── Trade Task: High-level one-shot command ────────────────────
|
|
6826
6922
|
async function cmdTradeTask(capability, description) {
|
|
6827
6923
|
if (!capability) { console.error('Usage: atel trade-task <capability> <description> [--budget N] [--executor DID] [--timeout 300]'); process.exit(1); }
|
|
6924
|
+
if (!description || !description.trim()) {
|
|
6925
|
+
console.error('Error: <description> is required and must contain the full task description.');
|
|
6926
|
+
console.error(' The executor can only understand what to do via this field.');
|
|
6927
|
+
console.error(' Pass the user\'s original task text verbatim, e.g.:');
|
|
6928
|
+
console.error(' atel trade-task <capability> "<full user message>" --budget 5');
|
|
6929
|
+
console.error(' Do NOT summarize, translate, or shorten the user\'s request.');
|
|
6930
|
+
process.exit(2);
|
|
6931
|
+
}
|
|
6828
6932
|
const id = requireIdentity();
|
|
6829
6933
|
const budget = parseFloat(rawArgs.find((a, i) => rawArgs[i-1] === '--budget') || '0');
|
|
6830
6934
|
const executorArg = rawArgs.find((a, i) => rawArgs[i-1] === '--executor') || '';
|
|
6831
6935
|
const timeout = parseInt(rawArgs.find((a, i) => rawArgs[i-1] === '--timeout') || '300') * 1000;
|
|
6832
|
-
const desc = description
|
|
6936
|
+
const desc = description;
|
|
6833
6937
|
|
|
6834
6938
|
// Step 1: Find executor
|
|
6835
6939
|
let executorDid = executorArg;
|
|
@@ -6851,6 +6955,7 @@ async function cmdTradeTask(capability, description) {
|
|
|
6851
6955
|
console.error(`[trade-task] Creating order: ${capability}, budget: $${budget}...`);
|
|
6852
6956
|
const orderData = await signedFetch('POST', '/trade/v1/order', {
|
|
6853
6957
|
executorDid, capabilityType: capability, priceAmount: budget, priceCurrency: 'USD', pricingModel: 'per_task',
|
|
6958
|
+
description: desc,
|
|
6854
6959
|
});
|
|
6855
6960
|
const orderId = orderData.orderId;
|
|
6856
6961
|
console.error(`[trade-task] Order created: ${orderId}`);
|
|
@@ -6898,12 +7003,27 @@ async function cmdTradeTask(capability, description) {
|
|
|
6898
7003
|
}
|
|
6899
7004
|
|
|
6900
7005
|
async function cmdOrder(executorDid, capType, price) {
|
|
6901
|
-
if (!executorDid || !capType || !price) { console.error('Usage: atel order <executorDid> <capabilityType> <price>
|
|
7006
|
+
if (!executorDid || !capType || !price) { console.error('Usage: atel order <executorDid> <capabilityType> <price> --desc "task description"'); process.exit(1); }
|
|
6902
7007
|
// Resolve @alias to full DID
|
|
6903
7008
|
if (executorDid.startsWith('@')) {
|
|
6904
7009
|
try { executorDid = resolveDID(executorDid); } catch (e) { console.error(e.message); process.exit(1); }
|
|
6905
7010
|
}
|
|
6906
7011
|
const description = rawArgs.find((a, i) => rawArgs[i-1] === '--desc') || '';
|
|
7012
|
+
// --desc is MANDATORY: the executor can only see the task via this field.
|
|
7013
|
+
// An empty description would make the executor work from its own imagination
|
|
7014
|
+
// (see production incident 2026-04-09, order ord-4c12a03d-ea8 where the
|
|
7015
|
+
// executor generated an off-topic "system validation" milestone because
|
|
7016
|
+
// orderDescription arrived empty). We fail loudly here so the caller
|
|
7017
|
+
// (human or ReAct agent loop) gets an actionable error and can retry
|
|
7018
|
+
// with the real task text from the user's message.
|
|
7019
|
+
if (!description || !description.trim()) {
|
|
7020
|
+
console.error('Error: --desc is required and must contain the full task description.');
|
|
7021
|
+
console.error(' The executor can only understand what to do via --desc.');
|
|
7022
|
+
console.error(' Pass the user\'s original task text verbatim, e.g.:');
|
|
7023
|
+
console.error(' atel order <executorDid> <capability> <price> --desc "<full user message>"');
|
|
7024
|
+
console.error(' Do NOT summarize, translate, or shorten the user\'s request.');
|
|
7025
|
+
process.exit(2);
|
|
7026
|
+
}
|
|
6907
7027
|
const chainArg = rawArgs.find((a, i) => rawArgs[i-1] === '--chain') || '';
|
|
6908
7028
|
const id = requireIdentity();
|
|
6909
7029
|
|
|
@@ -9322,6 +9442,134 @@ async function cmdAliasRemove(args) {
|
|
|
9322
9442
|
|
|
9323
9443
|
// ─── Main ────────────────────────────────────────────────────────
|
|
9324
9444
|
|
|
9445
|
+
// ⚠️ Fast-coop 待合作推进(2026-04):整个函数为与合作方集成预留。
|
|
9446
|
+
// 只在订单 chain === 'fast-coop' 时被调用,Platform 端 fast-coop 路径
|
|
9447
|
+
// 尚未全部打通,合作方就绪后可直接启用,届时再做联调验证。
|
|
9448
|
+
//
|
|
9449
|
+
// ── Fast-coop: Executor auto-submit deliverable to Fast escrow ──────
|
|
9450
|
+
// Called when executor receives milestone_verified with allComplete=true
|
|
9451
|
+
// for a fast-coop order. Signs Escrow::Submit with the agent's DID
|
|
9452
|
+
// private key (= Fast Ed25519 account key).
|
|
9453
|
+
async function submitFastDeliverable(orderId, identity) {
|
|
9454
|
+
try {
|
|
9455
|
+
// 1. Get order info
|
|
9456
|
+
const orderResp = await fetch(`${PLATFORM_URL}/trade/v1/order/${orderId}`, { signal: AbortSignal.timeout(10000) });
|
|
9457
|
+
if (!orderResp.ok) throw new Error(`order fetch failed: ${orderResp.status}`);
|
|
9458
|
+
const orderInfo = await orderResp.json();
|
|
9459
|
+
const chain = orderInfo.chain || orderInfo.Chain || '';
|
|
9460
|
+
if (chain !== 'fast-coop') return;
|
|
9461
|
+
|
|
9462
|
+
// 2. Find escrow job_id from chain-records
|
|
9463
|
+
const recordsResp = await fetch(`${PLATFORM_URL}/trade/v1/order/${orderId}`, {
|
|
9464
|
+
signal: AbortSignal.timeout(10000),
|
|
9465
|
+
});
|
|
9466
|
+
// The job_id is stored in the on_chain_records by the Platform.
|
|
9467
|
+
// We need to get it from the order or from the chain-records endpoint.
|
|
9468
|
+
// For now, compute it the same way Platform does: keccak256(orderId)
|
|
9469
|
+
const { createHash } = await import('node:crypto');
|
|
9470
|
+
|
|
9471
|
+
// 3. Get the agent's Fast account info
|
|
9472
|
+
const FAST_RPC = process.env.ATEL_FASTCOOP_RPC_URL || 'https://staging.api.fast.xyz/proxy-rest';
|
|
9473
|
+
const pubKeyHex = Buffer.from(identity.publicKey).toString('hex');
|
|
9474
|
+
|
|
9475
|
+
const accountResp = await fetch(`${FAST_RPC}/v1/accounts/${pubKeyHex}`, {
|
|
9476
|
+
signal: AbortSignal.timeout(10000),
|
|
9477
|
+
});
|
|
9478
|
+
if (!accountResp.ok) throw new Error(`Fast account query failed: ${accountResp.status}`);
|
|
9479
|
+
const accountData = (await accountResp.json()).data;
|
|
9480
|
+
const nonce = accountData.next_nonce;
|
|
9481
|
+
|
|
9482
|
+
// 4. Find escrow job for this order (query by provider)
|
|
9483
|
+
const jobsResp = await fetch(`${FAST_RPC}/v1/escrow-jobs?provider=${pubKeyHex}&status=Funded`, {
|
|
9484
|
+
signal: AbortSignal.timeout(10000),
|
|
9485
|
+
});
|
|
9486
|
+
if (!jobsResp.ok) throw new Error(`escrow jobs query failed: ${jobsResp.status}`);
|
|
9487
|
+
const jobs = (await jobsResp.json()).data || [];
|
|
9488
|
+
const job = jobs.find(j => j.description && j.description.includes(orderId));
|
|
9489
|
+
if (!job) {
|
|
9490
|
+
log({ event: 'fast_submit_no_job', orderId, note: 'no Funded escrow job found for this order' });
|
|
9491
|
+
return;
|
|
9492
|
+
}
|
|
9493
|
+
|
|
9494
|
+
// 5. Build Escrow::Submit transaction
|
|
9495
|
+
// deliverable = keccak256(orderId) as 32-byte hash
|
|
9496
|
+
const deliverableHash = createHash('sha3-256').update(orderId).digest('hex');
|
|
9497
|
+
|
|
9498
|
+
// BCS encode: Operation::Escrow(12) → Escrow::Submit(2) → jobId(32) + deliverable(32)
|
|
9499
|
+
const jobIdBytes = Buffer.from(job.job_id, 'hex');
|
|
9500
|
+
const deliverableBytes = Buffer.from(deliverableHash, 'hex');
|
|
9501
|
+
|
|
9502
|
+
// Build minimal BCS by hand (matching Go implementation):
|
|
9503
|
+
// ULEB128(12) = 0x0c, ULEB128(2) = 0x02, then 32+32 bytes
|
|
9504
|
+
const claimBytes = Buffer.concat([
|
|
9505
|
+
Buffer.from([0x0c]), // Operation::Escrow variant 12
|
|
9506
|
+
Buffer.from([0x02]), // Escrow::Submit variant 2
|
|
9507
|
+
jobIdBytes.subarray(0, 32), // job_id 32 bytes
|
|
9508
|
+
deliverableBytes.subarray(0, 32), // deliverable 32 bytes
|
|
9509
|
+
]);
|
|
9510
|
+
|
|
9511
|
+
// Transaction BCS: VersionedTransaction variant 1 (Release20260407)
|
|
9512
|
+
const networkId = process.env.ATEL_FASTCOOP_NETWORK_ID || 'fast:devnet';
|
|
9513
|
+
const senderBytes = Buffer.from(identity.publicKey);
|
|
9514
|
+
const tsNanos = BigInt(Date.now()) * 1000000n;
|
|
9515
|
+
|
|
9516
|
+
// ULEB128 encoder
|
|
9517
|
+
const uleb128 = (v) => {
|
|
9518
|
+
const buf = [];
|
|
9519
|
+
do { let b = Number(v & 0x7fn); v >>= 7n; if (v > 0n) b |= 0x80; buf.push(b); } while (v > 0n);
|
|
9520
|
+
return Buffer.from(buf.length ? buf : [0]);
|
|
9521
|
+
};
|
|
9522
|
+
const u64le = (v) => { const b = Buffer.alloc(8); b.writeBigUInt64LE(BigInt(v)); return b; };
|
|
9523
|
+
const u128le = (v) => { const b = Buffer.alloc(16); b.writeBigUInt64LE(v & 0xffffffffffffffffn); b.writeBigUInt64LE(v >> 64n, 8); return b; };
|
|
9524
|
+
const bcsString = (s) => { const b = Buffer.from(s, 'utf-8'); return Buffer.concat([uleb128(BigInt(b.length)), b]); };
|
|
9525
|
+
|
|
9526
|
+
// Transaction body
|
|
9527
|
+
const txBody = Buffer.concat([
|
|
9528
|
+
bcsString(networkId),
|
|
9529
|
+
senderBytes.subarray(0, 32),
|
|
9530
|
+
u64le(nonce),
|
|
9531
|
+
u128le(tsNanos),
|
|
9532
|
+
uleb128(1n), // claims vector length = 1
|
|
9533
|
+
claimBytes,
|
|
9534
|
+
Buffer.from([0x00]), // archival = false
|
|
9535
|
+
Buffer.from([0x00]), // fee_token = None
|
|
9536
|
+
]);
|
|
9537
|
+
|
|
9538
|
+
// VersionedTransaction = enum variant 1
|
|
9539
|
+
const versionedTx = Buffer.concat([uleb128(1n), txBody]);
|
|
9540
|
+
|
|
9541
|
+
// 6. Sign
|
|
9542
|
+
const { default: nacl } = await import('tweetnacl');
|
|
9543
|
+
const sigMessage = Buffer.concat([Buffer.from('VersionedTransaction::'), versionedTx]);
|
|
9544
|
+
const signature = nacl.sign.detached(sigMessage, identity.secretKey);
|
|
9545
|
+
|
|
9546
|
+
// SignatureOrMultiSig::Signature = variant 0 + 64 bytes
|
|
9547
|
+
const bcsSig = Buffer.concat([Buffer.from([0x00]), Buffer.from(signature)]);
|
|
9548
|
+
|
|
9549
|
+
// 7. Submit to Fast
|
|
9550
|
+
const submitResp = await fetch(`${FAST_RPC}/v1/submit-transaction`, {
|
|
9551
|
+
method: 'POST',
|
|
9552
|
+
headers: { 'Content-Type': 'application/json' },
|
|
9553
|
+
body: JSON.stringify({
|
|
9554
|
+
transaction: versionedTx.toString('hex'),
|
|
9555
|
+
signature: bcsSig.toString('hex'),
|
|
9556
|
+
}),
|
|
9557
|
+
signal: AbortSignal.timeout(15000),
|
|
9558
|
+
});
|
|
9559
|
+
|
|
9560
|
+
const submitResult = await submitResp.json();
|
|
9561
|
+
if (submitResult.error) {
|
|
9562
|
+
throw new Error(`Fast submit failed: ${submitResult.error.message || JSON.stringify(submitResult.error)}`);
|
|
9563
|
+
}
|
|
9564
|
+
|
|
9565
|
+
log({ event: 'fast_submit_success', orderId, jobId: job.job_id, nonce });
|
|
9566
|
+
console.log(`⚡ [Fast] Deliverable submitted for ${orderId} (job: ${job.job_id.substring(0, 16)}...)`);
|
|
9567
|
+
} catch (e) {
|
|
9568
|
+
log({ event: 'fast_submit_error', orderId, error: e.message });
|
|
9569
|
+
console.error(`⚠️ [Fast] Submit failed for ${orderId}: ${e.message}`);
|
|
9570
|
+
}
|
|
9571
|
+
}
|
|
9572
|
+
|
|
9325
9573
|
const [,, cmd, ...rawArgs] = process.argv;
|
|
9326
9574
|
const args = rawArgs.filter(a => !a.startsWith('--'));
|
|
9327
9575
|
const commands = {
|
|
@@ -9369,6 +9617,8 @@ const commands = {
|
|
|
9369
9617
|
'intent-info': () => cmdIntentInfo(args[0]),
|
|
9370
9618
|
'completion-proof': () => cmdCompletionProof(args[0]),
|
|
9371
9619
|
'verify-tx': () => cmdVerifyTx(args[0]),
|
|
9620
|
+
// AVIP-A2B Bitrefill board (atomic ops)
|
|
9621
|
+
bitrefill: () => cmdBitrefill(args[0], rawArgs),
|
|
9372
9622
|
// Dispute
|
|
9373
9623
|
dispute: () => cmdDispute(args[0], args[1], args[2]),
|
|
9374
9624
|
evidence: () => cmdEvidence(args[0], args[1]),
|
|
@@ -9720,6 +9970,20 @@ Dispute Commands:
|
|
|
9720
9970
|
disputes List your disputes
|
|
9721
9971
|
dispute-info <disputeId> Get dispute details
|
|
9722
9972
|
|
|
9973
|
+
Bitrefill A2B Commands (买礼品卡 / 充值 / 充话费 / esim / 订阅充值 — Web2 商户购买):
|
|
9974
|
+
bitrefill intent --category <cat> --max <usdc> Create signed intent
|
|
9975
|
+
bitrefill search --intent <id> "<query>" Search products (Gateway audited)
|
|
9976
|
+
bitrefill deposit --intent <id> --amount <usdc> Lock USDC into A2BOrderGateway contract
|
|
9977
|
+
bitrefill create-invoice --intent <id> --product <id> --value <n> Create Bitrefill invoice
|
|
9978
|
+
bitrefill pay --intent <id> Trigger contract executePayment
|
|
9979
|
+
bitrefill redemption --intent <id> Fetch gift code + confirmDelivery
|
|
9980
|
+
bitrefill status --intent <id> Query on-chain order state
|
|
9981
|
+
|
|
9982
|
+
bitrefill verify-intent <intentId> Verify your Intent's ed25519 signature + on-chain Order
|
|
9983
|
+
bitrefill audit <intentId> Recompute hash chain + merkle root, compare to on-chain audit_root
|
|
9984
|
+
bitrefill proof <intentId> Verify on-chain CompletionProof anchor tx (verdict + governance)
|
|
9985
|
+
(add --did <did> to verify someone else's intent — default is your own DID)
|
|
9986
|
+
|
|
9723
9987
|
Certification Commands:
|
|
9724
9988
|
cert-apply [level] Apply for certification (level: certified|enterprise)
|
|
9725
9989
|
cert-status [did] Check certification status
|
|
@@ -9802,4 +10066,414 @@ Task Mode: Configure .atel/policy.json taskMode (auto|confirm|off).
|
|
|
9802
10066
|
process.exit(cmd ? 1 : 0);
|
|
9803
10067
|
}
|
|
9804
10068
|
|
|
10069
|
+
// ─── AVIP-A2B Bitrefill board (atomic ops) ───────────────────────
|
|
10070
|
+
// Sub-commands:
|
|
10071
|
+
// atel bitrefill intent --category gift_card --max 50 [--confirm-above N] [--deadline-min N]
|
|
10072
|
+
// atel bitrefill search --intent <id> <query> [--country US] [--limit 5]
|
|
10073
|
+
// atel bitrefill deposit --intent <id> --amount 10.5
|
|
10074
|
+
// atel bitrefill create-invoice --intent <id> --product amazon-us --value 10
|
|
10075
|
+
// atel bitrefill pay --intent <id> --amount 10.05
|
|
10076
|
+
// atel bitrefill redemption --intent <id>
|
|
10077
|
+
// atel bitrefill status --intent <id>
|
|
10078
|
+
async function cmdBitrefill(sub, subArgs) {
|
|
10079
|
+
const flag = (name, def) => {
|
|
10080
|
+
const i = subArgs.indexOf('--' + name);
|
|
10081
|
+
if (i < 0) return def;
|
|
10082
|
+
return subArgs[i + 1];
|
|
10083
|
+
};
|
|
10084
|
+
|
|
10085
|
+
// Verification commands — default to caller's own identity (用户即审计).
|
|
10086
|
+
// --did flag overrides for verifying someone else's intent.
|
|
10087
|
+
// subArgs[0] is the sub-command itself; positional args start at [1].
|
|
10088
|
+
const rawArgsEarly = subArgs.filter(a => !a.startsWith('--')).slice(1);
|
|
10089
|
+
if (sub === 'verify-intent' || sub === 'audit' || sub === 'proof') {
|
|
10090
|
+
const intentId = rawArgsEarly[0];
|
|
10091
|
+
let did = flag('did');
|
|
10092
|
+
if (!did) {
|
|
10093
|
+
// Default to caller's own DID — most common case: "verify my own order"
|
|
10094
|
+
try {
|
|
10095
|
+
const me = requireIdentity();
|
|
10096
|
+
did = me.did;
|
|
10097
|
+
} catch {
|
|
10098
|
+
console.error(`Usage: atel bitrefill ${sub} <intentId> [--did <issuer_did>]`);
|
|
10099
|
+
console.error(` No local identity found. Either run 'atel init' first, or pass --did <did> for someone else's intent.`);
|
|
10100
|
+
process.exit(1);
|
|
10101
|
+
}
|
|
10102
|
+
}
|
|
10103
|
+
if (!intentId) {
|
|
10104
|
+
console.error(`Usage: atel bitrefill ${sub} <intentId> [--did <issuer_did>]`);
|
|
10105
|
+
process.exit(1);
|
|
10106
|
+
}
|
|
10107
|
+
if (sub === 'verify-intent') return cmdBitrefillVerifyIntent(intentId, did);
|
|
10108
|
+
if (sub === 'audit') return cmdBitrefillAudit(intentId, did);
|
|
10109
|
+
if (sub === 'proof') return cmdBitrefillProof(intentId, did);
|
|
10110
|
+
}
|
|
10111
|
+
|
|
10112
|
+
const id = requireIdentity();
|
|
10113
|
+
const { bitrefill } = await import('@lawrenceliang-btc/atel-sdk');
|
|
10114
|
+
const num = (name, def) => {
|
|
10115
|
+
const v = flag(name, def);
|
|
10116
|
+
return v === undefined ? undefined : Number(v);
|
|
10117
|
+
};
|
|
10118
|
+
|
|
10119
|
+
const userSA = process.env.ATEL_USER_SMART_ACCOUNT
|
|
10120
|
+
|| (id?.wallets && id.wallets.base)
|
|
10121
|
+
|| null;
|
|
10122
|
+
|
|
10123
|
+
try {
|
|
10124
|
+
switch (sub) {
|
|
10125
|
+
case 'intent': {
|
|
10126
|
+
if (!userSA) { console.error('ATEL_USER_SMART_ACCOUNT required'); process.exit(1); }
|
|
10127
|
+
const res = await bitrefill.createIntent(id, {
|
|
10128
|
+
category: flag('category', 'gift_card'),
|
|
10129
|
+
maxAmount: num('max', 50),
|
|
10130
|
+
requiresConfirmAbove: num('confirm-above', 50),
|
|
10131
|
+
deadlineMinutes: num('deadline-min', 10),
|
|
10132
|
+
userSmartAccount: userSA,
|
|
10133
|
+
});
|
|
10134
|
+
console.log(JSON.stringify(res, null, 2));
|
|
10135
|
+
return;
|
|
10136
|
+
}
|
|
10137
|
+
case 'search': {
|
|
10138
|
+
const intentId = flag('intent');
|
|
10139
|
+
const query = subArgs.find((a, i) => i > 0 && !a.startsWith('--') && subArgs[i-1] !== '--intent' && subArgs[i-1] !== '--country' && subArgs[i-1] !== '--limit') || '';
|
|
10140
|
+
if (!intentId || !query) { console.error('Usage: atel bitrefill search --intent <id> <query>'); process.exit(1); }
|
|
10141
|
+
const products = await bitrefill.search(id, intentId, query, { country: flag('country'), limit: num('limit') });
|
|
10142
|
+
console.log(JSON.stringify(products, null, 2));
|
|
10143
|
+
return;
|
|
10144
|
+
}
|
|
10145
|
+
case 'deposit': {
|
|
10146
|
+
if (!userSA) { console.error('ATEL_USER_SMART_ACCOUNT required'); process.exit(1); }
|
|
10147
|
+
const intentId = flag('intent');
|
|
10148
|
+
const amount = num('amount');
|
|
10149
|
+
if (!intentId || !amount) { console.error('Usage: atel bitrefill deposit --intent <id> --amount <n>'); process.exit(1); }
|
|
10150
|
+
const res = await bitrefill.deposit(id, intentId, amount, userSA);
|
|
10151
|
+
console.log(JSON.stringify(res, null, 2));
|
|
10152
|
+
return;
|
|
10153
|
+
}
|
|
10154
|
+
case 'create-invoice': {
|
|
10155
|
+
const intentId = flag('intent');
|
|
10156
|
+
const productId = flag('product');
|
|
10157
|
+
const value = num('value');
|
|
10158
|
+
if (!intentId || !productId) { console.error('Usage: atel bitrefill create-invoice --intent <id> --product <id> [--value n]'); process.exit(1); }
|
|
10159
|
+
const res = await bitrefill.createInvoice(id, intentId, productId, value || 0);
|
|
10160
|
+
console.log(JSON.stringify(res, null, 2));
|
|
10161
|
+
return;
|
|
10162
|
+
}
|
|
10163
|
+
case 'pay': {
|
|
10164
|
+
const intentId = flag('intent');
|
|
10165
|
+
const amount = num('amount');
|
|
10166
|
+
if (!intentId) { console.error('Usage: atel bitrefill pay --intent <id> --amount <n>'); process.exit(1); }
|
|
10167
|
+
const res = await bitrefill.pay(id, intentId, amount || 0);
|
|
10168
|
+
console.log(JSON.stringify(res, null, 2));
|
|
10169
|
+
return;
|
|
10170
|
+
}
|
|
10171
|
+
case 'redemption': {
|
|
10172
|
+
const intentId = flag('intent');
|
|
10173
|
+
if (!intentId) { console.error('Usage: atel bitrefill redemption --intent <id>'); process.exit(1); }
|
|
10174
|
+
const res = await bitrefill.getRedemption(id, intentId);
|
|
10175
|
+
console.log(JSON.stringify(res, null, 2));
|
|
10176
|
+
return;
|
|
10177
|
+
}
|
|
10178
|
+
case 'status': {
|
|
10179
|
+
const intentId = flag('intent');
|
|
10180
|
+
if (!intentId) { console.error('Usage: atel bitrefill status --intent <id>'); process.exit(1); }
|
|
10181
|
+
const res = await bitrefill.status(id, intentId);
|
|
10182
|
+
console.log(JSON.stringify(res, null, 2));
|
|
10183
|
+
return;
|
|
10184
|
+
}
|
|
10185
|
+
default:
|
|
10186
|
+
console.error('Usage: atel bitrefill <intent|search|deposit|create-invoice|pay|redemption|status|verify-intent|audit|proof> ...');
|
|
10187
|
+
process.exit(1);
|
|
10188
|
+
}
|
|
10189
|
+
} catch (e) {
|
|
10190
|
+
console.error(JSON.stringify({ ok: false, error: e.message, code: e.code, data: e.data }));
|
|
10191
|
+
process.exit(2);
|
|
10192
|
+
}
|
|
10193
|
+
}
|
|
10194
|
+
|
|
10195
|
+
// ─── Bitrefill A2B verification commands ─────────────────────────────────────
|
|
10196
|
+
// All three are READ-ONLY: anyone with the intentId + did can verify the on-chain
|
|
10197
|
+
// truth independently of Platform's word. These re-implement the same checks the
|
|
10198
|
+
// Platform's CompletionProof generation does, using only on-chain data + the
|
|
10199
|
+
// detail API's unsigned trace export.
|
|
10200
|
+
|
|
10201
|
+
const BASE_RPC = process.env.ATEL_BASE_RPC || 'https://mainnet.base.org';
|
|
10202
|
+
const BASESCAN_TX = (tx) => `https://basescan.org/tx/${tx}`;
|
|
10203
|
+
|
|
10204
|
+
async function fetchA2BDetail(intentId, did) {
|
|
10205
|
+
const url = `${PLATFORM_URL.replace(/\/+$/, '')}/trade/v1/a2b/detail/${encodeURIComponent(intentId)}?did=${encodeURIComponent(did)}`;
|
|
10206
|
+
const r = await fetch(url, { signal: AbortSignal.timeout(15000) });
|
|
10207
|
+
if (!r.ok) throw new Error(`detail fetch HTTP ${r.status}: ${(await r.text()).slice(0, 200)}`);
|
|
10208
|
+
return await r.json();
|
|
10209
|
+
}
|
|
10210
|
+
|
|
10211
|
+
async function ethCall(to, data) {
|
|
10212
|
+
const r = await fetch(BASE_RPC, {
|
|
10213
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
10214
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to, data }, 'latest'] }),
|
|
10215
|
+
signal: AbortSignal.timeout(15000),
|
|
10216
|
+
});
|
|
10217
|
+
const j = await r.json();
|
|
10218
|
+
if (j.error) throw new Error(`eth_call: ${j.error.message}`);
|
|
10219
|
+
return j.result;
|
|
10220
|
+
}
|
|
10221
|
+
|
|
10222
|
+
async function ethGetTxReceipt(tx) {
|
|
10223
|
+
const r = await fetch(BASE_RPC, {
|
|
10224
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
10225
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getTransactionReceipt', params: [tx] }),
|
|
10226
|
+
signal: AbortSignal.timeout(15000),
|
|
10227
|
+
});
|
|
10228
|
+
const j = await r.json();
|
|
10229
|
+
if (j.error) throw new Error(`receipt: ${j.error.message}`);
|
|
10230
|
+
return j.result;
|
|
10231
|
+
}
|
|
10232
|
+
|
|
10233
|
+
// ─── verify-intent: checks ed25519 sig + on-chain createOrder consistency ───
|
|
10234
|
+
async function cmdBitrefillVerifyIntent(intentId, did) {
|
|
10235
|
+
const naclMod = await import('tweetnacl');
|
|
10236
|
+
const bs58 = await import('bs58');
|
|
10237
|
+
const ethers = await import('ethers');
|
|
10238
|
+
|
|
10239
|
+
console.log(JSON.stringify({ event: 'verify_intent', intentId, did }));
|
|
10240
|
+
let detail;
|
|
10241
|
+
try { detail = await fetchA2BDetail(intentId, did); } catch (e) {
|
|
10242
|
+
console.log(JSON.stringify({ verified: false, error: e.message }));
|
|
10243
|
+
process.exit(1);
|
|
10244
|
+
}
|
|
10245
|
+
|
|
10246
|
+
const out = {
|
|
10247
|
+
intentId,
|
|
10248
|
+
issuer_did: detail.intent.issuer_did,
|
|
10249
|
+
checks: {},
|
|
10250
|
+
verdict: 'PASS',
|
|
10251
|
+
};
|
|
10252
|
+
|
|
10253
|
+
// 1. signature exists
|
|
10254
|
+
if (!detail.intent.signature || !detail.intent.signed_payload) {
|
|
10255
|
+
out.checks.signature_present = { ok: false, msg: 'missing signature or signed_payload' };
|
|
10256
|
+
out.verdict = 'FAIL';
|
|
10257
|
+
} else {
|
|
10258
|
+
out.checks.signature_present = { ok: true };
|
|
10259
|
+
}
|
|
10260
|
+
|
|
10261
|
+
// 2. ed25519 verify
|
|
10262
|
+
if (out.checks.signature_present.ok) {
|
|
10263
|
+
try {
|
|
10264
|
+
const did2 = detail.intent.issuer_did;
|
|
10265
|
+
const b58Key = did2.split(':').slice(-1)[0];
|
|
10266
|
+
const pubKey = bs58.default.decode(b58Key);
|
|
10267
|
+
const sigBytes = Buffer.from(detail.intent.signature, 'base64');
|
|
10268
|
+
const msgBytes = Buffer.from(detail.intent.signed_payload);
|
|
10269
|
+
const ok = naclMod.default.sign.detached.verify(msgBytes, sigBytes, pubKey);
|
|
10270
|
+
out.checks.ed25519_signature = { ok, msg: ok ? 'signature valid' : 'signature INVALID' };
|
|
10271
|
+
if (!ok) out.verdict = 'FAIL';
|
|
10272
|
+
} catch (e) {
|
|
10273
|
+
out.checks.ed25519_signature = { ok: false, msg: 'verify error: ' + e.message };
|
|
10274
|
+
out.verdict = 'FAIL';
|
|
10275
|
+
}
|
|
10276
|
+
}
|
|
10277
|
+
|
|
10278
|
+
// 3. on-chain createOrder hash matches keccak256(intentId)
|
|
10279
|
+
const expected = ethers.keccak256(ethers.toUtf8Bytes(intentId));
|
|
10280
|
+
if (detail.onchain && detail.onchain.user_addr) {
|
|
10281
|
+
out.checks.intent_onchain = {
|
|
10282
|
+
ok: true,
|
|
10283
|
+
msg: `on-chain Order exists: userAddr=${detail.onchain.user_addr} status=${detail.onchain.status_name}`,
|
|
10284
|
+
intent_hash: expected,
|
|
10285
|
+
};
|
|
10286
|
+
} else {
|
|
10287
|
+
out.checks.intent_onchain = { ok: false, msg: 'no on-chain Order found for this intentHash' };
|
|
10288
|
+
out.verdict = 'FAIL';
|
|
10289
|
+
}
|
|
10290
|
+
|
|
10291
|
+
console.log(JSON.stringify(out, null, 2));
|
|
10292
|
+
process.exit(out.verdict === 'PASS' ? 0 : 1);
|
|
10293
|
+
}
|
|
10294
|
+
|
|
10295
|
+
// ─── audit: recomputes ActionTrace hash chain + merkle root, compares on-chain ─
|
|
10296
|
+
async function cmdBitrefillAudit(intentId, did) {
|
|
10297
|
+
const ethers = await import('ethers');
|
|
10298
|
+
console.log(JSON.stringify({ event: 'audit_a2b', intentId, did }));
|
|
10299
|
+
let detail;
|
|
10300
|
+
try { detail = await fetchA2BDetail(intentId, did); } catch (e) {
|
|
10301
|
+
console.log(JSON.stringify({ verified: false, error: e.message }));
|
|
10302
|
+
process.exit(1);
|
|
10303
|
+
}
|
|
10304
|
+
|
|
10305
|
+
const out = { intentId, checks: {}, verdict: 'PASS' };
|
|
10306
|
+
|
|
10307
|
+
// 1. Hash chain integrity (each trace's prev_trace_hash should equal previous trace's trace_hash)
|
|
10308
|
+
const traces = (detail.traces || []).slice().sort((a, b) => a.sequence - b.sequence);
|
|
10309
|
+
let prev = '';
|
|
10310
|
+
let chainOk = true;
|
|
10311
|
+
const chainErrs = [];
|
|
10312
|
+
for (const t of traces) {
|
|
10313
|
+
if (t.prev_trace_hash !== prev) {
|
|
10314
|
+
chainOk = false;
|
|
10315
|
+
chainErrs.push(`seq #${t.sequence}: prev mismatch (expected '${prev}', got '${t.prev_trace_hash}')`);
|
|
10316
|
+
}
|
|
10317
|
+
prev = t.trace_hash;
|
|
10318
|
+
}
|
|
10319
|
+
out.checks.hash_chain = { ok: chainOk, traces: traces.length, errors: chainErrs.length ? chainErrs : undefined };
|
|
10320
|
+
if (!chainOk) out.verdict = 'FAIL';
|
|
10321
|
+
|
|
10322
|
+
// 2. Merkle root over PRE-PAY traces (search/deposit/createInvoice). Match Gateway's logic.
|
|
10323
|
+
// Per AVIP-A2B spec, on-chain audit_root only covers traces BEFORE wallet.pay.
|
|
10324
|
+
const prePay = [];
|
|
10325
|
+
for (const t of traces) {
|
|
10326
|
+
if (t.tool === 'wallet.pay') break;
|
|
10327
|
+
prePay.push(t);
|
|
10328
|
+
}
|
|
10329
|
+
const leaves = prePay.map(t => '0x' + t.trace_hash);
|
|
10330
|
+
const computedRoot = leaves.length === 0 ? '0x' + '00'.repeat(32) : merkleRootKeccak(leaves, ethers);
|
|
10331
|
+
const onchainAuditRoot = detail.onchain ? detail.onchain.audit_root : null;
|
|
10332
|
+
if (onchainAuditRoot) {
|
|
10333
|
+
const ok = computedRoot.toLowerCase() === onchainAuditRoot.toLowerCase();
|
|
10334
|
+
out.checks.audit_root = {
|
|
10335
|
+
ok,
|
|
10336
|
+
computed: computedRoot,
|
|
10337
|
+
onchain: onchainAuditRoot,
|
|
10338
|
+
pre_pay_traces: prePay.length,
|
|
10339
|
+
msg: ok ? 'merkle root matches on-chain audit_root' : 'MISMATCH — Platform may have tampered with traces',
|
|
10340
|
+
};
|
|
10341
|
+
if (!ok) out.verdict = 'FAIL';
|
|
10342
|
+
} else {
|
|
10343
|
+
out.checks.audit_root = { ok: false, msg: 'no on-chain audit_root (pay not yet executed?)' };
|
|
10344
|
+
}
|
|
10345
|
+
|
|
10346
|
+
// 3. Decision counts (informational)
|
|
10347
|
+
const allowed = traces.filter(t => t.policy_decision === 'allow').length;
|
|
10348
|
+
const denied = traces.filter(t => t.policy_decision === 'deny').length;
|
|
10349
|
+
out.checks.governance = { ok: true, total: traces.length, allowed, denied };
|
|
10350
|
+
|
|
10351
|
+
console.log(JSON.stringify(out, null, 2));
|
|
10352
|
+
process.exit(out.verdict === 'PASS' ? 0 : 1);
|
|
10353
|
+
}
|
|
10354
|
+
|
|
10355
|
+
// Standard binary merkle root with last-leaf duplication on odd levels.
|
|
10356
|
+
function merkleRootKeccak(leaves, ethers) {
|
|
10357
|
+
let level = leaves.slice();
|
|
10358
|
+
while (level.length > 1) {
|
|
10359
|
+
const next = [];
|
|
10360
|
+
for (let i = 0; i < level.length; i += 2) {
|
|
10361
|
+
const a = level[i];
|
|
10362
|
+
const b = (i + 1 < level.length) ? level[i + 1] : level[i];
|
|
10363
|
+
next.push(ethers.keccak256(ethers.concat([a, b])));
|
|
10364
|
+
}
|
|
10365
|
+
level = next;
|
|
10366
|
+
}
|
|
10367
|
+
return level[0];
|
|
10368
|
+
}
|
|
10369
|
+
|
|
10370
|
+
// ─── proof: fetches CompletionProof anchor tx + verifies content matches local ──
|
|
10371
|
+
async function cmdBitrefillProof(intentId, did) {
|
|
10372
|
+
console.log(JSON.stringify({ event: 'verify_proof_a2b', intentId, did }));
|
|
10373
|
+
let detail;
|
|
10374
|
+
try { detail = await fetchA2BDetail(intentId, did); } catch (e) {
|
|
10375
|
+
console.log(JSON.stringify({ verified: false, error: e.message }));
|
|
10376
|
+
process.exit(1);
|
|
10377
|
+
}
|
|
10378
|
+
|
|
10379
|
+
const out = { intentId, checks: {}, verdict: 'PASS' };
|
|
10380
|
+
|
|
10381
|
+
if (!detail.proof || !detail.proof.anchor_tx) {
|
|
10382
|
+
out.checks.anchored = { ok: false, msg: 'no CompletionProof anchor tx — order not yet completed' };
|
|
10383
|
+
out.verdict = 'FAIL';
|
|
10384
|
+
console.log(JSON.stringify(out, null, 2));
|
|
10385
|
+
process.exit(1);
|
|
10386
|
+
}
|
|
10387
|
+
|
|
10388
|
+
const anchorTx = detail.proof.anchor_tx;
|
|
10389
|
+
out.anchor_tx = anchorTx;
|
|
10390
|
+
out.basescan = BASESCAN_TX(anchorTx);
|
|
10391
|
+
|
|
10392
|
+
// 1. Receipt exists + status = 1
|
|
10393
|
+
let receipt;
|
|
10394
|
+
try { receipt = await ethGetTxReceipt(anchorTx); } catch (e) {
|
|
10395
|
+
out.checks.receipt = { ok: false, msg: 'fetch failed: ' + e.message };
|
|
10396
|
+
out.verdict = 'FAIL';
|
|
10397
|
+
console.log(JSON.stringify(out, null, 2));
|
|
10398
|
+
process.exit(1);
|
|
10399
|
+
}
|
|
10400
|
+
if (!receipt) {
|
|
10401
|
+
out.checks.receipt = { ok: false, msg: 'tx not found on Base mainnet' };
|
|
10402
|
+
out.verdict = 'FAIL';
|
|
10403
|
+
} else if (receipt.status !== '0x1') {
|
|
10404
|
+
out.checks.receipt = { ok: false, msg: `tx reverted (status=${receipt.status})` };
|
|
10405
|
+
out.verdict = 'FAIL';
|
|
10406
|
+
} else {
|
|
10407
|
+
out.checks.receipt = {
|
|
10408
|
+
ok: true,
|
|
10409
|
+
block: parseInt(receipt.blockNumber, 16),
|
|
10410
|
+
to: receipt.to,
|
|
10411
|
+
logs: receipt.logs.length,
|
|
10412
|
+
};
|
|
10413
|
+
}
|
|
10414
|
+
|
|
10415
|
+
// 2. Decode AnchorRegistry log → extract on-chain JSON
|
|
10416
|
+
let onchainJSON = null;
|
|
10417
|
+
if (receipt && receipt.logs) {
|
|
10418
|
+
for (const log of receipt.logs) {
|
|
10419
|
+
const data = log.data || '';
|
|
10420
|
+
// Logs include the dataJSON as a raw bytes UTF-8 string
|
|
10421
|
+
const buf = Buffer.from(data.slice(2), 'hex');
|
|
10422
|
+
// Find first '{' and matching close
|
|
10423
|
+
const start = buf.indexOf('{');
|
|
10424
|
+
if (start < 0) continue;
|
|
10425
|
+
const sub = buf.slice(start);
|
|
10426
|
+
const end = sub.lastIndexOf('}');
|
|
10427
|
+
if (end < 0) continue;
|
|
10428
|
+
try {
|
|
10429
|
+
onchainJSON = JSON.parse(sub.slice(0, end + 1).toString('utf8'));
|
|
10430
|
+
break;
|
|
10431
|
+
} catch {}
|
|
10432
|
+
}
|
|
10433
|
+
}
|
|
10434
|
+
|
|
10435
|
+
if (!onchainJSON) {
|
|
10436
|
+
out.checks.onchain_json = { ok: false, msg: 'failed to decode AnchorRegistry data JSON from logs' };
|
|
10437
|
+
out.verdict = 'FAIL';
|
|
10438
|
+
} else {
|
|
10439
|
+
out.checks.onchain_json = {
|
|
10440
|
+
ok: true,
|
|
10441
|
+
type: onchainJSON.type,
|
|
10442
|
+
verdict: onchainJSON.verdict,
|
|
10443
|
+
product: onchainJSON.product,
|
|
10444
|
+
actualAmount: onchainJSON.actualAmount,
|
|
10445
|
+
};
|
|
10446
|
+
|
|
10447
|
+
// 3. Compare on-chain verdict + governance with local DB version
|
|
10448
|
+
const local = detail.proof;
|
|
10449
|
+
const verdictMatch = onchainJSON.verdict === local.verdict;
|
|
10450
|
+
out.checks.verdict_consistency = {
|
|
10451
|
+
ok: verdictMatch,
|
|
10452
|
+
onchain: onchainJSON.verdict,
|
|
10453
|
+
local: local.verdict,
|
|
10454
|
+
msg: verdictMatch ? 'verdict matches' : 'MISMATCH — DB and on-chain disagree',
|
|
10455
|
+
};
|
|
10456
|
+
if (!verdictMatch) out.verdict = 'FAIL';
|
|
10457
|
+
|
|
10458
|
+
// 4. Compare action_governance numbers
|
|
10459
|
+
if (onchainJSON.actionGovernance && local.action_governance) {
|
|
10460
|
+
const fields = ['allowed', 'denied', 'total_agent_calls'];
|
|
10461
|
+
const mismatches = [];
|
|
10462
|
+
for (const f of fields) {
|
|
10463
|
+
if (onchainJSON.actionGovernance[f] !== local.action_governance[f]) {
|
|
10464
|
+
mismatches.push(`${f}: onchain=${onchainJSON.actionGovernance[f]} local=${local.action_governance[f]}`);
|
|
10465
|
+
}
|
|
10466
|
+
}
|
|
10467
|
+
out.checks.governance_consistency = {
|
|
10468
|
+
ok: mismatches.length === 0,
|
|
10469
|
+
mismatches: mismatches.length ? mismatches : undefined,
|
|
10470
|
+
};
|
|
10471
|
+
if (mismatches.length) out.verdict = 'FAIL';
|
|
10472
|
+
}
|
|
10473
|
+
}
|
|
10474
|
+
|
|
10475
|
+
console.log(JSON.stringify(out, null, 2));
|
|
10476
|
+
process.exit(out.verdict === 'PASS' ? 0 : 1);
|
|
10477
|
+
}
|
|
10478
|
+
|
|
9805
10479
|
commands[cmd]().catch(err => { console.error(JSON.stringify({ error: err.message })); process.exit(1); });
|