@x1scroll/agent-sdk 1.1.2 → 1.2.0

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 +312 -5
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.0",
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;
@@ -56,10 +57,14 @@ const DEFAULT_RPC_URL = 'https://rpc.x1.xyz';
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
  // ─────────────────────────────────────────────────────────────────────────────
@@ -900,15 +926,296 @@ class AgentClient {
900
926
  // Most recent first
901
927
  return results.reverse();
902
928
  }
929
+
930
+ // ── v2 Methods ──────────────────────────────────────────────────────────────
931
+
932
+ /**
933
+ * Write a decision to the on-chain decision tree.
934
+ *
935
+ * @param {Keypair} agentKeypair The agent keypair (signer + fee payer)
936
+ * @param {string} branchLabel Strategy branch this decision belongs to
937
+ * @param {string} cid IPFS CID of the decision payload
938
+ * @param {number} outcome 0=pending, 1=executed, 2=rejected
939
+ * @param {number} confidence 0-10000 basis points (8200 = 82%)
940
+ * @param {Buffer|null} [parentHash] 32-byte parent decision hash; zeros for root
941
+ * @returns {Promise<{ sig: string, decisionHash: string, pda: string }>}
942
+ */
943
+ async decisionWrite(agentKeypair, branchLabel, cid, outcome, confidence, parentHash = null) {
944
+ assertIsSigner(agentKeypair, 'agentKeypair');
945
+ validateString(branchLabel, 'branchLabel', 64);
946
+ validateString(cid, 'cid', 64);
947
+
948
+ if (typeof outcome !== 'number' || ![0, 1, 2].includes(outcome)) {
949
+ throw new AgentSDKError('outcome must be 0 (pending), 1 (executed), or 2 (rejected)', 'INVALID_INPUT');
950
+ }
951
+ if (typeof confidence !== 'number' || confidence < 0 || confidence > 10000) {
952
+ throw new AgentSDKError('confidence must be 0-10000 basis points', 'INVALID_INPUT');
953
+ }
954
+
955
+ // Derive decision_hash: sha256(JSON.stringify({cid, branchLabel, timestamp}))
956
+ const timestamp = Date.now();
957
+ const decisionHash = sha256Str(JSON.stringify({ cid, branchLabel, timestamp }));
958
+
959
+ // parent_hash: provided Buffer(32) or zeros
960
+ const parentHashBuf = (parentHash instanceof Buffer && parentHash.length === 32)
961
+ ? parentHash
962
+ : Buffer.alloc(32);
963
+
964
+ // Derive AgentRecord PDA
965
+ const { pda: agentRecordPDA } = AgentClient.deriveAgentRecord(agentKeypair.publicKey);
966
+
967
+ // Derive DecisionRecord PDA: [b"decision", agentRecord, decision_hash]
968
+ const [decisionRecordPDA] = PublicKey.findProgramAddressSync(
969
+ [Buffer.from('decision'), agentRecordPDA.toBuffer(), decisionHash],
970
+ PROGRAM_ID
971
+ );
972
+
973
+ // Encode confidence as u32 LE
974
+ const confidenceBuf = Buffer.alloc(4);
975
+ confidenceBuf.writeUInt32LE(confidence, 0);
976
+
977
+ const data = Buffer.concat([
978
+ DISCRIMINATORS.decision_write,
979
+ decisionHash, // [u8;32]
980
+ parentHashBuf, // [u8;32]
981
+ encodeString(branchLabel), // string (4-byte LE len + utf8)
982
+ encodeString(cid), // string
983
+ Buffer.from([outcome]), // u8
984
+ confidenceBuf, // u32 LE
985
+ ]);
986
+
987
+ const ix = new TransactionInstruction({
988
+ programId: PROGRAM_ID,
989
+ keys: [
990
+ { pubkey: agentKeypair.publicKey, isSigner: true, isWritable: true }, // agent_authority
991
+ { pubkey: agentRecordPDA, isSigner: false, isWritable: true }, // agent_record
992
+ { pubkey: decisionRecordPDA, isSigner: false, isWritable: true }, // decision_record (init)
993
+ { pubkey: TREASURY, isSigner: false, isWritable: true }, // treasury
994
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, // system_program
995
+ ],
996
+ data,
997
+ });
998
+
999
+ const tx = new Transaction().add(ix);
1000
+ const sig = await this._sendAndConfirm(tx, [agentKeypair]);
1001
+
1002
+ return {
1003
+ sig,
1004
+ decisionHash: decisionHash.toString('hex'),
1005
+ pda: decisionRecordPDA.toBase58(),
1006
+ };
1007
+ }
1008
+
1009
+ /**
1010
+ * Open a new strategy branch on-chain.
1011
+ *
1012
+ * @param {Keypair} agentKeypair The agent keypair (signer + fee payer)
1013
+ * @param {string} label Branch label (max 32 chars — also used for PDA seed)
1014
+ * @param {string} hypothesis Branch hypothesis description (max 256 chars)
1015
+ * @param {string} [parentBranch] Parent branch label, or '' for top-level
1016
+ * @returns {Promise<{ sig: string, branchPda: string }>}
1017
+ */
1018
+ async branchOpen(agentKeypair, label, hypothesis, parentBranch = '') {
1019
+ assertIsSigner(agentKeypair, 'agentKeypair');
1020
+ validateString(label, 'label', 32);
1021
+ validateString(hypothesis, 'hypothesis', 256);
1022
+ if (parentBranch !== '') {
1023
+ validateString(parentBranch, 'parentBranch', 32);
1024
+ }
1025
+
1026
+ const { pda: agentRecordPDA } = AgentClient.deriveAgentRecord(agentKeypair.publicKey);
1027
+
1028
+ // Derive BranchRecord PDA: [b"branch", agentRecord, padLabel(label)]
1029
+ const [branchRecordPDA] = PublicKey.findProgramAddressSync(
1030
+ [Buffer.from('branch'), agentRecordPDA.toBuffer(), padLabel(label)],
1031
+ PROGRAM_ID
1032
+ );
1033
+
1034
+ const data = Buffer.concat([
1035
+ DISCRIMINATORS.strategy_branch_open,
1036
+ encodeString(label),
1037
+ encodeString(parentBranch),
1038
+ encodeString(hypothesis),
1039
+ ]);
1040
+
1041
+ const ix = new TransactionInstruction({
1042
+ programId: PROGRAM_ID,
1043
+ keys: [
1044
+ { pubkey: agentKeypair.publicKey, isSigner: true, isWritable: true }, // agent_authority
1045
+ { pubkey: agentRecordPDA, isSigner: false, isWritable: true }, // agent_record
1046
+ { pubkey: branchRecordPDA, isSigner: false, isWritable: true }, // branch_record (init)
1047
+ { pubkey: TREASURY, isSigner: false, isWritable: true }, // treasury
1048
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, // system_program
1049
+ ],
1050
+ data,
1051
+ });
1052
+
1053
+ const tx = new Transaction().add(ix);
1054
+ const sig = await this._sendAndConfirm(tx, [agentKeypair]);
1055
+
1056
+ return {
1057
+ sig,
1058
+ branchPda: branchRecordPDA.toBase58(),
1059
+ };
1060
+ }
1061
+
1062
+ /**
1063
+ * Close a strategy branch on-chain.
1064
+ *
1065
+ * @param {Keypair} agentKeypair The agent keypair (signer + fee payer)
1066
+ * @param {string} label Branch label to close (must match opened label)
1067
+ * @param {number} outcome 1=success, 2=failure, 3=abandoned
1068
+ * @param {string} summaryCid IPFS CID of the closing summary
1069
+ * @returns {Promise<{ sig: string }>}
1070
+ */
1071
+ async branchClose(agentKeypair, label, outcome, summaryCid) {
1072
+ assertIsSigner(agentKeypair, 'agentKeypair');
1073
+ validateString(label, 'label', 32);
1074
+ validateString(summaryCid, 'summaryCid', 64);
1075
+
1076
+ if (typeof outcome !== 'number' || ![1, 2, 3].includes(outcome)) {
1077
+ throw new AgentSDKError('outcome must be 1 (success), 2 (failure), or 3 (abandoned)', 'INVALID_INPUT');
1078
+ }
1079
+
1080
+ const { pda: agentRecordPDA } = AgentClient.deriveAgentRecord(agentKeypair.publicKey);
1081
+
1082
+ // Derive BranchRecord PDA: [b"branch", agentRecord, padLabel(label)]
1083
+ const [branchRecordPDA] = PublicKey.findProgramAddressSync(
1084
+ [Buffer.from('branch'), agentRecordPDA.toBuffer(), padLabel(label)],
1085
+ PROGRAM_ID
1086
+ );
1087
+
1088
+ const data = Buffer.concat([
1089
+ DISCRIMINATORS.strategy_branch_close,
1090
+ encodeString(label),
1091
+ Buffer.from([outcome]), // u8
1092
+ encodeString(summaryCid),
1093
+ ]);
1094
+
1095
+ const ix = new TransactionInstruction({
1096
+ programId: PROGRAM_ID,
1097
+ keys: [
1098
+ { pubkey: agentKeypair.publicKey, isSigner: true, isWritable: true }, // agent_authority
1099
+ { pubkey: agentRecordPDA, isSigner: false, isWritable: false }, // agent_record (NOT writable for close)
1100
+ { pubkey: branchRecordPDA, isSigner: false, isWritable: true }, // branch_record
1101
+ { pubkey: TREASURY, isSigner: false, isWritable: true }, // treasury
1102
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, // system_program
1103
+ ],
1104
+ data,
1105
+ });
1106
+
1107
+ const tx = new Transaction().add(ix);
1108
+ const sig = await this._sendAndConfirm(tx, [agentKeypair]);
1109
+
1110
+ return { sig };
1111
+ }
1112
+
1113
+ /**
1114
+ * Fetch last N decisions/memories from chain for an agent by reading transaction history.
1115
+ * Does NOT call serve_context on-chain (requires validator signature).
1116
+ * Reads transactions directly from RPC and filters for program instructions.
1117
+ *
1118
+ * @param {PublicKey|string} agentPda The AgentRecord PDA address
1119
+ * @param {number} [limit] Max number of entries to return (default 10)
1120
+ * @returns {Promise<{ entries: Array<{slot: number, sig: string, instruction: string, blockTime: number|null}>, count: number }>}
1121
+ */
1122
+ async contextGet(agentPda, limit = 10) {
1123
+ if (!Number.isInteger(limit) || limit < 1) {
1124
+ throw new AgentSDKError('limit must be a positive integer', 'INVALID_INPUT');
1125
+ }
1126
+
1127
+ const agentPdaPubkey = new PublicKey(agentPda);
1128
+ const connection = this._getConnection();
1129
+
1130
+ // Fetch recent signatures for this address
1131
+ const sigs = await connection.getSignaturesForAddress(agentPdaPubkey, { limit });
1132
+
1133
+ const entries = [];
1134
+
1135
+ // Fetch and parse each transaction
1136
+ await Promise.all(sigs.map(async (sigInfo) => {
1137
+ try {
1138
+ const tx = await connection.getParsedTransaction(sigInfo.signature, {
1139
+ maxSupportedTransactionVersion: 0,
1140
+ });
1141
+ if (!tx || !tx.transaction) return;
1142
+
1143
+ const instructions = tx.transaction.message.instructions || [];
1144
+ for (const ix of instructions) {
1145
+ const programId = ix.programId ? ix.programId.toBase58() : null;
1146
+ if (programId !== PROGRAM_ID.toBase58()) continue;
1147
+
1148
+ // Classify instruction type from data discriminator if available
1149
+ let instructionType = 'Unknown';
1150
+ if (ix.data) {
1151
+ try {
1152
+ const rawData = Buffer.from(bs58decode(ix.data));
1153
+ const disc = rawData.slice(0, 8);
1154
+ if (disc.equals(DISCRIMINATORS.store_memory)) instructionType = 'StoreMemory';
1155
+ else if (disc.equals(DISCRIMINATORS.decision_write)) instructionType = 'DecisionWrite';
1156
+ else if (disc.equals(DISCRIMINATORS.strategy_branch_open)) instructionType = 'BranchOpen';
1157
+ else if (disc.equals(DISCRIMINATORS.strategy_branch_close)) instructionType = 'BranchClose';
1158
+ else if (disc.equals(DISCRIMINATORS.register_agent)) instructionType = 'RegisterAgent';
1159
+ } catch (_) { /* unparseable — leave as Unknown */ }
1160
+ }
1161
+
1162
+ entries.push({
1163
+ slot: sigInfo.slot,
1164
+ sig: sigInfo.signature,
1165
+ instruction: instructionType,
1166
+ blockTime: sigInfo.blockTime || null,
1167
+ });
1168
+ break; // one entry per tx
1169
+ }
1170
+ } catch (_) {
1171
+ // skip transactions we can't parse
1172
+ }
1173
+ }));
1174
+
1175
+ // Sort by slot descending (most recent first)
1176
+ entries.sort((a, b) => b.slot - a.slot);
1177
+
1178
+ return { entries, count: entries.length };
1179
+ }
1180
+
1181
+ /**
1182
+ * Utility: derive the AgentRecord PDA for any public key.
1183
+ *
1184
+ * @param {PublicKey|string} agentPublicKey
1185
+ * @returns {{ pda: PublicKey, bump: number }}
1186
+ */
1187
+ getAgentPda(agentPublicKey) {
1188
+ const [pda, bump] = PublicKey.findProgramAddressSync(
1189
+ [Buffer.from('agent'), new PublicKey(agentPublicKey).toBuffer()],
1190
+ PROGRAM_ID
1191
+ );
1192
+ return { pda, bump };
1193
+ }
903
1194
  }
904
1195
 
905
1196
  // ─────────────────────────────────────────────────────────────────────────────
906
1197
  // Exports
907
1198
  // ─────────────────────────────────────────────────────────────────────────────
908
1199
 
1200
+ /**
1201
+ * Standalone utility: derive the AgentRecord PDA for any public key.
1202
+ * Mirrors AgentClient#getAgentPda but available without instantiating a client.
1203
+ *
1204
+ * @param {PublicKey|string} agentPublicKey
1205
+ * @returns {{ pda: PublicKey, bump: number }}
1206
+ */
1207
+ function getAgentPda(agentPublicKey) {
1208
+ const [pda, bump] = PublicKey.findProgramAddressSync(
1209
+ [Buffer.from('agent'), new PublicKey(agentPublicKey).toBuffer()],
1210
+ PROGRAM_ID
1211
+ );
1212
+ return { pda, bump };
1213
+ }
1214
+
909
1215
  module.exports = {
910
1216
  AgentClient,
911
1217
  AgentSDKError,
1218
+ getAgentPda,
912
1219
  PROGRAM_ID,
913
1220
  TREASURY,
914
1221
  DEFAULT_RPC_URL,