@lawrenceliang-btc/atel-sdk 1.2.14 → 1.2.15

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 CHANGED
@@ -10,7 +10,7 @@ ATEL provides the cryptographic primitives and protocol building blocks that ena
10
10
  - **📋 Policy Enforcement** — Scoped consent tokens, call tracking, deterministic hashing
11
11
  - **🔍 Execution Tracing** — Tamper-evident, hash-chained audit logs with auto-checkpoints
12
12
  - **✅ Proof Generation** — Merkle-tree proof bundles with multi-check verification
13
- - **⚓ On-Chain Anchoring** — Multi-chain proof anchoring (Solana/Base/BSC)
13
+ - **⚓ On-Chain Anchoring** — Multi-chain proof anchoring (Base/BSC)
14
14
  - **📊 Trust Scoring** — Local trust computation based on execution history
15
15
  - **🔔 Notification & Callback Runtime** — Local notify, callback, inbox, and recovery flow
16
16
  - **👥 P2P Access Control** — Relationship-based friend system with temporary sessions
@@ -60,7 +60,7 @@ the `paymentTxHash` on Base, the `auditUrl` pointing at the CompletionProof (Int
60
60
  ### Trust & Verification
61
61
  - Tamper-evident execution traces
62
62
  - Merkle-tree proof generation
63
- - On-chain anchoring (Solana/Base/BSC)
63
+ - On-chain anchoring (Base/BSC)
64
64
  - Local trust score computation
65
65
  - Callback-driven execution and recovery
66
66
 
@@ -359,21 +359,21 @@ const report = ProofVerifier.verify(bundle, { trace });
359
359
  ### On-Chain Anchoring
360
360
 
361
361
  ```typescript
362
- import { SolanaAnchorProvider } from '@lawrenceliang-btc/atel-sdk';
362
+ import { BaseAnchorProvider } from '@lawrenceliang-btc/atel-sdk';
363
363
 
364
- const solana = new SolanaAnchorProvider({
365
- rpcUrl: 'https://api.mainnet-beta.solana.com',
366
- privateKey: process.env.ATEL_SOLANA_PRIVATE_KEY
364
+ const base = new BaseAnchorProvider({
365
+ rpcUrl: process.env.ATEL_BASE_RPC_URL || 'https://mainnet.base.org',
366
+ privateKey: process.env.ATEL_BASE_PRIVATE_KEY
367
367
  });
368
368
 
369
- const result = await solana.anchor(traceRoot, {
369
+ const result = await base.anchor(traceRoot, {
370
370
  executorDid: 'did:atel:ed25519:...',
371
371
  requesterDid: 'did:atel:ed25519:...',
372
372
  taskId: 'task-123'
373
373
  });
374
374
  // result.txHash, result.blockNumber
375
375
 
376
- const verified = await solana.verify(traceRoot, txHash);
376
+ const verified = await base.verify(traceRoot, txHash);
377
377
  // verified.valid, verified.detail
378
378
  ```
379
379
 
@@ -433,8 +433,6 @@ Friend system data is stored in `.atel/`:
433
433
  ## Environment Variables
434
434
 
435
435
  **On-Chain Anchoring:**
436
- - `ATEL_SOLANA_PRIVATE_KEY` - Solana wallet private key (base58)
437
- - `ATEL_SOLANA_RPC_URL` - Solana RPC endpoint
438
436
  - `ATEL_BASE_PRIVATE_KEY` - Base chain private key (hex)
439
437
  - `ATEL_BASE_RPC_URL` - Base RPC endpoint
440
438
  - `ATEL_BSC_PRIVATE_KEY` - BSC private key (hex)
package/bin/atel.mjs CHANGED
@@ -59,7 +59,7 @@ import crypto from 'node:crypto';
59
59
  import {
60
60
  AgentIdentity, AgentEndpoint, AgentClient, HandshakeManager,
61
61
  createMessage, verifyMessage, parseDID, RegistryClient, ExecutionTrace, ProofGenerator,
62
- SolanaAnchorProvider, BaseAnchorProvider, BSCAnchorProvider,
62
+ BaseAnchorProvider, BSCAnchorProvider,
63
63
  autoNetworkSetup, collectCandidates, connectToAgent,
64
64
  discoverPublicIP, checkReachable, verifyPortReachable, ContentAuditor, TrustScoreClient,
65
65
  RollbackManager, rotateKey, verifyKeyRotation, ToolGateway, PolicyEngine, mintConsentToken, sign,
@@ -107,7 +107,6 @@ function getEnvironmentProfile(url = '') {
107
107
  const host = String(parsed.hostname || '').toLowerCase();
108
108
  if (host === 'api.atelai.org') return { name: 'production', label: 'production', url: normalized };
109
109
  if (host === '127.0.0.1' || host === 'localhost') return { name: 'test', label: 'local-test', url: normalized };
110
- if (host === '43.160.230.129' || host === '43.160.211.180') return { name: 'test', label: 'host-test', url: normalized };
111
110
  return { name: 'custom', label: host || 'custom', url: normalized };
112
111
  } catch {
113
112
  return { name: 'custom', label: normalized, url: normalized };
@@ -2162,7 +2161,6 @@ function getChainPrivateKey(chain) {
2162
2161
  return config.chains[chain].privateKey;
2163
2162
  }
2164
2163
  // 2. Fall back to environment variables (backward compatibility)
2165
- if (chain === 'solana') return process.env.ATEL_SOLANA_PRIVATE_KEY;
2166
2164
  if (chain === 'base') return process.env.ATEL_BASE_PRIVATE_KEY;
2167
2165
  if (chain === 'bsc') return process.env.ATEL_BSC_PRIVATE_KEY;
2168
2166
  return null;
@@ -2173,19 +2171,6 @@ async function getWalletAddresses() {
2173
2171
  const wallets = {};
2174
2172
  const config = loadAnchorConfig();
2175
2173
 
2176
- // Solana: base58 private key → public key
2177
- const solKey = getChainPrivateKey('solana');
2178
- if (solKey) {
2179
- try {
2180
- const { Keypair } = await import('@solana/web3.js');
2181
- const bs58 = (await import('bs58')).default;
2182
- const kp = Keypair.fromSecretKey(bs58.decode(solKey));
2183
- wallets.solana = kp.publicKey.toBase58();
2184
- } catch {}
2185
- } else if (config?.chains?.solana?.address) {
2186
- wallets.solana = config.chains.solana.address;
2187
- }
2188
-
2189
2174
  // Base: hex private key → address
2190
2175
  const baseKey = getChainPrivateKey('base');
2191
2176
  if (baseKey) {
@@ -2961,16 +2946,23 @@ function riskAllowed(maxRisk, requestedRisk) { return (RISK_ORDER[requestedRisk]
2961
2946
  // Verify anchor_tx list on-chain, return count of valid proofs
2962
2947
  async function verifyAnchorTxList(anchorTxList, targetDid) {
2963
2948
  if (!anchorTxList || anchorTxList.length === 0) return { verified: 0, total: 0, proofs: [] };
2964
- const rpcUrl = process.env.ATEL_SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com';
2965
- const provider = new SolanaAnchorProvider({ rpcUrl });
2966
2949
  let verified = 0;
2967
2950
  const proofs = [];
2968
2951
  for (const tx of anchorTxList) {
2969
2952
  try {
2953
+ const chain = String(tx?.chain || detectPreferredChain() || 'base').toLowerCase();
2954
+ let provider;
2955
+ if (chain === 'base') {
2956
+ provider = new BaseAnchorProvider({ rpcUrl: process.env.ATEL_BASE_RPC_URL || 'https://mainnet.base.org' });
2957
+ } else if (chain === 'bsc') {
2958
+ provider = new BSCAnchorProvider({ rpcUrl: process.env.ATEL_BSC_RPC_URL || 'https://bsc-dataseed.binance.org' });
2959
+ } else {
2960
+ continue;
2961
+ }
2970
2962
  const result = await provider.verify(tx.trace_root || '', tx.txHash || tx.anchor_tx || '');
2971
2963
  if (result.valid) {
2972
2964
  verified++;
2973
- proofs.push({ proof_id: tx.proof_id || tx.txHash, trace_root: tx.trace_root, verified: true, anchor_tx: tx.txHash || tx.anchor_tx, timestamp: new Date().toISOString() });
2965
+ proofs.push({ proof_id: tx.proof_id || tx.txHash, trace_root: tx.trace_root, verified: true, anchor_tx: tx.txHash || tx.anchor_tx, chain, timestamp: new Date().toISOString() });
2974
2966
  }
2975
2967
  } catch {}
2976
2968
  }
@@ -3041,12 +3033,7 @@ async function anchorOnChain(traceRoot, metadata, preferredChain) {
3041
3033
  if (!key) return null;
3042
3034
 
3043
3035
  let provider;
3044
- if (chain === 'solana') {
3045
- provider = new SolanaAnchorProvider({
3046
- rpcUrl: process.env.ATEL_SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com',
3047
- privateKey: key
3048
- });
3049
- } else if (chain === 'base') {
3036
+ if (chain === 'base') {
3050
3037
  provider = new BaseAnchorProvider({
3051
3038
  rpcUrl: process.env.ATEL_BASE_RPC_URL || 'https://mainnet.base.org',
3052
3039
  privateKey: key,
@@ -3161,16 +3148,9 @@ async function configureAnchor() {
3161
3148
  // 3. Validate private key and derive address
3162
3149
  let address;
3163
3150
  try {
3164
- if (chain === 'solana') {
3165
- const { Keypair } = await import('@solana/web3.js');
3166
- const bs58 = (await import('bs58')).default;
3167
- const kp = Keypair.fromSecretKey(bs58.decode(privateKey));
3168
- address = kp.publicKey.toBase58();
3169
- } else {
3170
- const { ethers } = await import('ethers');
3171
- const wallet = new ethers.Wallet(privateKey);
3172
- address = wallet.address;
3173
- }
3151
+ const { ethers } = await import('ethers');
3152
+ const wallet = new ethers.Wallet(privateKey);
3153
+ address = wallet.address;
3174
3154
  } catch (e) {
3175
3155
  console.error(`❌ Invalid ${chain.toUpperCase()} private key: ${e.message}`);
3176
3156
  process.exit(1);
@@ -3734,14 +3714,6 @@ async function cmdStart(port) {
3734
3714
  if (!txHash || !traceRoot) return { checked: false, verified: false, reason: 'missing_anchor_or_root' };
3735
3715
  const c = (chain || 'base').toLowerCase();
3736
3716
 
3737
- if (c === 'solana') {
3738
- const rpcUrl = process.env.ATEL_SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com';
3739
- if (process.env.ATEL_DEBUG) console.error('[DEBUG] Solana RPC URL:', rpcUrl);
3740
- const provider = new SolanaAnchorProvider({ rpcUrl });
3741
- const r = await provider.verify(traceRoot, txHash);
3742
- if (process.env.ATEL_DEBUG) console.error('[DEBUG] Solana verify result:', r);
3743
- return { checked: true, verified: !!r?.valid, chain: 'solana', detail: r?.detail };
3744
- }
3745
3717
  if (c === 'base') {
3746
3718
  const rpcUrl = process.env.ATEL_BASE_RPC_URL || 'https://mainnet.base.org';
3747
3719
  if (process.env.ATEL_DEBUG) console.error('[DEBUG] Base RPC URL:', rpcUrl);
@@ -4303,6 +4275,15 @@ async function cmdStart(port) {
4303
4275
  'urllib.request.urlopen(req, timeout=10).read()',
4304
4276
  'PY',
4305
4277
  ].join('\n'),
4278
+ executor_milestone: [
4279
+ "python3 - <<'PY'",
4280
+ 'import json, urllib.request',
4281
+ `result = """Write the final deliverable here"""`,
4282
+ `body = {"dedupeKey": "${dedupeKey}", "status": "done", "eventType": "${eventType}", "orderId": "${payload?.orderId || ''}", "milestoneIndex": ${Number.isFinite(Number(payload?.currentMilestone)) ? Number(payload.currentMilestone) : Number.isFinite(Number(payload?.milestoneIndex)) ? Number(payload.milestoneIndex) : 0}, "result": result, "summary": result[:120]}`,
4283
+ `req = urllib.request.Request("${callbackUrl}", data=json.dumps(body).encode(), headers={"Content-Type": "application/json"})`,
4284
+ 'urllib.request.urlopen(req, timeout=10).read()',
4285
+ 'PY',
4286
+ ].join('\n'),
4306
4287
  default: [
4307
4288
  "python3 - <<'PY'",
4308
4289
  'import json, urllib.request',
@@ -4323,7 +4304,11 @@ async function cmdStart(port) {
4323
4304
  'PY',
4324
4305
  ].join('\n');
4325
4306
 
4326
- const callbackDone = eventType === 'milestone_submitted' ? callbackExamples.milestone_submitted : callbackExamples.default;
4307
+ const callbackDone = eventType === 'milestone_submitted'
4308
+ ? callbackExamples.milestone_submitted
4309
+ : (['milestone_plan_confirmed', 'milestone_verified', 'milestone_rejected'].includes(eventType)
4310
+ ? callbackExamples.executor_milestone
4311
+ : callbackExamples.default);
4327
4312
  const contextFile = join(cwd, 'ORDER_CONTEXT.md');
4328
4313
  const allowRepoAccess = shouldAllowRepoAccess(payload);
4329
4314
  const fileAccessRule = allowRepoAccess
@@ -4371,7 +4356,7 @@ ${callbackFailed}
4371
4356
  `;
4372
4357
  }
4373
4358
 
4374
- function buildLocalAgentPrompt(eventType, promptText) {
4359
+ function buildLocalAgentPrompt(eventType, promptText, payload = {}) {
4375
4360
  if (eventType === 'milestone_submitted') {
4376
4361
  return `${promptText}
4377
4362
 
@@ -4387,6 +4372,10 @@ For rejection:
4387
4372
  }
4388
4373
 
4389
4374
  if (['milestone_plan_confirmed', 'milestone_verified', 'milestone_rejected'].includes(eventType)) {
4375
+ const expectedIndex = eventType === 'milestone_plan_confirmed'
4376
+ ? (Number.isFinite(Number(payload?.milestoneIndex)) ? Number(payload.milestoneIndex) : 0)
4377
+ : (Number.isFinite(Number(payload?.currentMilestone)) ? Number(payload.currentMilestone) : Number.isFinite(Number(payload?.milestoneIndex)) ? Number(payload.milestoneIndex) : 0);
4378
+ const expectedOrderId = String(payload?.orderId || '').trim();
4390
4379
  return `${promptText}
4391
4380
 
4392
4381
  Important: you are not chatting with the user.
@@ -4400,9 +4389,11 @@ You must prefer factual evidence:
4400
4389
  - what concrete output / state / observation you actually got
4401
4390
  - whether that evidence satisfies the current milestone
4402
4391
  If you could not find evidence, say that explicitly in the JSON result, including what you checked.
4392
+ The JSON must include the exact current order and milestone identity.
4393
+ If you are not certain, fail closed instead of guessing.
4403
4394
 
4404
4395
  Format:
4405
- {"result":"factual deliverable with concrete evidence and observations only"}`;
4396
+ {"orderId":"${expectedOrderId}","milestoneIndex":${expectedIndex},"result":"factual deliverable with concrete evidence and observations only"}`;
4406
4397
  }
4407
4398
 
4408
4399
  return promptText;
@@ -4531,7 +4522,7 @@ Format:
4531
4522
  if (isKnownInvalidLocalAgentStdout(cleaned)) {
4532
4523
  return { ok: false, error: 'invalid_local_agent_stdout_known_failure' };
4533
4524
  }
4534
- return buildAgentCallbackAction(eventType, payload, { result: cleaned, summary: cleaned });
4525
+ return { ok: false, error: 'invalid_local_agent_stdout_requires_json' };
4535
4526
  }
4536
4527
  }
4537
4528
 
@@ -5301,15 +5292,15 @@ Advance the current milestone strictly based on these approved results. Do not i
5301
5292
  return;
5302
5293
  }
5303
5294
  log({ event: 'agent_session_spawn_error', eventType: hookEvent, dedupeKey: hookKey, error: gatewayResult.error, fallback: 'cli' });
5304
- spawnArgs[spawnArgs.length - 1] = buildLocalAgentPrompt(hookEvent, promptArg);
5295
+ spawnArgs[spawnArgs.length - 1] = buildLocalAgentPrompt(hookEvent, promptArg, hookPayload);
5305
5296
  } else if (spawnArgs.length > 0) {
5306
5297
  const promptArg = spawnArgs[spawnArgs.length - 1] || '';
5307
- spawnArgs[spawnArgs.length - 1] = buildLocalAgentPrompt(hookEvent, promptArg);
5298
+ spawnArgs[spawnArgs.length - 1] = buildLocalAgentPrompt(hookEvent, promptArg, hookPayload);
5308
5299
  }
5309
5300
 
5310
5301
  const MAX_ATTEMPTS = 5;
5311
5302
  const isMilestoneHook = ['milestone_plan_confirmed', 'milestone_verified', 'milestone_rejected', 'milestone_submitted'].includes(hookEvent);
5312
- const localHookTimeoutMs = isMilestoneHook ? 90000 : 600000;
5303
+ const localHookTimeoutMs = isMilestoneHook ? 240000 : 600000;
5313
5304
  const preparedInvocation = prepareHookInvocation(spawnCmd, spawnArgs, hookKey, Math.ceil(localHookTimeoutMs / 1000));
5314
5305
  const runHook = (attempt, invocation = preparedInvocation) => {
5315
5306
  const hookStartedAt = Date.now();
@@ -5607,7 +5598,7 @@ Advance the current milestone strictly based on these approved results. Do not i
5607
5598
 
5608
5599
  // ── Anchoring Warning ──
5609
5600
  if (!anchor) {
5610
- log({ event: 'anchor_missing', taskId, warning: 'Proof not anchored on-chain. Set ATEL_SOLANA_PRIVATE_KEY for verifiable trust.', timestamp: new Date().toISOString() });
5601
+ log({ event: 'anchor_missing', taskId, warning: 'Proof not anchored on-chain. Set a supported anchor key for verifiable trust.', timestamp: new Date().toISOString() });
5611
5602
  }
5612
5603
 
5613
5604
 
@@ -7193,22 +7184,32 @@ async function cmdVerifyProof(anchorTx, traceRoot) {
7193
7184
 
7194
7185
  console.log(JSON.stringify({ event: 'verifying_proof', anchor_tx: anchorTx, trace_root: traceRoot }));
7195
7186
 
7196
- const rpcUrl = process.env.ATEL_SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com';
7197
- try {
7198
- const provider = new SolanaAnchorProvider({ rpcUrl });
7199
- const result = await provider.verify(traceRoot, anchorTx);
7200
- console.log(JSON.stringify({
7201
- verified: result.valid,
7202
- chain: 'solana',
7203
- anchor_tx: anchorTx,
7204
- trace_root: traceRoot,
7205
- detail: result.detail || (result.valid ? 'Memo matches trace_root' : 'Memo does not match'),
7206
- block: result.blockNumber,
7207
- timestamp: result.timestamp,
7208
- }, null, 2));
7209
- } catch (e) {
7210
- console.log(JSON.stringify({ verified: false, error: e.message }));
7187
+ const providers = [
7188
+ { chain: 'base', provider: new BaseAnchorProvider({ rpcUrl: process.env.ATEL_BASE_RPC_URL || 'https://mainnet.base.org' }) },
7189
+ { chain: 'bsc', provider: new BSCAnchorProvider({ rpcUrl: process.env.ATEL_BSC_RPC_URL || 'https://bsc-dataseed.binance.org' }) },
7190
+ ];
7191
+ const attempts = [];
7192
+ for (const entry of providers) {
7193
+ try {
7194
+ const result = await entry.provider.verify(traceRoot, anchorTx);
7195
+ attempts.push({ chain: entry.chain, result });
7196
+ if (result.valid) {
7197
+ console.log(JSON.stringify({
7198
+ verified: true,
7199
+ chain: entry.chain,
7200
+ anchor_tx: anchorTx,
7201
+ trace_root: traceRoot,
7202
+ detail: result.detail || 'Anchor matches trace_root',
7203
+ block: result.blockNumber,
7204
+ timestamp: result.timestamp,
7205
+ }, null, 2));
7206
+ return;
7207
+ }
7208
+ } catch (e) {
7209
+ attempts.push({ chain: entry.chain, error: e.message });
7210
+ }
7211
7211
  }
7212
+ console.log(JSON.stringify({ verified: false, anchor_tx: anchorTx, trace_root: traceRoot, attempts }, null, 2));
7212
7213
  }
7213
7214
 
7214
7215
  async function cmdAudit(targetDidOrUrl, taskId) {
@@ -7320,13 +7321,16 @@ async function cmdRotate() {
7320
7321
 
7321
7322
  // Anchor rotation on-chain if possible
7322
7323
  let anchor = null;
7323
- const key = process.env.ATEL_SOLANA_PRIVATE_KEY;
7324
+ const chain = detectPreferredChain() || 'base';
7325
+ const key = getChainPrivateKey(chain);
7324
7326
  if (key) {
7325
7327
  try {
7326
- const s = new SolanaAnchorProvider({ rpcUrl: process.env.ATEL_SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com', privateKey: key });
7328
+ const provider = chain === 'bsc'
7329
+ ? new BSCAnchorProvider({ rpcUrl: process.env.ATEL_BSC_RPC_URL || 'https://bsc-dataseed.binance.org', privateKey: key })
7330
+ : new BaseAnchorProvider({ rpcUrl: process.env.ATEL_BASE_RPC_URL || 'https://mainnet.base.org', privateKey: key, anchorRegistryAddress: process.env.ATEL_ANCHOR_REGISTRY_ADDRESS || undefined });
7327
7331
  const { createHash } = await import('node:crypto');
7328
7332
  const rotationHash = createHash('sha256').update(JSON.stringify(proof)).digest('hex');
7329
- anchor = await s.anchor(`rotation:${rotationHash}`, { oldDid, newDid: newIdentity.did, type: 'key_rotation' });
7333
+ anchor = await provider.anchor(`rotation:${rotationHash}`, { oldDid, newDid: newIdentity.did, type: 'key_rotation' });
7330
7334
  } catch (e) { console.log(JSON.stringify({ warning: 'On-chain anchor failed', error: e.message })); }
7331
7335
  }
7332
7336
 
@@ -7524,6 +7528,10 @@ async function cmdWithdraw(amount, address, chain) {
7524
7528
  }
7525
7529
  // Smart wallet withdrawal (new flow)
7526
7530
  chain = chain || 'base';
7531
+ if (chain !== 'base' && chain !== 'bsc') {
7532
+ console.error('Unsupported withdrawal chain. Supported chains: base, bsc.');
7533
+ process.exit(1);
7534
+ }
7527
7535
 
7528
7536
  // Validate destination address
7529
7537
  const channel = 'crypto_' + chain;
@@ -7532,11 +7540,6 @@ async function cmdWithdraw(amount, address, chain) {
7532
7540
  console.error('Invalid EVM address. Must be 0x followed by 40 hex characters.');
7533
7541
  process.exit(1);
7534
7542
  }
7535
- } else if (channel === 'crypto_solana') {
7536
- if (!address.match(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/)) {
7537
- console.error('Invalid Solana address.');
7538
- process.exit(1);
7539
- }
7540
7543
  }
7541
7544
 
7542
7545
  try {
@@ -8232,9 +8235,9 @@ async function cmdComplete(orderId, taskId) {
8232
8235
  console.error('[complete] Anchoring proof on-chain...');
8233
8236
  anchor = await anchorOnChain(proof.trace_root, { proof_id: proof.proof_id, executorDid: id.did, requesterDid, taskId: effectiveTaskId, action: 'cli-complete' });
8234
8237
  if (anchor) {
8235
- console.error(`[complete] Anchored on Solana: ${anchor.txHash}`);
8238
+ console.error(`[complete] Anchored on-chain: ${anchor.txHash}`);
8236
8239
  } else {
8237
- console.error('[complete] WARNING: On-chain anchoring failed. Set ATEL_SOLANA_PRIVATE_KEY for verifiable trust.');
8240
+ console.error('[complete] WARNING: On-chain anchoring failed. Set a supported anchor key for verifiable trust.');
8238
8241
  }
8239
8242
  }
8240
8243
 
@@ -10790,8 +10793,6 @@ Environment:
10790
10793
  ATEL_REGISTRY Registry URL (default: https://api.atelai.org)
10791
10794
  ATEL_PLATFORM Platform URL (default: ATEL_REGISTRY value)
10792
10795
  ATEL_EXECUTOR_URL Local executor HTTP endpoint
10793
- ATEL_SOLANA_PRIVATE_KEY Solana key for on-chain anchoring
10794
- ATEL_SOLANA_RPC_URL Solana RPC (default: mainnet-beta)
10795
10796
  ATEL_BASE_PRIVATE_KEY Base chain key for on-chain anchoring
10796
10797
  ATEL_BSC_PRIVATE_KEY BSC chain key for on-chain anchoring
10797
10798
 
@@ -57,11 +57,13 @@ export function shouldSkipAgentHook(eventType, directExecutionSucceeded) {
57
57
  return eventType === 'order_accepted' && directExecutionSucceeded;
58
58
  }
59
59
 
60
+ const EXECUTOR_MILESTONE_EVENTS = new Set(['milestone_plan_confirmed', 'milestone_verified', 'milestone_rejected']);
61
+
60
62
  export function shouldUseGatewaySession(eventType) {
61
- // Keep gateway sub-sessions only for explicit P2P task execution.
62
- // Milestone automation must not depend on gateway callback health; use the
63
- // local structured fallback so order progression cannot stall on subagent I/O.
64
- return eventType === 'p2p_task';
63
+ // Use isolated gateway sub-sessions for milestone executor turns so each
64
+ // order+milestone attempt runs in a clean room instead of sharing the main
65
+ // agent runtime context.
66
+ return eventType === 'p2p_task' || EXECUTOR_MILESTONE_EVENTS.has(eventType);
65
67
  }
66
68
 
67
69
  export function normalizeGatewayBind(bind) {
@@ -81,6 +83,44 @@ function normalizeResult(value) {
81
83
  return value.trim();
82
84
  }
83
85
 
86
+ function normalizeOrderId(value) {
87
+ return typeof value === 'string' ? value.trim() : '';
88
+ }
89
+
90
+ function extractForeignOrderId(text, expectedOrderId) {
91
+ const current = normalizeOrderId(expectedOrderId);
92
+ if (!current) return '';
93
+ const found = Array.from(String(text || '').matchAll(/ord-[a-f0-9-]+/g)).map((m) => m[0]);
94
+ return found.find((item) => item !== current) || '';
95
+ }
96
+
97
+ function validateExecutorMilestoneBody(eventType, payload, body) {
98
+ if (!EXECUTOR_MILESTONE_EVENTS.has(eventType)) return { ok: true };
99
+ const expectedOrderId = normalizeOrderId(payload?.orderId);
100
+ if (!expectedOrderId) return { ok: false, error: 'missing_order_id' };
101
+ const actualOrderId = normalizeOrderId(body?.orderId);
102
+ if (!actualOrderId) return { ok: false, error: 'missing_result_order_id' };
103
+ if (actualOrderId !== expectedOrderId) return { ok: false, error: 'mismatched_result_order_id' };
104
+
105
+ const expectedIndex = eventType === 'milestone_plan_confirmed'
106
+ ? normalizeIndex(payload?.milestoneIndex, 0)
107
+ : normalizeIndex(payload?.currentMilestone ?? payload?.milestoneIndex, 0);
108
+ if (!Number.isFinite(Number(body?.milestoneIndex))) return { ok: false, error: 'missing_result_milestone_index' };
109
+ const actualIndex = normalizeIndex(body?.milestoneIndex, -1);
110
+ if (actualIndex !== expectedIndex) return { ok: false, error: 'mismatched_result_milestone_index' };
111
+
112
+ const result = normalizeResult(body?.result || body?.summary);
113
+ if (!result) return { ok: false, error: 'missing_result' };
114
+ const lowered = result.toLowerCase();
115
+ if (lowered.includes('invalid_cross_order_reference')) return { ok: false, error: 'invalid_cross_order_reference' };
116
+ if (lowered.includes('context overflow')) return { ok: false, error: 'context_overflow_output' };
117
+ if (lowered.includes('plugin register() called') || lowered.includes('plugin registration complete')) return { ok: false, error: 'plugin_noise_output' };
118
+ if (lowered.includes('session file locked') || lowered.includes('session locked')) return { ok: false, error: 'session_locked_output' };
119
+ const foreign = extractForeignOrderId(result, expectedOrderId);
120
+ if (foreign) return { ok: false, error: 'foreign_order_reference_detected' };
121
+ return { ok: true, result, orderId: expectedOrderId, milestoneIndex: expectedIndex };
122
+ }
123
+
84
124
  export function buildAgentCallbackAction(eventType, payload, body) {
85
125
  if (eventType === 'p2p_task') {
86
126
  const taskId = payload?.taskId;
@@ -102,44 +142,41 @@ export function buildAgentCallbackAction(eventType, payload, body) {
102
142
  if (!orderId) return { ok: false, error: 'missing_order_id' };
103
143
 
104
144
  if (eventType === 'milestone_plan_confirmed') {
105
- const result = normalizeResult(body?.result || body?.summary);
106
- if (!result) return { ok: false, error: 'missing_result' };
107
- const index = normalizeIndex(payload?.milestoneIndex, 0);
145
+ const validated = validateExecutorMilestoneBody(eventType, payload, body);
146
+ if (!validated.ok) return validated;
108
147
  return {
109
148
  ok: true,
110
149
  action: {
111
150
  type: 'cli',
112
151
  action: 'submit_milestone',
113
- command: ['atel', 'milestone-submit', orderId, String(index), '--result', result],
152
+ command: ['atel', 'milestone-submit', orderId, String(validated.milestoneIndex), '--result', validated.result],
114
153
  },
115
154
  };
116
155
  }
117
156
 
118
157
  if (eventType === 'milestone_verified') {
119
158
  if (payload?.allComplete) return { ok: false, skipped: true, reason: 'all_complete' };
120
- const result = normalizeResult(body?.result || body?.summary);
121
- if (!result) return { ok: false, error: 'missing_result' };
122
- const index = normalizeIndex(payload?.currentMilestone, 0);
159
+ const validated = validateExecutorMilestoneBody(eventType, payload, body);
160
+ if (!validated.ok) return validated;
123
161
  return {
124
162
  ok: true,
125
163
  action: {
126
164
  type: 'cli',
127
165
  action: 'submit_milestone',
128
- command: ['atel', 'milestone-submit', orderId, String(index), '--result', result],
166
+ command: ['atel', 'milestone-submit', orderId, String(validated.milestoneIndex), '--result', validated.result],
129
167
  },
130
168
  };
131
169
  }
132
170
 
133
171
  if (eventType === 'milestone_rejected') {
134
- const result = normalizeResult(body?.result || body?.summary);
135
- if (!result) return { ok: false, error: 'missing_result' };
136
- const index = normalizeIndex(payload?.milestoneIndex, 0);
172
+ const validated = validateExecutorMilestoneBody(eventType, payload, body);
173
+ if (!validated.ok) return validated;
137
174
  return {
138
175
  ok: true,
139
176
  action: {
140
177
  type: 'cli',
141
178
  action: 'resubmit',
142
- command: ['atel', 'milestone-submit', orderId, String(index), '--result', result],
179
+ command: ['atel', 'milestone-submit', orderId, String(validated.milestoneIndex), '--result', validated.result],
143
180
  },
144
181
  };
145
182
  }
@@ -5,11 +5,11 @@
5
5
  * (proof_id, trace_root, etc.) on public blockchains for tamper-evident
6
6
  * timestamping and auditability.
7
7
  *
8
- * Supported chains: Base (EVM), BSC (EVM), Solana.
8
+ * Supported chains: Base (EVM) and BSC (EVM).
9
9
  * A MockAnchorProvider is included for testing without real chain access.
10
10
  */
11
11
  /** Supported chain identifiers */
12
- export type ChainId = 'base' | 'solana' | 'bsc' | 'mock';
12
+ export type ChainId = 'base' | 'bsc' | 'mock';
13
13
  /**
14
14
  * A record of a hash anchored on-chain.
15
15
  */
@@ -169,5 +169,4 @@ export declare class AnchorManager {
169
169
  export { EvmAnchorProvider, type EvmAnchorConfig, type EvmAnchorMemoV2 } from './evm.js';
170
170
  export { BaseAnchorProvider } from './base.js';
171
171
  export { BSCAnchorProvider } from './bsc.js';
172
- export { SolanaAnchorProvider, type SolanaAnchorConfig, type AnchorMemoV2 } from './solana.js';
173
172
  export { MockAnchorProvider } from './mock.js';
@@ -5,7 +5,7 @@
5
5
  * (proof_id, trace_root, etc.) on public blockchains for tamper-evident
6
6
  * timestamping and auditability.
7
7
  *
8
- * Supported chains: Base (EVM), BSC (EVM), Solana.
8
+ * Supported chains: Base (EVM) and BSC (EVM).
9
9
  * A MockAnchorProvider is included for testing without real chain access.
10
10
  */
11
11
  // ─── AnchorManager ───────────────────────────────────────────────
@@ -161,5 +161,4 @@ export class AnchorManager {
161
161
  export { EvmAnchorProvider } from './evm.js';
162
162
  export { BaseAnchorProvider } from './base.js';
163
163
  export { BSCAnchorProvider } from './bsc.js';
164
- export { SolanaAnchorProvider } from './solana.js';
165
164
  export { MockAnchorProvider } from './mock.js';
@@ -55,7 +55,6 @@ export interface EndpointConfig {
55
55
  rateLimit?: RateLimitConfig;
56
56
  /** Wallet addresses for on-chain trust verification during handshake */
57
57
  wallets?: {
58
- solana?: string;
59
58
  base?: string;
60
59
  bsc?: string;
61
60
  };
@@ -124,7 +123,6 @@ export declare class AgentClient {
124
123
  * Establishes both identity verification and E2E encryption.
125
124
  */
126
125
  handshake(remoteEndpoint: string, handshakeManager: HandshakeManager, remoteDid: string, wallets?: {
127
- solana?: string;
128
126
  base?: string;
129
127
  bsc?: string;
130
128
  }): Promise<import('../handshake/index.js').Session>;
@@ -16,7 +16,6 @@ import { EncryptionManager } from '../crypto/index.js';
16
16
  /** Wallet addresses with DID-signed proof of ownership */
17
17
  export interface WalletBundle {
18
18
  addresses: {
19
- solana?: string;
20
19
  base?: string;
21
20
  bsc?: string;
22
21
  };
@@ -32,7 +31,6 @@ export interface HandshakeInitPayload {
32
31
  capabilities?: string[];
33
32
  /** Wallet addresses for on-chain trust verification */
34
33
  wallets?: {
35
- solana?: string;
36
34
  base?: string;
37
35
  bsc?: string;
38
36
  };
@@ -49,7 +47,6 @@ export interface HandshakeAckPayload {
49
47
  capabilities?: string[];
50
48
  /** Wallet addresses for on-chain trust verification */
51
49
  wallets?: {
52
- solana?: string;
53
50
  base?: string;
54
51
  bsc?: string;
55
52
  };
@@ -76,7 +73,6 @@ export interface Session {
76
73
  remoteCapabilities?: string[];
77
74
  /** Remote agent's wallet addresses (if provided) */
78
75
  remoteWallets?: {
79
- solana?: string;
80
76
  base?: string;
81
77
  bsc?: string;
82
78
  };
@@ -103,7 +99,6 @@ export declare class HandshakeError extends Error {
103
99
  }
104
100
  /** Create a signed wallet bundle proving DID ownership of wallet addresses */
105
101
  export declare function createWalletBundle(addresses: {
106
- solana?: string;
107
102
  base?: string;
108
103
  bsc?: string;
109
104
  }, secretKey: Uint8Array): WalletBundle;
@@ -131,7 +126,6 @@ export declare class HandshakeManager {
131
126
  * Create a handshake_init message (Step 1).
132
127
  */
133
128
  createInit(remoteDid: string, wallets?: {
134
- solana?: string;
135
129
  base?: string;
136
130
  bsc?: string;
137
131
  }): ATELMessage<HandshakeInitPayload>;
@@ -146,7 +140,6 @@ export declare class HandshakeManager {
146
140
  * Process a handshake_init message and create handshake_ack (Step 2).
147
141
  */
148
142
  processInit(initMessage: ATELMessage<HandshakeInitPayload>, wallets?: {
149
- solana?: string;
150
143
  base?: string;
151
144
  bsc?: string;
152
145
  }): ATELMessage<HandshakeAckPayload>;
@@ -154,7 +147,6 @@ export declare class HandshakeManager {
154
147
  * Process a handshake_confirm message (Step 3, responder side).
155
148
  */
156
149
  processConfirm(confirmMessage: ATELMessage<HandshakeConfirmPayload>, initiatorPublicKey: Uint8Array, initiatorCapabilities?: string[], initiatorWallets?: {
157
- solana?: string;
158
150
  base?: string;
159
151
  bsc?: string;
160
152
  }, initiatorWalletBundle?: WalletBundle): Session;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lawrenceliang-btc/atel-sdk",
3
- "version": "1.2.14",
3
+ "version": "1.2.15",
4
4
  "description": "ATEL Protocol SDK - Agent Trust & Exchange Layer",
5
5
  "repository": {
6
6
  "type": "git",
@@ -49,7 +49,6 @@
49
49
  "prepublishOnly": "npm run clean && npm run build && npm test"
50
50
  },
51
51
  "dependencies": {
52
- "@solana/web3.js": "^1.98.4",
53
52
  "ajv": "^8.17.1",
54
53
  "ajv-formats": "^3.0.1",
55
54
  "bs58": "^6.0.0",
@@ -523,7 +523,7 @@ atel withdraw 5 crypto_bsc <外部钱包地址> # 提现到用户的外部钱
523
523
  atel transactions # 交易记录
524
524
  ```
525
525
 
526
- 支持的充值渠道:`crypto_solana`、`crypto_base`、`crypto_bsc`、`stripe`、`alipay`、`manual`
526
+ 支持的充值渠道:`crypto_base`、`crypto_bsc`、`stripe`、`alipay`、`manual`
527
527
 
528
528
  ### 提现流程
529
529
 
@@ -33,27 +33,16 @@ Minimum commission: 0.5%. Free orders (price=0) have no commission.
33
33
  ## Payment Channels
34
34
 
35
35
  - `manual` — Transfer and contact admin for confirmation
36
- - `crypto_solana` — Solana on-chain deposit (auto-verified)
37
36
  - `crypto_base` — Base chain deposit (auto-verified)
38
37
  - `crypto_bsc` — BSC deposit (auto-verified)
39
38
 
40
- ### Solana deposit warning
41
-
42
- If guiding the owner to deposit USDC on Solana, explicitly tell them the transfer must use **`transferChecked`**.
43
- Do not describe plain `transfer` as acceptable for Solana USDC deposits.
44
-
45
- Operational rule:
46
- - when suggesting or explaining Solana USDC deposit steps, explicitly state that `transferChecked` is required
47
- - if a Solana deposit is pending but not recognized, check whether the user sent USDC with `transfer` instead of `transferChecked`
48
- - if the owner cannot ensure `transferChecked`, prefer recommending `crypto_base` or `crypto_bsc` instead of giving risky Solana instructions
49
-
50
39
  ### Deposit Info API
51
40
 
52
41
  Get platform deposit addresses (no auth required):
53
42
 
54
43
  ```bash
55
44
  curl https://api.atelai.org/account/v1/deposit-info
56
- # Returns: { "chains": [{ "chain": "solana", "address": "...", "minAmount": 5 }, ...] }
45
+ # Returns: { "chains": [{ "chain": "base", "address": "...", "minAmount": 5 }, ...] }
57
46
  ```
58
47
 
59
48
  ## Marketplace
@@ -139,8 +128,8 @@ Agents can withdraw funds from their platform balance:
139
128
  # Withdraw to Base wallet (instant on-chain transfer)
140
129
  atel withdraw 50 crypto_base 0xYOUR_WALLET_ADDRESS
141
130
 
142
- # Withdraw to Solana wallet (instant on-chain transfer)
143
- atel withdraw 50 crypto_solana YOUR_SOLANA_ADDRESS
131
+ # Withdraw to Base wallet (instant on-chain transfer)
132
+ atel withdraw 50 crypto_base YOUR_BASE_ADDRESS
144
133
 
145
134
  # Withdraw to BSC wallet (instant on-chain transfer)
146
135
  atel withdraw 50 crypto_bsc YOUR_BSC_ADDRESS
@@ -355,7 +355,7 @@ and pay gas). You do not need to configure any chain private key to receive
355
355
  paid orders.
356
356
 
357
357
  Legacy V1 behaviour (opt-in only, not recommended): if `atel anchor config` was
358
- run, the SDK will also detect `ATEL_SOLANA_PRIVATE_KEY`, `ATEL_BASE_PRIVATE_KEY`,
358
+ run, the SDK will also detect `ATEL_BASE_PRIVATE_KEY`,
359
359
  `ATEL_BSC_PRIVATE_KEY` environment variables and set `preferredChain` in the
360
360
  registry metadata as a marketplace hint. This is purely cosmetic — it does not
361
361
  affect whether you can execute a paid order.
@@ -5,9 +5,13 @@ Every completed task generates a cryptographic proof (ExecutionTrace → Merkle
5
5
  ## Setup
6
6
 
7
7
  ```bash
8
- # Solana (primary, ~$0.001/tx)
9
- export ATEL_SOLANA_PRIVATE_KEY=<base58-private-key>
10
- export ATEL_SOLANA_RPC_URL=https://api.mainnet-beta.solana.com
8
+ # Base (primary)
9
+ export ATEL_BASE_PRIVATE_KEY=<hex-private-key>
10
+ export ATEL_BASE_RPC_URL=https://mainnet.base.org
11
+
12
+ # BSC (secondary)
13
+ export ATEL_BSC_PRIVATE_KEY=<hex-private-key>
14
+ export ATEL_BSC_RPC_URL=https://bsc-dataseed.binance.org
11
15
 
12
16
  # Base (optional)
13
17
  export ATEL_BASE_PRIVATE_KEY=<hex-key-with-0x>
@@ -61,7 +61,7 @@ Always ask before deciding any of the following:
61
61
  anchors on behalf of agents using its own registered executor wallets and pays
62
62
  gas. The user's smart wallet (AA) is automatically derived from the ATEL
63
63
  identity key — no separate chain key is ever required to send or receive paid
64
- orders. Any prompt asking for a "Base / BSC / Solana private key" is a legacy
64
+ orders. Any prompt asking for a "Base / BSC private key" is a legacy
65
65
  V1 flow and should be declined.
66
66
 
67
67
  Rules:
@@ -92,10 +92,6 @@ If the owner's preferred language is known, use the owner's language instead.
92
92
 
93
93
  Do not spam the owner with every retry or low-level infrastructure event.
94
94
 
95
- ## Solana deposit caution
96
-
97
- If guiding the owner to deposit USDC on Solana, tell them the transfer must use `transferChecked`.
98
- Do not describe plain `transfer` as acceptable for Solana USDC deposits.
99
95
 
100
96
  ## Verify after upgrade
101
97
 
@@ -80,7 +80,7 @@ Agents below the threshold for a given risk level are rejected. Chain-verified p
80
80
  # Local-only (default, uses .atel/trust-history.json)
81
81
  atel check <did> medium
82
82
 
83
- # Chain-verified (queries Solana/Base/BSC RPC)
83
+ # Chain-verified (queries Base/BSC RPC)
84
84
  atel check <did> medium --chain
85
85
  ```
86
86
 
@@ -13,7 +13,7 @@ This includes:
13
13
  **Do NOT ask the owner for any on-chain private key.** In V2 the Platform
14
14
  anchors on behalf of agents using its own registered executor wallets and
15
15
  pays gas. The user's smart wallet (AA) is automatically derived from the
16
- ATEL identity key — no separate Base/BSC/Solana key is ever required to
16
+ ATEL identity key — no separate Base/BSC key is ever required to
17
17
  send or receive paid orders. Any prompt asking for a raw chain private key
18
18
  is a legacy V1 flow and should be declined.
19
19
 
@@ -1,95 +0,0 @@
1
- /**
2
- * Solana Anchor Provider.
3
- *
4
- * Anchors hashes on Solana using the official Memo Program
5
- * (`MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr`).
6
- *
7
- * The memo content is formatted as `ATEL_ANCHOR:<hash>` so anchored
8
- * transactions are easily identifiable.
9
- *
10
- * @remarks
11
- * ⚠️ SECURITY: The `privateKey` (Base58-encoded) is used to sign
12
- * transactions. Never hard-code it — use environment variables or a vault.
13
- */
14
- import type { AnchorProvider, AnchorRecord, AnchorVerification } from './index.js';
15
- /** Configuration for the Solana anchor provider */
16
- export interface SolanaAnchorConfig {
17
- /** Solana JSON-RPC endpoint URL */
18
- rpcUrl: string;
19
- /**
20
- * Base58-encoded private key for signing transactions.
21
- * Optional — if omitted the provider can only verify / lookup.
22
- *
23
- * ⚠️ SECURITY: Keep this value secret.
24
- */
25
- privateKey?: string;
26
- }
27
- /** Structured anchor metadata for v2 memo */
28
- export interface AnchorMemoV2 {
29
- version: 1;
30
- executorDid: string;
31
- requesterDid: string;
32
- taskId: string;
33
- traceRoot: string;
34
- }
35
- /**
36
- * Anchor provider for the Solana blockchain.
37
- */
38
- export declare class SolanaAnchorProvider implements AnchorProvider {
39
- readonly name = "Solana";
40
- readonly chain = "solana";
41
- /** Solana RPC connection */
42
- private readonly connection;
43
- /** Keypair for signing (undefined when no private key is supplied) */
44
- private readonly keypair?;
45
- /** Default Solana mainnet-beta RPC URL */
46
- static readonly DEFAULT_RPC_URL = "https://api.mainnet-beta.solana.com";
47
- /**
48
- * @param config - RPC URL and optional private key.
49
- * If `rpcUrl` is omitted, the Solana mainnet-beta default is used.
50
- */
51
- constructor(config?: Partial<SolanaAnchorConfig>);
52
- /**
53
- * Encode a hash into the memo data buffer (v2 structured format).
54
- * Falls back to legacy format if no metadata provided.
55
- */
56
- static encodeMemo(hash: string, meta?: {
57
- executorDid?: string;
58
- requesterDid?: string;
59
- taskId?: string;
60
- }): Buffer;
61
- /**
62
- * Decode a hash from memo data. Supports both v2 structured and legacy format.
63
- *
64
- * @returns The decoded hash, or `null` if the data doesn't match.
65
- */
66
- static decodeMemo(data: Buffer | Uint8Array | string): string | null;
67
- /**
68
- * Decode full structured memo (v2 only).
69
- * Returns null for legacy format memos.
70
- */
71
- static decodeMemoV2(data: Buffer | Uint8Array | string): AnchorMemoV2 | null;
72
- /** @inheritdoc */
73
- anchor(hash: string, metadata?: Record<string, unknown>): Promise<AnchorRecord>;
74
- /** @inheritdoc */
75
- verify(hash: string, txHash: string): Promise<AnchorVerification>;
76
- /** @inheritdoc */
77
- lookup(hash: string): Promise<AnchorRecord[]>;
78
- /** @inheritdoc */
79
- isAvailable(): Promise<boolean>;
80
- /**
81
- * Query all ATEL anchor transactions for a given wallet address.
82
- * Parses v2 structured memos to extract DID and task info.
83
- *
84
- * @param walletAddress - Solana wallet public key (base58)
85
- * @param options - limit (default 100), filterDid (only return records involving this DID)
86
- * @returns Array of parsed anchor memos with tx info
87
- */
88
- queryByWallet(walletAddress: string, options?: {
89
- limit?: number;
90
- filterDid?: string;
91
- }): Promise<Array<AnchorMemoV2 & {
92
- txHash: string;
93
- blockTime?: number;
94
- }>>;
95
- }
@@ -1,298 +0,0 @@
1
- /**
2
- * Solana Anchor Provider.
3
- *
4
- * Anchors hashes on Solana using the official Memo Program
5
- * (`MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr`).
6
- *
7
- * The memo content is formatted as `ATEL_ANCHOR:<hash>` so anchored
8
- * transactions are easily identifiable.
9
- *
10
- * @remarks
11
- * ⚠️ SECURITY: The `privateKey` (Base58-encoded) is used to sign
12
- * transactions. Never hard-code it — use environment variables or a vault.
13
- */
14
- import { Connection, Keypair, PublicKey, Transaction, TransactionInstruction, sendAndConfirmTransaction, } from '@solana/web3.js';
15
- import bs58 from 'bs58';
16
- /** Solana Memo Program address */
17
- const MEMO_PROGRAM_ID = new PublicKey('MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr');
18
- /** Prefix prepended to the hash in the memo (legacy format) */
19
- const ANCHOR_PREFIX = 'ATEL_ANCHOR:';
20
- /** V2 structured memo format: ATEL:1:<executorDID>:<requesterDID>:<taskId>:<trace_root> */
21
- const ANCHOR_V2_PREFIX = 'ATEL:1:';
22
- /**
23
- * Anchor provider for the Solana blockchain.
24
- */
25
- export class SolanaAnchorProvider {
26
- name = 'Solana';
27
- chain = 'solana';
28
- /** Solana RPC connection */
29
- connection;
30
- /** Keypair for signing (undefined when no private key is supplied) */
31
- keypair;
32
- /** Default Solana mainnet-beta RPC URL */
33
- static DEFAULT_RPC_URL = 'https://api.mainnet-beta.solana.com';
34
- /**
35
- * @param config - RPC URL and optional private key.
36
- * If `rpcUrl` is omitted, the Solana mainnet-beta default is used.
37
- */
38
- constructor(config) {
39
- this.connection = new Connection(config?.rpcUrl ?? SolanaAnchorProvider.DEFAULT_RPC_URL, 'confirmed');
40
- if (config?.privateKey) {
41
- // ⚠️ SECURITY: The keypair holds the private key in memory.
42
- const secretKey = bs58.decode(config.privateKey);
43
- this.keypair = Keypair.fromSecretKey(secretKey);
44
- }
45
- }
46
- /**
47
- * Encode a hash into the memo data buffer (v2 structured format).
48
- * Falls back to legacy format if no metadata provided.
49
- */
50
- static encodeMemo(hash, meta) {
51
- if (meta?.executorDid && meta?.requesterDid && meta?.taskId) {
52
- return Buffer.from(`${ANCHOR_V2_PREFIX}${meta.executorDid}:${meta.requesterDid}:${meta.taskId}:${hash}`, 'utf-8');
53
- }
54
- return Buffer.from(`${ANCHOR_PREFIX}${hash}`, 'utf-8');
55
- }
56
- /**
57
- * Decode a hash from memo data. Supports both v2 structured and legacy format.
58
- *
59
- * @returns The decoded hash, or `null` if the data doesn't match.
60
- */
61
- static decodeMemo(data) {
62
- const text = typeof data === 'string' ? data : Buffer.from(data).toString('utf-8');
63
- // V2 structured format
64
- if (text.startsWith(ANCHOR_V2_PREFIX)) {
65
- const parts = text.slice(ANCHOR_V2_PREFIX.length).split(':');
66
- if (parts.length >= 4)
67
- return parts[parts.length - 1]; // last part is trace_root
68
- }
69
- // Legacy format
70
- if (text.startsWith(ANCHOR_PREFIX)) {
71
- return text.slice(ANCHOR_PREFIX.length);
72
- }
73
- return null;
74
- }
75
- /**
76
- * Decode full structured memo (v2 only).
77
- * Returns null for legacy format memos.
78
- */
79
- static decodeMemoV2(data) {
80
- const text = typeof data === 'string' ? data : Buffer.from(data).toString('utf-8');
81
- if (!text.startsWith(ANCHOR_V2_PREFIX))
82
- return null;
83
- const rest = text.slice(ANCHOR_V2_PREFIX.length);
84
- // Format: executorDID:requesterDID:taskId:traceRoot
85
- // DIDs contain colons (did:atel:ed25519:xxx), so we need smart parsing
86
- // Split by ':' and reconstruct DIDs
87
- const parts = rest.split(':');
88
- // Minimum: did:atel:ed25519:key : did:atel:ed25519:key : taskId : traceRoot = 4+4+1+1 = 10 parts
89
- if (parts.length < 10)
90
- return null;
91
- // Each DID is 4 parts: did:atel:ed25519:base58
92
- const executorDid = parts.slice(0, 4).join(':');
93
- const requesterDid = parts.slice(4, 8).join(':');
94
- const taskId = parts[8];
95
- const traceRoot = parts.slice(9).join(':');
96
- if (!executorDid.startsWith('did:atel:') || !requesterDid.startsWith('did:atel:'))
97
- return null;
98
- return { version: 1, executorDid, requesterDid, taskId, traceRoot };
99
- }
100
- /** @inheritdoc */
101
- async anchor(hash, metadata) {
102
- if (!this.keypair) {
103
- throw new Error('Solana: Cannot anchor without a private key');
104
- }
105
- const memoData = SolanaAnchorProvider.encodeMemo(hash, metadata);
106
- const instruction = new TransactionInstruction({
107
- keys: [{ pubkey: this.keypair.publicKey, isSigner: true, isWritable: true }],
108
- programId: MEMO_PROGRAM_ID,
109
- data: memoData,
110
- });
111
- const transaction = new Transaction().add(instruction);
112
- try {
113
- const signature = await sendAndConfirmTransaction(this.connection, transaction, [this.keypair], { commitment: 'confirmed' });
114
- // Fetch the confirmed transaction to get slot/block info
115
- let blockNumber;
116
- try {
117
- const txInfo = await this.connection.getTransaction(signature, {
118
- commitment: 'confirmed',
119
- maxSupportedTransactionVersion: 0,
120
- });
121
- blockNumber = txInfo?.slot;
122
- }
123
- catch {
124
- // Non-critical
125
- }
126
- return {
127
- hash,
128
- txHash: signature,
129
- chain: 'solana',
130
- timestamp: Date.now(),
131
- blockNumber,
132
- metadata,
133
- };
134
- }
135
- catch (err) {
136
- const message = err instanceof Error ? err.message : String(err);
137
- throw new Error(`Solana anchor failed: ${message}`);
138
- }
139
- }
140
- /** @inheritdoc */
141
- async verify(hash, txHash) {
142
- try {
143
- const txInfo = await this.connection.getTransaction(txHash, {
144
- commitment: 'confirmed',
145
- maxSupportedTransactionVersion: 0,
146
- });
147
- if (!txInfo) {
148
- return {
149
- valid: false,
150
- hash,
151
- txHash,
152
- chain: this.chain,
153
- detail: 'Transaction not found',
154
- };
155
- }
156
- // Search through instructions for a memo matching our hash
157
- const message = txInfo.transaction.message;
158
- let foundHash = null;
159
- for (let i = 0; i < message.compiledInstructions.length; i++) {
160
- const ix = message.compiledInstructions[i];
161
- const keys = message.getAccountKeys();
162
- const programId = keys.get(ix.programIdIndex);
163
- if (programId?.equals(MEMO_PROGRAM_ID)) {
164
- foundHash = SolanaAnchorProvider.decodeMemo(Buffer.from(ix.data));
165
- if (foundHash)
166
- break;
167
- }
168
- }
169
- // Fallback: check log messages for memo content
170
- if (!foundHash && txInfo.meta?.logMessages) {
171
- for (const log of txInfo.meta.logMessages) {
172
- if (log.includes(ANCHOR_PREFIX)) {
173
- const idx = log.indexOf(ANCHOR_PREFIX);
174
- foundHash = log.slice(idx + ANCHOR_PREFIX.length);
175
- break;
176
- }
177
- }
178
- }
179
- if (foundHash === null) {
180
- return {
181
- valid: false,
182
- hash,
183
- txHash,
184
- chain: this.chain,
185
- detail: 'No anchor memo found in transaction',
186
- };
187
- }
188
- const valid = foundHash === hash;
189
- const blockTimestamp = txInfo.blockTime ? txInfo.blockTime * 1000 : undefined;
190
- return {
191
- valid,
192
- hash,
193
- txHash,
194
- chain: this.chain,
195
- blockTimestamp,
196
- detail: valid
197
- ? 'Hash matches on-chain memo'
198
- : `Hash mismatch: expected "${hash}", found "${foundHash}"`,
199
- };
200
- }
201
- catch (err) {
202
- const message = err instanceof Error ? err.message : String(err);
203
- return {
204
- valid: false,
205
- hash,
206
- txHash,
207
- chain: this.chain,
208
- detail: `Verification error: ${message}`,
209
- };
210
- }
211
- }
212
- /** @inheritdoc */
213
- async lookup(hash) {
214
- // On-chain lookup without an indexer is not feasible for Solana.
215
- // In production, integrate with a Solana indexer or Helius API.
216
- return [];
217
- }
218
- /** @inheritdoc */
219
- async isAvailable() {
220
- try {
221
- await this.connection.getSlot();
222
- return true;
223
- }
224
- catch {
225
- return false;
226
- }
227
- }
228
- /**
229
- * Query all ATEL anchor transactions for a given wallet address.
230
- * Parses v2 structured memos to extract DID and task info.
231
- *
232
- * @param walletAddress - Solana wallet public key (base58)
233
- * @param options - limit (default 100), filterDid (only return records involving this DID)
234
- * @returns Array of parsed anchor memos with tx info
235
- */
236
- async queryByWallet(walletAddress, options) {
237
- const limit = options?.limit ?? 100;
238
- const pubkey = new PublicKey(walletAddress);
239
- const results = [];
240
- try {
241
- const signatures = await this.connection.getSignaturesForAddress(pubkey, { limit });
242
- for (const sig of signatures) {
243
- try {
244
- const txInfo = await this.connection.getTransaction(sig.signature, {
245
- commitment: 'confirmed',
246
- maxSupportedTransactionVersion: 0,
247
- });
248
- if (!txInfo)
249
- continue;
250
- // Search for ATEL memo in instructions
251
- const message = txInfo.transaction.message;
252
- for (let i = 0; i < message.compiledInstructions.length; i++) {
253
- const ix = message.compiledInstructions[i];
254
- const keys = message.getAccountKeys();
255
- const programId = keys.get(ix.programIdIndex);
256
- if (!programId?.equals(MEMO_PROGRAM_ID))
257
- continue;
258
- const memoText = Buffer.from(ix.data).toString('utf-8');
259
- const parsed = SolanaAnchorProvider.decodeMemoV2(memoText);
260
- if (!parsed)
261
- continue;
262
- // Filter by DID if requested
263
- if (options?.filterDid && parsed.executorDid !== options.filterDid && parsed.requesterDid !== options.filterDid)
264
- continue;
265
- results.push({
266
- ...parsed,
267
- txHash: sig.signature,
268
- blockTime: txInfo.blockTime ? txInfo.blockTime * 1000 : undefined,
269
- });
270
- }
271
- // Fallback: check log messages
272
- if (txInfo.meta?.logMessages) {
273
- for (const log of txInfo.meta.logMessages) {
274
- if (!log.includes(ANCHOR_V2_PREFIX))
275
- continue;
276
- const idx = log.indexOf(ANCHOR_V2_PREFIX);
277
- const parsed = SolanaAnchorProvider.decodeMemoV2(log.slice(idx));
278
- if (!parsed)
279
- continue;
280
- if (options?.filterDid && parsed.executorDid !== options.filterDid && parsed.requesterDid !== options.filterDid)
281
- continue;
282
- if (!results.some(r => r.txHash === sig.signature)) {
283
- results.push({ ...parsed, txHash: sig.signature, blockTime: txInfo.blockTime ? txInfo.blockTime * 1000 : undefined });
284
- }
285
- }
286
- }
287
- }
288
- catch {
289
- // Skip individual tx failures
290
- }
291
- }
292
- }
293
- catch {
294
- // Query failed — return empty
295
- }
296
- return results;
297
- }
298
- }