@brainai/satp-client 2.0.0 → 2.0.2

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/v3-sdk.js ADDED
@@ -0,0 +1,1800 @@
1
+ const {
2
+ Connection,
3
+ PublicKey,
4
+ Transaction,
5
+ TransactionInstruction,
6
+ SystemProgram,
7
+ } = require('@solana/web3.js');
8
+ const crypto = require('crypto');
9
+ const {
10
+ getV3ProgramIds,
11
+ hashAgentId,
12
+ hashName,
13
+ getGenesisPDA,
14
+ getV3ReputationAuthorityPDA,
15
+ getV3ValidationAuthorityPDA,
16
+ getV3MintTrackerPDA,
17
+ getNameRegistryPDA,
18
+ getLinkedWalletPDA,
19
+ getV3ReviewPDA,
20
+ getV3ReviewCounterPDA,
21
+ getV3AttestationPDA,
22
+ getV3EscrowPDA,
23
+ } = require('./v3-pda');
24
+
25
+ const DEVNET_RPC = 'https://api.devnet.solana.com';
26
+ const MAINNET_RPC = 'https://api.mainnet-beta.solana.com';
27
+
28
+ function isMainnetRpc(rpcUrl) {
29
+ return typeof rpcUrl === 'string' && /mainnet/i.test(rpcUrl);
30
+ }
31
+
32
+ function normalizeSDKOptions(opts = {}) {
33
+ let normalized = opts;
34
+ if (typeof opts === 'string') {
35
+ normalized = opts === 'devnet' || opts === 'mainnet'
36
+ ? { network: opts }
37
+ : { rpcUrl: opts };
38
+ }
39
+ const rpcUrl = normalized.rpcUrl || normalized.url || normalized.endpoint;
40
+ if (!normalized.network && isMainnetRpc(rpcUrl)) {
41
+ throw new Error('Mainnet RPC requires network=mainnet, but SATP V3 mainnet program IDs are not configured');
42
+ }
43
+ return {
44
+ ...normalized,
45
+ rpcUrl,
46
+ network: normalized.network || 'devnet',
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Compute Anchor instruction discriminator.
52
+ * @param {string} ixName - e.g. "create_identity"
53
+ * @returns {Buffer} 8-byte discriminator
54
+ */
55
+ function anchorDiscriminator(ixName) {
56
+ return crypto.createHash('sha256')
57
+ .update(`global:${ixName}`)
58
+ .digest()
59
+ .slice(0, 8);
60
+ }
61
+
62
+ /**
63
+ * Serialize a Rust String: 4-byte LE length prefix + UTF-8 bytes.
64
+ */
65
+ function serializeString(str) {
66
+ const bytes = Buffer.from(str, 'utf8');
67
+ const len = Buffer.alloc(4);
68
+ len.writeUInt32LE(bytes.length);
69
+ return Buffer.concat([len, bytes]);
70
+ }
71
+
72
+ /**
73
+ * Serialize a Vec<String>: 4-byte LE count + each string serialized.
74
+ */
75
+ function serializeVecString(arr) {
76
+ const count = Buffer.alloc(4);
77
+ count.writeUInt32LE(arr.length);
78
+ const parts = arr.map(s => serializeString(s));
79
+ return Buffer.concat([count, ...parts]);
80
+ }
81
+
82
+ class SATPV3SDK {
83
+ /**
84
+ * @param {object} opts
85
+ * @param {'mainnet'|'devnet'} [opts.network='devnet']
86
+ * @param {string} [opts.rpcUrl]
87
+ * @param {string} [opts.commitment='confirmed']
88
+ */
89
+ constructor(opts = {}) {
90
+ const normalized = normalizeSDKOptions(opts);
91
+ this.network = normalized.network;
92
+ this.rpcUrl = normalized.rpcUrl || (this.network === 'mainnet' ? MAINNET_RPC : DEVNET_RPC);
93
+ this.commitment = normalized.commitment || 'confirmed';
94
+ this.connection = new Connection(this.rpcUrl, this.commitment);
95
+ this.programIds = getV3ProgramIds(this.network);
96
+ }
97
+
98
+ // ═══════════════════════════════════════════════════
99
+ // IDENTITY — Genesis Record CRUD
100
+ // ═══════════════════════════════════════════════════
101
+
102
+ /**
103
+ * Build createIdentity transaction.
104
+ * @param {PublicKey|string} creator - Wallet that pays rent and becomes authority
105
+ * @param {string} agentId - Agent identifier (hashed to derive PDA)
106
+ * @param {object} meta - { name, description, category, capabilities, metadataUri }
107
+ * @returns {{ transaction: Transaction, genesisPDA: PublicKey, agentIdHash: Buffer }}
108
+ */
109
+ async buildCreateIdentity(creator, agentId, meta) {
110
+ const creatorKey = new PublicKey(creator);
111
+ const agentIdHash = hashAgentId(agentId);
112
+ const [genesisPDA] = getGenesisPDA(agentIdHash, this.network);
113
+
114
+ const disc = anchorDiscriminator('create_identity');
115
+ const data = Buffer.concat([
116
+ disc,
117
+ agentIdHash, // [u8; 32]
118
+ serializeString(meta.name || ''), // String
119
+ serializeString(meta.description || ''), // String
120
+ serializeString(meta.category || ''), // String
121
+ serializeVecString(meta.capabilities || []), // Vec<String>
122
+ serializeString(meta.metadataUri || ''), // String
123
+ ]);
124
+
125
+ const ix = new TransactionInstruction({
126
+ programId: this.programIds.IDENTITY,
127
+ keys: [
128
+ { pubkey: genesisPDA, isSigner: false, isWritable: true },
129
+ { pubkey: creatorKey, isSigner: true, isWritable: true },
130
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
131
+ ],
132
+ data,
133
+ });
134
+
135
+ const tx = new Transaction().add(ix);
136
+ tx.feePayer = creatorKey;
137
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
138
+
139
+ return { transaction: tx, genesisPDA, agentIdHash };
140
+ }
141
+
142
+ /**
143
+ * Build burnToBecome transaction (agent's permanent face/birth event).
144
+ * @param {PublicKey|string} authority - Genesis record authority (signer)
145
+ * @param {string|Buffer} agentIdOrHash - Agent ID string or 32-byte hash
146
+ * @param {string} faceImage - Arweave/IPFS URL to face image
147
+ * @param {PublicKey|string} faceMint - Soulbound BOA NFT mint address
148
+ * @param {string} faceBurnTx - Burn transaction signature
149
+ * @returns {{ transaction: Transaction }}
150
+ */
151
+ async buildBurnToBecome(authority, agentIdOrHash, faceImage, faceMint, faceBurnTx) {
152
+ const authorityKey = new PublicKey(authority);
153
+ const [genesisPDA] = getGenesisPDA(agentIdOrHash, this.network);
154
+ const faceMintKey = new PublicKey(faceMint);
155
+
156
+ const disc = anchorDiscriminator('burn_to_become');
157
+ const data = Buffer.concat([
158
+ disc,
159
+ serializeString(faceImage),
160
+ faceMintKey.toBuffer(), // Pubkey (32 bytes)
161
+ serializeString(faceBurnTx),
162
+ ]);
163
+
164
+ const ix = new TransactionInstruction({
165
+ programId: this.programIds.IDENTITY,
166
+ keys: [
167
+ { pubkey: genesisPDA, isSigner: false, isWritable: true },
168
+ { pubkey: authorityKey, isSigner: true, isWritable: false },
169
+ ],
170
+ data,
171
+ });
172
+
173
+ const tx = new Transaction().add(ix);
174
+ tx.feePayer = authorityKey;
175
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
176
+
177
+ return { transaction: tx };
178
+ }
179
+
180
+ /**
181
+ * Build updateIdentity transaction.
182
+ * Pass null/undefined for fields to leave unchanged.
183
+ * @param {PublicKey|string} authority
184
+ * @param {string|Buffer} agentIdOrHash
185
+ * @param {object} updates - { name?, description?, category?, capabilities?, metadataUri? }
186
+ * @returns {{ transaction: Transaction }}
187
+ */
188
+ async buildUpdateIdentity(authority, agentIdOrHash, updates) {
189
+ const authorityKey = new PublicKey(authority);
190
+ const [genesisPDA] = getGenesisPDA(agentIdOrHash, this.network);
191
+
192
+ const disc = anchorDiscriminator('update_identity');
193
+
194
+ // Serialize Option<String> — 0x00 for None, 0x01 + string for Some
195
+ function optString(val) {
196
+ if (val == null) return Buffer.from([0x00]);
197
+ return Buffer.concat([Buffer.from([0x01]), serializeString(val)]);
198
+ }
199
+
200
+ // Serialize Option<Vec<String>>
201
+ function optVecString(val) {
202
+ if (val == null) return Buffer.from([0x00]);
203
+ return Buffer.concat([Buffer.from([0x01]), serializeVecString(val)]);
204
+ }
205
+
206
+ const data = Buffer.concat([
207
+ disc,
208
+ optString(updates.name),
209
+ optString(updates.description),
210
+ optString(updates.category),
211
+ optVecString(updates.capabilities),
212
+ optString(updates.metadataUri),
213
+ ]);
214
+
215
+ const ix = new TransactionInstruction({
216
+ programId: this.programIds.IDENTITY,
217
+ keys: [
218
+ { pubkey: genesisPDA, isSigner: false, isWritable: true },
219
+ { pubkey: authorityKey, isSigner: true, isWritable: false },
220
+ ],
221
+ data,
222
+ });
223
+
224
+ const tx = new Transaction().add(ix);
225
+ tx.feePayer = authorityKey;
226
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
227
+
228
+ return { transaction: tx };
229
+ }
230
+
231
+ /**
232
+ * Build proposeAuthority transaction (2-step rotation).
233
+ * @param {PublicKey|string} authority - Current authority (signer)
234
+ * @param {string|Buffer} agentIdOrHash
235
+ * @param {PublicKey|string} newAuthority
236
+ * @returns {{ transaction: Transaction }}
237
+ */
238
+ async buildProposeAuthority(authority, agentIdOrHash, newAuthority) {
239
+ const authorityKey = new PublicKey(authority);
240
+ const newAuthKey = new PublicKey(newAuthority);
241
+ const [genesisPDA] = getGenesisPDA(agentIdOrHash, this.network);
242
+
243
+ const disc = anchorDiscriminator('propose_authority');
244
+ const data = Buffer.concat([disc, newAuthKey.toBuffer()]);
245
+
246
+ const ix = new TransactionInstruction({
247
+ programId: this.programIds.IDENTITY,
248
+ keys: [
249
+ { pubkey: genesisPDA, isSigner: false, isWritable: true },
250
+ { pubkey: authorityKey, isSigner: true, isWritable: false },
251
+ ],
252
+ data,
253
+ });
254
+
255
+ const tx = new Transaction().add(ix);
256
+ tx.feePayer = authorityKey;
257
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
258
+
259
+ return { transaction: tx };
260
+ }
261
+
262
+ /**
263
+ * Build acceptAuthority transaction.
264
+ * @param {PublicKey|string} newAuthority - Pending authority (signer)
265
+ * @param {string|Buffer} agentIdOrHash
266
+ * @returns {{ transaction: Transaction }}
267
+ */
268
+ async buildAcceptAuthority(newAuthority, agentIdOrHash) {
269
+ const newAuthKey = new PublicKey(newAuthority);
270
+ const [genesisPDA] = getGenesisPDA(agentIdOrHash, this.network);
271
+
272
+ const disc = anchorDiscriminator('accept_authority');
273
+
274
+ const ix = new TransactionInstruction({
275
+ programId: this.programIds.IDENTITY,
276
+ keys: [
277
+ { pubkey: genesisPDA, isSigner: false, isWritable: true },
278
+ { pubkey: newAuthKey, isSigner: true, isWritable: false },
279
+ ],
280
+ data: disc,
281
+ });
282
+
283
+ const tx = new Transaction().add(ix);
284
+ tx.feePayer = newAuthKey;
285
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
286
+
287
+ return { transaction: tx };
288
+ }
289
+
290
+ // ═══════════════════════════════════════════════════
291
+ // IDENTITY — Name Registry
292
+ // ═══════════════════════════════════════════════════
293
+
294
+ /**
295
+ * Build registerName transaction.
296
+ * @param {PublicKey|string} authority - Identity authority (signer + payer)
297
+ * @param {string|Buffer} agentIdOrHash
298
+ * @param {string} name - Display name (2-32 chars)
299
+ * @returns {{ transaction: Transaction, nameRegistryPDA: PublicKey }}
300
+ */
301
+ async buildRegisterName(authority, agentIdOrHash, name) {
302
+ const authorityKey = new PublicKey(authority);
303
+ const [genesisPDA] = getGenesisPDA(agentIdOrHash, this.network);
304
+ const nameHash = hashName(name);
305
+ const [nameRegistryPDA] = getNameRegistryPDA(nameHash, this.network);
306
+
307
+ const disc = anchorDiscriminator('register_name');
308
+ const data = Buffer.concat([
309
+ disc,
310
+ serializeString(name),
311
+ nameHash, // [u8; 32]
312
+ ]);
313
+
314
+ const ix = new TransactionInstruction({
315
+ programId: this.programIds.IDENTITY,
316
+ keys: [
317
+ { pubkey: genesisPDA, isSigner: false, isWritable: true },
318
+ { pubkey: nameRegistryPDA, isSigner: false, isWritable: true },
319
+ { pubkey: authorityKey, isSigner: true, isWritable: true },
320
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
321
+ ],
322
+ data,
323
+ });
324
+
325
+ const tx = new Transaction().add(ix);
326
+ tx.feePayer = authorityKey;
327
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
328
+
329
+ return { transaction: tx, nameRegistryPDA };
330
+ }
331
+
332
+ /**
333
+ * Build releaseName transaction (frees name for others to claim).
334
+ * @param {PublicKey|string} authority
335
+ * @param {string|Buffer} agentIdOrHash
336
+ * @param {string} name - The registered name to release
337
+ * @returns {{ transaction: Transaction }}
338
+ */
339
+ async buildReleaseName(authority, agentIdOrHash, name) {
340
+ const authorityKey = new PublicKey(authority);
341
+ const [genesisPDA] = getGenesisPDA(agentIdOrHash, this.network);
342
+ const nameHash = hashName(name);
343
+ const [nameRegistryPDA] = getNameRegistryPDA(nameHash, this.network);
344
+
345
+ const disc = anchorDiscriminator('release_name');
346
+
347
+ const ix = new TransactionInstruction({
348
+ programId: this.programIds.IDENTITY,
349
+ keys: [
350
+ { pubkey: genesisPDA, isSigner: false, isWritable: false },
351
+ { pubkey: nameRegistryPDA, isSigner: false, isWritable: true },
352
+ { pubkey: authorityKey, isSigner: true, isWritable: false },
353
+ ],
354
+ data: disc,
355
+ });
356
+
357
+ const tx = new Transaction().add(ix);
358
+ tx.feePayer = authorityKey;
359
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
360
+
361
+ return { transaction: tx };
362
+ }
363
+
364
+ /**
365
+ * Build cancelAuthorityTransfer transaction.
366
+ * @param {PublicKey|string} authority - Current authority (signer)
367
+ * @param {string|Buffer} agentIdOrHash
368
+ * @returns {{ transaction: Transaction }}
369
+ */
370
+ async buildCancelAuthorityTransfer(authority, agentIdOrHash) {
371
+ const authorityKey = new PublicKey(authority);
372
+ const [genesisPDA] = getGenesisPDA(agentIdOrHash, this.network);
373
+
374
+ const disc = anchorDiscriminator('cancel_authority_transfer');
375
+
376
+ const ix = new TransactionInstruction({
377
+ programId: this.programIds.IDENTITY,
378
+ keys: [
379
+ { pubkey: genesisPDA, isSigner: false, isWritable: true },
380
+ { pubkey: authorityKey, isSigner: true, isWritable: false },
381
+ ],
382
+ data: disc,
383
+ });
384
+
385
+ const tx = new Transaction().add(ix);
386
+ tx.feePayer = authorityKey;
387
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
388
+
389
+ return { transaction: tx };
390
+ }
391
+
392
+ // ═══════════════════════════════════════════════════
393
+ // IDENTITY — Linked Wallets
394
+ // ═══════════════════════════════════════════════════
395
+
396
+ /**
397
+ * Build linkWallet transaction.
398
+ * @param {PublicKey|string} authority
399
+ * @param {string|Buffer} agentIdOrHash
400
+ * @param {PublicKey|string} wallet - Wallet to link
401
+ * @param {string} chain - Chain identifier (e.g. "solana", max 16 chars)
402
+ * @param {string} label - Label (e.g. "deploy", max 32 chars)
403
+ * @returns {{ transaction: Transaction, linkedWalletPDA: PublicKey }}
404
+ */
405
+ async buildLinkWallet(authority, agentIdOrHash, wallet, chain, label) {
406
+ const authorityKey = new PublicKey(authority);
407
+ const walletKey = new PublicKey(wallet);
408
+ const [genesisPDA] = getGenesisPDA(agentIdOrHash, this.network);
409
+ const [linkedWalletPDA] = getLinkedWalletPDA(genesisPDA, walletKey, this.network);
410
+
411
+ const disc = anchorDiscriminator('link_wallet');
412
+ const data = Buffer.concat([
413
+ disc,
414
+ walletKey.toBuffer(),
415
+ serializeString(chain),
416
+ serializeString(label),
417
+ ]);
418
+
419
+ const ix = new TransactionInstruction({
420
+ programId: this.programIds.IDENTITY,
421
+ keys: [
422
+ { pubkey: genesisPDA, isSigner: false, isWritable: false },
423
+ { pubkey: linkedWalletPDA, isSigner: false, isWritable: true },
424
+ { pubkey: authorityKey, isSigner: true, isWritable: true },
425
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
426
+ ],
427
+ data,
428
+ });
429
+
430
+ const tx = new Transaction().add(ix);
431
+ tx.feePayer = authorityKey;
432
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
433
+
434
+ return { transaction: tx, linkedWalletPDA };
435
+ }
436
+
437
+ /**
438
+ * Build unlinkWallet transaction.
439
+ * @param {PublicKey|string} authority
440
+ * @param {string|Buffer} agentIdOrHash
441
+ * @param {PublicKey|string} wallet - Wallet to unlink
442
+ * @returns {{ transaction: Transaction }}
443
+ */
444
+ async buildUnlinkWallet(authority, agentIdOrHash, wallet) {
445
+ const authorityKey = new PublicKey(authority);
446
+ const walletKey = new PublicKey(wallet);
447
+ const [genesisPDA] = getGenesisPDA(agentIdOrHash, this.network);
448
+ const [linkedWalletPDA] = getLinkedWalletPDA(genesisPDA, walletKey, this.network);
449
+
450
+ const disc = anchorDiscriminator('unlink_wallet');
451
+
452
+ const ix = new TransactionInstruction({
453
+ programId: this.programIds.IDENTITY,
454
+ keys: [
455
+ { pubkey: genesisPDA, isSigner: false, isWritable: false },
456
+ { pubkey: linkedWalletPDA, isSigner: false, isWritable: true },
457
+ { pubkey: authorityKey, isSigner: true, isWritable: false },
458
+ ],
459
+ data: disc,
460
+ });
461
+
462
+ const tx = new Transaction().add(ix);
463
+ tx.feePayer = authorityKey;
464
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
465
+
466
+ return { transaction: tx };
467
+ }
468
+
469
+ // ═══════════════════════════════════════════════════
470
+ // IDENTITY — MintTracker
471
+ // ═══════════════════════════════════════════════════
472
+
473
+ /**
474
+ * Build initMintTracker transaction.
475
+ * @param {PublicKey|string} authority
476
+ * @param {string|Buffer} agentIdOrHash
477
+ * @returns {{ transaction: Transaction, mintTrackerPDA: PublicKey }}
478
+ */
479
+ async buildInitMintTracker(authority, agentIdOrHash) {
480
+ const authorityKey = new PublicKey(authority);
481
+ const [genesisPDA] = getGenesisPDA(agentIdOrHash, this.network);
482
+ const [mintTrackerPDA] = getV3MintTrackerPDA(genesisPDA, this.network);
483
+
484
+ const disc = anchorDiscriminator('init_mint_tracker');
485
+
486
+ const ix = new TransactionInstruction({
487
+ programId: this.programIds.IDENTITY,
488
+ keys: [
489
+ { pubkey: genesisPDA, isSigner: false, isWritable: false },
490
+ { pubkey: mintTrackerPDA, isSigner: false, isWritable: true },
491
+ { pubkey: authorityKey, isSigner: true, isWritable: true },
492
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
493
+ ],
494
+ data: disc,
495
+ });
496
+
497
+ const tx = new Transaction().add(ix);
498
+ tx.feePayer = authorityKey;
499
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
500
+
501
+ return { transaction: tx, mintTrackerPDA };
502
+ }
503
+
504
+ /**
505
+ * Build recordMint transaction (tracks mints, max 3 per identity).
506
+ * @param {PublicKey|string} authority
507
+ * @param {string|Buffer} agentIdOrHash
508
+ * @returns {{ transaction: Transaction }}
509
+ */
510
+ async buildRecordMint(authority, agentIdOrHash) {
511
+ const authorityKey = new PublicKey(authority);
512
+ const [genesisPDA] = getGenesisPDA(agentIdOrHash, this.network);
513
+ const [mintTrackerPDA] = getV3MintTrackerPDA(genesisPDA, this.network);
514
+
515
+ const disc = anchorDiscriminator('record_mint');
516
+
517
+ const ix = new TransactionInstruction({
518
+ programId: this.programIds.IDENTITY,
519
+ keys: [
520
+ { pubkey: genesisPDA, isSigner: false, isWritable: false },
521
+ { pubkey: mintTrackerPDA, isSigner: false, isWritable: true },
522
+ { pubkey: authorityKey, isSigner: true, isWritable: false },
523
+ ],
524
+ data: disc,
525
+ });
526
+
527
+ const tx = new Transaction().add(ix);
528
+ tx.feePayer = authorityKey;
529
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
530
+
531
+ return { transaction: tx };
532
+ }
533
+
534
+ // ═══════════════════════════════════════════════════
535
+ // IDENTITY — Deactivation
536
+ // ═══════════════════════════════════════════════════
537
+
538
+ /**
539
+ * Build deactivateIdentity transaction.
540
+ * @param {PublicKey|string} authority
541
+ * @param {string|Buffer} agentIdOrHash
542
+ * @returns {{ transaction: Transaction }}
543
+ */
544
+ async buildDeactivateIdentity(authority, agentIdOrHash) {
545
+ const authorityKey = new PublicKey(authority);
546
+ const [genesisPDA] = getGenesisPDA(agentIdOrHash, this.network);
547
+
548
+ const disc = anchorDiscriminator('deactivate_identity');
549
+
550
+ const ix = new TransactionInstruction({
551
+ programId: this.programIds.IDENTITY,
552
+ keys: [
553
+ { pubkey: genesisPDA, isSigner: false, isWritable: true },
554
+ { pubkey: authorityKey, isSigner: true, isWritable: false },
555
+ ],
556
+ data: disc,
557
+ });
558
+
559
+ const tx = new Transaction().add(ix);
560
+ tx.feePayer = authorityKey;
561
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
562
+
563
+ return { transaction: tx };
564
+ }
565
+
566
+ /**
567
+ * Build reactivateIdentity transaction.
568
+ * @param {PublicKey|string} authority
569
+ * @param {string|Buffer} agentIdOrHash
570
+ * @returns {{ transaction: Transaction }}
571
+ */
572
+ async buildReactivateIdentity(authority, agentIdOrHash) {
573
+ const authorityKey = new PublicKey(authority);
574
+ const [genesisPDA] = getGenesisPDA(agentIdOrHash, this.network);
575
+
576
+ const disc = anchorDiscriminator('reactivate_identity');
577
+
578
+ const ix = new TransactionInstruction({
579
+ programId: this.programIds.IDENTITY,
580
+ keys: [
581
+ { pubkey: genesisPDA, isSigner: false, isWritable: true },
582
+ { pubkey: authorityKey, isSigner: true, isWritable: false },
583
+ ],
584
+ data: disc,
585
+ });
586
+
587
+ const tx = new Transaction().add(ix);
588
+ tx.feePayer = authorityKey;
589
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
590
+
591
+ return { transaction: tx };
592
+ }
593
+
594
+ // ═══════════════════════════════════════════════════
595
+ // ATTESTATIONS V3 — Create, Verify, Revoke
596
+ // ═══════════════════════════════════════════════════
597
+
598
+ /**
599
+ * Build createAttestation transaction.
600
+ * @param {PublicKey|string} issuer - Signer + fee payer
601
+ * @param {string} agentId - Agent identifier
602
+ * @param {string} attestationType - Type of attestation (max 32 chars)
603
+ * @param {string} proofData - JSON proof data (max 512 chars)
604
+ * @param {number|null} expiresAt - Optional Unix timestamp for expiry
605
+ * @returns {{ transaction: Transaction, attestationPDA: PublicKey }}
606
+ */
607
+ async buildCreateAttestation(issuer, agentId, attestationType, proofData, expiresAt = null) {
608
+ const issuerKey = new PublicKey(issuer);
609
+ const [attPDA] = getV3AttestationPDA(agentId, issuerKey, attestationType, this.network);
610
+
611
+ const disc = anchorDiscriminator('create_attestation');
612
+
613
+ // Encode args: agent_id (string), attestation_type (string), proof_data (string), expires_at (Option<i64>)
614
+ const agentIdBuf = Buffer.from(agentId);
615
+ const typeBuf = Buffer.from(attestationType);
616
+ const proofBuf = Buffer.from(proofData);
617
+
618
+ const parts = [
619
+ disc,
620
+ // agent_id: string (4-byte len + data)
621
+ Buffer.alloc(4),
622
+ agentIdBuf,
623
+ // attestation_type: string (4-byte len + data)
624
+ Buffer.alloc(4),
625
+ typeBuf,
626
+ // proof_data: string (4-byte len + data)
627
+ Buffer.alloc(4),
628
+ proofBuf,
629
+ ];
630
+
631
+ parts[1].writeUInt32LE(agentIdBuf.length);
632
+ parts[3].writeUInt32LE(typeBuf.length);
633
+ parts[5].writeUInt32LE(proofBuf.length);
634
+
635
+ // expires_at: Option<i64>
636
+ if (expiresAt !== null && expiresAt !== undefined) {
637
+ const optBuf = Buffer.alloc(9);
638
+ optBuf.writeUInt8(1, 0); // Some
639
+ optBuf.writeBigInt64LE(BigInt(expiresAt), 1);
640
+ parts.push(optBuf);
641
+ } else {
642
+ parts.push(Buffer.from([0])); // None
643
+ }
644
+
645
+ const data = Buffer.concat(parts);
646
+
647
+ const ix = new TransactionInstruction({
648
+ programId: this.programIds.ATTESTATIONS,
649
+ keys: [
650
+ { pubkey: attPDA, isSigner: false, isWritable: true },
651
+ { pubkey: issuerKey, isSigner: true, isWritable: true },
652
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
653
+ ],
654
+ data,
655
+ });
656
+
657
+ const tx = new Transaction().add(ix);
658
+ tx.feePayer = issuerKey;
659
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
660
+
661
+ return { transaction: tx, attestationPDA: attPDA };
662
+ }
663
+
664
+ /**
665
+ * Build verifyAttestation transaction (issuer-only).
666
+ * @param {PublicKey|string} issuer - Must be the original issuer
667
+ * @param {PublicKey|string} attestationPDA - Attestation account address
668
+ * @returns {{ transaction: Transaction }}
669
+ */
670
+ async buildVerifyAttestation(issuer, attestationPDA) {
671
+ const issuerKey = new PublicKey(issuer);
672
+ const attKey = new PublicKey(attestationPDA);
673
+
674
+ const disc = anchorDiscriminator('verify_attestation');
675
+
676
+ const ix = new TransactionInstruction({
677
+ programId: this.programIds.ATTESTATIONS,
678
+ keys: [
679
+ { pubkey: attKey, isSigner: false, isWritable: true },
680
+ { pubkey: issuerKey, isSigner: true, isWritable: false },
681
+ ],
682
+ data: disc,
683
+ });
684
+
685
+ const tx = new Transaction().add(ix);
686
+ tx.feePayer = issuerKey;
687
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
688
+
689
+ return { transaction: tx };
690
+ }
691
+
692
+ /**
693
+ * Build revokeAttestation transaction (issuer-only).
694
+ * @param {PublicKey|string} issuer - Must be the original issuer
695
+ * @param {PublicKey|string} attestationPDA - Attestation account address
696
+ * @returns {{ transaction: Transaction }}
697
+ */
698
+ async buildRevokeAttestation(issuer, attestationPDA) {
699
+ const issuerKey = new PublicKey(issuer);
700
+ const attKey = new PublicKey(attestationPDA);
701
+
702
+ const disc = anchorDiscriminator('revoke_attestation');
703
+
704
+ const ix = new TransactionInstruction({
705
+ programId: this.programIds.ATTESTATIONS,
706
+ keys: [
707
+ { pubkey: attKey, isSigner: false, isWritable: true },
708
+ { pubkey: issuerKey, isSigner: true, isWritable: false },
709
+ ],
710
+ data: disc,
711
+ });
712
+
713
+ const tx = new Transaction().add(ix);
714
+ tx.feePayer = issuerKey;
715
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
716
+
717
+ return { transaction: tx };
718
+ }
719
+
720
+ // ═══════════════════════════════════════════════════
721
+ // REVIEWS — Create / Update / Delete / Counter
722
+ // ═══════════════════════════════════════════════════
723
+
724
+ /**
725
+ * Build initReviewCounter transaction. Must be called once per agent before any reviews.
726
+ * PDA: ["review_counter_v3", SHA256(agent_id)]
727
+ * @param {PublicKey|string} payer - Transaction signer + fee payer
728
+ * @param {string} agentId - Agent identifier
729
+ * @returns {{ transaction: Transaction, counterPDA: PublicKey }}
730
+ */
731
+ async buildInitReviewCounter(payer, agentId) {
732
+ const payerKey = new PublicKey(payer);
733
+ const [counterPDA] = getV3ReviewCounterPDA(agentId, this.network);
734
+
735
+ const disc = anchorDiscriminator('init_review_counter');
736
+ const data = Buffer.concat([disc, serializeString(agentId)]);
737
+
738
+ const ix = new TransactionInstruction({
739
+ programId: this.programIds.REVIEWS,
740
+ keys: [
741
+ { pubkey: counterPDA, isSigner: false, isWritable: true },
742
+ { pubkey: payerKey, isSigner: true, isWritable: true },
743
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
744
+ ],
745
+ data,
746
+ });
747
+
748
+ const tx = new Transaction().add(ix);
749
+ tx.feePayer = payerKey;
750
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
751
+
752
+ return { transaction: tx, counterPDA };
753
+ }
754
+
755
+ /**
756
+ * Build createReview transaction (V3.1 with self-review prevention).
757
+ * PDA: ["review_v3", SHA256(agent_id), reviewer]
758
+ *
759
+ * @param {PublicKey|string} reviewer - Reviewer wallet (signer + fee payer)
760
+ * @param {string} agentId - Agent identifier being reviewed
761
+ * @param {number} rating - 1-5 rating
762
+ * @param {string} reviewText - Review text (max 512 chars)
763
+ * @param {string} [metadata=''] - Optional metadata JSON (max 256 chars)
764
+ * @param {object} [opts={}] - Optional self-review check params
765
+ * @param {PublicKey|string} [opts.identityProgram] - Identity program ID for self-review check
766
+ * @param {PublicKey|string} [opts.identityAccount] - Identity PDA for self-review check
767
+ * @returns {{ transaction: Transaction, reviewPDA: PublicKey }}
768
+ */
769
+ async buildCreateReview(reviewer, agentId, rating, reviewText, metadata = '', opts = {}) {
770
+ const reviewerKey = new PublicKey(reviewer);
771
+ const [reviewPDA] = getV3ReviewPDA(agentId, reviewerKey, this.network);
772
+ const [counterPDA] = getV3ReviewCounterPDA(agentId, this.network);
773
+
774
+ // Self-review check: if identityProgram is provided, use it; otherwise use system_program (skip check)
775
+ const identityProgram = opts.identityProgram
776
+ ? new PublicKey(opts.identityProgram)
777
+ : SystemProgram.programId;
778
+ const identityAccount = opts.identityAccount
779
+ ? new PublicKey(opts.identityAccount)
780
+ : SystemProgram.programId; // placeholder when check is skipped
781
+
782
+ const disc = anchorDiscriminator('create_review');
783
+ const data = Buffer.concat([
784
+ disc,
785
+ serializeString(agentId),
786
+ Buffer.from([rating]),
787
+ serializeString(reviewText),
788
+ serializeString(metadata),
789
+ ]);
790
+
791
+ const ix = new TransactionInstruction({
792
+ programId: this.programIds.REVIEWS,
793
+ keys: [
794
+ { pubkey: reviewPDA, isSigner: false, isWritable: true },
795
+ { pubkey: counterPDA, isSigner: false, isWritable: true },
796
+ { pubkey: reviewerKey, isSigner: true, isWritable: true },
797
+ { pubkey: identityProgram, isSigner: false, isWritable: false },
798
+ { pubkey: identityAccount, isSigner: false, isWritable: false },
799
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
800
+ ],
801
+ data,
802
+ });
803
+
804
+ const tx = new Transaction().add(ix);
805
+ tx.feePayer = reviewerKey;
806
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
807
+
808
+ return { transaction: tx, reviewPDA };
809
+ }
810
+
811
+ /**
812
+ * Build createReview with self-review prevention enabled (convenience).
813
+ * Automatically resolves identity PDA from agentId.
814
+ * @param {PublicKey|string} reviewer
815
+ * @param {string} agentId
816
+ * @param {number} rating
817
+ * @param {string} reviewText
818
+ * @param {string} [metadata='']
819
+ * @returns {{ transaction: Transaction, reviewPDA: PublicKey }}
820
+ */
821
+ async buildCreateReviewWithSelfCheck(reviewer, agentId, rating, reviewText, metadata = '') {
822
+ const [identityAccount] = getGenesisPDA(agentId, this.network);
823
+ return this.buildCreateReview(reviewer, agentId, rating, reviewText, metadata, {
824
+ identityProgram: this.programIds.IDENTITY,
825
+ identityAccount,
826
+ });
827
+ }
828
+
829
+ /**
830
+ * Build updateReview transaction (reviewer only).
831
+ * @param {PublicKey|string} reviewer - Must be the original reviewer
832
+ * @param {PublicKey|string} reviewPDA - Review account address
833
+ * @param {object} updates - { rating?: number, reviewText?: string, metadata?: string }
834
+ * @returns {{ transaction: Transaction }}
835
+ */
836
+ async buildUpdateReview(reviewer, reviewPDA, updates = {}) {
837
+ const reviewerKey = new PublicKey(reviewer);
838
+ const reviewKey = new PublicKey(reviewPDA);
839
+
840
+ const disc = anchorDiscriminator('update_review');
841
+
842
+ // Encode Option<u8> rating
843
+ const ratingBuf = updates.rating != null
844
+ ? Buffer.from([1, updates.rating])
845
+ : Buffer.from([0]);
846
+
847
+ // Encode Option<String> review_text
848
+ const textBuf = updates.reviewText != null
849
+ ? Buffer.concat([Buffer.from([1]), serializeString(updates.reviewText)])
850
+ : Buffer.from([0]);
851
+
852
+ // Encode Option<String> metadata
853
+ const metaBuf = updates.metadata != null
854
+ ? Buffer.concat([Buffer.from([1]), serializeString(updates.metadata)])
855
+ : Buffer.from([0]);
856
+
857
+ const data = Buffer.concat([disc, ratingBuf, textBuf, metaBuf]);
858
+
859
+ const ix = new TransactionInstruction({
860
+ programId: this.programIds.REVIEWS,
861
+ keys: [
862
+ { pubkey: reviewKey, isSigner: false, isWritable: true },
863
+ { pubkey: reviewerKey, isSigner: true, isWritable: false },
864
+ ],
865
+ data,
866
+ });
867
+
868
+ const tx = new Transaction().add(ix);
869
+ tx.feePayer = reviewerKey;
870
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
871
+
872
+ return { transaction: tx };
873
+ }
874
+
875
+ /**
876
+ * Build deleteReview transaction (soft-delete, reviewer only).
877
+ * @param {PublicKey|string} reviewer - Must be the original reviewer
878
+ * @param {PublicKey|string} reviewPDA - Review account address
879
+ * @returns {{ transaction: Transaction }}
880
+ */
881
+ async buildDeleteReview(reviewer, reviewPDA) {
882
+ const reviewerKey = new PublicKey(reviewer);
883
+ const reviewKey = new PublicKey(reviewPDA);
884
+
885
+ const disc = anchorDiscriminator('delete_review');
886
+
887
+ const ix = new TransactionInstruction({
888
+ programId: this.programIds.REVIEWS,
889
+ keys: [
890
+ { pubkey: reviewKey, isSigner: false, isWritable: true },
891
+ { pubkey: reviewerKey, isSigner: true, isWritable: false },
892
+ ],
893
+ data: disc,
894
+ });
895
+
896
+ const tx = new Transaction().add(ix);
897
+ tx.feePayer = reviewerKey;
898
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
899
+
900
+ return { transaction: tx };
901
+ }
902
+
903
+ /**
904
+ * Fetch a Review account.
905
+ * @param {string} agentId - Agent identifier
906
+ * @param {PublicKey|string} reviewer - Reviewer pubkey
907
+ * @returns {object|null}
908
+ */
909
+ async getReview(agentId, reviewer) {
910
+ const reviewerKey = new PublicKey(reviewer);
911
+ const [pda] = getV3ReviewPDA(agentId, reviewerKey, this.network);
912
+ const acct = await this.connection.getAccountInfo(pda);
913
+ if (!acct) return null;
914
+
915
+ try {
916
+ const data = acct.data.slice(8); // skip Anchor discriminator
917
+ let offset = 0;
918
+
919
+ const readString = () => {
920
+ const len = data.readUInt32LE(offset); offset += 4;
921
+ const str = data.slice(offset, offset + len).toString('utf8'); offset += len;
922
+ return str;
923
+ };
924
+
925
+ const reviewAgentId = readString();
926
+ const agentIdHash = data.slice(offset, offset + 32); offset += 32;
927
+ const reviewerPk = new PublicKey(data.slice(offset, offset + 32)); offset += 32;
928
+ const rating = data[offset]; offset += 1;
929
+ const reviewText = readString();
930
+ const metadata = readString();
931
+ const createdAt = Number(data.readBigInt64LE(offset)); offset += 8;
932
+ const updatedAt = Number(data.readBigInt64LE(offset)); offset += 8;
933
+ const isActive = data[offset] === 1; offset += 1;
934
+ const bump = data[offset]; offset += 1;
935
+
936
+ return {
937
+ pda: pda.toBase58(),
938
+ agentId: reviewAgentId,
939
+ agentIdHash: Buffer.from(agentIdHash).toString('hex'),
940
+ reviewer: reviewerPk.toBase58(),
941
+ rating,
942
+ reviewText,
943
+ metadata,
944
+ createdAt,
945
+ updatedAt,
946
+ bump,
947
+ };
948
+ } catch (e) {
949
+ return { pda: pda.toBase58(), raw: acct.data.toString('hex'), error: e.message };
950
+ }
951
+ }
952
+
953
+ /**
954
+ * Fetch review counter for an agent.
955
+ * @param {string} agentId
956
+ * @returns {{ count: number, pda: string }|null}
957
+ */
958
+ async getReviewCount(agentId) {
959
+ const [pda] = getV3ReviewCounterPDA(agentId, this.network);
960
+ const acct = await this.connection.getAccountInfo(pda);
961
+ if (!acct) return null;
962
+
963
+ try {
964
+ const data = acct.data.slice(8);
965
+ let offset = 0;
966
+
967
+ const readString = () => {
968
+ const len = data.readUInt32LE(offset); offset += 4;
969
+ const str = data.slice(offset, offset + len).toString('utf8'); offset += len;
970
+ return str;
971
+ };
972
+
973
+ const counterAgentId = readString();
974
+ offset += 32; // agent_id_hash
975
+ const count = Number(data.readBigUInt64LE(offset)); offset += 8;
976
+ const bump = data[offset]; offset += 1;
977
+
978
+ return { pda: pda.toBase58(), agentId: counterAgentId, count, bump };
979
+ } catch (e) {
980
+ return { pda: pda.toBase58(), raw: acct.data.toString('hex'), error: e.message };
981
+ }
982
+ }
983
+
984
+ // ═══════════════════════════════════════════════════
985
+ // REPUTATION — Permissionless Recompute
986
+ // ═══════════════════════════════════════════════════
987
+
988
+ /**
989
+ * Build recomputeReputation transaction (permissionless).
990
+ * Reads review accounts from remaining_accounts and CPIs into Identity.
991
+ * @param {PublicKey|string} caller - Transaction signer + fee payer
992
+ * @param {string|Buffer} agentIdOrHash
993
+ * @param {PublicKey[]} reviewAccounts - Array of Review account pubkeys to include
994
+ * @returns {{ transaction: Transaction }}
995
+ */
996
+ async buildRecomputeReputation(caller, agentIdOrHash, reviewAccounts = []) {
997
+ const callerKey = new PublicKey(caller);
998
+ const [genesisPDA] = getGenesisPDA(agentIdOrHash, this.network);
999
+ const [repAuthority] = getV3ReputationAuthorityPDA(this.network);
1000
+
1001
+ const disc = anchorDiscriminator('recompute_reputation');
1002
+
1003
+ const keys = [
1004
+ { pubkey: genesisPDA, isSigner: false, isWritable: true },
1005
+ { pubkey: repAuthority, isSigner: false, isWritable: false },
1006
+ { pubkey: this.programIds.IDENTITY, isSigner: false, isWritable: false },
1007
+ { pubkey: callerKey, isSigner: true, isWritable: true },
1008
+ ];
1009
+
1010
+ // Add review accounts as remaining_accounts (read-only)
1011
+ for (const acct of reviewAccounts) {
1012
+ keys.push({ pubkey: new PublicKey(acct), isSigner: false, isWritable: false });
1013
+ }
1014
+
1015
+ const ix = new TransactionInstruction({
1016
+ programId: this.programIds.REPUTATION,
1017
+ keys,
1018
+ data: disc,
1019
+ });
1020
+
1021
+ const tx = new Transaction().add(ix);
1022
+ tx.feePayer = callerKey;
1023
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
1024
+
1025
+ return { transaction: tx };
1026
+ }
1027
+
1028
+ // ═══════════════════════════════════════════════════
1029
+ // VALIDATION — Permissionless Recompute
1030
+ // ═══════════════════════════════════════════════════
1031
+
1032
+ /**
1033
+ * Build recomputeLevel transaction (permissionless).
1034
+ * Reads attestation accounts from remaining_accounts and CPIs into Identity.
1035
+ * @param {PublicKey|string} caller
1036
+ * @param {string|Buffer} agentIdOrHash
1037
+ * @param {PublicKey[]} attestationAccounts - Array of Attestation account pubkeys
1038
+ * @returns {{ transaction: Transaction }}
1039
+ */
1040
+ async buildRecomputeLevel(caller, agentIdOrHash, attestationAccounts = []) {
1041
+ const callerKey = new PublicKey(caller);
1042
+ const [genesisPDA] = getGenesisPDA(agentIdOrHash, this.network);
1043
+ const [valAuthority] = getV3ValidationAuthorityPDA(this.network);
1044
+
1045
+ const disc = anchorDiscriminator('recompute_level');
1046
+
1047
+ const keys = [
1048
+ { pubkey: genesisPDA, isSigner: false, isWritable: true },
1049
+ { pubkey: valAuthority, isSigner: false, isWritable: false },
1050
+ { pubkey: this.programIds.IDENTITY, isSigner: false, isWritable: false },
1051
+ { pubkey: callerKey, isSigner: true, isWritable: true },
1052
+ ];
1053
+
1054
+ for (const acct of attestationAccounts) {
1055
+ keys.push({ pubkey: new PublicKey(acct), isSigner: false, isWritable: false });
1056
+ }
1057
+
1058
+ const ix = new TransactionInstruction({
1059
+ programId: this.programIds.VALIDATION,
1060
+ keys,
1061
+ data: disc,
1062
+ });
1063
+
1064
+ const tx = new Transaction().add(ix);
1065
+ tx.feePayer = callerKey;
1066
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
1067
+
1068
+ return { transaction: tx };
1069
+ }
1070
+
1071
+ // ═══════════════════════════════════════════════════
1072
+ // ESCROW V3 — Identity-Verified On-Chain Escrow
1073
+ // ═══════════════════════════════════════════════════
1074
+
1075
+ /**
1076
+ * Build createEscrow transaction (V3 — identity-verified).
1077
+ *
1078
+ * Creates an escrow between a client and a verified SATP V3 agent.
1079
+ * Verifies agent's Genesis Record on-chain (PDA derivation + owner check).
1080
+ * Optionally enforces minimum verification level and born status.
1081
+ *
1082
+ * PDA seeds: ["escrow_v3", client, description_hash, nonce_le_bytes]
1083
+ *
1084
+ * @param {PublicKey|string} client - Client wallet (signer + payer)
1085
+ * @param {PublicKey|string} agentWallet - Agent's wallet to receive funds
1086
+ * @param {string} agentId - Agent identifier (for Genesis Record lookup)
1087
+ * @param {number} amount - Lamports to escrow
1088
+ * @param {string|Buffer} descriptionOrHash - Job description string (will be SHA-256 hashed) or 32-byte hash buffer
1089
+ * @param {number} deadline - Unix timestamp deadline
1090
+ * @param {number} [nonce=0] - Nonce for uniqueness (multiple escrows between same parties)
1091
+ * @param {object} [opts={}] - Optional trust requirements
1092
+ * @param {number} [opts.minVerificationLevel=0] - Minimum verification level (0-5)
1093
+ * @param {boolean} [opts.requireBorn=false] - Require agent to have completed burn-to-become
1094
+ * @param {PublicKey|string} [opts.arbiter] - Arbiter for dispute resolution (defaults to client)
1095
+ * @returns {{ transaction: Transaction, escrowPDA: PublicKey, descriptionHash: Buffer }}
1096
+ */
1097
+ async buildCreateEscrow(client, agentWallet, agentId, amount, descriptionOrHash, deadline, nonce = 0, opts = {}) {
1098
+ const clientKey = new PublicKey(client);
1099
+ const agentWalletKey = new PublicKey(agentWallet);
1100
+ const descriptionHash = Buffer.isBuffer(descriptionOrHash) && descriptionOrHash.length === 32
1101
+ ? descriptionOrHash
1102
+ : crypto.createHash('sha256').update(typeof descriptionOrHash === 'string' ? descriptionOrHash : Buffer.from(descriptionOrHash)).digest();
1103
+
1104
+ const [escrowPDA] = getV3EscrowPDA(clientKey, descriptionHash, nonce, this.network);
1105
+ const [agentIdentityPDA] = getGenesisPDA(agentId, this.network);
1106
+
1107
+ const arbiterKey = opts.arbiter ? new PublicKey(opts.arbiter) : clientKey;
1108
+ const minVerificationLevel = opts.minVerificationLevel || 0;
1109
+ const requireBorn = opts.requireBorn || false;
1110
+
1111
+ const disc = anchorDiscriminator('create_escrow');
1112
+
1113
+ // Serialize args: agent_id (String), amount (u64), description_hash ([u8;32]),
1114
+ // deadline (i64), nonce (u64), min_verification_level (u8), require_born (bool)
1115
+ const amountBuf = Buffer.alloc(8);
1116
+ amountBuf.writeBigUInt64LE(BigInt(amount));
1117
+ const deadlineBuf = Buffer.alloc(8);
1118
+ deadlineBuf.writeBigInt64LE(BigInt(deadline));
1119
+ const nonceBuf = Buffer.alloc(8);
1120
+ nonceBuf.writeBigUInt64LE(BigInt(nonce));
1121
+
1122
+ const data = Buffer.concat([
1123
+ disc,
1124
+ serializeString(agentId), // String
1125
+ amountBuf, // u64
1126
+ descriptionHash, // [u8; 32]
1127
+ deadlineBuf, // i64
1128
+ nonceBuf, // u64
1129
+ Buffer.from([minVerificationLevel]), // u8
1130
+ Buffer.from([requireBorn ? 1 : 0]), // bool
1131
+ ]);
1132
+
1133
+ const ix = new TransactionInstruction({
1134
+ programId: this.programIds.ESCROW,
1135
+ keys: [
1136
+ { pubkey: clientKey, isSigner: true, isWritable: true },
1137
+ { pubkey: agentWalletKey, isSigner: false, isWritable: false },
1138
+ { pubkey: agentIdentityPDA, isSigner: false, isWritable: false },
1139
+ { pubkey: arbiterKey, isSigner: false, isWritable: false },
1140
+ { pubkey: escrowPDA, isSigner: false, isWritable: true },
1141
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
1142
+ ],
1143
+ data,
1144
+ });
1145
+
1146
+ const tx = new Transaction().add(ix);
1147
+ tx.feePayer = clientKey;
1148
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
1149
+
1150
+ return { transaction: tx, escrowPDA, descriptionHash };
1151
+ }
1152
+
1153
+ /**
1154
+ * Build submitWork transaction (agent submits work proof).
1155
+ * @param {PublicKey|string} agent - Agent wallet (signer)
1156
+ * @param {PublicKey|string} escrowPDA - Escrow account address
1157
+ * @param {string|Buffer} workProofOrHash - Work proof string (SHA-256 hashed) or 32-byte hash
1158
+ * @returns {{ transaction: Transaction, workHash: Buffer }}
1159
+ */
1160
+ async buildSubmitWork(agent, escrowPDA, workProofOrHash) {
1161
+ const agentKey = new PublicKey(agent);
1162
+ const escrowKey = new PublicKey(escrowPDA);
1163
+ const workHash = Buffer.isBuffer(workProofOrHash) && workProofOrHash.length === 32
1164
+ ? workProofOrHash
1165
+ : crypto.createHash('sha256').update(typeof workProofOrHash === 'string' ? workProofOrHash : Buffer.from(workProofOrHash)).digest();
1166
+
1167
+ const disc = anchorDiscriminator('submit_work');
1168
+ const data = Buffer.concat([disc, workHash]);
1169
+
1170
+ const ix = new TransactionInstruction({
1171
+ programId: this.programIds.ESCROW,
1172
+ keys: [
1173
+ { pubkey: escrowKey, isSigner: false, isWritable: true },
1174
+ { pubkey: agentKey, isSigner: true, isWritable: false },
1175
+ ],
1176
+ data,
1177
+ });
1178
+
1179
+ const tx = new Transaction().add(ix);
1180
+ tx.feePayer = agentKey;
1181
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
1182
+
1183
+ return { transaction: tx, workHash };
1184
+ }
1185
+
1186
+ /**
1187
+ * Build release transaction (client releases full remaining funds to agent).
1188
+ * @param {PublicKey|string} client - Client wallet (signer)
1189
+ * @param {PublicKey|string} agent - Agent wallet (receives funds)
1190
+ * @param {PublicKey|string} escrowPDA - Escrow account address
1191
+ * @returns {{ transaction: Transaction }}
1192
+ */
1193
+ async buildEscrowRelease(client, agent, escrowPDA) {
1194
+ const clientKey = new PublicKey(client);
1195
+ const agentKey = new PublicKey(agent);
1196
+ const escrowKey = new PublicKey(escrowPDA);
1197
+
1198
+ const disc = anchorDiscriminator('release');
1199
+
1200
+ const ix = new TransactionInstruction({
1201
+ programId: this.programIds.ESCROW,
1202
+ keys: [
1203
+ { pubkey: escrowKey, isSigner: false, isWritable: true },
1204
+ { pubkey: clientKey, isSigner: true, isWritable: false },
1205
+ { pubkey: agentKey, isSigner: false, isWritable: true },
1206
+ ],
1207
+ data: disc,
1208
+ });
1209
+
1210
+ const tx = new Transaction().add(ix);
1211
+ tx.feePayer = clientKey;
1212
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
1213
+
1214
+ return { transaction: tx };
1215
+ }
1216
+
1217
+ /**
1218
+ * Build partialRelease transaction (milestone payment).
1219
+ * @param {PublicKey|string} client - Client wallet (signer)
1220
+ * @param {PublicKey|string} agent - Agent wallet (receives funds)
1221
+ * @param {PublicKey|string} escrowPDA - Escrow account address
1222
+ * @param {number} amount - Lamports to release
1223
+ * @returns {{ transaction: Transaction }}
1224
+ */
1225
+ async buildPartialRelease(client, agent, escrowPDA, amount) {
1226
+ const clientKey = new PublicKey(client);
1227
+ const agentKey = new PublicKey(agent);
1228
+ const escrowKey = new PublicKey(escrowPDA);
1229
+
1230
+ const disc = anchorDiscriminator('partial_release');
1231
+ const amountBuf = Buffer.alloc(8);
1232
+ amountBuf.writeBigUInt64LE(BigInt(amount));
1233
+
1234
+ const data = Buffer.concat([disc, amountBuf]);
1235
+
1236
+ const ix = new TransactionInstruction({
1237
+ programId: this.programIds.ESCROW,
1238
+ keys: [
1239
+ { pubkey: escrowKey, isSigner: false, isWritable: true },
1240
+ { pubkey: clientKey, isSigner: true, isWritable: false },
1241
+ { pubkey: agentKey, isSigner: false, isWritable: true },
1242
+ ],
1243
+ data,
1244
+ });
1245
+
1246
+ const tx = new Transaction().add(ix);
1247
+ tx.feePayer = clientKey;
1248
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
1249
+
1250
+ return { transaction: tx };
1251
+ }
1252
+
1253
+ /**
1254
+ * Build cancel transaction (client cancels after deadline, gets refund).
1255
+ * @param {PublicKey|string} client - Client wallet (signer)
1256
+ * @param {PublicKey|string} escrowPDA - Escrow account address
1257
+ * @returns {{ transaction: Transaction }}
1258
+ */
1259
+ async buildCancelEscrow(client, escrowPDA) {
1260
+ const clientKey = new PublicKey(client);
1261
+ const escrowKey = new PublicKey(escrowPDA);
1262
+
1263
+ const disc = anchorDiscriminator('cancel');
1264
+
1265
+ const ix = new TransactionInstruction({
1266
+ programId: this.programIds.ESCROW,
1267
+ keys: [
1268
+ { pubkey: escrowKey, isSigner: false, isWritable: true },
1269
+ { pubkey: clientKey, isSigner: true, isWritable: true },
1270
+ ],
1271
+ data: disc,
1272
+ });
1273
+
1274
+ const tx = new Transaction().add(ix);
1275
+ tx.feePayer = clientKey;
1276
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
1277
+
1278
+ return { transaction: tx };
1279
+ }
1280
+
1281
+ /**
1282
+ * Build raiseDispute transaction (either client or agent).
1283
+ * @param {PublicKey|string} signer - Client or agent wallet (signer)
1284
+ * @param {PublicKey|string} escrowPDA - Escrow account address
1285
+ * @param {string|Buffer} reasonOrHash - Dispute reason string (SHA-256 hashed) or 32-byte hash
1286
+ * @returns {{ transaction: Transaction, reasonHash: Buffer }}
1287
+ */
1288
+ async buildRaiseDispute(signer, escrowPDA, reasonOrHash) {
1289
+ const signerKey = new PublicKey(signer);
1290
+ const escrowKey = new PublicKey(escrowPDA);
1291
+ const reasonHash = Buffer.isBuffer(reasonOrHash) && reasonOrHash.length === 32
1292
+ ? reasonOrHash
1293
+ : crypto.createHash('sha256').update(typeof reasonOrHash === 'string' ? reasonOrHash : Buffer.from(reasonOrHash)).digest();
1294
+
1295
+ const disc = anchorDiscriminator('raise_dispute');
1296
+ const data = Buffer.concat([disc, reasonHash]);
1297
+
1298
+ const ix = new TransactionInstruction({
1299
+ programId: this.programIds.ESCROW,
1300
+ keys: [
1301
+ { pubkey: escrowKey, isSigner: false, isWritable: true },
1302
+ { pubkey: signerKey, isSigner: true, isWritable: false },
1303
+ ],
1304
+ data,
1305
+ });
1306
+
1307
+ const tx = new Transaction().add(ix);
1308
+ tx.feePayer = signerKey;
1309
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
1310
+
1311
+ return { transaction: tx, reasonHash };
1312
+ }
1313
+
1314
+ /**
1315
+ * Build resolveDispute transaction (arbiter splits funds).
1316
+ * @param {PublicKey|string} arbiter - Designated arbiter (signer)
1317
+ * @param {PublicKey|string} agent - Agent wallet (receives agent_amount)
1318
+ * @param {PublicKey|string} clientWallet - Client wallet (receives client_amount)
1319
+ * @param {PublicKey|string} escrowPDA - Escrow account address
1320
+ * @param {number} agentAmount - Lamports to release to agent
1321
+ * @param {number} clientAmount - Lamports to refund to client
1322
+ * @returns {{ transaction: Transaction }}
1323
+ */
1324
+ async buildResolveDispute(arbiter, agent, clientWallet, escrowPDA, agentAmount, clientAmount) {
1325
+ const arbiterKey = new PublicKey(arbiter);
1326
+ const agentKey = new PublicKey(agent);
1327
+ const clientKey = new PublicKey(clientWallet);
1328
+ const escrowKey = new PublicKey(escrowPDA);
1329
+
1330
+ const disc = anchorDiscriminator('resolve_dispute');
1331
+ const agentAmtBuf = Buffer.alloc(8);
1332
+ agentAmtBuf.writeBigUInt64LE(BigInt(agentAmount));
1333
+ const clientAmtBuf = Buffer.alloc(8);
1334
+ clientAmtBuf.writeBigUInt64LE(BigInt(clientAmount));
1335
+
1336
+ const data = Buffer.concat([disc, agentAmtBuf, clientAmtBuf]);
1337
+
1338
+ const ix = new TransactionInstruction({
1339
+ programId: this.programIds.ESCROW,
1340
+ keys: [
1341
+ { pubkey: escrowKey, isSigner: false, isWritable: true },
1342
+ { pubkey: arbiterKey, isSigner: true, isWritable: false },
1343
+ { pubkey: agentKey, isSigner: false, isWritable: true },
1344
+ { pubkey: clientKey, isSigner: false, isWritable: true },
1345
+ ],
1346
+ data,
1347
+ });
1348
+
1349
+ const tx = new Transaction().add(ix);
1350
+ tx.feePayer = arbiterKey;
1351
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
1352
+
1353
+ return { transaction: tx };
1354
+ }
1355
+
1356
+ /**
1357
+ * Build extendDeadline transaction (client extends escrow deadline).
1358
+ * Only when Active. New deadline must be strictly after current.
1359
+ * @param {PublicKey|string} client - Client wallet (signer)
1360
+ * @param {PublicKey|string} escrowPDA - Escrow account address
1361
+ * @param {number} newDeadline - New Unix timestamp deadline
1362
+ * @returns {{ transaction: Transaction }}
1363
+ */
1364
+ async buildExtendDeadline(client, escrowPDA, newDeadline) {
1365
+ const clientKey = new PublicKey(client);
1366
+ const escrowKey = new PublicKey(escrowPDA);
1367
+
1368
+ const disc = anchorDiscriminator('extend_deadline');
1369
+ const deadlineBuf = Buffer.alloc(8);
1370
+ deadlineBuf.writeBigInt64LE(BigInt(newDeadline));
1371
+
1372
+ const data = Buffer.concat([disc, deadlineBuf]);
1373
+
1374
+ const ix = new TransactionInstruction({
1375
+ programId: this.programIds.ESCROW,
1376
+ keys: [
1377
+ { pubkey: escrowKey, isSigner: false, isWritable: true },
1378
+ { pubkey: clientKey, isSigner: true, isWritable: false },
1379
+ ],
1380
+ data,
1381
+ });
1382
+
1383
+ const tx = new Transaction().add(ix);
1384
+ tx.feePayer = clientKey;
1385
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
1386
+
1387
+ return { transaction: tx };
1388
+ }
1389
+
1390
+ /**
1391
+ * Build closeEscrow transaction (returns rent to client).
1392
+ * Only when Released, Cancelled, or Resolved.
1393
+ * @param {PublicKey|string} client - Client wallet (signer, receives rent)
1394
+ * @param {PublicKey|string} escrowPDA - Escrow account address
1395
+ * @returns {{ transaction: Transaction }}
1396
+ */
1397
+ async buildCloseEscrow(client, escrowPDA) {
1398
+ const clientKey = new PublicKey(client);
1399
+ const escrowKey = new PublicKey(escrowPDA);
1400
+
1401
+ const disc = anchorDiscriminator('close_escrow');
1402
+
1403
+ const ix = new TransactionInstruction({
1404
+ programId: this.programIds.ESCROW,
1405
+ keys: [
1406
+ { pubkey: escrowKey, isSigner: false, isWritable: true },
1407
+ { pubkey: clientKey, isSigner: true, isWritable: true },
1408
+ ],
1409
+ data: disc,
1410
+ });
1411
+
1412
+ const tx = new Transaction().add(ix);
1413
+ tx.feePayer = clientKey;
1414
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
1415
+
1416
+ return { transaction: tx };
1417
+ }
1418
+
1419
+ /**
1420
+ * Fetch and deserialize an Escrow V3 account.
1421
+ * @param {PublicKey|string} escrowPDA - Escrow account address
1422
+ * @returns {object|null}
1423
+ */
1424
+ async getEscrow(escrowPDA) {
1425
+ const escrowKey = new PublicKey(escrowPDA);
1426
+ const acct = await this.connection.getAccountInfo(escrowKey);
1427
+ if (!acct) return null;
1428
+
1429
+ try {
1430
+ const data = acct.data.slice(8); // skip Anchor discriminator
1431
+ let offset = 0;
1432
+
1433
+ const client = new PublicKey(data.slice(offset, offset + 32)); offset += 32;
1434
+ const agent = new PublicKey(data.slice(offset, offset + 32)); offset += 32;
1435
+ const agentIdHash = data.slice(offset, offset + 32); offset += 32;
1436
+ const amount = Number(data.readBigUInt64LE(offset)); offset += 8;
1437
+ const releasedAmount = Number(data.readBigUInt64LE(offset)); offset += 8;
1438
+ const descriptionHash = data.slice(offset, offset + 32); offset += 32;
1439
+ const deadline = Number(data.readBigInt64LE(offset)); offset += 8;
1440
+ const nonce = Number(data.readBigUInt64LE(offset)); offset += 8;
1441
+ const statusByte = data[offset]; offset += 1;
1442
+ const minVerificationLevel = data[offset]; offset += 1;
1443
+ const requireBorn = data[offset] === 1; offset += 1;
1444
+ const createdAt = Number(data.readBigInt64LE(offset)); offset += 8;
1445
+ const arbiter = new PublicKey(data.slice(offset, offset + 32)); offset += 32;
1446
+
1447
+ // Option<[u8; 32]> work_hash
1448
+ const hasWorkHash = data[offset] === 1; offset += 1;
1449
+ let workHash = null;
1450
+ if (hasWorkHash) {
1451
+ workHash = Buffer.from(data.slice(offset, offset + 32)).toString('hex');
1452
+ }
1453
+ offset += 32;
1454
+
1455
+ // Option<i64> work_submitted_at
1456
+ const hasWorkSubmittedAt = data[offset] === 1; offset += 1;
1457
+ let workSubmittedAt = null;
1458
+ if (hasWorkSubmittedAt) {
1459
+ workSubmittedAt = Number(data.readBigInt64LE(offset));
1460
+ }
1461
+ offset += 8;
1462
+
1463
+ // Option<[u8; 32]> dispute_reason_hash
1464
+ const hasDisputeHash = data[offset] === 1; offset += 1;
1465
+ let disputeReasonHash = null;
1466
+ if (hasDisputeHash) {
1467
+ disputeReasonHash = Buffer.from(data.slice(offset, offset + 32)).toString('hex');
1468
+ }
1469
+ offset += 32;
1470
+
1471
+ // Option<i64> disputed_at
1472
+ const hasDisputedAt = data[offset] === 1; offset += 1;
1473
+ let disputedAt = null;
1474
+ if (hasDisputedAt) {
1475
+ disputedAt = Number(data.readBigInt64LE(offset));
1476
+ }
1477
+ offset += 8;
1478
+
1479
+ // Option<Pubkey> disputed_by
1480
+ const hasDisputedBy = data[offset] === 1; offset += 1;
1481
+ let disputedBy = null;
1482
+ if (hasDisputedBy) {
1483
+ disputedBy = new PublicKey(data.slice(offset, offset + 32)).toBase58();
1484
+ }
1485
+ offset += 32;
1486
+
1487
+ const bump = data[offset]; offset += 1;
1488
+
1489
+ const STATUS_MAP = ['Active', 'WorkSubmitted', 'Released', 'Cancelled', 'Disputed', 'Resolved'];
1490
+
1491
+ return {
1492
+ pda: escrowKey.toBase58(),
1493
+ client: client.toBase58(),
1494
+ agent: agent.toBase58(),
1495
+ agentIdHash: Buffer.from(agentIdHash).toString('hex'),
1496
+ amount,
1497
+ releasedAmount,
1498
+ remaining: amount - releasedAmount,
1499
+ descriptionHash: Buffer.from(descriptionHash).toString('hex'),
1500
+ deadline,
1501
+ nonce,
1502
+ status: STATUS_MAP[statusByte] || `Unknown(${statusByte})`,
1503
+ statusCode: statusByte,
1504
+ minVerificationLevel,
1505
+ requireBorn,
1506
+ createdAt,
1507
+ arbiter: arbiter.toBase58(),
1508
+ workHash,
1509
+ workSubmittedAt,
1510
+ disputeReasonHash,
1511
+ disputedAt,
1512
+ disputedBy,
1513
+ bump,
1514
+ };
1515
+ } catch (e) {
1516
+ return { pda: escrowKey.toBase58(), raw: acct.data.toString('hex'), error: e.message };
1517
+ }
1518
+ }
1519
+
1520
+ /**
1521
+ * Derive Escrow V3 PDA without RPC calls.
1522
+ * @param {PublicKey|string} client - Client wallet
1523
+ * @param {string|Buffer} descriptionOrHash - Job description or 32-byte hash
1524
+ * @param {number} [nonce=0]
1525
+ * @returns {{ escrowPDA: string, bump: number, descriptionHash: string }}
1526
+ */
1527
+ getEscrowPDA(client, descriptionOrHash, nonce = 0) {
1528
+ const descriptionHash = Buffer.isBuffer(descriptionOrHash) && descriptionOrHash.length === 32
1529
+ ? descriptionOrHash
1530
+ : crypto.createHash('sha256').update(typeof descriptionOrHash === 'string' ? descriptionOrHash : Buffer.from(descriptionOrHash)).digest();
1531
+ const [pda, bump] = getV3EscrowPDA(client, descriptionHash, nonce, this.network);
1532
+ return {
1533
+ escrowPDA: pda.toBase58(),
1534
+ bump,
1535
+ descriptionHash: descriptionHash.toString('hex'),
1536
+ };
1537
+ }
1538
+
1539
+ // ═══════════════════════════════════════════════════
1540
+ // V2 → V3 MIGRATION
1541
+ // ═══════════════════════════════════════════════════
1542
+
1543
+ /**
1544
+ * Build migrateV2ToV3 transaction.
1545
+ * @param {PublicKey|string} v2Authority - V2 identity authority (signer + payer)
1546
+ * @param {string} agentId - Agent identifier
1547
+ * @param {object} meta - { name, description, category, capabilities, metadataUri }
1548
+ * @returns {{ transaction: Transaction, genesisPDA: PublicKey }}
1549
+ */
1550
+ async buildMigrateV2ToV3(v2Authority, agentId, meta) {
1551
+ const v2AuthKey = new PublicKey(v2Authority);
1552
+ const agentIdHash = hashAgentId(agentId);
1553
+ const [genesisPDA] = getGenesisPDA(agentIdHash, this.network);
1554
+
1555
+ const disc = anchorDiscriminator('migrate_v2_to_v3');
1556
+ const data = Buffer.concat([
1557
+ disc,
1558
+ agentIdHash,
1559
+ serializeString(meta.name || ''),
1560
+ serializeString(meta.description || ''),
1561
+ serializeString(meta.category || ''),
1562
+ serializeVecString(meta.capabilities || []),
1563
+ serializeString(meta.metadataUri || ''),
1564
+ ]);
1565
+
1566
+ const ix = new TransactionInstruction({
1567
+ programId: this.programIds.IDENTITY,
1568
+ keys: [
1569
+ { pubkey: genesisPDA, isSigner: false, isWritable: true },
1570
+ { pubkey: v2AuthKey, isSigner: true, isWritable: true },
1571
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
1572
+ ],
1573
+ data,
1574
+ });
1575
+
1576
+ const tx = new Transaction().add(ix);
1577
+ tx.feePayer = v2AuthKey;
1578
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
1579
+
1580
+ return { transaction: tx, genesisPDA };
1581
+ }
1582
+
1583
+ // ═══════════════════════════════════════════════════
1584
+ // READ — Fetch On-Chain State
1585
+ // ═══════════════════════════════════════════════════
1586
+
1587
+ /**
1588
+ * Fetch Genesis Record data.
1589
+ * @param {string|Buffer} agentIdOrHash
1590
+ * @returns {object|null}
1591
+ */
1592
+ async getGenesisRecord(agentIdOrHash) {
1593
+ const [pda] = getGenesisPDA(agentIdOrHash, this.network);
1594
+ const acct = await this.connection.getAccountInfo(pda);
1595
+ if (!acct) return null;
1596
+
1597
+ try {
1598
+ const data = acct.data.slice(8); // skip Anchor discriminator
1599
+ let offset = 0;
1600
+
1601
+ const agentIdHash = data.slice(offset, offset + 32); offset += 32;
1602
+
1603
+ // Read strings
1604
+ const readString = () => {
1605
+ const len = data.readUInt32LE(offset); offset += 4;
1606
+ const str = data.slice(offset, offset + len).toString('utf8'); offset += len;
1607
+ return str;
1608
+ };
1609
+
1610
+ const readVecString = () => {
1611
+ const count = data.readUInt32LE(offset); offset += 4;
1612
+ const arr = [];
1613
+ for (let i = 0; i < count; i++) arr.push(readString());
1614
+ return arr;
1615
+ };
1616
+
1617
+ const agentName = readString();
1618
+ const description = readString();
1619
+ const category = readString();
1620
+ const capabilities = readVecString();
1621
+ const metadataUri = readString();
1622
+ const faceImage = readString();
1623
+ const faceMint = new PublicKey(data.slice(offset, offset + 32)); offset += 32;
1624
+ const faceBurnTx = readString();
1625
+ const genesisRecord = Number(data.readBigInt64LE(offset)); offset += 8;
1626
+ // No isActive on current deployed program GTppU4E44BqXTQg...
1627
+ const authority = new PublicKey(data.slice(offset, offset + 32)); offset += 32;
1628
+
1629
+ // Option<Pubkey> — Borsh: 0x00 = None (1 byte only), 0x01 + 32 bytes = Some
1630
+ const hasPending = data[offset] === 1; offset += 1;
1631
+ let pendingAuthority = null;
1632
+ if (hasPending) {
1633
+ pendingAuthority = new PublicKey(data.slice(offset, offset + 32)).toBase58();
1634
+ offset += 32;
1635
+ }
1636
+
1637
+ const reputationScore = Number(data.readBigUInt64LE(offset)); offset += 8;
1638
+ const verificationLevel = data[offset]; offset += 1;
1639
+ const reputationUpdatedAt = Number(data.readBigInt64LE(offset)); offset += 8;
1640
+ const verificationUpdatedAt = Number(data.readBigInt64LE(offset)); offset += 8;
1641
+ const createdAt = Number(data.readBigInt64LE(offset)); offset += 8;
1642
+ const updatedAt = Number(data.readBigInt64LE(offset)); offset += 8;
1643
+ const bump = data[offset]; offset += 1;
1644
+
1645
+ const isBorn = genesisRecord !== 0;
1646
+
1647
+ return {
1648
+ pda: pda.toBase58(),
1649
+ agentIdHash: Buffer.from(agentIdHash).toString('hex'),
1650
+ agentName,
1651
+ description,
1652
+ category,
1653
+ capabilities,
1654
+ metadataUri,
1655
+ faceImage: faceImage || null,
1656
+ faceMint: faceMint.equals(PublicKey.default) ? null : faceMint.toBase58(),
1657
+ faceBurnTx: faceBurnTx || null,
1658
+ genesisRecord,
1659
+ isBorn,
1660
+ authority: authority.toBase58(),
1661
+ pendingAuthority,
1662
+ reputationScore,
1663
+ verificationLevel,
1664
+ reputationUpdatedAt,
1665
+ verificationUpdatedAt,
1666
+ createdAt,
1667
+ updatedAt,
1668
+ bump,
1669
+ };
1670
+ } catch (e) {
1671
+ return { pda: pda.toBase58(), raw: acct.data.toString('hex'), error: e.message };
1672
+ }
1673
+ }
1674
+
1675
+ /**
1676
+ * Check if a name is available.
1677
+ * @param {string} name
1678
+ * @returns {boolean} true if available (not registered)
1679
+ */
1680
+ async isNameAvailable(name) {
1681
+ const [pda] = getNameRegistryPDA(name, this.network);
1682
+ const acct = await this.connection.getAccountInfo(pda);
1683
+ if (!acct) return true;
1684
+ // Account exists — check if still active
1685
+ try {
1686
+ const data = acct.data.slice(8);
1687
+ // NameRegistry: name (string), name_hash (32), identity (32), authority (32), registered_at (8), is_active (1), bump (1)
1688
+ let offset = 0;
1689
+ const len = data.readUInt32LE(offset); offset += 4 + len;
1690
+ offset += 32; // name_hash
1691
+ offset += 32; // identity
1692
+ offset += 32; // authority
1693
+ offset += 8; // registered_at
1694
+ const isActive = data[offset] === 1;
1695
+ return !isActive; // available if released
1696
+ } catch {
1697
+ return false;
1698
+ }
1699
+ }
1700
+
1701
+ // ═══════════════════════════════════════════════════
1702
+ // UTILITY — PDA Derivation (no RPC)
1703
+ // ═══════════════════════════════════════════════════
1704
+
1705
+ /**
1706
+ * Derive all V3 PDAs for an agent.
1707
+ * @param {string} agentId
1708
+ * @returns {object}
1709
+ */
1710
+ getV3PDAs(agentId) {
1711
+ const hash = hashAgentId(agentId);
1712
+ const [genesis] = getGenesisPDA(hash, this.network);
1713
+ const [mintTracker] = getV3MintTrackerPDA(genesis, this.network);
1714
+ const [repAuthority] = getV3ReputationAuthorityPDA(this.network);
1715
+ const [valAuthority] = getV3ValidationAuthorityPDA(this.network);
1716
+
1717
+ return {
1718
+ agentIdHash: hash.toString('hex'),
1719
+ genesis: genesis.toBase58(),
1720
+ mintTracker: mintTracker.toBase58(),
1721
+ reputationAuthority: repAuthority.toBase58(),
1722
+ validationAuthority: valAuthority.toBase58(),
1723
+ };
1724
+ }
1725
+
1726
+ /**
1727
+ * Hash an agent ID to 32-byte seed.
1728
+ * @param {string} agentId
1729
+ * @returns {Buffer}
1730
+ */
1731
+ hashAgentId(agentId) {
1732
+ return hashAgentId(agentId);
1733
+ }
1734
+
1735
+ /**
1736
+ * Check if an agent has a registered identity.
1737
+ * @param {string} agentId
1738
+ * @returns {boolean}
1739
+ */
1740
+ async hasIdentity(agentId) {
1741
+ const record = await this.getGenesisRecord(agentId);
1742
+ return record !== null && !record.error;
1743
+ }
1744
+ }
1745
+
1746
+ module.exports = {
1747
+ createSATPClient,
1748
+ SATPV3SDK,
1749
+ anchorDiscriminator,
1750
+ serializeString,
1751
+ serializeVecString,
1752
+ };
1753
+ // ═══════════════════════════════════════════════
1754
+ // createSATPClient — Factory for route compatibility
1755
+ // ═══════════════════════════════════════════════
1756
+
1757
+ function createSATPClient(opts = {}) {
1758
+ const sdk = new SATPV3SDK(opts);
1759
+
1760
+ sdk.resolveAgent = function(agentId) {
1761
+ const agentIdHash = hashAgentId(agentId);
1762
+ const [pda] = getGenesisPDA(agentIdHash, sdk.network);
1763
+ return pda;
1764
+ };
1765
+
1766
+ sdk.getNameRegistry = async function(name) {
1767
+ try {
1768
+ const nameHash = hashName(name);
1769
+ const [pda] = getNameRegistryPDA(nameHash, sdk.network);
1770
+ const info = await sdk.connection.getAccountInfo(pda);
1771
+ if (!info || !info.data) return null;
1772
+ return { pda: pda.toBase58(), data: info.data };
1773
+ } catch (e) { return null; }
1774
+ };
1775
+
1776
+ sdk.isNameTaken = async function(name) {
1777
+ const reg = await sdk.getNameRegistry(name);
1778
+ return reg !== null;
1779
+ };
1780
+
1781
+ sdk.getLinkedWallets = async function(agentId) {
1782
+ try {
1783
+ const agentIdHash = hashAgentId(agentId);
1784
+ const [genesisPDA] = getGenesisPDA(agentIdHash, sdk.network);
1785
+ const identityProgramId = sdk.programIds.IDENTITY;
1786
+ const accounts = await sdk.connection.getProgramAccounts(identityProgramId, {
1787
+ filters: [
1788
+ { memcmp: { offset: 8, bytes: genesisPDA.toBase58() } },
1789
+ { dataSize: 138 }
1790
+ ]
1791
+ });
1792
+ return accounts.map(a => ({
1793
+ pubkey: a.pubkey.toBase58(),
1794
+ data: a.account.data,
1795
+ }));
1796
+ } catch (e) { return []; }
1797
+ };
1798
+
1799
+ return sdk;
1800
+ }