@x1scroll/agent-sdk 1.1.2 → 1.2.1

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/package.json +1 -1
  2. package/src/index.js +368 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x1scroll/agent-sdk",
3
- "version": "1.1.2",
3
+ "version": "1.2.1",
4
4
  "description": "Agent identity and on-chain memory protocol for X1 blockchain",
5
5
  "license": "BSL-1.1",
6
6
  "main": "src/index.js",
package/src/index.js CHANGED
@@ -21,7 +21,8 @@ const {
21
21
  SystemProgram,
22
22
  LAMPORTS_PER_SOL,
23
23
  } = require('@solana/web3.js');
24
- const bs58 = require('bs58');
24
+ const bs58 = require('bs58');
25
+ const crypto = require('crypto');
25
26
 
26
27
  // ── bs58 compat (v4 vs v5 API shape) ─────────────────────────────────────────
27
28
  const bs58encode = (typeof bs58.encode === 'function') ? bs58.encode : bs58.default.encode;
@@ -51,15 +52,19 @@ const PROGRAM_ID = new PublicKey('ECgaMEwH4KLSz3awDo1vz84mSrx5n6h1ZCrbmunB5UxB')
51
52
  */
52
53
  const TREASURY = new PublicKey('GmvrL1ymC9ENuQCUqymC9robGa9t9L59AbFiwhDDd4Ld');
53
54
 
54
- const DEFAULT_RPC_URL = 'https://rpc.x1.xyz';
55
+ const DEFAULT_RPC_URL = 'https://x1scroll.io/rpc';
55
56
 
56
57
  // ── Anchor instruction discriminators (sha256("global:<name>")[0..8]) ─────────
57
58
  // Pre-computed from the IDL. These are fixed for the deployed program version.
58
59
  const DISCRIMINATORS = {
59
- register_agent: Buffer.from([135, 157, 66, 55, 116, 253, 50, 45]),
60
- store_memory: Buffer.from([31, 139, 69, 89, 102, 57, 218, 246]),
61
- update_agent: Buffer.from([220, 76, 168, 212, 224, 211, 185, 76]),
62
- transfer_agent: Buffer.from([39, 202, 189, 195, 254, 40, 59, 198]),
60
+ register_agent: Buffer.from([135, 157, 66, 55, 116, 253, 50, 45]),
61
+ store_memory: Buffer.from([31, 139, 69, 89, 102, 57, 218, 246]),
62
+ update_agent: Buffer.from([220, 76, 168, 212, 224, 211, 185, 76]),
63
+ transfer_agent: Buffer.from([39, 202, 189, 195, 254, 40, 59, 198]),
64
+ // v2 discriminators — sha256("global:<name>")[0:8]
65
+ decision_write: crypto.createHash('sha256').update('global:decision_write').digest().slice(0, 8),
66
+ strategy_branch_open: crypto.createHash('sha256').update('global:strategy_branch_open').digest().slice(0, 8),
67
+ strategy_branch_close: crypto.createHash('sha256').update('global:strategy_branch_close').digest().slice(0, 8),
63
68
  };
64
69
 
65
70
  // ── Anchor account discriminators (sha256("account:<Name>")[0..8]) ─────────────
@@ -134,6 +139,27 @@ function encodeU64(n) {
134
139
  return buf;
135
140
  }
136
141
 
142
+ /**
143
+ * Pad or truncate a label string to exactly 32 bytes (for branch PDA seeds).
144
+ * @param {string} label
145
+ * @returns {Buffer} 32-byte buffer
146
+ */
147
+ function padLabel(label) {
148
+ const src = Buffer.from(label, 'utf8');
149
+ const out = Buffer.alloc(32);
150
+ src.copy(out, 0, 0, Math.min(src.length, 32));
151
+ return out;
152
+ }
153
+
154
+ /**
155
+ * Compute sha256 of a string and return a 32-byte Buffer.
156
+ * @param {string} s
157
+ * @returns {Buffer}
158
+ */
159
+ function sha256Str(s) {
160
+ return crypto.createHash('sha256').update(s).digest();
161
+ }
162
+
137
163
  // ─────────────────────────────────────────────────────────────────────────────
138
164
  // Borsh decoding helpers
139
165
  // ─────────────────────────────────────────────────────────────────────────────
@@ -329,6 +355,54 @@ class AgentClient {
329
355
  this._registryCacheExpiry = 0;
330
356
  }
331
357
 
358
+ /**
359
+ * Check RPC connectivity. Throws AgentSDKError with fallback suggestion if unreachable.
360
+ * @returns {Promise<{ ok: boolean, slot: number, rpcUrl: string }>}
361
+ */
362
+ async healthCheck() {
363
+ const conn = this._getConnection();
364
+ try {
365
+ const slot = await Promise.race([
366
+ conn.getSlot(),
367
+ new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 8000)),
368
+ ]);
369
+ return { ok: true, slot, rpcUrl: this.rpcUrl };
370
+ } catch (err) {
371
+ throw new AgentSDKError(
372
+ `RPC endpoint unreachable: ${this.rpcUrl}. ` +
373
+ `Try the public fallback: https://x1scroll.io/rpc\n` +
374
+ `Original error: ${err.message}`,
375
+ 'RPC_UNREACHABLE',
376
+ err
377
+ );
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Check that a keypair has sufficient XNT balance for an operation.
383
+ * Throws AgentSDKError with current balance and required amount if insufficient.
384
+ * @param {Keypair} keypair
385
+ * @param {number} requiredLamports
386
+ * @param {string} [operationName]
387
+ */
388
+ async _assertSufficientBalance(keypair, requiredLamports, operationName = 'operation') {
389
+ const conn = this._getConnection();
390
+ const balance = await conn.getBalance(keypair.publicKey);
391
+ // Add 5000 lamports buffer for network tx fee
392
+ const totalRequired = requiredLamports + 5000;
393
+ if (balance < totalRequired) {
394
+ const balanceXNT = (balance / 1e9).toFixed(6);
395
+ const requiredXNT = (totalRequired / 1e9).toFixed(6);
396
+ throw new AgentSDKError(
397
+ `Insufficient balance for ${operationName}. ` +
398
+ `Have: ${balanceXNT} XNT, Need: ${requiredXNT} XNT. ` +
399
+ `Fund wallet: ${keypair.publicKey.toBase58()}`,
400
+ 'INSUFFICIENT_BALANCE'
401
+ );
402
+ }
403
+ return balance;
404
+ }
405
+
332
406
  // ── Internal ────────────────────────────────────────────────────────────────
333
407
 
334
408
  /** @returns {Connection} */
@@ -492,6 +566,10 @@ class AgentClient {
492
566
 
493
567
  const humanKeypair = this.keypair;
494
568
  const agentPubkey = agentKeypair.publicKey;
569
+
570
+ // ── Pre-flight balance check (0.05 XNT registration fee) ──
571
+ await this._assertSufficientBalance(humanKeypair, 50_000_000, 'register_agent (0.05 XNT)');
572
+
495
573
  const { pda: agentRecordPDA } = AgentClient.deriveAgentRecord(agentPubkey);
496
574
 
497
575
  // Borsh-encode instruction data: discriminator + name + metadata_uri
@@ -900,15 +978,299 @@ class AgentClient {
900
978
  // Most recent first
901
979
  return results.reverse();
902
980
  }
981
+
982
+ // ── v2 Methods ──────────────────────────────────────────────────────────────
983
+
984
+ /**
985
+ * Write a decision to the on-chain decision tree.
986
+ *
987
+ * @param {Keypair} agentKeypair The agent keypair (signer + fee payer)
988
+ * @param {string} branchLabel Strategy branch this decision belongs to
989
+ * @param {string} cid IPFS CID of the decision payload
990
+ * @param {number} outcome 0=pending, 1=executed, 2=rejected
991
+ * @param {number} confidence 0-10000 basis points (8200 = 82%)
992
+ * @param {Buffer|null} [parentHash] 32-byte parent decision hash; zeros for root
993
+ * @returns {Promise<{ sig: string, decisionHash: string, pda: string }>}
994
+ */
995
+ async decisionWrite(agentKeypair, branchLabel, cid, outcome, confidence, parentHash = null) {
996
+ assertIsSigner(agentKeypair, 'agentKeypair');
997
+ validateString(branchLabel, 'branchLabel', 64);
998
+ validateString(cid, 'cid', 64);
999
+
1000
+ // ── Pre-flight balance check (0.001 XNT decision write fee) ──
1001
+ await this._assertSufficientBalance(agentKeypair, 1_000_000, 'decision_write (0.001 XNT)');
1002
+
1003
+ if (typeof outcome !== 'number' || ![0, 1, 2].includes(outcome)) {
1004
+ throw new AgentSDKError('outcome must be 0 (pending), 1 (executed), or 2 (rejected)', 'INVALID_INPUT');
1005
+ }
1006
+ if (typeof confidence !== 'number' || confidence < 0 || confidence > 10000) {
1007
+ throw new AgentSDKError('confidence must be 0-10000 basis points', 'INVALID_INPUT');
1008
+ }
1009
+
1010
+ // Derive decision_hash: sha256(JSON.stringify({cid, branchLabel, timestamp}))
1011
+ const timestamp = Date.now();
1012
+ const decisionHash = sha256Str(JSON.stringify({ cid, branchLabel, timestamp }));
1013
+
1014
+ // parent_hash: provided Buffer(32) or zeros
1015
+ const parentHashBuf = (parentHash instanceof Buffer && parentHash.length === 32)
1016
+ ? parentHash
1017
+ : Buffer.alloc(32);
1018
+
1019
+ // Derive AgentRecord PDA
1020
+ const { pda: agentRecordPDA } = AgentClient.deriveAgentRecord(agentKeypair.publicKey);
1021
+
1022
+ // Derive DecisionRecord PDA: [b"decision", agentRecord, decision_hash]
1023
+ const [decisionRecordPDA] = PublicKey.findProgramAddressSync(
1024
+ [Buffer.from('decision'), agentRecordPDA.toBuffer(), decisionHash],
1025
+ PROGRAM_ID
1026
+ );
1027
+
1028
+ // Encode confidence as u32 LE
1029
+ const confidenceBuf = Buffer.alloc(4);
1030
+ confidenceBuf.writeUInt32LE(confidence, 0);
1031
+
1032
+ const data = Buffer.concat([
1033
+ DISCRIMINATORS.decision_write,
1034
+ decisionHash, // [u8;32]
1035
+ parentHashBuf, // [u8;32]
1036
+ encodeString(branchLabel), // string (4-byte LE len + utf8)
1037
+ encodeString(cid), // string
1038
+ Buffer.from([outcome]), // u8
1039
+ confidenceBuf, // u32 LE
1040
+ ]);
1041
+
1042
+ const ix = new TransactionInstruction({
1043
+ programId: PROGRAM_ID,
1044
+ keys: [
1045
+ { pubkey: agentKeypair.publicKey, isSigner: true, isWritable: true }, // agent_authority
1046
+ { pubkey: agentRecordPDA, isSigner: false, isWritable: true }, // agent_record
1047
+ { pubkey: decisionRecordPDA, isSigner: false, isWritable: true }, // decision_record (init)
1048
+ { pubkey: TREASURY, isSigner: false, isWritable: true }, // treasury
1049
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, // system_program
1050
+ ],
1051
+ data,
1052
+ });
1053
+
1054
+ const tx = new Transaction().add(ix);
1055
+ const sig = await this._sendAndConfirm(tx, [agentKeypair]);
1056
+
1057
+ return {
1058
+ sig,
1059
+ decisionHash: decisionHash.toString('hex'),
1060
+ pda: decisionRecordPDA.toBase58(),
1061
+ };
1062
+ }
1063
+
1064
+ /**
1065
+ * Open a new strategy branch on-chain.
1066
+ *
1067
+ * @param {Keypair} agentKeypair The agent keypair (signer + fee payer)
1068
+ * @param {string} label Branch label (max 32 chars — also used for PDA seed)
1069
+ * @param {string} hypothesis Branch hypothesis description (max 256 chars)
1070
+ * @param {string} [parentBranch] Parent branch label, or '' for top-level
1071
+ * @returns {Promise<{ sig: string, branchPda: string }>}
1072
+ */
1073
+ async branchOpen(agentKeypair, label, hypothesis, parentBranch = '') {
1074
+ assertIsSigner(agentKeypair, 'agentKeypair');
1075
+ validateString(label, 'label', 32);
1076
+ validateString(hypothesis, 'hypothesis', 256);
1077
+ if (parentBranch !== '') {
1078
+ validateString(parentBranch, 'parentBranch', 32);
1079
+ }
1080
+
1081
+ const { pda: agentRecordPDA } = AgentClient.deriveAgentRecord(agentKeypair.publicKey);
1082
+
1083
+ // Derive BranchRecord PDA: [b"branch", agentRecord, padLabel(label)]
1084
+ const [branchRecordPDA] = PublicKey.findProgramAddressSync(
1085
+ [Buffer.from('branch'), agentRecordPDA.toBuffer(), padLabel(label)],
1086
+ PROGRAM_ID
1087
+ );
1088
+
1089
+ const data = Buffer.concat([
1090
+ DISCRIMINATORS.strategy_branch_open,
1091
+ encodeString(label),
1092
+ encodeString(parentBranch),
1093
+ encodeString(hypothesis),
1094
+ ]);
1095
+
1096
+ const ix = new TransactionInstruction({
1097
+ programId: PROGRAM_ID,
1098
+ keys: [
1099
+ { pubkey: agentKeypair.publicKey, isSigner: true, isWritable: true }, // agent_authority
1100
+ { pubkey: agentRecordPDA, isSigner: false, isWritable: true }, // agent_record
1101
+ { pubkey: branchRecordPDA, isSigner: false, isWritable: true }, // branch_record (init)
1102
+ { pubkey: TREASURY, isSigner: false, isWritable: true }, // treasury
1103
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, // system_program
1104
+ ],
1105
+ data,
1106
+ });
1107
+
1108
+ const tx = new Transaction().add(ix);
1109
+ const sig = await this._sendAndConfirm(tx, [agentKeypair]);
1110
+
1111
+ return {
1112
+ sig,
1113
+ branchPda: branchRecordPDA.toBase58(),
1114
+ };
1115
+ }
1116
+
1117
+ /**
1118
+ * Close a strategy branch on-chain.
1119
+ *
1120
+ * @param {Keypair} agentKeypair The agent keypair (signer + fee payer)
1121
+ * @param {string} label Branch label to close (must match opened label)
1122
+ * @param {number} outcome 1=success, 2=failure, 3=abandoned
1123
+ * @param {string} summaryCid IPFS CID of the closing summary
1124
+ * @returns {Promise<{ sig: string }>}
1125
+ */
1126
+ async branchClose(agentKeypair, label, outcome, summaryCid) {
1127
+ assertIsSigner(agentKeypair, 'agentKeypair');
1128
+ validateString(label, 'label', 32);
1129
+ validateString(summaryCid, 'summaryCid', 64);
1130
+
1131
+ if (typeof outcome !== 'number' || ![1, 2, 3].includes(outcome)) {
1132
+ throw new AgentSDKError('outcome must be 1 (success), 2 (failure), or 3 (abandoned)', 'INVALID_INPUT');
1133
+ }
1134
+
1135
+ const { pda: agentRecordPDA } = AgentClient.deriveAgentRecord(agentKeypair.publicKey);
1136
+
1137
+ // Derive BranchRecord PDA: [b"branch", agentRecord, padLabel(label)]
1138
+ const [branchRecordPDA] = PublicKey.findProgramAddressSync(
1139
+ [Buffer.from('branch'), agentRecordPDA.toBuffer(), padLabel(label)],
1140
+ PROGRAM_ID
1141
+ );
1142
+
1143
+ const data = Buffer.concat([
1144
+ DISCRIMINATORS.strategy_branch_close,
1145
+ encodeString(label),
1146
+ Buffer.from([outcome]), // u8
1147
+ encodeString(summaryCid),
1148
+ ]);
1149
+
1150
+ const ix = new TransactionInstruction({
1151
+ programId: PROGRAM_ID,
1152
+ keys: [
1153
+ { pubkey: agentKeypair.publicKey, isSigner: true, isWritable: true }, // agent_authority
1154
+ { pubkey: agentRecordPDA, isSigner: false, isWritable: false }, // agent_record (NOT writable for close)
1155
+ { pubkey: branchRecordPDA, isSigner: false, isWritable: true }, // branch_record
1156
+ { pubkey: TREASURY, isSigner: false, isWritable: true }, // treasury
1157
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, // system_program
1158
+ ],
1159
+ data,
1160
+ });
1161
+
1162
+ const tx = new Transaction().add(ix);
1163
+ const sig = await this._sendAndConfirm(tx, [agentKeypair]);
1164
+
1165
+ return { sig };
1166
+ }
1167
+
1168
+ /**
1169
+ * Fetch last N decisions/memories from chain for an agent by reading transaction history.
1170
+ * Does NOT call serve_context on-chain (requires validator signature).
1171
+ * Reads transactions directly from RPC and filters for program instructions.
1172
+ *
1173
+ * @param {PublicKey|string} agentPda The AgentRecord PDA address
1174
+ * @param {number} [limit] Max number of entries to return (default 10)
1175
+ * @returns {Promise<{ entries: Array<{slot: number, sig: string, instruction: string, blockTime: number|null}>, count: number }>}
1176
+ */
1177
+ async contextGet(agentPda, limit = 10) {
1178
+ if (!Number.isInteger(limit) || limit < 1) {
1179
+ throw new AgentSDKError('limit must be a positive integer', 'INVALID_INPUT');
1180
+ }
1181
+
1182
+ const agentPdaPubkey = new PublicKey(agentPda);
1183
+ const connection = this._getConnection();
1184
+
1185
+ // Fetch recent signatures for this address
1186
+ const sigs = await connection.getSignaturesForAddress(agentPdaPubkey, { limit });
1187
+
1188
+ const entries = [];
1189
+
1190
+ // Fetch and parse each transaction
1191
+ await Promise.all(sigs.map(async (sigInfo) => {
1192
+ try {
1193
+ const tx = await connection.getParsedTransaction(sigInfo.signature, {
1194
+ maxSupportedTransactionVersion: 0,
1195
+ });
1196
+ if (!tx || !tx.transaction) return;
1197
+
1198
+ const instructions = tx.transaction.message.instructions || [];
1199
+ for (const ix of instructions) {
1200
+ const programId = ix.programId ? ix.programId.toBase58() : null;
1201
+ if (programId !== PROGRAM_ID.toBase58()) continue;
1202
+
1203
+ // Classify instruction type from data discriminator if available
1204
+ let instructionType = 'Unknown';
1205
+ if (ix.data) {
1206
+ try {
1207
+ const rawData = Buffer.from(bs58decode(ix.data));
1208
+ const disc = rawData.slice(0, 8);
1209
+ if (disc.equals(DISCRIMINATORS.store_memory)) instructionType = 'StoreMemory';
1210
+ else if (disc.equals(DISCRIMINATORS.decision_write)) instructionType = 'DecisionWrite';
1211
+ else if (disc.equals(DISCRIMINATORS.strategy_branch_open)) instructionType = 'BranchOpen';
1212
+ else if (disc.equals(DISCRIMINATORS.strategy_branch_close)) instructionType = 'BranchClose';
1213
+ else if (disc.equals(DISCRIMINATORS.register_agent)) instructionType = 'RegisterAgent';
1214
+ } catch (_) { /* unparseable — leave as Unknown */ }
1215
+ }
1216
+
1217
+ entries.push({
1218
+ slot: sigInfo.slot,
1219
+ sig: sigInfo.signature,
1220
+ instruction: instructionType,
1221
+ blockTime: sigInfo.blockTime || null,
1222
+ });
1223
+ break; // one entry per tx
1224
+ }
1225
+ } catch (_) {
1226
+ // skip transactions we can't parse
1227
+ }
1228
+ }));
1229
+
1230
+ // Sort by slot descending (most recent first)
1231
+ entries.sort((a, b) => b.slot - a.slot);
1232
+
1233
+ return { entries, count: entries.length };
1234
+ }
1235
+
1236
+ /**
1237
+ * Utility: derive the AgentRecord PDA for any public key.
1238
+ *
1239
+ * @param {PublicKey|string} agentPublicKey
1240
+ * @returns {{ pda: PublicKey, bump: number }}
1241
+ */
1242
+ getAgentPda(agentPublicKey) {
1243
+ const [pda, bump] = PublicKey.findProgramAddressSync(
1244
+ [Buffer.from('agent'), new PublicKey(agentPublicKey).toBuffer()],
1245
+ PROGRAM_ID
1246
+ );
1247
+ return { pda, bump };
1248
+ }
903
1249
  }
904
1250
 
905
1251
  // ─────────────────────────────────────────────────────────────────────────────
906
1252
  // Exports
907
1253
  // ─────────────────────────────────────────────────────────────────────────────
908
1254
 
1255
+ /**
1256
+ * Standalone utility: derive the AgentRecord PDA for any public key.
1257
+ * Mirrors AgentClient#getAgentPda but available without instantiating a client.
1258
+ *
1259
+ * @param {PublicKey|string} agentPublicKey
1260
+ * @returns {{ pda: PublicKey, bump: number }}
1261
+ */
1262
+ function getAgentPda(agentPublicKey) {
1263
+ const [pda, bump] = PublicKey.findProgramAddressSync(
1264
+ [Buffer.from('agent'), new PublicKey(agentPublicKey).toBuffer()],
1265
+ PROGRAM_ID
1266
+ );
1267
+ return { pda, bump };
1268
+ }
1269
+
909
1270
  module.exports = {
910
1271
  AgentClient,
911
1272
  AgentSDKError,
1273
+ getAgentPda,
912
1274
  PROGRAM_ID,
913
1275
  TREASURY,
914
1276
  DEFAULT_RPC_URL,