@oobe-protocol-labs/synapse-sap-sdk 0.12.8 → 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.
- package/dist/cjs/modules/base.js +61 -0
- package/dist/cjs/modules/base.js.map +1 -1
- package/dist/cjs/modules/escrow-v2.js +250 -0
- package/dist/cjs/modules/escrow-v2.js.map +1 -1
- package/dist/cjs/modules/escrow.js +36 -0
- package/dist/cjs/modules/escrow.js.map +1 -1
- package/dist/cjs/modules/staking.js +35 -0
- package/dist/cjs/modules/staking.js.map +1 -1
- package/dist/cjs/modules/tools.js +26 -0
- package/dist/cjs/modules/tools.js.map +1 -1
- package/dist/cjs/modules/vault.js +17 -0
- package/dist/cjs/modules/vault.js.map +1 -1
- package/dist/cjs/utils/anchor-errors.js +453 -0
- package/dist/cjs/utils/anchor-errors.js.map +1 -0
- package/dist/cjs/utils/index.js +12 -1
- package/dist/cjs/utils/index.js.map +1 -1
- package/dist/cjs/utils/volume-curve.js +117 -0
- package/dist/cjs/utils/volume-curve.js.map +1 -0
- package/dist/esm/modules/base.js +61 -0
- package/dist/esm/modules/base.js.map +1 -1
- package/dist/esm/modules/escrow-v2.js +250 -0
- package/dist/esm/modules/escrow-v2.js.map +1 -1
- package/dist/esm/modules/escrow.js +36 -0
- package/dist/esm/modules/escrow.js.map +1 -1
- package/dist/esm/modules/staking.js +35 -0
- package/dist/esm/modules/staking.js.map +1 -1
- package/dist/esm/modules/tools.js +26 -0
- package/dist/esm/modules/tools.js.map +1 -1
- package/dist/esm/modules/vault.js +17 -0
- package/dist/esm/modules/vault.js.map +1 -1
- package/dist/esm/utils/anchor-errors.js +447 -0
- package/dist/esm/utils/anchor-errors.js.map +1 -0
- package/dist/esm/utils/index.js +4 -0
- package/dist/esm/utils/index.js.map +1 -1
- package/dist/esm/utils/volume-curve.js +114 -0
- package/dist/esm/utils/volume-curve.js.map +1 -0
- package/dist/types/modules/base.d.ts +35 -0
- package/dist/types/modules/base.d.ts.map +1 -1
- package/dist/types/modules/escrow-v2.d.ts +98 -1
- package/dist/types/modules/escrow-v2.d.ts.map +1 -1
- package/dist/types/modules/escrow.d.ts.map +1 -1
- package/dist/types/modules/staking.d.ts.map +1 -1
- package/dist/types/modules/tools.d.ts.map +1 -1
- package/dist/types/modules/vault.d.ts.map +1 -1
- package/dist/types/utils/anchor-errors.d.ts +61 -0
- package/dist/types/utils/anchor-errors.d.ts.map +1 -0
- package/dist/types/utils/index.d.ts +3 -0
- package/dist/types/utils/index.d.ts.map +1 -1
- package/dist/types/utils/volume-curve.d.ts +60 -0
- package/dist/types/utils/volume-curve.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/modules/base.ts +89 -0
- package/src/modules/escrow-v2.ts +327 -1
- package/src/modules/escrow.ts +56 -0
- package/src/modules/staking.ts +66 -0
- package/src/modules/tools.ts +43 -0
- package/src/modules/vault.ts +25 -0
- package/src/utils/anchor-errors.ts +461 -0
- package/src/utils/index.ts +16 -0
- package/src/utils/volume-curve.ts +131 -0
package/src/modules/escrow-v2.ts
CHANGED
|
@@ -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,
|
|
@@ -377,6 +551,49 @@ export class EscrowV2Module extends BaseModule {
|
|
|
377
551
|
const [pendingPda] = this.derivePendingSettlement(escrowPda, settlementIndex);
|
|
378
552
|
const [statsPda] = deriveAgentStats(agentPda);
|
|
379
553
|
|
|
554
|
+
// v0.12.9: preflight against ArithmeticOverflow at finalize.
|
|
555
|
+
//
|
|
556
|
+
// The on-chain handler subtracts `pending_settlement.amount` from BOTH
|
|
557
|
+
// `escrow.balance` AND `escrow.pending_amount`. If the PendingSettlement
|
|
558
|
+
// PDA was created without a preceding `settle_calls_v2` (orphan PDA from
|
|
559
|
+
// legacy probe loops, or a buggy caller that skipped the settle step),
|
|
560
|
+
// `escrow.pending_amount` is smaller than `pending_settlement.amount`
|
|
561
|
+
// and the program aborts with ArithmeticOverflow (error 6075) at
|
|
562
|
+
// escrow_v2.rs:633. Each retry burns ~5 000 lamports of base fee.
|
|
563
|
+
//
|
|
564
|
+
// Detect this BEFORE signing and throw with a clear, actionable message
|
|
565
|
+
// pointing at the orphan-recovery path.
|
|
566
|
+
const [escrowAcc, pendingAcc] = await Promise.all([
|
|
567
|
+
this.fetchAccountNullable<EscrowAccountV2Data>("escrowAccountV2", escrowPda),
|
|
568
|
+
this.fetchAccountNullable<PendingSettlementData>("pendingSettlement", pendingPda),
|
|
569
|
+
]);
|
|
570
|
+
if (!escrowAcc) {
|
|
571
|
+
throw new Error(
|
|
572
|
+
`finalizeSettlement: escrow PDA ${escrowPda.toBase58()} not found on-chain.`,
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
if (!pendingAcc) {
|
|
576
|
+
throw new Error(
|
|
577
|
+
`finalizeSettlement: pending PDA ${pendingPda.toBase58()} not found on-chain ` +
|
|
578
|
+
`(settlementIndex=${settlementIndex.toString()}). Nothing to finalize.`,
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
const psAmount = BigInt(pendingAcc.amount.toString());
|
|
582
|
+
const escrowPendingAmount = BigInt(escrowAcc.pendingAmount.toString());
|
|
583
|
+
const escrowBalance = BigInt(escrowAcc.balance.toString());
|
|
584
|
+
if (psAmount > escrowPendingAmount || psAmount > escrowBalance) {
|
|
585
|
+
throw new Error(
|
|
586
|
+
`finalizeSettlement: orphan/inconsistent PendingSettlement detected ` +
|
|
587
|
+
`at ${pendingPda.toBase58()} (settlementIndex=${settlementIndex.toString()}). ` +
|
|
588
|
+
`pending.amount=${psAmount} but escrow.pending_amount=${escrowPendingAmount}, ` +
|
|
589
|
+
`escrow.balance=${escrowBalance}. The on-chain finalize would abort with ` +
|
|
590
|
+
`ArithmeticOverflow (6075). This PDA was almost certainly created by a ` +
|
|
591
|
+
`caller that skipped settle_calls_v2 (legacy probe loop). It cannot be ` +
|
|
592
|
+
`finalized and cannot be closed (close_pending_settlement requires ` +
|
|
593
|
+
`is_finalized=true). Skip this index permanently in your settle queue.`,
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
380
597
|
return this.methods
|
|
381
598
|
.finalizeSettlement()
|
|
382
599
|
.accounts({
|
|
@@ -389,6 +606,76 @@ export class EscrowV2Module extends BaseModule {
|
|
|
389
606
|
.rpc();
|
|
390
607
|
}
|
|
391
608
|
|
|
609
|
+
/**
|
|
610
|
+
* Identify orphan PendingSettlement PDAs that cannot be finalized.
|
|
611
|
+
*
|
|
612
|
+
* @returns `null` if the PDA is finalizable (or already finalized / disputed).
|
|
613
|
+
* Otherwise an object describing the inconsistency, suitable for
|
|
614
|
+
* logging or feeding into a quarantine list. Use this from a
|
|
615
|
+
* recovery script to scan a range of `settlement_index` values:
|
|
616
|
+
*
|
|
617
|
+
* ```ts
|
|
618
|
+
* for (let idx = 0n; idx < currentIdx; idx++) {
|
|
619
|
+
* const orphan = await sap.escrowV2.diagnoseOrphanPending(
|
|
620
|
+
* agentWallet, depositorWallet, nonce, idx,
|
|
621
|
+
* );
|
|
622
|
+
* if (orphan) log.warn({ idx, ...orphan }, "skip orphan");
|
|
623
|
+
* }
|
|
624
|
+
* ```
|
|
625
|
+
*
|
|
626
|
+
* @since v0.12.9
|
|
627
|
+
*/
|
|
628
|
+
async diagnoseOrphanPending(
|
|
629
|
+
agentWallet: PublicKey,
|
|
630
|
+
depositorWallet: PublicKey,
|
|
631
|
+
nonce: BN | number | bigint,
|
|
632
|
+
settlementIndex: BN | number | bigint,
|
|
633
|
+
): Promise<{
|
|
634
|
+
pendingPda: PublicKey;
|
|
635
|
+
psAmount: bigint;
|
|
636
|
+
escrowPendingAmount: bigint;
|
|
637
|
+
escrowBalance: bigint;
|
|
638
|
+
isFinalized: boolean;
|
|
639
|
+
isDisputed: boolean;
|
|
640
|
+
reason: "ok" | "missing" | "amount_exceeds_pending" | "amount_exceeds_balance" | "already_finalized" | "disputed";
|
|
641
|
+
} | null> {
|
|
642
|
+
const [agentPda] = deriveAgent(agentWallet);
|
|
643
|
+
const [escrowPda] = this.deriveEscrow(agentPda, depositorWallet, nonce);
|
|
644
|
+
const [pendingPda] = this.derivePendingSettlement(escrowPda, settlementIndex);
|
|
645
|
+
const [escrowAcc, pendingAcc] = await Promise.all([
|
|
646
|
+
this.fetchAccountNullable<EscrowAccountV2Data>("escrowAccountV2", escrowPda),
|
|
647
|
+
this.fetchAccountNullable<PendingSettlementData>("pendingSettlement", pendingPda),
|
|
648
|
+
]);
|
|
649
|
+
if (!escrowAcc) return null;
|
|
650
|
+
if (!pendingAcc) {
|
|
651
|
+
return {
|
|
652
|
+
pendingPda,
|
|
653
|
+
psAmount: 0n,
|
|
654
|
+
escrowPendingAmount: BigInt(escrowAcc.pendingAmount.toString()),
|
|
655
|
+
escrowBalance: BigInt(escrowAcc.balance.toString()),
|
|
656
|
+
isFinalized: false,
|
|
657
|
+
isDisputed: false,
|
|
658
|
+
reason: "missing",
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
const psAmount = BigInt(pendingAcc.amount.toString());
|
|
662
|
+
const escrowPendingAmount = BigInt(escrowAcc.pendingAmount.toString());
|
|
663
|
+
const escrowBalance = BigInt(escrowAcc.balance.toString());
|
|
664
|
+
const base = {
|
|
665
|
+
pendingPda,
|
|
666
|
+
psAmount,
|
|
667
|
+
escrowPendingAmount,
|
|
668
|
+
escrowBalance,
|
|
669
|
+
isFinalized: pendingAcc.isFinalized,
|
|
670
|
+
isDisputed: pendingAcc.isDisputed,
|
|
671
|
+
};
|
|
672
|
+
if (pendingAcc.isFinalized) return { ...base, reason: "already_finalized" };
|
|
673
|
+
if (pendingAcc.isDisputed) return { ...base, reason: "disputed" };
|
|
674
|
+
if (psAmount > escrowPendingAmount) return { ...base, reason: "amount_exceeds_pending" };
|
|
675
|
+
if (psAmount > escrowBalance) return { ...base, reason: "amount_exceeds_balance" };
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
|
|
392
679
|
async fileDispute(
|
|
393
680
|
agentWallet: PublicKey,
|
|
394
681
|
nonce: BN | number | bigint,
|
|
@@ -461,6 +748,25 @@ export class EscrowV2Module extends BaseModule {
|
|
|
461
748
|
const [agentPda] = deriveAgent(agentWallet);
|
|
462
749
|
const [escrowPda] = this.deriveEscrow(agentPda, undefined, nonce);
|
|
463
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
|
+
|
|
464
770
|
return this.methods
|
|
465
771
|
.withdrawEscrowV2(this.bn(amount))
|
|
466
772
|
.accounts({
|
|
@@ -477,6 +783,26 @@ export class EscrowV2Module extends BaseModule {
|
|
|
477
783
|
const [agentPda] = deriveAgent(agentWallet);
|
|
478
784
|
const [escrowPda] = this.deriveEscrow(agentPda, undefined, nonce);
|
|
479
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
|
+
|
|
480
806
|
return this.methods
|
|
481
807
|
.closeEscrowV2()
|
|
482
808
|
.accounts({
|
package/src/modules/escrow.ts
CHANGED
|
@@ -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({
|
package/src/modules/staking.ts
CHANGED
|
@@ -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({
|
package/src/modules/tools.ts
CHANGED
|
@@ -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)),
|
package/src/modules/vault.ts
CHANGED
|
@@ -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({
|