@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.
- package/build/chains/networks_dev.json +10 -0
- package/build/chains/networks_prod-bu.json +0 -14
- package/build/chains/networks_prod-dz.json +22 -1
- package/build/errors/TransactionsErrors.d.ts +0 -1
- package/build/errors/TransactionsErrors.d.ts.map +1 -1
- package/build/errors/TransactionsErrors.js +0 -4
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +4 -2
- package/build/services/ClaimJobService.d.ts.map +1 -1
- package/build/services/ClaimJobService.js +12 -5
- package/build/services/Redis.js +2 -2
- package/build/services/RedisPubSub.js +1 -1
- package/build/services/db/timelock/ScheduleTransactionService.d.ts +16 -2
- package/build/services/db/timelock/ScheduleTransactionService.d.ts.map +1 -1
- package/build/services/db/timelock/ScheduleTransactionService.js +265 -1
- package/build/services/rpc/RPC.d.ts +20 -0
- package/build/services/rpc/RPC.d.ts.map +1 -0
- package/build/services/rpc/RPC.js +49 -0
- package/build/services/solana/escrow/SolanaEscrowAdmin.d.ts +41 -1
- package/build/services/solana/escrow/SolanaEscrowAdmin.d.ts.map +1 -1
- package/build/services/solana/escrow/SolanaEscrowAdmin.js +35 -3
- package/build/services/solana/escrow/idl/escrow.json +4267 -988
- package/build/services/solana/escrow/services/EscrowAdminUtility.d.ts +116 -3
- package/build/services/solana/escrow/services/EscrowAdminUtility.d.ts.map +1 -1
- package/build/services/solana/escrow/services/EscrowAdminUtility.js +543 -55
- package/build/services/solana/escrow/services/TreasuryWithdrawalSignature.d.ts +58 -0
- package/build/services/solana/escrow/services/TreasuryWithdrawalSignature.d.ts.map +1 -0
- package/build/services/solana/escrow/services/TreasuryWithdrawalSignature.js +109 -0
- package/build/services/solana/escrow/types/escrow.d.ts +4262 -983
- package/build/services/solana/escrow/types/escrow.d.ts.map +1 -1
- package/build/services/solana/escrow/types/types.d.ts +17 -0
- package/build/services/solana/escrow/types/types.d.ts.map +1 -1
- package/build/services/types/claim.d.ts +1 -0
- package/build/services/types/claim.d.ts.map +1 -1
- package/build/services/types/db/timelock.d.ts +56 -0
- package/build/services/types/db/timelock.d.ts.map +1 -1
- package/build/utils/solana.d.ts.map +1 -1
- package/build/utils/solana.js +5 -3
- 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
|
-
/**
|
|
378
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
|
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;
|