@ercworldio/blockchain-shared 1.0.5-dev.7.PROJ-1464.0 → 1.0.5-dev.8

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 (40) hide show
  1. package/build/chains/networks_dev.json +10 -0
  2. package/build/chains/networks_prod-bu.json +0 -14
  3. package/build/chains/networks_prod-dz.json +22 -1
  4. package/build/errors/TransactionsErrors.d.ts +0 -1
  5. package/build/errors/TransactionsErrors.d.ts.map +1 -1
  6. package/build/errors/TransactionsErrors.js +0 -4
  7. package/build/index.d.ts +1 -0
  8. package/build/index.d.ts.map +1 -1
  9. package/build/index.js +4 -2
  10. package/build/services/ClaimJobService.d.ts.map +1 -1
  11. package/build/services/ClaimJobService.js +12 -5
  12. package/build/services/Redis.js +2 -2
  13. package/build/services/RedisPubSub.js +1 -1
  14. package/build/services/db/timelock/ScheduleTransactionService.d.ts +16 -2
  15. package/build/services/db/timelock/ScheduleTransactionService.d.ts.map +1 -1
  16. package/build/services/db/timelock/ScheduleTransactionService.js +265 -1
  17. package/build/services/rpc/RPC.d.ts +20 -0
  18. package/build/services/rpc/RPC.d.ts.map +1 -0
  19. package/build/services/rpc/RPC.js +49 -0
  20. package/build/services/solana/escrow/SolanaEscrowAdmin.d.ts +41 -1
  21. package/build/services/solana/escrow/SolanaEscrowAdmin.d.ts.map +1 -1
  22. package/build/services/solana/escrow/SolanaEscrowAdmin.js +35 -3
  23. package/build/services/solana/escrow/idl/escrow.json +4267 -988
  24. package/build/services/solana/escrow/services/EscrowAdminUtility.d.ts +116 -3
  25. package/build/services/solana/escrow/services/EscrowAdminUtility.d.ts.map +1 -1
  26. package/build/services/solana/escrow/services/EscrowAdminUtility.js +543 -55
  27. package/build/services/solana/escrow/services/TreasuryWithdrawalSignature.d.ts +58 -0
  28. package/build/services/solana/escrow/services/TreasuryWithdrawalSignature.d.ts.map +1 -0
  29. package/build/services/solana/escrow/services/TreasuryWithdrawalSignature.js +109 -0
  30. package/build/services/solana/escrow/types/escrow.d.ts +4262 -983
  31. package/build/services/solana/escrow/types/escrow.d.ts.map +1 -1
  32. package/build/services/solana/escrow/types/types.d.ts +17 -0
  33. package/build/services/solana/escrow/types/types.d.ts.map +1 -1
  34. package/build/services/types/claim.d.ts +1 -0
  35. package/build/services/types/claim.d.ts.map +1 -1
  36. package/build/services/types/db/timelock.d.ts +56 -0
  37. package/build/services/types/db/timelock.d.ts.map +1 -1
  38. package/build/utils/solana.d.ts.map +1 -1
  39. package/build/utils/solana.js +5 -3
  40. package/package.json +1 -1
@@ -56,10 +56,188 @@ const TransactionsService_1 = require("./TransactionsService");
56
56
  const Errors_1 = __importDefault(require("./Errors"));
57
57
  const TokenAccountService_1 = require("./TokenAccountService");
58
58
  const SolanaTokenProgramCache_1 = require("./SolanaTokenProgramCache");
59
+ const TreasuryWithdrawalSignature_1 = require("./TreasuryWithdrawalSignature");
59
60
  class EscrowAdminUtility {
60
- constructor(program, provider) {
61
+ constructor(program, provider, lookupTableAddress) {
61
62
  this.program = program;
62
63
  this.provider = provider;
64
+ if (lookupTableAddress)
65
+ this.lookupTableAddress = new web3_js_1.PublicKey(lookupTableAddress);
66
+ }
67
+ /** Accounts that are identical on every batch withdrawal — the ALT payload. */
68
+ lookup_table_addresses(mints) {
69
+ const programId = this.program.programId;
70
+ const global = [
71
+ EscrowService.findEscrowConfigPda(programId),
72
+ EscrowService.findVaultAuthorityPda(programId),
73
+ web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("treasury-config")], programId)[0],
74
+ web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("timelock-config")], programId)[0],
75
+ spl_token_1.TOKEN_PROGRAM_ID,
76
+ web3_js_1.SystemProgram.programId,
77
+ web3_js_1.SYSVAR_INSTRUCTIONS_PUBKEY,
78
+ ];
79
+ const perMint = mints.flatMap(m => {
80
+ const mint = new web3_js_1.PublicKey(m);
81
+ return [
82
+ mint,
83
+ EscrowService.findVaultAta(mint, programId),
84
+ web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("token-threshold"), mint.toBytes()], programId)[0],
85
+ web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("token-timelock"), mint.toBytes()], programId)[0],
86
+ ];
87
+ });
88
+ return [...global, ...perMint];
89
+ }
90
+ /**
91
+ * Idempotently guarantees a lookup table covering `mints` exists and is usable.
92
+ *
93
+ * Deliberately keeps no external state: tables are discovered on-chain by their authority
94
+ * (the funding wallet), so there is nothing to persist, migrate, or keep in sync across
95
+ * environments and worker instances. Extends a table that is missing addresses, creates one
96
+ * when none exists, and returns the usable address.
97
+ *
98
+ * Returns null if provisioning fails — the lookup table is an optimisation, so callers fall
99
+ * back to legacy transactions rather than failing a withdrawal that would otherwise succeed.
100
+ */
101
+ ensure_lookup_table(mints, signer) {
102
+ return __awaiter(this, void 0, void 0, function* () {
103
+ const required = this.lookup_table_addresses(mints);
104
+ const existing = this.lookupTableAddress
105
+ ? yield this.fetchLookupTable(this.lookupTableAddress)
106
+ : yield this.discoverLookupTable();
107
+ let table;
108
+ if (existing) {
109
+ const missing = this.missingAddresses(existing.state.addresses, required);
110
+ if (missing.length)
111
+ yield this.extendLookupTable(existing.key, missing, signer);
112
+ table = existing.key;
113
+ }
114
+ else {
115
+ table = yield this.createLookupTable(required, signer);
116
+ }
117
+ // Re-read and confirm the table actually covers what we need. A half-provisioned table
118
+ // (created but not extended) silently compiles to a v0 message with zero lookups, giving
119
+ // none of the size saving — so treat partial coverage as a hard failure rather than
120
+ // reporting success and overflowing later.
121
+ const settled = yield this.fetchLookupTable(table);
122
+ const stillMissing = settled ? this.missingAddresses(settled.state.addresses, required) : required;
123
+ if (stillMissing.length) {
124
+ throw new Error(`Lookup table ${table.toBase58()} is incomplete: ${stillMissing.length}/${required.length} addresses missing`);
125
+ }
126
+ this.lookupTableAddress = table;
127
+ this.lookupTableCache = undefined;
128
+ return table;
129
+ });
130
+ }
131
+ /** Back-compat wrapper for explicit one-time provisioning; prefer `ensure_lookup_table`. */
132
+ create_lookup_table(mints, signer) {
133
+ return __awaiter(this, void 0, void 0, function* () {
134
+ const table = yield this.ensure_lookup_table(mints, signer);
135
+ if (!table)
136
+ throw new Error("Failed to provision the escrow lookup table");
137
+ return table.toBase58();
138
+ });
139
+ }
140
+ fetchLookupTable(key) {
141
+ return __awaiter(this, void 0, void 0, function* () {
142
+ const { value } = yield this.provider.connection.getAddressLookupTable(key);
143
+ return value ? { key, state: value.state } : null;
144
+ });
145
+ }
146
+ /**
147
+ * Finds a live table already owned by this authority. `getProgramAccounts` is filtered
148
+ * server-side on the authority pubkey, which sits at a fixed offset in the table header:
149
+ * u32 typeIndex + u64 deactivationSlot + u64 lastExtendedSlot + u8 startIndex + u8 option tag.
150
+ */
151
+ discoverLookupTable() {
152
+ return __awaiter(this, void 0, void 0, function* () {
153
+ const AUTHORITY_OFFSET = 22;
154
+ const authority = this.provider.wallet.publicKey;
155
+ const accounts = yield this.provider.connection.getProgramAccounts(web3_js_1.AddressLookupTableProgram.programId, { filters: [{ memcmp: { offset: AUTHORITY_OFFSET, bytes: authority.toBase58() } }] });
156
+ const live = accounts
157
+ .map(a => {
158
+ try {
159
+ return { key: a.pubkey, state: web3_js_1.AddressLookupTableAccount.deserialize(a.account.data) };
160
+ }
161
+ catch (_a) {
162
+ return null;
163
+ }
164
+ })
165
+ .filter((t) => t !== null)
166
+ // A deactivated table cannot be used; MAX u64 means "never deactivated".
167
+ .filter(t => t.state.deactivationSlot === BigInt("18446744073709551615"));
168
+ if (!live.length)
169
+ return null;
170
+ // Prefer the most complete table so repeated runs converge on one.
171
+ return live.sort((a, b) => b.state.addresses.length - a.state.addresses.length)[0];
172
+ });
173
+ }
174
+ missingAddresses(present, required) {
175
+ const have = new Set(present.map(a => a.toBase58()));
176
+ const seen = new Set();
177
+ return required.filter(a => {
178
+ const k = a.toBase58();
179
+ if (have.has(k) || seen.has(k))
180
+ return false;
181
+ seen.add(k);
182
+ return true;
183
+ });
184
+ }
185
+ createLookupTable(addresses, signer) {
186
+ return __awaiter(this, void 0, void 0, function* () {
187
+ const authority = this.provider.wallet.publicKey;
188
+ const slot = yield this.provider.connection.getSlot("finalized");
189
+ const [createIx, table] = web3_js_1.AddressLookupTableProgram.createLookupTable({
190
+ authority, payer: authority, recentSlot: slot,
191
+ });
192
+ yield this.sendLegacyTransaction([createIx], signer);
193
+ yield this.extendLookupTable(table, addresses, signer);
194
+ return table;
195
+ });
196
+ }
197
+ extendLookupTable(table, addresses, signer) {
198
+ return __awaiter(this, void 0, void 0, function* () {
199
+ const authority = this.provider.wallet.publicKey;
200
+ // Chunked so the extend instruction itself stays under the transaction size limit.
201
+ for (let i = 0; i < addresses.length; i += 20) {
202
+ yield this.sendLegacyTransaction([
203
+ web3_js_1.AddressLookupTableProgram.extendLookupTable({
204
+ payer: authority, authority, lookupTable: table, addresses: addresses.slice(i, i + 20),
205
+ }),
206
+ ], signer);
207
+ }
208
+ // A table is only usable from the slot after it was written; wait so the first v0
209
+ // transaction referencing it can actually resolve the accounts.
210
+ yield this.waitForLookupTableActivation(table);
211
+ });
212
+ }
213
+ waitForLookupTableActivation(table_1) {
214
+ return __awaiter(this, arguments, void 0, function* (table, timeoutMs = 30000) {
215
+ const startSlot = yield this.provider.connection.getSlot("confirmed");
216
+ const deadline = Date.now() + timeoutMs;
217
+ while (Date.now() < deadline) {
218
+ const [{ value }, slot] = yield Promise.all([
219
+ this.provider.connection.getAddressLookupTable(table),
220
+ this.provider.connection.getSlot("confirmed"),
221
+ ]);
222
+ if (value && slot > startSlot)
223
+ return;
224
+ yield new Promise(r => setTimeout(r, 1000));
225
+ }
226
+ });
227
+ }
228
+ /** Resolves (and caches) the configured lookup table; [] when none is set or it's missing. */
229
+ resolveLookupTables() {
230
+ return __awaiter(this, void 0, void 0, function* () {
231
+ if (!this.lookupTableAddress)
232
+ return [];
233
+ if (this.lookupTableCache)
234
+ return [this.lookupTableCache];
235
+ const { value } = yield this.provider.connection.getAddressLookupTable(this.lookupTableAddress);
236
+ if (!value)
237
+ return [];
238
+ this.lookupTableCache = value;
239
+ return [value];
240
+ });
63
241
  }
64
242
  // ==========================
65
243
  // Admin View Methods
@@ -358,40 +536,284 @@ class EscrowAdminUtility {
358
536
  /**
359
537
  * Accepts an array of receivers and amounts and performs a batch withdrawal to the specified receivers
360
538
  * for the specified token (SOL or SPL)
539
+ * @param chain_id Network's chain id
361
540
  * @param token_address Address of the token to withdraw
362
541
  * @param recipients Array of receivers and their amounts
363
542
  * @param signer optional signer keypair
364
543
  * @returns string - signature
365
544
  */
366
- withdraw_batch(token_address, recipients, signer) {
545
+ withdraw_batch(chain_id, token_address, recipients, signer) {
367
546
  return __awaiter(this, void 0, void 0, function* () {
368
547
  const token_pubkey = new web3_js_1.PublicKey(token_address);
548
+ // A batch carrying treasury signatures only fits in 1232 bytes as a v0 transaction, so make
549
+ // sure the lookup table exists before building it. This is idempotent and self-healing: the
550
+ // first signature-gated withdrawal provisions the table, every later one reuses it, and a
551
+ // failure here simply leaves us on legacy transactions rather than blocking the withdrawal.
552
+ if (recipients.some(r => { var _a; return (_a = r.signatures) === null || _a === void 0 ? void 0 : _a.length; })) {
553
+ try {
554
+ yield this.ensure_lookup_table([token_address], signer);
555
+ }
556
+ catch (_a) {
557
+ // Degrade to legacy rather than block: two-signature batches still fit without it.
558
+ // Larger ones will fail loudly on the size limit, which is the honest outcome.
559
+ this.lookupTableAddress = undefined;
560
+ this.lookupTableCache = undefined;
561
+ }
562
+ }
369
563
  switch (token_pubkey.toBase58()) {
370
564
  case (web3_js_1.SystemProgram.programId.toBase58()):
371
- return this.batch_withdraw_sol(recipients, signer);
565
+ return this.batch_withdraw_sol(chain_id, recipients, signer);
372
566
  default:
373
- return this.batch_withdraw_spl(token_pubkey, recipients, signer);
567
+ return this.batch_withdraw_spl(chain_id, token_pubkey, recipients, signer);
374
568
  }
375
569
  });
376
570
  }
377
- /** @dev this might fail if the recipient does not have an ATA - take care of it in funding worker task */
378
- batch_withdraw_spl(mint, recipients, signer) {
571
+ /**
572
+ * Executes previously-scheduled (timelocked) withdrawals whose delay has elapsed. The amounts
573
+ * and recipient are read from each commitment on-chain, so only the request ids + their
574
+ * commitment/recipient accounts are supplied. Pays out and closes each commitment.
575
+ *
576
+ * Sent as a v0 transaction over the escrow lookup table: a batch's per-request accounts
577
+ * (commitment + recipient [+ ATA]) plus the static config accounts overflow a legacy
578
+ * transaction beyond a handful of requests, so we reuse the same ALT as the withdraw path.
579
+ */
580
+ batch_execute_withdrawal(chain_id, token_address, receivers, signer) {
581
+ return __awaiter(this, void 0, void 0, function* () {
582
+ if (!receivers.length)
583
+ throw new Error("batch_execute_withdrawal: receivers must not be empty");
584
+ const token_pubkey = new web3_js_1.PublicKey(token_address);
585
+ // Provision/reuse the lookup table so larger execute batches fit in a v0 transaction.
586
+ try {
587
+ yield this.ensure_lookup_table([token_address], signer);
588
+ }
589
+ catch (_a) {
590
+ this.lookupTableAddress = undefined;
591
+ this.lookupTableCache = undefined;
592
+ }
593
+ return token_pubkey.equals(web3_js_1.SystemProgram.programId)
594
+ ? this.batch_execute_withdrawal_sol(chain_id, receivers, signer)
595
+ : this.batch_execute_withdrawal_spl(chain_id, token_pubkey, receivers, signer);
596
+ });
597
+ }
598
+ /** Builds the SOL batch-execute instruction. Shared by the send and simulate paths. */
599
+ build_batch_execute_sol_ixs(receivers) {
600
+ return __awaiter(this, void 0, void 0, function* () {
601
+ const escrowConfigPda = EscrowService.findEscrowConfigPda(this.program.programId);
602
+ const vaultAuthority = EscrowService.findVaultAuthorityPda(this.program.programId);
603
+ const requestIds = receivers.map(r => new anchor.BN(r.request_id));
604
+ // Per request: [commitment, recipient]
605
+ const remaining = receivers.flatMap(r => {
606
+ const wallet = new web3_js_1.PublicKey(r.receiver_pubkey);
607
+ const { commitment } = this.request_pdas(wallet, r.request_id);
608
+ return [
609
+ { isSigner: false, pubkey: commitment, isWritable: true },
610
+ { isSigner: false, pubkey: wallet, isWritable: true },
611
+ ];
612
+ });
613
+ const ix = yield this.program.methods
614
+ .batchExecuteWithdrawalSol(requestIds)
615
+ .accountsPartial({
616
+ escrowConfig: escrowConfigPda,
617
+ vaultAuthority,
618
+ caller: this.provider.wallet.publicKey,
619
+ systemProgram: web3_js_1.SystemProgram.programId,
620
+ })
621
+ .remainingAccounts(remaining)
622
+ .instruction();
623
+ return [ix];
624
+ });
625
+ }
626
+ /** Builds the SPL batch-execute instruction. Shared by the send and simulate paths. */
627
+ build_batch_execute_spl_ixs(mint, receivers) {
628
+ return __awaiter(this, void 0, void 0, function* () {
629
+ const escrowConfigPda = EscrowService.findEscrowConfigPda(this.program.programId);
630
+ const vaultAuthority = EscrowService.findVaultAuthorityPda(this.program.programId);
631
+ const vaultTokenAccount = EscrowService.findVaultAta(mint, this.program.programId);
632
+ const requestIds = receivers.map(r => new anchor.BN(r.request_id));
633
+ // Per request: [commitment, recipient, recipient_token_account]
634
+ const remaining = receivers.flatMap(r => {
635
+ const wallet = new web3_js_1.PublicKey(r.receiver_pubkey);
636
+ const ata = (0, spl_token_1.getAssociatedTokenAddressSync)(mint, wallet);
637
+ const { commitment } = this.request_pdas(wallet, r.request_id);
638
+ return [
639
+ { isSigner: false, pubkey: commitment, isWritable: true },
640
+ { isSigner: false, pubkey: wallet, isWritable: true },
641
+ { isSigner: false, pubkey: ata, isWritable: true },
642
+ ];
643
+ });
644
+ const ix = yield this.program.methods
645
+ .batchExecuteWithdrawalSpl(requestIds)
646
+ .accountsPartial({
647
+ escrowConfig: escrowConfigPda,
648
+ mint,
649
+ vaultAuthority,
650
+ vaultTokenAccount,
651
+ caller: this.provider.wallet.publicKey,
652
+ tokenProgram: spl_token_1.TOKEN_PROGRAM_ID,
653
+ systemProgram: web3_js_1.SystemProgram.programId,
654
+ })
655
+ .remainingAccounts(remaining)
656
+ .instruction();
657
+ return [ix];
658
+ });
659
+ }
660
+ batch_execute_withdrawal_sol(_chainId, receivers, signer) {
661
+ return __awaiter(this, void 0, void 0, function* () {
662
+ const ixs = yield this.build_batch_execute_sol_ixs(receivers);
663
+ const signature = yield this.sendTransaction(ixs, signer);
664
+ yield this.waitForConfirmation(signature, "confirmed");
665
+ return signature;
666
+ });
667
+ }
668
+ batch_execute_withdrawal_spl(_chainId, mint, receivers, signer) {
669
+ return __awaiter(this, void 0, void 0, function* () {
670
+ const ixs = yield this.build_batch_execute_spl_ixs(mint, receivers);
671
+ const signature = yield this.sendTransaction(ixs, signer);
672
+ yield this.waitForConfirmation(signature, "confirmed");
673
+ return signature;
674
+ });
675
+ }
676
+ /**
677
+ * Simulates a batch execute without sending, returning `{ ok, error }`. The funding worker
678
+ * uses it to quarantine requests that would revert (timelock not elapsed, commitment already
679
+ * executed, …) before committing the real batch — the program reverts the whole transaction
680
+ * without naming the offending request, so callers simulate one request at a time.
681
+ */
682
+ simulate_batch_execute_withdrawal(chain_id, token_address, receivers) {
683
+ return __awaiter(this, void 0, void 0, function* () {
684
+ const token_pubkey = new web3_js_1.PublicKey(token_address);
685
+ try {
686
+ yield this.ensure_lookup_table([token_address]);
687
+ }
688
+ catch (_a) {
689
+ this.lookupTableAddress = undefined;
690
+ this.lookupTableCache = undefined;
691
+ }
692
+ const ixs = token_pubkey.equals(web3_js_1.SystemProgram.programId)
693
+ ? yield this.build_batch_execute_sol_ixs(receivers)
694
+ : yield this.build_batch_execute_spl_ixs(token_pubkey, receivers);
695
+ return this.simulateInstructions(ixs);
696
+ });
697
+ }
698
+ /**
699
+ * Builds an (unsigned) transaction from `ixs` and simulates it. Returns `{ ok:false, error }`
700
+ * with the revert reason from the program logs when it would fail. `sigVerify:false` skips the
701
+ * missing fee-payer signature; the Ed25519 program instruction still executes, so invalid or
702
+ * insufficient treasury signatures are caught here too.
703
+ */
704
+ simulateInstructions(ixs) {
705
+ return __awaiter(this, void 0, void 0, function* () {
706
+ var _a, _b, _c;
707
+ try {
708
+ const lookupTables = yield this.resolveLookupTables();
709
+ const message = new web3_js_1.TransactionMessage({
710
+ payerKey: this.provider.wallet.publicKey,
711
+ recentBlockhash: (yield this.provider.connection.getLatestBlockhash()).blockhash,
712
+ instructions: ixs,
713
+ }).compileToV0Message(lookupTables);
714
+ const tx = new web3_js_1.VersionedTransaction(message);
715
+ const sim = yield this.provider.connection.simulateTransaction(tx, {
716
+ sigVerify: false,
717
+ replaceRecentBlockhash: true,
718
+ });
719
+ if (sim.value.err) {
720
+ const logs = (_a = sim.value.logs) !== null && _a !== void 0 ? _a : [];
721
+ const reason = (_b = logs.find(l => l.includes("Error") || l.includes("failed"))) !== null && _b !== void 0 ? _b : JSON.stringify(sim.value.err);
722
+ return { ok: false, error: reason };
723
+ }
724
+ return { ok: true, error: null };
725
+ }
726
+ catch (error) {
727
+ return { ok: false, error: (_c = error === null || error === void 0 ? void 0 : error.message) !== null && _c !== void 0 ? _c : "simulation failed" };
728
+ }
729
+ });
730
+ }
731
+ /** Per-request PDAs the folded batch path expects: receiver timelock override + commitment. */
732
+ request_pdas(receiver, request_id) {
733
+ const ridBuf = Buffer.alloc(8);
734
+ ridBuf.writeBigUInt64LE(BigInt(request_id));
735
+ const [receiverTimelock] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from('receiver-timelock'), receiver.toBytes()], this.program.programId);
736
+ const [commitment] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from('withdrawal'), ridBuf], this.program.programId);
737
+ return { receiverTimelock, commitment };
738
+ }
739
+ /**
740
+ * The distinct replay-bitmap page accounts a batch touches. The program appends these AFTER
741
+ * the per-request accounts and locates each request's page by key, so a page shared by several
742
+ * requests is passed once. Metas are writable (the program may create/flip bits) and non-signer.
743
+ */
744
+ bitmap_page_accounts(request_ids) {
745
+ const pages = new Set();
746
+ const metas = [];
747
+ for (const id of request_ids) {
748
+ const page = BigInt(id) / EscrowAdminUtility.IDS_PER_PAGE;
749
+ const pageBuf = Buffer.alloc(8);
750
+ pageBuf.writeBigUInt64LE(page);
751
+ const [pda] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from('withdrawal-bitmap'), pageBuf], this.program.programId);
752
+ const key = pda.toBase58();
753
+ if (pages.has(key))
754
+ continue;
755
+ pages.add(key);
756
+ metas.push({ isSigner: false, pubkey: pda, isWritable: true });
757
+ }
758
+ return metas;
759
+ }
760
+ /**
761
+ * Builds the Ed25519SigVerify instruction the program scans to enforce treasury multisig.
762
+ * Each request contributes its signers' approvals over that request's message (which binds
763
+ * the committed amount + token + chain + request id). Returns null when no request carries
764
+ * signatures, so plain batches send just the withdraw instruction.
765
+ */
766
+ buildBatchTreasuryEd25519(chainId, tokenMint, recipients) {
767
+ var _a;
768
+ const entries = [];
769
+ for (const r of recipients) {
770
+ if (!((_a = r.signatures) === null || _a === void 0 ? void 0 : _a.length))
771
+ continue;
772
+ const message = (0, TreasuryWithdrawalSignature_1.buildWithdrawalApprovalMessage)({
773
+ recipient: new web3_js_1.PublicKey(r.receiver_pubkey),
774
+ tokenMint,
775
+ committedAmount: BigInt(r.amount_in_lamports),
776
+ chainId: BigInt(chainId),
777
+ requestId: BigInt(r.request_id),
778
+ });
779
+ for (const s of r.signatures) {
780
+ entries.push({
781
+ publicKey: new web3_js_1.PublicKey(s.signer).toBytes(),
782
+ signature: (0, TreasuryWithdrawalSignature_1.decodeEd25519Signature)(s.signature),
783
+ message,
784
+ });
785
+ }
786
+ }
787
+ return entries.length ? (0, TreasuryWithdrawalSignature_1.buildTreasuryEd25519Instruction)(entries) : null;
788
+ }
789
+ /**
790
+ * Builds the instruction list for an SPL batch withdraw: an optional treasury Ed25519 proof
791
+ * followed by the batch instruction. Shared by the send and simulate paths so they never drift.
792
+ */
793
+ build_batch_withdraw_spl_ixs(chainId, mint, recipients) {
379
794
  return __awaiter(this, void 0, void 0, function* () {
380
795
  const escrowConfigPda = EscrowService.findEscrowConfigPda(this.program.programId);
381
796
  const vaultAuthority = EscrowService.findVaultAuthorityPda(this.program.programId);
382
797
  const vaultTokenAccount = EscrowService.findVaultAta(mint, this.program.programId);
383
798
  const amounts = recipients.map(r => new anchor.BN(r.amount_in_lamports));
799
+ const net_amounts = recipients.map(r => new anchor.BN(r.net_amount_in_lamports));
384
800
  const request_ids = recipients.map(r => new anchor.BN(r.request_id));
385
- const receivers = recipients.map(r => {
386
- const ata = (0, spl_token_1.getAssociatedTokenAddressSync)(mint, new web3_js_1.PublicKey(r.receiver_pubkey));
387
- return {
388
- isSigner: false,
389
- pubkey: ata,
390
- isWritable: true
391
- };
801
+ // Per request: [recipient, recipient_token_account, receiver_timelock, commitment]
802
+ const receivers = recipients.flatMap(r => {
803
+ const wallet = new web3_js_1.PublicKey(r.receiver_pubkey);
804
+ const ata = (0, spl_token_1.getAssociatedTokenAddressSync)(mint, wallet);
805
+ const { receiverTimelock, commitment } = this.request_pdas(wallet, r.request_id);
806
+ return [
807
+ { isSigner: false, pubkey: wallet, isWritable: true },
808
+ { isSigner: false, pubkey: ata, isWritable: true },
809
+ { isSigner: false, pubkey: receiverTimelock, isWritable: false },
810
+ { isSigner: false, pubkey: commitment, isWritable: true },
811
+ ];
392
812
  });
813
+ // Distinct replay-bitmap pages follow the per-request accounts.
814
+ receivers.push(...this.bitmap_page_accounts(recipients.map(r => r.request_id)));
393
815
  const ix = yield this.program.methods
394
- .batchWithdrawTokens(request_ids, amounts)
816
+ .batchWithdrawTokens(new anchor.BN(chainId), request_ids, amounts, net_amounts)
395
817
  .accountsPartial({
396
818
  escrowConfig: escrowConfigPda,
397
819
  vaultAuthority: vaultAuthority,
@@ -402,26 +824,41 @@ class EscrowAdminUtility {
402
824
  })
403
825
  .remainingAccounts(receivers)
404
826
  .instruction();
405
- const signature = yield this.sendTransaction([ix], signer);
827
+ const ed25519 = this.buildBatchTreasuryEd25519(chainId, mint, recipients);
828
+ return ed25519 ? [ed25519, ix] : [ix];
829
+ });
830
+ }
831
+ /** @dev this might fail if the recipient does not have an ATA - take care of it in funding worker task */
832
+ batch_withdraw_spl(chainId, mint, recipients, signer) {
833
+ return __awaiter(this, void 0, void 0, function* () {
834
+ const ixs = yield this.build_batch_withdraw_spl_ixs(chainId, mint, recipients);
835
+ const signature = yield this.sendTransaction(ixs, signer);
406
836
  yield this.waitForConfirmation(signature, "confirmed");
407
837
  return signature;
408
838
  });
409
839
  }
410
- batch_withdraw_sol(recipients, signer) {
840
+ /** Builds the instruction list for a SOL batch withdraw (optional treasury proof + batch ix). */
841
+ build_batch_withdraw_sol_ixs(chainId, recipients) {
411
842
  return __awaiter(this, void 0, void 0, function* () {
412
843
  const escrowConfigPda = EscrowService.findEscrowConfigPda(this.program.programId);
413
844
  const vaultAuthority = EscrowService.findVaultAuthorityPda(this.program.programId);
414
845
  const amounts = recipients.map(r => new anchor.BN(r.amount_in_lamports));
415
- const receivers = recipients.map(r => {
416
- return {
417
- isSigner: false,
418
- pubkey: new web3_js_1.PublicKey(r.receiver_pubkey),
419
- isWritable: true
420
- };
421
- });
846
+ const net_amounts = recipients.map(r => new anchor.BN(r.net_amount_in_lamports));
422
847
  const request_ids = recipients.map(r => new anchor.BN(r.request_id));
848
+ // Per request: [recipient, receiver_timelock, commitment]
849
+ const receivers = recipients.flatMap(r => {
850
+ const wallet = new web3_js_1.PublicKey(r.receiver_pubkey);
851
+ const { receiverTimelock, commitment } = this.request_pdas(wallet, r.request_id);
852
+ return [
853
+ { isSigner: false, pubkey: wallet, isWritable: true },
854
+ { isSigner: false, pubkey: receiverTimelock, isWritable: false },
855
+ { isSigner: false, pubkey: commitment, isWritable: true },
856
+ ];
857
+ });
858
+ // Distinct replay-bitmap pages follow the per-request accounts.
859
+ receivers.push(...this.bitmap_page_accounts(recipients.map(r => r.request_id)));
423
860
  const ix = yield this.program.methods
424
- .batchWithdrawSol(request_ids, amounts)
861
+ .batchWithdrawSol(new anchor.BN(chainId), request_ids, amounts, net_amounts)
425
862
  .accountsPartial({
426
863
  escrowConfig: escrowConfigPda,
427
864
  vaultAuthority: vaultAuthority,
@@ -430,19 +867,38 @@ class EscrowAdminUtility {
430
867
  })
431
868
  .remainingAccounts(receivers)
432
869
  .instruction();
433
- // let preimage = `global:batchWithdrawSol`;
434
- // let ixBuffer = Buffer.from(sha256.hash(preimage)).slice(0, 8);
435
- // const ixData = Buffer.concat([
436
- // new Uint8Array(ixBuffer),
437
- // new Uint8Array(ix.data)
438
- // ])
439
- // const transactionIx = new TransactionInstruction({
440
- // programId: this.program.programId,
441
- // data: ixData,
442
- // keys: ix.keys
443
- // })
444
- const signature = yield this.sendTransaction([ix], signer);
445
- return signature;
870
+ const ed25519 = this.buildBatchTreasuryEd25519(chainId, web3_js_1.SystemProgram.programId, recipients);
871
+ return ed25519 ? [ed25519, ix] : [ix];
872
+ });
873
+ }
874
+ batch_withdraw_sol(chainId, recipients, signer) {
875
+ return __awaiter(this, void 0, void 0, function* () {
876
+ const ixs = yield this.build_batch_withdraw_sol_ixs(chainId, recipients);
877
+ return this.sendTransaction(ixs, signer);
878
+ });
879
+ }
880
+ /**
881
+ * Simulates a batch withdraw without sending, returning `{ ok, error }`. Used by the funding
882
+ * worker to quarantine individual requests that would revert (bad/insufficient signatures,
883
+ * already scheduled, …) before committing the real batch — the program reverts the whole
884
+ * transaction without naming the offending request, so callers simulate one request at a time.
885
+ */
886
+ simulate_withdraw_batch(chain_id, token_address, recipients) {
887
+ return __awaiter(this, void 0, void 0, function* () {
888
+ const token_pubkey = new web3_js_1.PublicKey(token_address);
889
+ if (recipients.some(r => { var _a; return (_a = r.signatures) === null || _a === void 0 ? void 0 : _a.length; })) {
890
+ try {
891
+ yield this.ensure_lookup_table([token_address]);
892
+ }
893
+ catch (_a) {
894
+ this.lookupTableAddress = undefined;
895
+ this.lookupTableCache = undefined;
896
+ }
897
+ }
898
+ const ixs = token_pubkey.equals(web3_js_1.SystemProgram.programId)
899
+ ? yield this.build_batch_withdraw_sol_ixs(chain_id, recipients)
900
+ : yield this.build_batch_withdraw_spl_ixs(chain_id, token_pubkey, recipients);
901
+ return this.simulateInstructions(ixs);
446
902
  });
447
903
  }
448
904
  withdraw_sol(amount_in_decimals, receiver_pubkey, signer) {
@@ -582,29 +1038,59 @@ class EscrowAdminUtility {
582
1038
  sendTransaction(ixs, signer) {
583
1039
  return __awaiter(this, void 0, void 0, function* () {
584
1040
  try {
585
- const tx = new web3_js_1.Transaction();
586
- for (let ix of ixs) {
587
- tx.add(ix);
588
- }
589
- tx.feePayer = this.provider.wallet.publicKey;
590
- tx.recentBlockhash = (yield this.provider.connection.getLatestBlockhash()).blockhash;
591
- let signature = "";
592
- if (signer) {
593
- tx.sign(signer);
594
- signature = yield this.provider.connection.sendRawTransaction(tx.serialize());
595
- yield (0, TransactionsService_1.waitForTransactionConfirmation)(this.provider, signature, "confirmed");
596
- }
597
- else {
598
- signature = yield this.provider.sendAndConfirm(tx);
599
- yield (0, TransactionsService_1.waitForTransactionConfirmation)(this.provider, signature, "confirmed");
1041
+ // Prefer a v0 transaction when a lookup table is configured — it is the only way a
1042
+ // batch carrying more than two treasury signatures fits in 1232 bytes.
1043
+ const lookupTables = yield this.resolveLookupTables();
1044
+ if (lookupTables.length) {
1045
+ return yield this.sendVersionedTransaction(ixs, lookupTables, signer);
600
1046
  }
601
- return signature;
1047
+ return yield this.sendLegacyTransaction(ixs, signer);
602
1048
  }
603
1049
  catch (error) {
604
1050
  throw Errors_1.default.decode(error);
605
1051
  }
606
1052
  });
607
1053
  }
1054
+ sendVersionedTransaction(ixs, lookupTables, signer) {
1055
+ return __awaiter(this, void 0, void 0, function* () {
1056
+ const message = new web3_js_1.TransactionMessage({
1057
+ payerKey: this.provider.wallet.publicKey,
1058
+ recentBlockhash: (yield this.provider.connection.getLatestBlockhash()).blockhash,
1059
+ instructions: ixs,
1060
+ }).compileToV0Message(lookupTables);
1061
+ const tx = new web3_js_1.VersionedTransaction(message);
1062
+ if (signer) {
1063
+ tx.sign([signer]);
1064
+ }
1065
+ else {
1066
+ yield this.provider.wallet.signTransaction(tx);
1067
+ }
1068
+ const signature = yield this.provider.connection.sendRawTransaction(tx.serialize());
1069
+ yield (0, TransactionsService_1.waitForTransactionConfirmation)(this.provider, signature, "confirmed");
1070
+ return signature;
1071
+ });
1072
+ }
1073
+ sendLegacyTransaction(ixs, signer) {
1074
+ return __awaiter(this, void 0, void 0, function* () {
1075
+ const tx = new web3_js_1.Transaction();
1076
+ for (let ix of ixs) {
1077
+ tx.add(ix);
1078
+ }
1079
+ tx.feePayer = this.provider.wallet.publicKey;
1080
+ tx.recentBlockhash = (yield this.provider.connection.getLatestBlockhash()).blockhash;
1081
+ let signature = "";
1082
+ if (signer) {
1083
+ tx.sign(signer);
1084
+ signature = yield this.provider.connection.sendRawTransaction(tx.serialize());
1085
+ yield (0, TransactionsService_1.waitForTransactionConfirmation)(this.provider, signature, "confirmed");
1086
+ }
1087
+ else {
1088
+ signature = yield this.provider.sendAndConfirm(tx);
1089
+ yield (0, TransactionsService_1.waitForTransactionConfirmation)(this.provider, signature, "confirmed");
1090
+ }
1091
+ return signature;
1092
+ });
1093
+ }
608
1094
  confirmTransaction(signature_1) {
609
1095
  return __awaiter(this, arguments, void 0, function* (signature, commitment = "finalized") {
610
1096
  yield this.provider.connection.confirmTransaction(signature, commitment);
@@ -657,12 +1143,14 @@ class EscrowAdminUtility {
657
1143
  * @param idl IDL file - Escrow program idl
658
1144
  * @returns
659
1145
  */
660
- static createWithCustomProgramId(provider, programId, idl) {
1146
+ static createWithCustomProgramId(provider, programId, idl, lookupTableAddress) {
661
1147
  // Override the program ID in the IDL
662
1148
  const modifiedIdl = Object.assign(Object.assign({}, idl), { address: programId.toBase58() });
663
1149
  const program = new anchor_1.Program(modifiedIdl, provider);
664
- return new EscrowAdminUtility(program, provider);
1150
+ return new EscrowAdminUtility(program, provider, lookupTableAddress);
665
1151
  }
666
1152
  }
667
1153
  exports.EscrowAdminUtility = EscrowAdminUtility;
1154
+ /** Matches the program's IDS_PER_PAGE — the replay-bitmap page a request id maps to. */
1155
+ EscrowAdminUtility.IDS_PER_PAGE = BigInt(8192);
668
1156
  exports.default = EscrowAdminUtility;