@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/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
- console.log(JSON.stringify({
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
- next: 'Run: atel start [port] — auto-configures network and registers'
2730
- }, null, 2));
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 sdkSkillPath = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'skill', 'atel-agent', 'SKILL.md');
2734
- if (existsSync(sdkSkillPath)) {
2735
- const home = process.env.HOME || '';
2736
- const candidates = [
2737
- join(home, '.openclaw', 'skills', 'atel-agent'),
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 || capability;
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> [--desc "task description"]'); process.exit(1); }
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); });