@x1scroll/agent-sdk 1.0.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.
package/src/index.js ADDED
@@ -0,0 +1,744 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @x1scroll/agent-sdk
5
+ * Agent Identity Protocol — persistent agent identity and on-chain memory for X1 blockchain.
6
+ *
7
+ * Program ID: 52EW3sn2Tkq6EMnp86JWUzXrNzrFujpdEgovsjwapbAM (immutable)
8
+ * Treasury: HYP2VdVk2QNGKMBfWGFZpaFqMoqQkB7Vp5F12eSxCxtf (immutable)
9
+ * Network: X1 Mainnet
10
+ * License: BSL-1.1 — https://x1scroll.io/license
11
+ *
12
+ * https://x1scroll.io | https://github.com/x1scroll/agent-identity-sdk
13
+ */
14
+
15
+ const {
16
+ Keypair,
17
+ Connection,
18
+ PublicKey,
19
+ Transaction,
20
+ TransactionInstruction,
21
+ SystemProgram,
22
+ LAMPORTS_PER_SOL,
23
+ } = require('@solana/web3.js');
24
+ const bs58 = require('bs58');
25
+
26
+ // ── bs58 compat (v4 vs v5 API shape) ─────────────────────────────────────────
27
+ const bs58encode = (typeof bs58.encode === 'function') ? bs58.encode : bs58.default.encode;
28
+ const bs58decode = (typeof bs58.decode === 'function') ? bs58.decode : bs58.default.decode;
29
+
30
+ // ── Protocol constants — hardcoded, do not change ─────────────────────────────
31
+ /**
32
+ * On-chain program address. Immutable — this SDK only talks to this program.
33
+ * Forks that swap this address are out of the protocol.
34
+ */
35
+ const PROGRAM_ID = new PublicKey('52EW3sn2Tkq6EMnp86JWUzXrNzrFujpdEgovsjwapbAM');
36
+
37
+ /**
38
+ * Fee collector wallet. Built into every instruction on-chain.
39
+ * Developers don't configure fees — the program handles it automatically.
40
+ */
41
+ const TREASURY = new PublicKey('HYP2VdVk2QNGKMBfWGFZpaFqMoqQkB7Vp5F12eSxCxtf');
42
+
43
+ const DEFAULT_RPC_URL = 'https://rpc.x1.xyz';
44
+
45
+ // ── Anchor instruction discriminators (sha256("global:<name>")[0..8]) ─────────
46
+ // Pre-computed from the IDL. These are fixed for the deployed program version.
47
+ const DISCRIMINATORS = {
48
+ register_agent: Buffer.from([135, 157, 66, 55, 116, 253, 50, 45]),
49
+ store_memory: Buffer.from([31, 139, 69, 89, 102, 57, 218, 246]),
50
+ update_agent: Buffer.from([220, 76, 168, 212, 224, 211, 185, 76]),
51
+ transfer_agent: Buffer.from([39, 202, 189, 195, 254, 40, 59, 198]),
52
+ };
53
+
54
+ // ── Anchor account discriminators (sha256("account:<Name>")[0..8]) ─────────────
55
+ const ACCOUNT_DISCRIMINATORS = {
56
+ AgentRecord: Buffer.from([145, 32, 212, 194, 68, 255, 174, 93]),
57
+ MemoryEntry: Buffer.from([118, 222, 57, 170, 233, 233, 20, 38]),
58
+ };
59
+
60
+ // ─────────────────────────────────────────────────────────────────────────────
61
+ // Errors
62
+ // ─────────────────────────────────────────────────────────────────────────────
63
+
64
+ class AgentSDKError extends Error {
65
+ /**
66
+ * @param {string} message
67
+ * @param {string} [code]
68
+ * @param {any} [cause]
69
+ */
70
+ constructor(message, code, cause) {
71
+ super(message);
72
+ this.name = 'AgentSDKError';
73
+ this.code = code || 'UNKNOWN';
74
+ this.cause = cause || null;
75
+ }
76
+ }
77
+
78
+ // ─────────────────────────────────────────────────────────────────────────────
79
+ // Encoding helpers
80
+ // ─────────────────────────────────────────────────────────────────────────────
81
+
82
+ /**
83
+ * Encode a Borsh string: 4-byte LE u32 length prefix + UTF-8 bytes.
84
+ * @param {string} s
85
+ * @returns {Buffer}
86
+ */
87
+ function encodeString(s) {
88
+ const bytes = Buffer.from(s, 'utf8');
89
+ const prefix = Buffer.alloc(4);
90
+ prefix.writeUInt32LE(bytes.length, 0);
91
+ return Buffer.concat([prefix, bytes]);
92
+ }
93
+
94
+ /**
95
+ * Encode a Borsh Vec<String>: 4-byte LE u32 count + each item encoded as string.
96
+ * @param {string[]} arr
97
+ * @returns {Buffer}
98
+ */
99
+ function encodeStringVec(arr) {
100
+ const countBuf = Buffer.alloc(4);
101
+ countBuf.writeUInt32LE(arr.length, 0);
102
+ const items = arr.map(encodeString);
103
+ return Buffer.concat([countBuf, ...items]);
104
+ }
105
+
106
+ /**
107
+ * Encode a bool as a single byte.
108
+ * @param {boolean} b
109
+ * @returns {Buffer}
110
+ */
111
+ function encodeBool(b) {
112
+ return Buffer.from([b ? 1 : 0]);
113
+ }
114
+
115
+ /**
116
+ * Encode a u64 as 8-byte little-endian buffer.
117
+ * @param {number|bigint} n
118
+ * @returns {Buffer}
119
+ */
120
+ function encodeU64(n) {
121
+ const buf = Buffer.alloc(8);
122
+ buf.writeBigUInt64LE(BigInt(n), 0);
123
+ return buf;
124
+ }
125
+
126
+ // ─────────────────────────────────────────────────────────────────────────────
127
+ // Borsh decoding helpers
128
+ // ─────────────────────────────────────────────────────────────────────────────
129
+
130
+ class BorshReader {
131
+ constructor(data) {
132
+ this.buf = data;
133
+ this.offset = 0;
134
+ }
135
+
136
+ readU8() {
137
+ const v = this.buf.readUInt8(this.offset);
138
+ this.offset += 1;
139
+ return v;
140
+ }
141
+
142
+ readU32() {
143
+ const v = this.buf.readUInt32LE(this.offset);
144
+ this.offset += 4;
145
+ return v;
146
+ }
147
+
148
+ readU64() {
149
+ const v = this.buf.readBigUInt64LE(this.offset);
150
+ this.offset += 8;
151
+ return Number(v);
152
+ }
153
+
154
+ readI64() {
155
+ const v = this.buf.readBigInt64LE(this.offset);
156
+ this.offset += 8;
157
+ return Number(v);
158
+ }
159
+
160
+ readBytes(n) {
161
+ const v = this.buf.slice(this.offset, this.offset + n);
162
+ this.offset += n;
163
+ return v;
164
+ }
165
+
166
+ readPublicKey() {
167
+ return new PublicKey(this.readBytes(32));
168
+ }
169
+
170
+ readString() {
171
+ const len = this.readU32();
172
+ const bytes = this.readBytes(len);
173
+ return bytes.toString('utf8');
174
+ }
175
+
176
+ readStringVec() {
177
+ const count = this.readU32();
178
+ const items = [];
179
+ for (let i = 0; i < count; i++) {
180
+ items.push(this.readString());
181
+ }
182
+ return items;
183
+ }
184
+
185
+ readBool() {
186
+ return this.readU8() !== 0;
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Decode an AgentRecord account (skip 8-byte discriminator).
192
+ * @param {Buffer} data
193
+ * @returns {object}
194
+ */
195
+ function decodeAgentRecord(data) {
196
+ const r = new BorshReader(data.slice(8));
197
+ return {
198
+ human: r.readPublicKey().toBase58(),
199
+ agentPubkey: r.readPublicKey().toBase58(),
200
+ name: r.readString(),
201
+ metadataUri: r.readString(),
202
+ createdAt: r.readI64(),
203
+ memoryCount: r.readU64(),
204
+ lastActive: r.readI64(),
205
+ bump: r.readU8(),
206
+ };
207
+ }
208
+
209
+ /**
210
+ * Decode a MemoryEntry account (skip 8-byte discriminator).
211
+ * @param {Buffer} data
212
+ * @returns {object}
213
+ */
214
+ function decodeMemoryEntry(data) {
215
+ const r = new BorshReader(data.slice(8));
216
+ return {
217
+ agent: r.readPublicKey().toBase58(),
218
+ topic: r.readString(),
219
+ cid: r.readString(),
220
+ tags: r.readStringVec(),
221
+ encrypted: r.readBool(),
222
+ timestamp: r.readI64(),
223
+ slot: r.readU64(),
224
+ bump: r.readU8(),
225
+ };
226
+ }
227
+
228
+ // ─────────────────────────────────────────────────────────────────────────────
229
+ // Validation helpers
230
+ // ─────────────────────────────────────────────────────────────────────────────
231
+
232
+ /**
233
+ * Reject any string containing null bytes (to prevent injection/truncation bugs).
234
+ * @param {string} s
235
+ * @param {string} field
236
+ */
237
+ function rejectNullBytes(s, field) {
238
+ if (s.includes('\0')) {
239
+ throw new AgentSDKError(`${field} must not contain null bytes`, 'INVALID_INPUT');
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Validate a string field: non-empty, no null bytes, within max length.
245
+ * @param {string} value
246
+ * @param {string} field
247
+ * @param {number} maxLen
248
+ */
249
+ function validateString(value, field, maxLen) {
250
+ if (typeof value !== 'string' || value.length === 0) {
251
+ throw new AgentSDKError(`${field} must be a non-empty string`, 'INVALID_INPUT');
252
+ }
253
+ rejectNullBytes(value, field);
254
+ if (value.length > maxLen) {
255
+ throw new AgentSDKError(`${field} exceeds ${maxLen} character limit`, 'INVALID_INPUT');
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Assert that the provided keypair is a Signer (has a secretKey).
261
+ * This prevents callers from passing a PublicKey where a signing keypair is required,
262
+ * which would allow PDA squatting or unauthorised instruction submission.
263
+ * @param {any} keypair
264
+ * @param {string} paramName
265
+ */
266
+ function assertIsSigner(keypair, paramName) {
267
+ if (!keypair || !keypair.secretKey) {
268
+ throw new AgentSDKError(
269
+ `${paramName} must be a Keypair with a secretKey — a PublicKey alone cannot sign transactions. ` +
270
+ 'This validation prevents PDA squatting.',
271
+ 'NOT_A_SIGNER'
272
+ );
273
+ }
274
+ }
275
+
276
+ // ─────────────────────────────────────────────────────────────────────────────
277
+ // AgentClient — main SDK class
278
+ // ─────────────────────────────────────────────────────────────────────────────
279
+
280
+ class AgentClient {
281
+ /**
282
+ * Create an AgentClient instance.
283
+ *
284
+ * @param {object} opts
285
+ * @param {import('@solana/web3.js').Keypair|string|null} [opts.wallet]
286
+ * The human/operator wallet — a @solana/web3.js Keypair OR a base58 secret key string.
287
+ * Pass null for read-only use (getAgent, getMemory, listMemories).
288
+ * @param {string} [opts.rpcUrl]
289
+ * X1 RPC endpoint (default: https://rpc.x1.xyz).
290
+ * Use https://rpc.x1scroll.io for our dedicated node.
291
+ */
292
+ constructor({ wallet = null, rpcUrl = DEFAULT_RPC_URL } = {}) {
293
+ // ── resolve wallet ──
294
+ if (wallet === null || wallet === undefined) {
295
+ this.keypair = null;
296
+ this.walletAddress = null;
297
+ } else if (wallet instanceof Keypair) {
298
+ this.keypair = wallet;
299
+ this.walletAddress = wallet.publicKey.toBase58();
300
+ } else if (typeof wallet === 'string') {
301
+ const secretKey = bs58decode(wallet);
302
+ this.keypair = Keypair.fromSecretKey(secretKey);
303
+ this.walletAddress = this.keypair.publicKey.toBase58();
304
+ } else if (wallet && wallet.secretKey && wallet.publicKey) {
305
+ // Keypair-like object
306
+ this.keypair = wallet;
307
+ this.walletAddress = wallet.publicKey.toBase58();
308
+ } else {
309
+ throw new AgentSDKError(
310
+ 'wallet must be a Keypair, a base58 secret key string, or null',
311
+ 'INVALID_WALLET'
312
+ );
313
+ }
314
+
315
+ this.rpcUrl = rpcUrl;
316
+ this._connection = null; // lazy
317
+ }
318
+
319
+ // ── Internal ────────────────────────────────────────────────────────────────
320
+
321
+ /** @returns {Connection} */
322
+ _getConnection() {
323
+ if (!this._connection) {
324
+ this._connection = new Connection(this.rpcUrl, 'confirmed');
325
+ }
326
+ return this._connection;
327
+ }
328
+
329
+ /**
330
+ * Send and confirm a transaction.
331
+ * @param {Transaction} tx
332
+ * @param {Keypair[]} signers
333
+ * @returns {Promise<string>} transaction signature
334
+ */
335
+ async _sendAndConfirm(tx, signers) {
336
+ const connection = this._getConnection();
337
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('confirmed');
338
+ tx.recentBlockhash = blockhash;
339
+ tx.feePayer = signers[0].publicKey;
340
+
341
+ tx.sign(...signers);
342
+
343
+ const rawTx = tx.serialize();
344
+ const sig = await connection.sendRawTransaction(rawTx, { skipPreflight: false });
345
+
346
+ await connection.confirmTransaction(
347
+ { signature: sig, blockhash, lastValidBlockHeight },
348
+ 'confirmed'
349
+ );
350
+
351
+ return sig;
352
+ }
353
+
354
+ // ── Static PDA Helpers ──────────────────────────────────────────────────────
355
+
356
+ /**
357
+ * Derive the AgentRecord PDA for a given agent public key.
358
+ * Seeds: [b"agent", agentPubkey]
359
+ *
360
+ * @param {PublicKey|string} agentPubkey
361
+ * @param {PublicKey|string} [programId]
362
+ * @returns {{ pda: PublicKey, bump: number }}
363
+ */
364
+ static deriveAgentRecord(agentPubkey, programId = PROGRAM_ID) {
365
+ const agentKey = new PublicKey(agentPubkey);
366
+ const progKey = new PublicKey(programId);
367
+ const [pda, bump] = PublicKey.findProgramAddressSync(
368
+ [Buffer.from('agent'), agentKey.toBuffer()],
369
+ progKey
370
+ );
371
+ return { pda, bump };
372
+ }
373
+
374
+ /**
375
+ * Derive the MemoryEntry PDA for a given agent at a specific memory index.
376
+ * Seeds: [b"memory", agentPubkey, memoryCount (u64 LE)]
377
+ *
378
+ * @param {PublicKey|string} agentPubkey
379
+ * @param {number} memoryCount — the index of this memory (0-based; pass agent.memoryCount BEFORE storing)
380
+ * @param {PublicKey|string} [programId]
381
+ * @returns {{ pda: PublicKey, bump: number }}
382
+ */
383
+ static deriveMemoryEntry(agentPubkey, memoryCount, programId = PROGRAM_ID) {
384
+ const agentKey = new PublicKey(agentPubkey);
385
+ const progKey = new PublicKey(programId);
386
+ const indexBytes = encodeU64(memoryCount);
387
+ const [pda, bump] = PublicKey.findProgramAddressSync(
388
+ [Buffer.from('memory'), agentKey.toBuffer(), indexBytes],
389
+ progKey
390
+ );
391
+ return { pda, bump };
392
+ }
393
+
394
+ // ── Write Methods ───────────────────────────────────────────────────────────
395
+
396
+ /**
397
+ * Register a new agent on-chain.
398
+ *
399
+ * The agent keypair MUST co-sign — this prevents PDA squatting by ensuring
400
+ * only the real agent key can claim its own identity record.
401
+ *
402
+ * Fee: 0.05 XNT (automatic — built into the instruction).
403
+ * Fee payer: humanKeypair (the wallet that owns this agent).
404
+ *
405
+ * @param {Keypair} agentKeypair The agent's keypair — MUST be a real Keypair (has secretKey)
406
+ * @param {string} name Agent display name (max 32 chars)
407
+ * @param {string} metadataUri URI to agent metadata JSON (max 128 chars)
408
+ * @returns {Promise<{ txSig: string, agentRecordPDA: string }>}
409
+ */
410
+ async register(agentKeypair, name, metadataUri) {
411
+ if (!this.keypair) {
412
+ throw new AgentSDKError('AgentClient must be initialised with a wallet to call register()', 'NO_WALLET');
413
+ }
414
+
415
+ // ── Anti-squatting: agentKeypair MUST be a real signer ──
416
+ assertIsSigner(agentKeypair, 'agentKeypair');
417
+
418
+ // ── Input validation ──
419
+ validateString(name, 'name', 32);
420
+ validateString(metadataUri, 'metadataUri', 128);
421
+
422
+ const humanKeypair = this.keypair;
423
+ const agentPubkey = agentKeypair.publicKey;
424
+ const { pda: agentRecordPDA } = AgentClient.deriveAgentRecord(agentPubkey);
425
+
426
+ // Borsh-encode instruction data: discriminator + name + metadata_uri
427
+ const data = Buffer.concat([
428
+ DISCRIMINATORS.register_agent,
429
+ encodeString(name),
430
+ encodeString(metadataUri),
431
+ ]);
432
+
433
+ const ix = new TransactionInstruction({
434
+ programId: PROGRAM_ID,
435
+ keys: [
436
+ { pubkey: humanKeypair.publicKey, isSigner: true, isWritable: true }, // human
437
+ { pubkey: agentPubkey, isSigner: false, isWritable: false }, // agent_pubkey (CHECK)
438
+ { pubkey: agentRecordPDA, isSigner: false, isWritable: true }, // agent_record
439
+ { pubkey: TREASURY, isSigner: false, isWritable: true }, // treasury
440
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, // system_program
441
+ ],
442
+ data,
443
+ });
444
+
445
+ const tx = new Transaction().add(ix);
446
+ const sig = await this._sendAndConfirm(tx, [humanKeypair]);
447
+
448
+ return {
449
+ txSig: sig,
450
+ agentRecordPDA: agentRecordPDA.toBase58(),
451
+ };
452
+ }
453
+
454
+ /**
455
+ * Store a memory entry on-chain.
456
+ *
457
+ * The agent keypair signs and pays the 0.001 XNT fee (THE DRIP).
458
+ * This is the core loop: compress → IPFS → storeMemory(CID).
459
+ *
460
+ * Fee: 0.001 XNT per call (automatic).
461
+ *
462
+ * @param {Keypair} agentKeypair The agent's keypair — must be a real Signer
463
+ * @param {string} agentRecordHuman The human wallet address that owns this agent (used for PDA lookup)
464
+ * @param {string} topic Memory topic label (max 64 chars)
465
+ * @param {string} cid IPFS CID of the memory content (max 64 chars)
466
+ * @param {string[]} [tags=[]] Optional tags, max 5, each max 32 chars
467
+ * @param {boolean} [encrypted=false] Whether the IPFS content is encrypted
468
+ * @returns {Promise<{ txSig: string, memoryEntryPDA: string }>}
469
+ */
470
+ async storeMemory(agentKeypair, agentRecordHuman, topic, cid, tags = [], encrypted = false) {
471
+ // ── Anti-squatting ──
472
+ assertIsSigner(agentKeypair, 'agentKeypair');
473
+
474
+ // ── Input validation ──
475
+ validateString(topic, 'topic', 64);
476
+ validateString(cid, 'cid', 64);
477
+ if (!Array.isArray(tags)) {
478
+ throw new AgentSDKError('tags must be an array', 'INVALID_INPUT');
479
+ }
480
+ if (tags.length > 5) {
481
+ throw new AgentSDKError('Maximum 5 tags allowed', 'INVALID_INPUT');
482
+ }
483
+ for (const tag of tags) {
484
+ validateString(tag, 'tag', 32);
485
+ }
486
+ if (typeof encrypted !== 'boolean') {
487
+ throw new AgentSDKError('encrypted must be a boolean', 'INVALID_INPUT');
488
+ }
489
+
490
+ const agentPubkey = agentKeypair.publicKey;
491
+ const { pda: agentRecordPDA } = AgentClient.deriveAgentRecord(agentPubkey);
492
+
493
+ // Read current memory_count from chain to derive the correct PDA index
494
+ const agentData = await this.getAgent(agentPubkey.toBase58());
495
+ const memoryIndex = agentData.memoryCount;
496
+
497
+ const { pda: memoryEntryPDA } = AgentClient.deriveMemoryEntry(agentPubkey, memoryIndex);
498
+
499
+ // Borsh-encode: discriminator + topic + cid + tags + encrypted
500
+ const data = Buffer.concat([
501
+ DISCRIMINATORS.store_memory,
502
+ encodeString(topic),
503
+ encodeString(cid),
504
+ encodeStringVec(tags),
505
+ encodeBool(encrypted),
506
+ ]);
507
+
508
+ const ix = new TransactionInstruction({
509
+ programId: PROGRAM_ID,
510
+ keys: [
511
+ { pubkey: agentPubkey, isSigner: true, isWritable: true }, // agent_pubkey (Signer + fee payer)
512
+ { pubkey: agentRecordPDA, isSigner: false, isWritable: true }, // agent_record
513
+ { pubkey: memoryEntryPDA, isSigner: false, isWritable: true }, // memory_entry
514
+ { pubkey: TREASURY, isSigner: false, isWritable: true }, // treasury
515
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, // system_program
516
+ ],
517
+ data,
518
+ });
519
+
520
+ const tx = new Transaction().add(ix);
521
+ const sig = await this._sendAndConfirm(tx, [agentKeypair]);
522
+
523
+ return {
524
+ txSig: sig,
525
+ memoryEntryPDA: memoryEntryPDA.toBase58(),
526
+ };
527
+ }
528
+
529
+ /**
530
+ * Update an agent's name and metadata URI.
531
+ * Only the human owner can call this. Free (network tx fee only).
532
+ *
533
+ * @param {Keypair} humanKeypair The human wallet keypair (owner)
534
+ * @param {PublicKey|string} agentPubkey The agent's public key
535
+ * @param {string} name New name (max 32 chars)
536
+ * @param {string} metadataUri New metadata URI (max 128 chars)
537
+ * @returns {Promise<{ txSig: string }>}
538
+ */
539
+ async updateAgent(humanKeypair, agentPubkey, name, metadataUri) {
540
+ assertIsSigner(humanKeypair, 'humanKeypair');
541
+ validateString(name, 'name', 32);
542
+ validateString(metadataUri, 'metadataUri', 128);
543
+
544
+ const agentKey = new PublicKey(agentPubkey);
545
+ const { pda: agentRecordPDA } = AgentClient.deriveAgentRecord(agentKey);
546
+
547
+ const data = Buffer.concat([
548
+ DISCRIMINATORS.update_agent,
549
+ encodeString(name),
550
+ encodeString(metadataUri),
551
+ ]);
552
+
553
+ const ix = new TransactionInstruction({
554
+ programId: PROGRAM_ID,
555
+ keys: [
556
+ { pubkey: humanKeypair.publicKey, isSigner: true, isWritable: true }, // human
557
+ { pubkey: agentKey, isSigner: false, isWritable: false }, // agent_pubkey (CHECK)
558
+ { pubkey: agentRecordPDA, isSigner: false, isWritable: true }, // agent_record
559
+ ],
560
+ data,
561
+ });
562
+
563
+ const tx = new Transaction().add(ix);
564
+ const sig = await this._sendAndConfirm(tx, [humanKeypair]);
565
+
566
+ return { txSig: sig };
567
+ }
568
+
569
+ /**
570
+ * Transfer agent ownership to a new human wallet.
571
+ * Only the current human owner can call this.
572
+ *
573
+ * Fee: 0.01 XNT (automatic).
574
+ *
575
+ * @param {Keypair} humanKeypair Current human owner keypair
576
+ * @param {PublicKey|string} agentPubkey The agent's public key
577
+ * @param {PublicKey|string} newHuman New owner's public key
578
+ * @returns {Promise<{ txSig: string }>}
579
+ */
580
+ async transferAgent(humanKeypair, agentPubkey, newHuman) {
581
+ assertIsSigner(humanKeypair, 'humanKeypair');
582
+
583
+ const agentKey = new PublicKey(agentPubkey);
584
+ const newHumanKey = new PublicKey(newHuman);
585
+ const { pda: agentRecordPDA } = AgentClient.deriveAgentRecord(agentKey);
586
+
587
+ // Encode: discriminator + new_human (32 bytes Pubkey)
588
+ const data = Buffer.concat([
589
+ DISCRIMINATORS.transfer_agent,
590
+ newHumanKey.toBuffer(),
591
+ ]);
592
+
593
+ const ix = new TransactionInstruction({
594
+ programId: PROGRAM_ID,
595
+ keys: [
596
+ { pubkey: humanKeypair.publicKey, isSigner: true, isWritable: true }, // human
597
+ { pubkey: agentKey, isSigner: false, isWritable: false }, // agent_pubkey (CHECK)
598
+ { pubkey: agentRecordPDA, isSigner: false, isWritable: true }, // agent_record
599
+ { pubkey: TREASURY, isSigner: false, isWritable: true }, // treasury
600
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, // system_program
601
+ ],
602
+ data,
603
+ });
604
+
605
+ const tx = new Transaction().add(ix);
606
+ const sig = await this._sendAndConfirm(tx, [humanKeypair]);
607
+
608
+ return { txSig: sig };
609
+ }
610
+
611
+ // ── Read Methods ────────────────────────────────────────────────────────────
612
+
613
+ /**
614
+ * Fetch and decode an AgentRecord from chain.
615
+ *
616
+ * @param {PublicKey|string} agentPubkey The agent's public key
617
+ * @returns {Promise<{
618
+ * pda: string,
619
+ * human: string,
620
+ * agentPubkey: string,
621
+ * name: string,
622
+ * metadataUri: string,
623
+ * createdAt: number,
624
+ * memoryCount: number,
625
+ * lastActive: number,
626
+ * bump: number
627
+ * }>}
628
+ */
629
+ async getAgent(agentPubkey) {
630
+ const agentKey = new PublicKey(agentPubkey);
631
+ const { pda } = AgentClient.deriveAgentRecord(agentKey);
632
+ const connection = this._getConnection();
633
+ const info = await connection.getAccountInfo(pda);
634
+
635
+ if (!info || !info.data) {
636
+ throw new AgentSDKError(
637
+ `No AgentRecord found for ${agentPubkey}. Has this agent been registered?`,
638
+ 'NOT_FOUND'
639
+ );
640
+ }
641
+
642
+ const decoded = decodeAgentRecord(info.data);
643
+ return { pda: pda.toBase58(), ...decoded };
644
+ }
645
+
646
+ /**
647
+ * Fetch and decode a single MemoryEntry at a given index.
648
+ *
649
+ * @param {PublicKey|string} agentPubkey The agent's public key
650
+ * @param {number} index Memory index (0-based)
651
+ * @returns {Promise<{
652
+ * pda: string,
653
+ * agent: string,
654
+ * topic: string,
655
+ * cid: string,
656
+ * tags: string[],
657
+ * encrypted: boolean,
658
+ * timestamp: number,
659
+ * slot: number,
660
+ * bump: number
661
+ * }>}
662
+ */
663
+ async getMemory(agentPubkey, index) {
664
+ if (typeof index !== 'number' || index < 0 || !Number.isInteger(index)) {
665
+ throw new AgentSDKError('index must be a non-negative integer', 'INVALID_INPUT');
666
+ }
667
+
668
+ const agentKey = new PublicKey(agentPubkey);
669
+ const { pda } = AgentClient.deriveMemoryEntry(agentKey, index);
670
+ const connection = this._getConnection();
671
+ const info = await connection.getAccountInfo(pda);
672
+
673
+ if (!info || !info.data) {
674
+ throw new AgentSDKError(
675
+ `No MemoryEntry found at index ${index} for agent ${agentPubkey}`,
676
+ 'NOT_FOUND'
677
+ );
678
+ }
679
+
680
+ const decoded = decodeMemoryEntry(info.data);
681
+ return { pda: pda.toBase58(), ...decoded };
682
+ }
683
+
684
+ /**
685
+ * Fetch multiple memories for an agent (most recent first).
686
+ *
687
+ * Reads memories from index (memoryCount - 1) downward, up to `limit` entries.
688
+ * Entries that don't exist on-chain (e.g. if memoryCount changed) are skipped.
689
+ *
690
+ * @param {PublicKey|string} agentPubkey The agent's public key
691
+ * @param {number} [limit=10] Max number of memories to return
692
+ * @returns {Promise<Array>} Array of decoded MemoryEntry objects
693
+ */
694
+ async listMemories(agentPubkey, limit = 10) {
695
+ if (!Number.isInteger(limit) || limit < 1) {
696
+ throw new AgentSDKError('limit must be a positive integer', 'INVALID_INPUT');
697
+ }
698
+
699
+ const agent = await this.getAgent(agentPubkey);
700
+ const memoryCount = agent.memoryCount;
701
+
702
+ if (memoryCount === 0) return [];
703
+
704
+ const startIndex = Math.max(0, memoryCount - limit);
705
+ const indices = [];
706
+ for (let i = startIndex; i < memoryCount; i++) {
707
+ indices.push(i);
708
+ }
709
+
710
+ const agentKey = new PublicKey(agentPubkey);
711
+ const connection = this._getConnection();
712
+
713
+ // Derive all PDAs, then batch-fetch
714
+ const pdas = indices.map(i => AgentClient.deriveMemoryEntry(agentKey, i).pda);
715
+ const infos = await connection.getMultipleAccountsInfo(pdas);
716
+
717
+ const results = [];
718
+ for (let j = 0; j < pdas.length; j++) {
719
+ const info = infos[j];
720
+ if (!info || !info.data) continue;
721
+ try {
722
+ const decoded = decodeMemoryEntry(info.data);
723
+ results.push({ pda: pdas[j].toBase58(), ...decoded });
724
+ } catch (_) {
725
+ // skip malformed entries
726
+ }
727
+ }
728
+
729
+ // Most recent first
730
+ return results.reverse();
731
+ }
732
+ }
733
+
734
+ // ─────────────────────────────────────────────────────────────────────────────
735
+ // Exports
736
+ // ─────────────────────────────────────────────────────────────────────────────
737
+
738
+ module.exports = {
739
+ AgentClient,
740
+ AgentSDKError,
741
+ PROGRAM_ID,
742
+ TREASURY,
743
+ DEFAULT_RPC_URL,
744
+ };