@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/README.md +350 -0
- package/examples/basic.js +120 -0
- package/examples/context-saver.js +272 -0
- package/package.json +54 -0
- package/src/index.js +744 -0
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
|
+
};
|