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