@oobe-protocol-labs/synapse-sap-sdk 0.12.9 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/cjs/modules/base.js +61 -0
  2. package/dist/cjs/modules/base.js.map +1 -1
  3. package/dist/cjs/modules/escrow-v2.js +153 -0
  4. package/dist/cjs/modules/escrow-v2.js.map +1 -1
  5. package/dist/cjs/modules/escrow.js +36 -0
  6. package/dist/cjs/modules/escrow.js.map +1 -1
  7. package/dist/cjs/modules/staking.js +35 -0
  8. package/dist/cjs/modules/staking.js.map +1 -1
  9. package/dist/cjs/modules/tools.js +26 -0
  10. package/dist/cjs/modules/tools.js.map +1 -1
  11. package/dist/cjs/modules/vault.js +17 -0
  12. package/dist/cjs/modules/vault.js.map +1 -1
  13. package/dist/cjs/utils/anchor-errors.js +453 -0
  14. package/dist/cjs/utils/anchor-errors.js.map +1 -0
  15. package/dist/cjs/utils/index.js +12 -1
  16. package/dist/cjs/utils/index.js.map +1 -1
  17. package/dist/cjs/utils/volume-curve.js +117 -0
  18. package/dist/cjs/utils/volume-curve.js.map +1 -0
  19. package/dist/esm/modules/base.js +61 -0
  20. package/dist/esm/modules/base.js.map +1 -1
  21. package/dist/esm/modules/escrow-v2.js +153 -0
  22. package/dist/esm/modules/escrow-v2.js.map +1 -1
  23. package/dist/esm/modules/escrow.js +36 -0
  24. package/dist/esm/modules/escrow.js.map +1 -1
  25. package/dist/esm/modules/staking.js +35 -0
  26. package/dist/esm/modules/staking.js.map +1 -1
  27. package/dist/esm/modules/tools.js +26 -0
  28. package/dist/esm/modules/tools.js.map +1 -1
  29. package/dist/esm/modules/vault.js +17 -0
  30. package/dist/esm/modules/vault.js.map +1 -1
  31. package/dist/esm/utils/anchor-errors.js +447 -0
  32. package/dist/esm/utils/anchor-errors.js.map +1 -0
  33. package/dist/esm/utils/index.js +4 -0
  34. package/dist/esm/utils/index.js.map +1 -1
  35. package/dist/esm/utils/volume-curve.js +114 -0
  36. package/dist/esm/utils/volume-curve.js.map +1 -0
  37. package/dist/types/modules/base.d.ts +35 -0
  38. package/dist/types/modules/base.d.ts.map +1 -1
  39. package/dist/types/modules/escrow-v2.d.ts +70 -1
  40. package/dist/types/modules/escrow-v2.d.ts.map +1 -1
  41. package/dist/types/modules/escrow.d.ts.map +1 -1
  42. package/dist/types/modules/staking.d.ts.map +1 -1
  43. package/dist/types/modules/tools.d.ts.map +1 -1
  44. package/dist/types/modules/vault.d.ts.map +1 -1
  45. package/dist/types/utils/anchor-errors.d.ts +61 -0
  46. package/dist/types/utils/anchor-errors.d.ts.map +1 -0
  47. package/dist/types/utils/index.d.ts +3 -0
  48. package/dist/types/utils/index.d.ts.map +1 -1
  49. package/dist/types/utils/volume-curve.d.ts +60 -0
  50. package/dist/types/utils/volume-curve.d.ts.map +1 -0
  51. package/package.json +1 -1
  52. package/src/modules/base.ts +89 -0
  53. package/src/modules/escrow-v2.ts +214 -1
  54. package/src/modules/escrow.ts +56 -0
  55. package/src/modules/staking.ts +66 -0
  56. package/src/modules/tools.ts +43 -0
  57. package/src/modules/vault.ts +25 -0
  58. package/src/utils/anchor-errors.ts +461 -0
  59. package/src/utils/index.ts +16 -0
  60. package/src/utils/volume-curve.ts +131 -0
@@ -39,6 +39,8 @@ import {
39
39
  } from "../utils/priority-fee";
40
40
  import type { SettleOptions } from "../utils/priority-fee";
41
41
  import { isAcceptedPaymentToken, computeRequiredStakeLamports } from "../constants/payments";
42
+ import { throwPredicted } from "../utils/anchor-errors";
43
+ import { calculateSettleAmount } from "../utils/volume-curve";
42
44
 
43
45
  /**
44
46
  * @name EscrowV2Module
@@ -221,6 +223,22 @@ export class EscrowV2Module extends BaseModule {
221
223
  const [agentPda] = deriveAgent(agentWallet);
222
224
  const [escrowPda] = this.deriveEscrow(agentPda, undefined, nonce);
223
225
 
226
+ // v0.13.0 preflights — escrow exists, token shape matches, amount > 0
227
+ const escrow = await this.requireAccountExists<EscrowAccountV2Data>(
228
+ "escrowAccountV2",
229
+ escrowPda,
230
+ { predicted: "NotAuthority", hint: "Escrow V2 PDA not found — call createEscrow first" },
231
+ );
232
+ const want = BigInt(this.bn(amount).toString());
233
+ if (want <= 0n) throwPredicted("InsufficientEscrowBalance", "Deposit amount must be > 0");
234
+ const isSpl = escrow.tokenMint != null;
235
+ if (isSpl && splAccounts.length < 4) {
236
+ throwPredicted("SplTokenRequired", "Pass [depositorAta, escrowAta, tokenMint, tokenProgram]");
237
+ }
238
+ if (!isSpl && splAccounts.length > 0) {
239
+ throwPredicted("InvalidTokenAccount", "SOL escrow does not accept splAccounts");
240
+ }
241
+
224
242
  return this.methods
225
243
  .depositEscrowV2(this.bn(nonce), this.bn(amount))
226
244
  .accounts({
@@ -232,13 +250,64 @@ export class EscrowV2Module extends BaseModule {
232
250
  .rpc();
233
251
  }
234
252
 
253
+ /**
254
+ * Settle a batch of calls against a V2 escrow.
255
+ *
256
+ * **v0.13.0 — Auto-bundle DisputeWindow:** when the escrow's
257
+ * `settlementSecurity` is `DisputeWindow`, this method now bundles
258
+ * **`settleCallsV2` + `createPendingSettlement`** into the SAME
259
+ * transaction. This eliminates the foot-gun where a caller would call
260
+ * `createPendingSettlement` directly without a preceding `settleCallsV2`,
261
+ * leaving `escrow.pending_amount = 0` and causing `finalizeSettlement`
262
+ * to abort with `ArithmeticOverflow` (6075).
263
+ *
264
+ * Flow per security mode:
265
+ * - **CoSigned** — single IX (`settleCallsV2`) with co-signer in
266
+ * remaining accounts; funds move immediately.
267
+ * - **DisputeWindow** — two IXs in one tx:
268
+ * 1. `settleCallsV2` — bumps `pending_amount` and `settlement_index`
269
+ * 2. `createPendingSettlement(idx)` — locks the dispute tracker PDA
270
+ *
271
+ * After this tx confirms, wait `escrow.disputeWindowSlots` slots and
272
+ * call {@link finalizeSettlement} with the index returned via
273
+ * `SettlementPendingEvent` or readable from `escrow.settlement_index - 1`.
274
+ *
275
+ * Pass `opts.skipAutoPending = true` to opt out of the auto-bundle (e.g.
276
+ * if you want to drive `createPendingSettlement` separately for advanced
277
+ * flows like batched off-chain receipt aggregation).
278
+ *
279
+ * @param depositorWallet - Depositor of the escrow being settled.
280
+ * @param nonce - Escrow nonce (default 0 for the canonical escrow).
281
+ * @param callsToSettle - Number of calls to settle in this batch.
282
+ * @param serviceHash - 32-byte sha256 of the service payload.
283
+ * @param splAccounts - Optional remaining accounts (SPL transfer + co-signer).
284
+ * @param opts - Priority-fee + auto-pending options.
285
+ * @param coSigner - Required for CoSigned escrows.
286
+ * @returns The transaction signature.
287
+ * @since v0.7.0 — initial release
288
+ * @since v0.13.0 — auto-bundles `createPendingSettlement` for DisputeWindow
289
+ */
235
290
  async settle(
236
291
  depositorWallet: PublicKey,
237
292
  nonce: BN | number | bigint,
238
293
  callsToSettle: BN | number | bigint,
239
294
  serviceHash: number[],
240
295
  splAccounts: AccountMeta[] = [],
241
- opts?: SettleOptions,
296
+ opts?: SettleOptions & {
297
+ /**
298
+ * v0.13.0 — Opt out of the DisputeWindow auto-bundle. When `true`,
299
+ * `settle()` only emits `settleCallsV2` and the caller is responsible
300
+ * for sending `createPendingSettlement` afterwards (legacy 2-tx flow).
301
+ * Default: `false` (auto-bundle is on).
302
+ */
303
+ skipAutoPending?: boolean;
304
+ /**
305
+ * v0.13.0 — `receiptMerkleRoot` to inscribe in the auto-bundled
306
+ * `createPendingSettlement`. Defaults to 32 zero bytes (no receipt
307
+ * batch). Ignored when `skipAutoPending = true`.
308
+ */
309
+ receiptMerkleRoot?: number[];
310
+ },
242
311
  coSigner?: Signer,
243
312
  ): Promise<TransactionSignature> {
244
313
  const [agentPda] = deriveAgent(this.walletPubkey);
@@ -260,6 +329,28 @@ export class EscrowV2Module extends BaseModule {
260
329
  ]
261
330
  : splAccounts;
262
331
 
332
+ // ── v0.13.0 preflight: fetch escrow once to drive both branches ──
333
+ // We need it to (a) detect DisputeWindow vs CoSigned, (b) read the
334
+ // current `settlement_index` to feed `createPendingSettlement`, and
335
+ // (c) compute `amount` via the volume curve so the bundled IX matches
336
+ // what `settle_calls_v2` will compute on-chain.
337
+ const escrowAcc = await this.fetchAccountNullable<EscrowAccountV2Data>(
338
+ "escrowAccountV2",
339
+ escrowPda,
340
+ );
341
+ if (!escrowAcc) {
342
+ throw new Error(
343
+ `escrowV2.settle: escrow PDA ${escrowPda.toBase58()} not found on-chain ` +
344
+ `(agent=${agentPda.toBase58()}, depositor=${depositorWallet.toBase58()}, nonce=${this.bn(nonce).toString()}). ` +
345
+ `Did the depositor call escrowV2.create() yet?`,
346
+ );
347
+ }
348
+
349
+ const isDisputeWindow =
350
+ typeof escrowAcc.settlementSecurity === "object" &&
351
+ escrowAcc.settlementSecurity !== null &&
352
+ "disputeWindow" in (escrowAcc.settlementSecurity as Record<string, unknown>);
353
+
263
354
  let builder = this.methods
264
355
  .settleCallsV2(this.bn(nonce), this.bn(callsToSettle), serviceHash)
265
356
  .accountsPartial({
@@ -280,6 +371,71 @@ export class EscrowV2Module extends BaseModule {
280
371
  builder = builder.preInstructions(preIxs);
281
372
  }
282
373
 
374
+ // ── v0.13.0: auto-bundle createPendingSettlement on DisputeWindow ──
375
+ // The on-chain `settle_calls_v2_handler` (DisputeWindow branch) only
376
+ // bumps `escrow.pending_amount` and `escrow.settlement_index` — it
377
+ // does NOT create the PendingSettlement PDA. Without the followup
378
+ // `createPendingSettlement` IX in the SAME tx, a buggy caller can
379
+ // (a) call `createPendingSettlement` later with a stale index, or
380
+ // (b) skip `settleCallsV2` entirely on a fresh escrow → orphan PDA
381
+ // whose `amount > escrow.pending_amount` → finalize aborts forever
382
+ // with `ArithmeticOverflow` (6075). Bundling is the only way to
383
+ // make that race impossible.
384
+ const skipAutoPending = opts?.skipAutoPending === true;
385
+ if (isDisputeWindow && !skipAutoPending) {
386
+ // PRE-increment settlement_index — settle_calls_v2 will bump it
387
+ // to (current + 1) AFTER our IX runs, but the PDA seed used by
388
+ // create_pending_settlement is the PRE-increment value (matches
389
+ // `SettlementPendingEvent.settlement_index`).
390
+ const settlementIndex = escrowAcc.settlementIndex;
391
+
392
+ // Mirror the on-chain volume-curve math so `pending.amount`
393
+ // matches what `escrow.pending_amount` was bumped by.
394
+ const totalCallsBefore = escrowAcc.totalCallsSettled.add(escrowAcc.pendingCalls);
395
+ const amount = calculateSettleAmount(
396
+ escrowAcc.pricePerCall,
397
+ escrowAcc.volumeCurve,
398
+ totalCallsBefore,
399
+ this.bn(callsToSettle),
400
+ );
401
+
402
+ const [pendingPda] = this.derivePendingSettlement(escrowPda, settlementIndex);
403
+
404
+ // Defensive: if a stale pending PDA exists at this index (orphan
405
+ // from an aborted prior run), refuse to send — otherwise the IX
406
+ // will fail with `Allocate: account already in use` (custom 0x0).
407
+ const existing = await this.provider.connection.getAccountInfo(pendingPda);
408
+ if (existing) {
409
+ throw new Error(
410
+ `escrowV2.settle (auto-bundle): pending PDA ${pendingPda.toBase58()} ` +
411
+ `already exists for settlementIndex=${settlementIndex.toString()}. ` +
412
+ `An earlier run created it but did not finalize. Either finalize ` +
413
+ `that index first or skip it permanently. (Pass skipAutoPending=true ` +
414
+ `to bypass this guard and emit only settleCallsV2.)`,
415
+ );
416
+ }
417
+
418
+ const receiptMerkleRoot = opts?.receiptMerkleRoot ?? new Array(32).fill(0);
419
+ const pendingIx = await this.methods
420
+ .createPendingSettlement(
421
+ settlementIndex,
422
+ this.bn(callsToSettle),
423
+ amount,
424
+ serviceHash,
425
+ receiptMerkleRoot,
426
+ )
427
+ .accounts({
428
+ wallet: this.walletPubkey,
429
+ agent: agentPda,
430
+ escrow: escrowPda,
431
+ pendingSettlement: pendingPda,
432
+ systemProgram: SystemProgram.programId,
433
+ })
434
+ .instruction();
435
+
436
+ builder = builder.postInstructions([pendingIx]);
437
+ }
438
+
283
439
  return builder.rpc(rpcOpts);
284
440
  }
285
441
 
@@ -315,6 +471,24 @@ export class EscrowV2Module extends BaseModule {
315
471
  return BigInt(escrow.settlementIndex.toString());
316
472
  }
317
473
 
474
+ /**
475
+ * Create the PendingSettlement PDA for a DisputeWindow batch.
476
+ *
477
+ * **v0.13.0 NOTE:** in almost all cases you should call
478
+ * {@link settle} instead — it auto-bundles `settleCallsV2 +
479
+ * createPendingSettlement` in a single transaction so the two
480
+ * cannot drift out of sync. Use this method standalone ONLY when
481
+ * you intentionally pass `skipAutoPending: true` to `settle()`
482
+ * (e.g. batched receipt-merkle aggregation across multiple
483
+ * `settleCallsV2` runs).
484
+ *
485
+ * Calling this without a preceding `settleCallsV2` (which bumps
486
+ * `escrow.pending_amount`) creates an orphan PDA whose
487
+ * `pending.amount > escrow.pending_amount` — `finalizeSettlement`
488
+ * will then abort with `ArithmeticOverflow` (6075) forever.
489
+ *
490
+ * @since v0.7.0
491
+ */
318
492
  async createPendingSettlement(
319
493
  agentWallet: PublicKey,
320
494
  depositorWallet: PublicKey,
@@ -574,6 +748,25 @@ export class EscrowV2Module extends BaseModule {
574
748
  const [agentPda] = deriveAgent(agentWallet);
575
749
  const [escrowPda] = this.deriveEscrow(agentPda, undefined, nonce);
576
750
 
751
+ // v0.13.0 preflight — amount must fit (balance - pendingAmount); the
752
+ // on-chain handler subtracts pending_amount from withdrawable funds.
753
+ const escrow = await this.requireAccountExists<EscrowAccountV2Data>(
754
+ "escrowAccountV2",
755
+ escrowPda,
756
+ { predicted: "NotAuthority", hint: "Escrow V2 PDA not found" },
757
+ );
758
+ const want = BigInt(this.bn(amount).toString());
759
+ if (want <= 0n) throwPredicted("InsufficientEscrowBalance", "Withdraw amount must be > 0");
760
+ const balance = BigInt(escrow.balance.toString());
761
+ const pending = BigInt(escrow.pendingAmount.toString());
762
+ const free = balance > pending ? balance - pending : 0n;
763
+ if (want > free) {
764
+ throwPredicted(
765
+ "InsufficientEscrowBalance",
766
+ `requested ${want}, withdrawable ${free} (balance ${balance} − pending ${pending})`,
767
+ );
768
+ }
769
+
577
770
  return this.methods
578
771
  .withdrawEscrowV2(this.bn(amount))
579
772
  .accounts({
@@ -590,6 +783,26 @@ export class EscrowV2Module extends BaseModule {
590
783
  const [agentPda] = deriveAgent(agentWallet);
591
784
  const [escrowPda] = this.deriveEscrow(agentPda, undefined, nonce);
592
785
 
786
+ // v0.13.0 preflight — close fails if balance != 0 OR pending_amount != 0.
787
+ // Pending != 0 commonly indicates orphan PendingSettlement PDAs;
788
+ // run diagnoseOrphanPending() across the index range to identify them.
789
+ const escrow = await this.requireAccountExists<EscrowAccountV2Data>(
790
+ "escrowAccountV2",
791
+ escrowPda,
792
+ { predicted: "NotAuthority", hint: "Escrow V2 PDA already closed" },
793
+ );
794
+ const balance = BigInt(escrow.balance.toString());
795
+ const pending = BigInt(escrow.pendingAmount.toString());
796
+ if (balance !== 0n) {
797
+ throwPredicted("EscrowNotEmpty", `balance ${balance} > 0 — withdraw first`);
798
+ }
799
+ if (pending !== 0n) {
800
+ throwPredicted(
801
+ "EscrowNotClosed",
802
+ `pending_amount ${pending} > 0 — finalize all PendingSettlements first or quarantine orphans via diagnoseOrphanPending`,
803
+ );
804
+ }
805
+
593
806
  return this.methods
594
807
  .closeEscrowV2()
595
808
  .accounts({
@@ -39,6 +39,7 @@ import {
39
39
  import type { SettleOptions } from "../utils/priority-fee";
40
40
  import { computeBatchRoot, hashToArray } from "../utils/hash";
41
41
  import { isAcceptedPaymentToken } from "../constants/payments";
42
+ import { throwPredicted } from "../utils/anchor-errors";
42
43
 
43
44
  /**
44
45
  * @name EscrowModule
@@ -160,6 +161,24 @@ export class EscrowModule extends BaseModule {
160
161
  const [agentPda] = deriveAgent(agentWallet);
161
162
  const [escrowPda] = this.deriveEscrow(agentPda);
162
163
 
164
+ // v0.13.0 preflight — escrow must exist; SPL deposits must include the
165
+ // 4 expected remaining accounts; SOL deposits must NOT include them.
166
+ const escrow = await this.requireAccountExists<EscrowAccountData>(
167
+ "escrowAccount",
168
+ escrowPda,
169
+ { predicted: "NotAuthority", hint: "Escrow PDA not found — call createEscrow first" },
170
+ );
171
+ const isSpl = escrow.tokenMint != null;
172
+ if (isSpl && splAccounts.length < 4) {
173
+ throwPredicted("SplTokenRequired", "Pass [depositorAta, escrowAta, tokenMint, tokenProgram] in splAccounts");
174
+ }
175
+ if (!isSpl && splAccounts.length > 0) {
176
+ throwPredicted("InvalidTokenAccount", "SOL escrow does not accept splAccounts; pass an empty array");
177
+ }
178
+ if (BigInt(this.bn(amount).toString()) <= 0n) {
179
+ throwPredicted("InsufficientEscrowBalance", "Deposit amount must be > 0");
180
+ }
181
+
163
182
  return this.methods
164
183
  .depositEscrow(this.bn(amount))
165
184
  .accounts({
@@ -235,6 +254,30 @@ export class EscrowModule extends BaseModule {
235
254
  const [agentPda] = deriveAgent(agentWallet);
236
255
  const [escrowPda] = this.deriveEscrow(agentPda);
237
256
 
257
+ // v0.13.0 preflight — verify balance covers amount, token program shape
258
+ // matches, and depositor matches the on-chain depositor.
259
+ const escrow = await this.requireAccountExists<EscrowAccountData>(
260
+ "escrowAccount",
261
+ escrowPda,
262
+ { predicted: "NotAuthority", hint: "Escrow PDA not found" },
263
+ );
264
+ const wantBN = this.bn(amount);
265
+ const want = BigInt(wantBN.toString());
266
+ if (want <= 0n) throwPredicted("InsufficientEscrowBalance", "Withdraw amount must be > 0");
267
+ if (want > BigInt(escrow.balance.toString())) {
268
+ throwPredicted(
269
+ "InsufficientEscrowBalance",
270
+ `requested ${want}, escrow.balance ${escrow.balance.toString()}`,
271
+ );
272
+ }
273
+ const isSpl = escrow.tokenMint != null;
274
+ if (isSpl && splAccounts.length < 4) {
275
+ throwPredicted("SplTokenRequired", "Pass [depositorAta, escrowAta, tokenMint, tokenProgram]");
276
+ }
277
+ if (!isSpl && splAccounts.length > 0) {
278
+ throwPredicted("InvalidTokenAccount", "SOL escrow does not accept splAccounts");
279
+ }
280
+
238
281
  return this.methods
239
282
  .withdrawEscrow(this.bn(amount))
240
283
  .accounts({
@@ -257,6 +300,19 @@ export class EscrowModule extends BaseModule {
257
300
  const [agentPda] = deriveAgent(agentWallet);
258
301
  const [escrowPda] = this.deriveEscrow(agentPda);
259
302
 
303
+ // v0.13.0 preflight — close fails on-chain if balance != 0.
304
+ const escrow = await this.requireAccountExists<EscrowAccountData>(
305
+ "escrowAccount",
306
+ escrowPda,
307
+ { predicted: "NotAuthority", hint: "Escrow PDA already closed or never created" },
308
+ );
309
+ if (BigInt(escrow.balance.toString()) !== 0n) {
310
+ throwPredicted(
311
+ "EscrowNotEmpty",
312
+ `balance ${escrow.balance.toString()} > 0 — withdraw remaining funds first`,
313
+ );
314
+ }
315
+
260
316
  return this.methods
261
317
  .closeEscrow()
262
318
  .accounts({
@@ -21,6 +21,7 @@ import {
21
21
  MIN_AGENT_STAKE_LAMPORTS,
22
22
  computeRequiredStakeLamports,
23
23
  } from "../constants/payments";
24
+ import { throwPredicted } from "../utils/anchor-errors";
24
25
 
25
26
  /**
26
27
  * @name StakingModule
@@ -46,6 +47,20 @@ export class StakingModule extends BaseModule {
46
47
  const [agentPda] = deriveAgent(agentWallet);
47
48
  const [stakePda] = this.deriveStake(agentPda);
48
49
 
50
+ // v0.13.0 preflights
51
+ const want = BigInt(this.bn(initialDeposit).toString());
52
+ if (want < MIN_AGENT_STAKE_LAMPORTS) {
53
+ throwPredicted(
54
+ "StakeBelowMinimum",
55
+ `initial deposit ${want} < MIN_AGENT_STAKE_LAMPORTS ${MIN_AGENT_STAKE_LAMPORTS}`,
56
+ );
57
+ }
58
+ await this.requireAccountAbsent(
59
+ "agentStake",
60
+ stakePda,
61
+ "Stake already initialized — use depositStake to top up",
62
+ );
63
+
49
64
  return this.methods
50
65
  .initStake(this.bn(initialDeposit))
51
66
  .accounts({
@@ -64,6 +79,16 @@ export class StakingModule extends BaseModule {
64
79
  const [agentPda] = deriveAgent(agentWallet);
65
80
  const [stakePda] = this.deriveStake(agentPda);
66
81
 
82
+ // v0.13.0 preflight — stake must already exist; amount > 0
83
+ if (BigInt(this.bn(amount).toString()) <= 0n) {
84
+ throwPredicted("InsufficientStake", "Deposit amount must be > 0");
85
+ }
86
+ await this.requireAccountExists<AgentStakeData>(
87
+ "agentStake",
88
+ stakePda,
89
+ { predicted: "NoStakeAccount", hint: "Call initStake first" },
90
+ );
91
+
67
92
  return this.methods
68
93
  .depositStake(this.bn(amount))
69
94
  .accounts({
@@ -82,6 +107,28 @@ export class StakingModule extends BaseModule {
82
107
  const [agentPda] = deriveAgent(agentWallet);
83
108
  const [stakePda] = this.deriveStake(agentPda);
84
109
 
110
+ // v0.13.0 preflight — enforce MIN_STAKE floor + no double-pending
111
+ const stake = await this.requireAccountExists<AgentStakeData>(
112
+ "agentStake",
113
+ stakePda,
114
+ { predicted: "NoStakeAccount", hint: "No stake account to unstake from" },
115
+ );
116
+ const want = BigInt(this.bn(amount).toString());
117
+ if (want <= 0n) throwPredicted("InsufficientStake", "Unstake amount must be > 0");
118
+ const max = this.getMaxUnstakeLamports(stake);
119
+ if (want > max) {
120
+ throwPredicted(
121
+ "StakeBelowMinimum",
122
+ `requested ${want} would drop stake below MIN_AGENT_STAKE_LAMPORTS (max unstake = ${max})`,
123
+ );
124
+ }
125
+ if (BigInt(stake.unstakeAmount.toString()) > 0n) {
126
+ throwPredicted(
127
+ "UnstakeAlreadyPending",
128
+ "A pending unstake already exists — completeUnstake first or wait for cooldown",
129
+ );
130
+ }
131
+
85
132
  return this.methods
86
133
  .requestUnstake(this.bn(amount))
87
134
  .accounts({
@@ -98,6 +145,25 @@ export class StakingModule extends BaseModule {
98
145
  const [agentPda] = deriveAgent(agentWallet);
99
146
  const [stakePda] = this.deriveStake(agentPda);
100
147
 
148
+ // v0.13.0 preflight — cooldown must have elapsed and an unstake must be pending
149
+ const stake = await this.requireAccountExists<AgentStakeData>(
150
+ "agentStake",
151
+ stakePda,
152
+ { predicted: "NoStakeAccount", hint: "No stake account" },
153
+ );
154
+ const pending = BigInt(stake.unstakeAmount.toString());
155
+ if (pending === 0n) {
156
+ throwPredicted("NoUnstakePending", "Call requestUnstake first");
157
+ }
158
+ const availableAt = BigInt(stake.unstakeAvailableAt.toString());
159
+ const nowSec = BigInt(Math.floor(Date.now() / 1000));
160
+ if (nowSec < availableAt) {
161
+ throwPredicted(
162
+ "UnstakeCooldownNotMet",
163
+ `available at unix ${availableAt}, now ${nowSec} (Δ ${availableAt - nowSec}s)`,
164
+ );
165
+ }
166
+
101
167
  return this.methods
102
168
  .completeUnstake()
103
169
  .accounts({
@@ -27,6 +27,7 @@ import type {
27
27
  InscribeToolSchemaArgs,
28
28
  } from "../types";
29
29
  import { sha256, hashToArray } from "../utils";
30
+ import { throwPredicted } from "../utils/anchor-errors";
30
31
 
31
32
  /**
32
33
  * @name ToolsModule
@@ -85,6 +86,34 @@ export class ToolsModule extends BaseModule {
85
86
  const [toolPda] = deriveTool(agentPda, new Uint8Array(args.toolNameHash));
86
87
  const [globalPda] = deriveGlobalRegistry();
87
88
 
89
+ // v0.13.0 preflights — enforce SAP v0.2.0 hardening rules
90
+ if (!args.toolName || args.toolName.length === 0) {
91
+ throwPredicted("EmptyToolName");
92
+ }
93
+ if (Buffer.byteLength(args.toolName, "utf8") > 32) {
94
+ throwPredicted("ToolNameTooLong", `${Buffer.byteLength(args.toolName, "utf8")} > 32 bytes`);
95
+ }
96
+ // v0.2.0 hardening: every tool MUST publish a non-empty input/output schema
97
+ // hash. Zero-hash tools are not callable by automated clients (LLMs/routers).
98
+ const isZeroHash = (h: number[]): boolean => h.length === 32 && h.every((b) => b === 0);
99
+ if (!args.inputSchemaHash || args.inputSchemaHash.length !== 32 || isZeroHash(args.inputSchemaHash)) {
100
+ throwPredicted(
101
+ "InvalidSchemaHash",
102
+ "inputSchemaHash is empty/zero — v0.2.0 requires every tool to declare a JSON-Schema. Use publishByName() or precompute sha256(schemaJson).",
103
+ );
104
+ }
105
+ if (!args.outputSchemaHash || args.outputSchemaHash.length !== 32 || isZeroHash(args.outputSchemaHash)) {
106
+ throwPredicted(
107
+ "InvalidSchemaHash",
108
+ "outputSchemaHash is empty/zero — v0.2.0 requires every tool to declare a JSON-Schema.",
109
+ );
110
+ }
111
+ await this.requireAccountAbsent(
112
+ "toolDescriptor",
113
+ toolPda,
114
+ "Tool already published with this name; use update() to change schema/version",
115
+ );
116
+
88
117
  return this.methods
89
118
  .publishTool(
90
119
  args.toolName,
@@ -138,6 +167,20 @@ export class ToolsModule extends BaseModule {
138
167
  requiredParams: number,
139
168
  isCompound: boolean,
140
169
  ): Promise<TransactionSignature> {
170
+ // v0.13.0 preflight — reject empty schemas BEFORE hashing (otherwise the
171
+ // hash of an empty string slips past the publish() schema-hash check).
172
+ if (!inputSchema || inputSchema.trim().length === 0) {
173
+ throwPredicted(
174
+ "InvalidSchemaHash",
175
+ "inputSchema string is empty — pass a valid JSON-Schema (v0.2.0 requirement)",
176
+ );
177
+ }
178
+ if (!outputSchema || outputSchema.trim().length === 0) {
179
+ throwPredicted(
180
+ "InvalidSchemaHash",
181
+ "outputSchema string is empty — pass a valid JSON-Schema (v0.2.0 requirement)",
182
+ );
183
+ }
141
184
  return this.publish({
142
185
  toolName,
143
186
  toolNameHash: hashToArray(sha256(toolName)),
@@ -29,6 +29,8 @@ import type {
29
29
  InscribeMemoryArgs,
30
30
  CompactInscribeArgs,
31
31
  } from "../types";
32
+ import { MAX_DELEGATE_DURATION_SECS } from "../constants/payments";
33
+ import { throwPredicted } from "../utils/anchor-errors";
32
34
 
33
35
  /**
34
36
  * @name VaultModule
@@ -360,6 +362,29 @@ export class VaultModule extends BaseModule {
360
362
  const [vaultPda] = deriveVault(agentPda);
361
363
  const [delegatePda] = deriveVaultDelegate(vaultPda, delegatePubkey);
362
364
 
365
+ // v0.13.0 preflight — on-chain rejects expiry > now + MAX_DELEGATE_DURATION_SECS
366
+ // (365 days). Catch this client-side instead of burning the tx.
367
+ const exp = typeof expiresAt === "bigint" ? expiresAt : BigInt(expiresAt);
368
+ const nowSec = BigInt(Math.floor(Date.now() / 1000));
369
+ if (exp <= nowSec) {
370
+ throwPredicted("DelegateExpired", `expiresAt ${exp} is in the past (now ${nowSec})`);
371
+ }
372
+ const maxExp = nowSec + BigInt(MAX_DELEGATE_DURATION_SECS);
373
+ if (exp > maxExp) {
374
+ throwPredicted(
375
+ "DelegateExpiryInvalid",
376
+ `expiresAt ${exp} > now+MAX (${maxExp}); cap is ${MAX_DELEGATE_DURATION_SECS}s (365 days)`,
377
+ );
378
+ }
379
+ if (permissions === 0) {
380
+ throwPredicted("InvalidDelegate", "permissions bitmask cannot be 0");
381
+ }
382
+ await this.requireAccountAbsent(
383
+ "vaultDelegate",
384
+ delegatePda,
385
+ "Delegate already exists — revokeDelegate first or use a different delegate wallet",
386
+ );
387
+
363
388
  return this.methods
364
389
  .addVaultDelegate(permissions, this.bn(expiresAt))
365
390
  .accounts({