@lawrenceliang-btc/atel-sdk 1.1.38 → 1.1.40

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.
Files changed (2) hide show
  1. package/bin/atel.mjs +145 -0
  2. package/package.json +1 -1
package/bin/atel.mjs CHANGED
@@ -3638,6 +3638,28 @@ Format:
3638
3638
  dedupeKey: body.dedupeKey,
3639
3639
  });
3640
3640
 
3641
+ // ── Fast-coop: auto-submit deliverable when all milestones verified ──
3642
+ // When executor receives the final milestone_verified for a fast-coop order,
3643
+ // sign and submit Escrow::Submit on Fast staging so the Platform can Complete.
3644
+ if (event === 'milestone_verified' && payload.allComplete === true) {
3645
+ const fastOrderId = body.orderId || payload.orderId || '';
3646
+ if (fastOrderId) {
3647
+ // Payload may not include chain — fetch order to check
3648
+ try {
3649
+ const orderInfo = await fetchOrderState(fastOrderId);
3650
+ const orderChain = orderInfo?.chain || orderInfo?.Chain || '';
3651
+ if (orderChain === 'fast-coop') {
3652
+ log({ event: 'fast_submit_triggered', orderId: fastOrderId, chain: orderChain });
3653
+ submitFastDeliverable(fastOrderId, id).catch(e => {
3654
+ log({ event: 'fast_submit_error', orderId: fastOrderId, error: e.message });
3655
+ });
3656
+ }
3657
+ } catch (e) {
3658
+ log({ event: 'fast_submit_check_error', orderId: fastOrderId, error: e.message });
3659
+ }
3660
+ }
3661
+ }
3662
+
3641
3663
  const dedupeKey = body.dedupeKey || `${event}:${body.orderId || payload.orderId || ''}`;
3642
3664
  const orderIdForCwd = body.orderId || payload.orderId || '';
3643
3665
  let workspace = getOrderWorkspace(orderIdForCwd, {
@@ -8369,6 +8391,129 @@ async function cmdAliasRemove(args) {
8369
8391
 
8370
8392
  // ─── Main ────────────────────────────────────────────────────────
8371
8393
 
8394
+ // ── Fast-coop: Executor auto-submit deliverable to Fast escrow ──────
8395
+ // Called when executor receives milestone_verified with allComplete=true
8396
+ // for a fast-coop order. Signs Escrow::Submit with the agent's DID
8397
+ // private key (= Fast Ed25519 account key).
8398
+ async function submitFastDeliverable(orderId, identity) {
8399
+ try {
8400
+ // 1. Get order info to find the escrow job_id
8401
+ const orderInfo = await fetchOrderState(orderId);
8402
+ if (!orderInfo) throw new Error('cannot fetch order');
8403
+ const chain = orderInfo.chain || orderInfo.Chain || '';
8404
+ if (chain !== 'fast-coop') return;
8405
+
8406
+ // 2. Find escrow job_id from chain-records
8407
+ const recordsResp = await fetch(`${PLATFORM_URL}/trade/v1/order/${orderId}`, {
8408
+ signal: AbortSignal.timeout(10000),
8409
+ });
8410
+ // The job_id is stored in the on_chain_records by the Platform.
8411
+ // We need to get it from the order or from the chain-records endpoint.
8412
+ // For now, compute it the same way Platform does: keccak256(orderId)
8413
+ const { createHash } = await import('node:crypto');
8414
+
8415
+ // 3. Get the agent's Fast account info
8416
+ const FAST_RPC = process.env.ATEL_FASTCOOP_RPC_URL || 'https://staging.api.fast.xyz/proxy-rest';
8417
+ const pubKeyHex = Buffer.from(identity.publicKey).toString('hex');
8418
+
8419
+ const accountResp = await fetch(`${FAST_RPC}/v1/accounts/${pubKeyHex}`, {
8420
+ signal: AbortSignal.timeout(10000),
8421
+ });
8422
+ if (!accountResp.ok) throw new Error(`Fast account query failed: ${accountResp.status}`);
8423
+ const accountData = (await accountResp.json()).data;
8424
+ const nonce = accountData.next_nonce;
8425
+
8426
+ // 4. Find escrow job for this order (query by provider)
8427
+ const jobsResp = await fetch(`${FAST_RPC}/v1/escrow-jobs?provider=${pubKeyHex}&status=Funded`, {
8428
+ signal: AbortSignal.timeout(10000),
8429
+ });
8430
+ if (!jobsResp.ok) throw new Error(`escrow jobs query failed: ${jobsResp.status}`);
8431
+ const jobs = (await jobsResp.json()).data || [];
8432
+ const job = jobs.find(j => j.description && j.description.includes(orderId));
8433
+ if (!job) {
8434
+ log({ event: 'fast_submit_no_job', orderId, note: 'no Funded escrow job found for this order' });
8435
+ return;
8436
+ }
8437
+
8438
+ // 5. Build Escrow::Submit transaction
8439
+ // deliverable = keccak256(orderId) as 32-byte hash
8440
+ const deliverableHash = createHash('sha3-256').update(orderId).digest('hex');
8441
+
8442
+ // BCS encode: Operation::Escrow(12) → Escrow::Submit(2) → jobId(32) + deliverable(32)
8443
+ const jobIdBytes = Buffer.from(job.job_id, 'hex');
8444
+ const deliverableBytes = Buffer.from(deliverableHash, 'hex');
8445
+
8446
+ // Build minimal BCS by hand (matching Go implementation):
8447
+ // ULEB128(12) = 0x0c, ULEB128(2) = 0x02, then 32+32 bytes
8448
+ const claimBytes = Buffer.concat([
8449
+ Buffer.from([0x0c]), // Operation::Escrow variant 12
8450
+ Buffer.from([0x02]), // Escrow::Submit variant 2
8451
+ jobIdBytes.subarray(0, 32), // job_id 32 bytes
8452
+ deliverableBytes.subarray(0, 32), // deliverable 32 bytes
8453
+ ]);
8454
+
8455
+ // Transaction BCS: VersionedTransaction variant 1 (Release20260407)
8456
+ const networkId = process.env.ATEL_FASTCOOP_NETWORK_ID || 'fast:devnet';
8457
+ const senderBytes = Buffer.from(identity.publicKey);
8458
+ const tsNanos = BigInt(Date.now()) * 1000000n;
8459
+
8460
+ // ULEB128 encoder
8461
+ const uleb128 = (v) => {
8462
+ const buf = [];
8463
+ do { let b = Number(v & 0x7fn); v >>= 7n; if (v > 0n) b |= 0x80; buf.push(b); } while (v > 0n);
8464
+ return Buffer.from(buf.length ? buf : [0]);
8465
+ };
8466
+ const u64le = (v) => { const b = Buffer.alloc(8); b.writeBigUInt64LE(BigInt(v)); return b; };
8467
+ const u128le = (v) => { const b = Buffer.alloc(16); b.writeBigUInt64LE(v & 0xffffffffffffffffn); b.writeBigUInt64LE(v >> 64n, 8); return b; };
8468
+ const bcsString = (s) => { const b = Buffer.from(s, 'utf-8'); return Buffer.concat([uleb128(BigInt(b.length)), b]); };
8469
+
8470
+ // Transaction body
8471
+ const txBody = Buffer.concat([
8472
+ bcsString(networkId),
8473
+ senderBytes.subarray(0, 32),
8474
+ u64le(nonce),
8475
+ u128le(tsNanos),
8476
+ uleb128(1n), // claims vector length = 1
8477
+ claimBytes,
8478
+ Buffer.from([0x00]), // archival = false
8479
+ Buffer.from([0x00]), // fee_token = None
8480
+ ]);
8481
+
8482
+ // VersionedTransaction = enum variant 1
8483
+ const versionedTx = Buffer.concat([uleb128(1n), txBody]);
8484
+
8485
+ // 6. Sign
8486
+ const { default: nacl } = await import('tweetnacl');
8487
+ const sigMessage = Buffer.concat([Buffer.from('VersionedTransaction::'), versionedTx]);
8488
+ const signature = nacl.sign.detached(sigMessage, identity.secretKey);
8489
+
8490
+ // SignatureOrMultiSig::Signature = variant 0 + 64 bytes
8491
+ const bcsSig = Buffer.concat([Buffer.from([0x00]), Buffer.from(signature)]);
8492
+
8493
+ // 7. Submit to Fast
8494
+ const submitResp = await fetch(`${FAST_RPC}/v1/submit-transaction`, {
8495
+ method: 'POST',
8496
+ headers: { 'Content-Type': 'application/json' },
8497
+ body: JSON.stringify({
8498
+ transaction: versionedTx.toString('hex'),
8499
+ signature: bcsSig.toString('hex'),
8500
+ }),
8501
+ signal: AbortSignal.timeout(15000),
8502
+ });
8503
+
8504
+ const submitResult = await submitResp.json();
8505
+ if (submitResult.error) {
8506
+ throw new Error(`Fast submit failed: ${submitResult.error.message || JSON.stringify(submitResult.error)}`);
8507
+ }
8508
+
8509
+ log({ event: 'fast_submit_success', orderId, jobId: job.job_id, nonce });
8510
+ console.log(`⚡ [Fast] Deliverable submitted for ${orderId} (job: ${job.job_id.substring(0, 16)}...)`);
8511
+ } catch (e) {
8512
+ log({ event: 'fast_submit_error', orderId, error: e.message });
8513
+ console.error(`⚠️ [Fast] Submit failed for ${orderId}: ${e.message}`);
8514
+ }
8515
+ }
8516
+
8372
8517
  const [,, cmd, ...rawArgs] = process.argv;
8373
8518
  const args = rawArgs.filter(a => !a.startsWith('--'));
8374
8519
  const commands = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lawrenceliang-btc/atel-sdk",
3
- "version": "1.1.38",
3
+ "version": "1.1.40",
4
4
  "description": "ATEL Protocol SDK - Agent Trust & Exchange Layer",
5
5
  "repository": {
6
6
  "type": "git",