@augustdigital/sdk 9.11.0 → 9.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/lib/adapters/solana/constants.d.ts +12 -4
  2. package/lib/adapters/solana/constants.js +8 -3
  3. package/lib/adapters/solana/getters.d.ts +5 -0
  4. package/lib/adapters/solana/getters.js +35 -1
  5. package/lib/adapters/solana/idl/vault-idl.d.ts +334 -7
  6. package/lib/adapters/solana/idl/vault-idl.js +600 -59
  7. package/lib/adapters/solana/idl/withdrawal-queue-idl.d.ts +790 -0
  8. package/lib/adapters/solana/idl/withdrawal-queue-idl.js +1868 -0
  9. package/lib/adapters/solana/index.d.ts +108 -10
  10. package/lib/adapters/solana/index.js +137 -12
  11. package/lib/adapters/solana/pricing.d.ts +11 -0
  12. package/lib/adapters/solana/pricing.js +24 -8
  13. package/lib/adapters/solana/queue.actions.d.ts +98 -0
  14. package/lib/adapters/solana/queue.actions.js +488 -0
  15. package/lib/adapters/solana/token-reads.d.ts +108 -0
  16. package/lib/adapters/solana/token-reads.js +145 -0
  17. package/lib/adapters/solana/types.d.ts +200 -1
  18. package/lib/adapters/solana/utils.d.ts +16 -0
  19. package/lib/adapters/solana/utils.js +41 -4
  20. package/lib/adapters/solana/vault.actions.d.ts +17 -0
  21. package/lib/adapters/solana/vault.actions.js +14 -125
  22. package/lib/adapters/solana/withdrawal-queue.d.ts +125 -0
  23. package/lib/adapters/solana/withdrawal-queue.js +301 -0
  24. package/lib/core/analytics/method-taxonomy.js +2 -0
  25. package/lib/core/analytics/version.d.ts +1 -1
  26. package/lib/core/analytics/version.js +1 -1
  27. package/lib/main.d.ts +16 -0
  28. package/lib/main.js +16 -0
  29. package/lib/modules/vaults/getters.d.ts +2 -2
  30. package/lib/modules/vaults/getters.js +103 -3
  31. package/lib/modules/vaults/main.d.ts +12 -0
  32. package/lib/modules/vaults/main.js +17 -1
  33. package/lib/modules/vaults/types.d.ts +14 -1
  34. package/lib/sdk.d.ts +2389 -1723
  35. package/lib/types/vaults.d.ts +47 -1
  36. package/lib/types/vaults.js +5 -0
  37. package/lib/types/webserver.d.ts +8 -0
  38. package/package.json +1 -1
@@ -0,0 +1,488 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.queueHint = queueHint;
4
+ exports.deriveWithdrawalRequestPda = deriveWithdrawalRequestPda;
5
+ exports.handleSolanaRequestRedeem = handleSolanaRequestRedeem;
6
+ exports.handleSolanaClaimRedeem = handleSolanaClaimRedeem;
7
+ exports.handleSolanaCancelRedeemRequest = handleSolanaCancelRedeemRequest;
8
+ /**
9
+ * Write paths for the august_withdrawal_queue program: request a withdrawal,
10
+ * claim (finalize) a ready one, cancel a pending one.
11
+ *
12
+ * Built like the vault's deposit and redeem handlers (`vault.actions.ts`):
13
+ * every check the SDK can make runs before the wallet is asked to sign, every
14
+ * account the transaction needs is created inside it with an idempotent
15
+ * instruction, and failures surface as `AugustValidationError` (bad input or
16
+ * state the caller can fix) or `AugustSDKError` (the transaction failed, with
17
+ * the program's reason and the numeric code).
18
+ */
19
+ const anchor_1 = require("@coral-xyz/anchor");
20
+ const spl_token_1 = require("@solana/spl-token");
21
+ const web3_js_1 = require("@solana/web3.js");
22
+ const core_1 = require("../../core");
23
+ const vault_idl_1 = require("./idl/vault-idl");
24
+ const withdrawal_queue_idl_1 = require("./idl/withdrawal-queue-idl");
25
+ const pricing_1 = require("./pricing");
26
+ const utils_1 = require("./utils");
27
+ const token_reads_1 = require("./token-reads");
28
+ const vault_actions_1 = require("./vault.actions");
29
+ const withdrawal_queue_1 = require("./withdrawal-queue");
30
+ const WITHDRAWAL_REQUEST_SEED = 'withdrawal_request';
31
+ /** Largest request id the program's `u64` can hold. */
32
+ const U64_MAX = 18446744073709551615n;
33
+ /**
34
+ * What to do next, for the queue errors a caller can act on. Matched by the
35
+ * error's name, not its number: both programs number their errors from 6000,
36
+ * so a bare code is ambiguous between them.
37
+ */
38
+ const QUEUE_HINTS = {
39
+ WithdrawalQueueRequired: ' This vault redeems through its withdrawal queue: request with `vaultRequestRedeem`, then claim with `vaultRedeem` and `options.request`.',
40
+ CooldownNotElapsed: ' The request is still in its cooldown; claim it once its eligibility time has passed.',
41
+ RequestExpired: ' The request is past its fulfillment window and can no longer be claimed; cancel it to get the shares back.',
42
+ FinalizerNotAllowed: ' Only the holder or the finalizer the request names may claim it.',
43
+ StaleRequestSequence: ' The request changed since it was read (it was claimed or cancelled, and possibly recreated); read it again.',
44
+ QueueNotActiveOnVault: ' The vault is no longer attached to this queue, so it takes no new requests; redeem directly instead.',
45
+ NotRequestOwner: ' Only the holder who made the request may cancel it.',
46
+ PayoutBelowFloor: ' The shares are worth less than the floor set when the request was made; claim once the price recovers, or cancel and request again with a lower `minAssetsOut` or higher `slippageBps`.',
47
+ // Raised by the vault inside the claim's CPI; only claims reach this map.
48
+ NotEnoughLiquidity: " The vault's liquid reserve cannot cover this payout yet; the request stays pending, so claim it again later.",
49
+ };
50
+ /** The sentence to append for a queue error the caller can act on, else empty. */
51
+ function queueHint(e) {
52
+ const name = e
53
+ ?.error?.errorCode?.code;
54
+ return (name && QUEUE_HINTS[name]) || '';
55
+ }
56
+ /** A queue program client that signs through `provider`'s wallet. */
57
+ function queueProgram(provider, queueProgramId) {
58
+ return new anchor_1.Program({ ...withdrawal_queue_idl_1.withdrawalQueueIdl, address: queueProgramId.toBase58() }, provider);
59
+ }
60
+ /**
61
+ * The queue's instruction builders, typed by what these handlers call. The IDL
62
+ * is a plain object here, not Anchor's generated type, so `program.methods` is
63
+ * untyped without this.
64
+ */
65
+ function queueMethods(program) {
66
+ return program.methods;
67
+ }
68
+ /** A vault program client, used only for its account coder. */
69
+ function vaultCoder(provider) {
70
+ return new anchor_1.Program(vault_idl_1.vaultIdl, provider).coder.accounts;
71
+ }
72
+ /** The request PDA: `["withdrawal_request", queue, owner, request_id as u64 LE]`. */
73
+ function deriveWithdrawalRequestPda(queue, owner, requestId, queueProgramId) {
74
+ return web3_js_1.PublicKey.findProgramAddressSync([
75
+ Buffer.from(WITHDRAWAL_REQUEST_SEED),
76
+ queue.toBuffer(),
77
+ owner.toBuffer(),
78
+ new anchor_1.BN(requestId.toString()).toArrayLike(Buffer, 'le', 8),
79
+ ], queueProgramId)[0];
80
+ }
81
+ /**
82
+ * A random `u64` request id. `crypto.getRandomValues` is available in every
83
+ * browser and in Node 19+, the SDK's floor being 22. A collision needs another
84
+ * pending request of the same holder with the same id: about 2^-64 per pair,
85
+ * and it would fail cleanly, the request account already existing.
86
+ */
87
+ function randomRequestId() {
88
+ const bytes = new Uint8Array(8);
89
+ globalThis.crypto.getRandomValues(bytes);
90
+ return bytes.reduce((id, byte) => (id << 8n) | BigInt(byte), 0n);
91
+ }
92
+ /** The commitment reads use, so writes confirm where reads will see them. */
93
+ function commitmentOf(connection) {
94
+ return connection.commitment ?? 'finalized';
95
+ }
96
+ /** Decode `info` as `name` with `coder`, or throw naming the account. */
97
+ function decodeOrThrow(coder, name, address, info, missing) {
98
+ if (!info)
99
+ throw new core_1.AugustValidationError('INVALID_INPUT', missing);
100
+ try {
101
+ return coder.decode(name, info.data);
102
+ }
103
+ catch (e) {
104
+ throw new core_1.AugustValidationError('INVALID_INPUT', `Account ${address.toBase58()} is not a ${name} account.`, { cause: e });
105
+ }
106
+ }
107
+ /** Wrap a non-SDK failure the way the vault handlers do, keeping `cause`. */
108
+ function wrapFailure(operation, e, context) {
109
+ if (e instanceof core_1.AugustSDKError)
110
+ throw e;
111
+ core_1.Logger.log.error(operation, e, context);
112
+ throw new core_1.AugustSDKError('UNKNOWN', `Solana ${operation} failed: ${(0, vault_actions_1.describeSolanaError)(e)}${queueHint(e)}`, { cause: e, context });
113
+ }
114
+ /**
115
+ * Escrow shares in a withdrawal request on a vault with a queue attached.
116
+ *
117
+ * Reads the vault state and queue in one call, checks the gate points at the
118
+ * queue, and submits `request_withdrawal` from the holder's share account. The
119
+ * recipient's deposit-token account is created in the same transaction when it
120
+ * has none. Unless `minAssetsOut` is given, the request's floor is the current
121
+ * net quote lowered by `slippageBps`, the floor `vaultRedeem` would use.
122
+ *
123
+ * RPC: one `getMultipleAccountsInfo` (vault state and queue), one
124
+ * `getAccountInfo` for the share mint (decimals for a UI `amount`, supply for
125
+ * the default floor) unless `amount` is raw and `minAssetsOut` is given, one
126
+ * `getParsedTokenAccountsByOwner` for the share account, and one more read for
127
+ * the payout account (the holder's, or another recipient's ATA, which may be
128
+ * off-curve), then the send.
129
+ *
130
+ * @returns The signature, the new request's address and its id.
131
+ * @throws AugustValidationError for bad input, a vault without an attached
132
+ * queue, a frozen account or too few shares, all before signing.
133
+ * @throws AugustSDKError when the transaction fails.
134
+ */
135
+ async function handleSolanaRequestRedeem({ provider, connection, queueProgramId, params, }) {
136
+ const context = {
137
+ vault: String(params.vault),
138
+ amount: String(params.amount),
139
+ };
140
+ try {
141
+ const { amount, requestId } = params;
142
+ if (!params.wallet)
143
+ throw new core_1.AugustValidationError('INVALID_INPUT', 'vaultRequestRedeem: wallet not connected');
144
+ if (!params.vault)
145
+ throw new core_1.AugustValidationError('INVALID_INPUT', 'vaultRequestRedeem: vault is required');
146
+ if ((typeof amount === 'bigint' && amount <= 0n) ||
147
+ (typeof amount === 'number' && !(amount > 0)) ||
148
+ (typeof amount !== 'bigint' && typeof amount !== 'number'))
149
+ throw new core_1.AugustValidationError('INVALID_INPUT', 'vaultRequestRedeem: amount is required and must be > 0');
150
+ if (requestId !== undefined &&
151
+ (typeof requestId !== 'bigint' || requestId < 0n || requestId > U64_MAX))
152
+ throw new core_1.AugustValidationError('INVALID_INPUT', `vaultRequestRedeem: requestId must be a bigint between 0 and ${U64_MAX}, got ${String(requestId)}.`);
153
+ // Before any RPC: an unusable floor should cost the caller nothing.
154
+ if (params.minAssetsOut !== undefined)
155
+ (0, vault_actions_1.validateExplicitFloor)(params.minAssetsOut, 'minAssetsOut');
156
+ else if (params.slippageBps !== undefined)
157
+ (0, pricing_1.validateSlippageBps)(params.slippageBps);
158
+ const owner = new web3_js_1.PublicKey(params.wallet);
159
+ const vaultState = new web3_js_1.PublicKey(params.vault);
160
+ const queue = (0, withdrawal_queue_1.deriveWithdrawalQueuePda)(vaultState, queueProgramId);
161
+ const program = queueProgram(provider, queueProgramId);
162
+ const [stateInfo, queueInfo] = await connection.getMultipleAccountsInfo([
163
+ vaultState,
164
+ queue,
165
+ ]);
166
+ const vaultStateData = decodeOrThrow(vaultCoder(provider), 'vaultState', vaultState, stateInfo, `vaultRequestRedeem: no vault state at ${vaultState.toBase58()}.`);
167
+ const gate = vaultStateData.withdrawalQueueAuthority;
168
+ if (!gate || !new web3_js_1.PublicKey(gate).equals(queue)) {
169
+ throw new core_1.AugustValidationError('INVALID_INPUT', `vaultRequestRedeem: vault ${vaultState.toBase58()} has no withdrawal queue attached; redeem directly with \`vaultRedeem\`.`);
170
+ }
171
+ const queueState = decodeOrThrow(program.coder.accounts, 'withdrawalQueue', queue, queueInfo, `vaultRequestRedeem: the vault's queue ${queue.toBase58()} does not exist.`);
172
+ const shareMint = new web3_js_1.PublicKey(queueState.shareMint);
173
+ const depositMint = new web3_js_1.PublicKey(queueState.depositMint);
174
+ // Decimals for a UI amount, supply for the default floor: read at most once.
175
+ let shareMintState;
176
+ const readShareMint = async () => {
177
+ if (shareMintState)
178
+ return shareMintState;
179
+ const mintInfo = await connection.getAccountInfo(shareMint);
180
+ if (!mintInfo)
181
+ throw new core_1.AugustSDKError('UNKNOWN', `Share mint ${shareMint.toBase58()} does not exist.`);
182
+ shareMintState = (0, spl_token_1.unpackMint)(shareMint, mintInfo, spl_token_1.TOKEN_PROGRAM_ID);
183
+ return shareMintState;
184
+ };
185
+ let shares;
186
+ if (typeof amount === 'bigint') {
187
+ shares = new anchor_1.BN(amount.toString());
188
+ }
189
+ else {
190
+ const { decimals } = await readShareMint();
191
+ shares = (0, utils_1.uiAmountToRawBn)(amount, decimals);
192
+ if (shares.lten(0)) {
193
+ throw new core_1.AugustValidationError('INVALID_INPUT', `vaultRequestRedeem: ${amount} is below one raw unit of a ${decimals}-decimal share mint.`);
194
+ }
195
+ }
196
+ const shareAccount = await (0, token_reads_1.resolveUserTokenAccount)({
197
+ connection,
198
+ owner,
199
+ mint: shareMint,
200
+ });
201
+ if (shareAccount.frozen) {
202
+ throw new core_1.AugustValidationError('INVALID_INPUT', 'Your share account is frozen by the token issuer, so these shares cannot be escrowed.');
203
+ }
204
+ if (shareAccount.balance < BigInt(shares.toString())) {
205
+ throw new core_1.AugustValidationError('INVALID_INPUT', `Insufficient shares to request: this wallet holds ${shareAccount.balance} raw share units, and the request needs ${shares.toString()}.`);
206
+ }
207
+ // The holder pays out to their own richest deposit-token account, as a
208
+ // direct redeem does; anyone else, to their associated account.
209
+ const preInstructions = [];
210
+ let recipientTokenAccount;
211
+ const recipient = params.recipient
212
+ ? new web3_js_1.PublicKey(params.recipient)
213
+ : owner;
214
+ if (recipient.equals(owner)) {
215
+ const payout = await (0, token_reads_1.resolveUserTokenAccount)({
216
+ connection,
217
+ owner,
218
+ mint: depositMint,
219
+ });
220
+ if (payout.frozen) {
221
+ throw new core_1.AugustValidationError('INVALID_INPUT', 'Your token account for the deposit mint is frozen by the token issuer, so it cannot receive the payout.');
222
+ }
223
+ recipientTokenAccount = payout.address;
224
+ if (payout.createIx)
225
+ preInstructions.push(payout.createIx);
226
+ }
227
+ else {
228
+ // Off-curve owners are allowed: a multisig vault or another program's
229
+ // PDA is a valid recipient, and the program accepts it.
230
+ recipientTokenAccount = (0, spl_token_1.getAssociatedTokenAddressSync)(depositMint, recipient, true);
231
+ const existing = await connection.getAccountInfo(recipientTokenAccount);
232
+ if (!existing) {
233
+ preInstructions.push((0, spl_token_1.createAssociatedTokenAccountIdempotentInstruction)(owner, recipientTokenAccount, recipient, depositMint));
234
+ }
235
+ else if ((0, spl_token_1.unpackAccount)(recipientTokenAccount, existing, existing.owner).isFrozen) {
236
+ // The request would land, but every claim would then fail paying it.
237
+ throw new core_1.AugustValidationError('INVALID_INPUT', `The recipient's token account ${recipientTokenAccount.toBase58()} is frozen by the token issuer, so it could not receive the payout.`);
238
+ }
239
+ }
240
+ // Quoted from the vault state read above and the mint's supply. The program
241
+ // checks it at claim time against the shares' value then, net of the fee.
242
+ // The two reads can straddle a state change; the slippage allowance absorbs
243
+ // that, and an exact floor goes in `minAssetsOut`.
244
+ let minAssetsOut;
245
+ if (params.minAssetsOut !== undefined) {
246
+ minAssetsOut = params.minAssetsOut;
247
+ }
248
+ else {
249
+ const { supply } = await readShareMint();
250
+ const totalAssets = (0, pricing_1.totalAssetsOf)(vaultStateData);
251
+ // A marked-down vault can still queue a request, just not at a quote.
252
+ if (!(0, pricing_1.hasSharePrice)(supply, totalAssets)) {
253
+ throw new core_1.AugustValidationError('INVALID_INPUT', 'vaultRequestRedeem: this vault holds no assets while shares are outstanding, so there is no share price to quote a floor from; pass `minAssetsOut: 0n` to request without a floor.');
254
+ }
255
+ minAssetsOut = (0, pricing_1.quoteRedeemFloor)({
256
+ supply,
257
+ totalAssets,
258
+ shares: BigInt(shares.toString()),
259
+ offset: (0, pricing_1.resolveShareOffset)(vaultStateData.shareOffset),
260
+ feePpm: (0, pricing_1.toBigInt)(vaultStateData.withdrawalFee, 'withdrawalFee'),
261
+ slippageBps: params.slippageBps ?? pricing_1.DEFAULT_SLIPPAGE_BPS,
262
+ });
263
+ }
264
+ const id = requestId ?? randomRequestId();
265
+ const request = deriveWithdrawalRequestPda(queue, owner, id, queueProgramId);
266
+ const finalizer = params.finalizer
267
+ ? new web3_js_1.PublicKey(params.finalizer)
268
+ : web3_js_1.PublicKey.default;
269
+ const commitment = commitmentOf(connection);
270
+ const signature = await queueMethods(program)
271
+ .requestWithdrawal(new anchor_1.BN(id.toString()), shares, new anchor_1.BN(minAssetsOut.toString()), finalizer)
272
+ .accountsPartial({
273
+ queue,
274
+ vaultState,
275
+ owner,
276
+ ownerShareAccount: shareAccount.address,
277
+ escrowShares: queueState.escrowShares,
278
+ shareMint,
279
+ recipientTokenAccount,
280
+ request,
281
+ tokenProgram: spl_token_1.TOKEN_PROGRAM_ID,
282
+ })
283
+ .preInstructions(preInstructions)
284
+ .rpc({
285
+ skipPreflight: false,
286
+ preflightCommitment: commitment,
287
+ commitment,
288
+ });
289
+ core_1.Logger.log.info('vaultRequestRedeem', 'Request submitted:', signature);
290
+ return { signature, request: request.toBase58(), requestId: id.toString() };
291
+ }
292
+ catch (e) {
293
+ wrapFailure('withdrawal request', e, context);
294
+ }
295
+ }
296
+ /**
297
+ * Read a request and the queue it names: two reads in turn, since the queue's
298
+ * address comes from the request.
299
+ */
300
+ async function loadRequest({ provider, connection, queueProgramId, request, operation, }) {
301
+ const program = queueProgram(provider, queueProgramId);
302
+ const requestInfo = await connection.getAccountInfo(request);
303
+ const requestState = decodeOrThrow(program.coder.accounts, 'withdrawalRequest', request, requestInfo, `${operation}: no pending request at ${request.toBase58()}; it may already have been claimed or cancelled.`);
304
+ const queue = new web3_js_1.PublicKey(requestState.queue);
305
+ const [queueInfo] = await connection.getMultipleAccountsInfo([queue]);
306
+ const queueState = decodeOrThrow(program.coder.accounts, 'withdrawalQueue', queue, queueInfo, `${operation}: the request's queue ${queue.toBase58()} does not exist.`);
307
+ return { program, requestState, queue, queueState };
308
+ }
309
+ /**
310
+ * Claim a request whose cooldown has run: `finalize_withdrawal`, which redeems
311
+ * the escrowed shares through the vault and pays the request's recipient.
312
+ *
313
+ * Anyone the request permits may claim it: the holder always, and anyone else
314
+ * unless it names a finalizer, in which case only that key. It is refused here,
315
+ * before signing, when the caller is not permitted, the cooldown has not run,
316
+ * or the window has closed; the wall clock stands in for the chain's, so at the
317
+ * boundary the program may still disagree by a slot. The vault's fee-recipient
318
+ * account is created in the same transaction when it has none, and so is the
319
+ * request's payout account when it is the holder's own ATA and has been closed
320
+ * since; any other closed or a frozen payout account is refused before
321
+ * signing, since the claim could only fail.
322
+ *
323
+ * RPC: three reads in turn, each address coming from the account before it
324
+ * (the request, its queue, then the queue's vault together with the payout
325
+ * account), then the send.
326
+ *
327
+ * @throws AugustValidationError for a missing request or one the caller cannot
328
+ * claim yet, before signing.
329
+ * @throws AugustSDKError when the transaction fails, e.g. `NotEnoughLiquidity`
330
+ * while the vault's reserve is deployed; the request stays pending.
331
+ */
332
+ async function handleSolanaClaimRedeem({ provider, connection, vaultProgramId, queueProgramId, wallet, request: requestAddress, nowSeconds, }) {
333
+ const context = { request: String(requestAddress) };
334
+ try {
335
+ if (!wallet)
336
+ throw new core_1.AugustValidationError('INVALID_INPUT', 'vaultRedeem: wallet not connected');
337
+ const finalizer = new web3_js_1.PublicKey(wallet);
338
+ const request = new web3_js_1.PublicKey(requestAddress);
339
+ const { program, requestState, queue, queueState } = await loadRequest({
340
+ provider,
341
+ connection,
342
+ queueProgramId,
343
+ request,
344
+ operation: 'vaultRedeem',
345
+ });
346
+ const named = new web3_js_1.PublicKey(requestState.finalizer);
347
+ const permitted = finalizer.equals(new web3_js_1.PublicKey(requestState.owner)) ||
348
+ named.equals(web3_js_1.PublicKey.default) ||
349
+ named.equals(finalizer);
350
+ if (!permitted) {
351
+ throw new core_1.AugustValidationError('INVALID_INPUT', `vaultRedeem: only the holder or ${named.toBase58()} may claim this request.`);
352
+ }
353
+ const now = BigInt(nowSeconds ?? Math.floor(Date.now() / 1000));
354
+ const expiresAt = BigInt(requestState.expiresAt.toString());
355
+ if (expiresAt !== 0n && now >= expiresAt) {
356
+ throw new core_1.AugustValidationError('INVALID_INPUT', 'vaultRedeem: this request is past its fulfillment window and can no longer be claimed; cancel it with `vaultCancelRedeemRequest` to get the shares back.');
357
+ }
358
+ const eligibleAt = BigInt(requestState.eligibleAt.toString());
359
+ if (now < eligibleAt) {
360
+ throw new core_1.AugustValidationError('INVALID_INPUT', `vaultRedeem: this request is still in its cooldown; it can be claimed from ${new Date(Number(eligibleAt) * 1000).toISOString()}.`);
361
+ }
362
+ const vaultState = new web3_js_1.PublicKey(queueState.vaultState);
363
+ const recipientTokenAccount = new web3_js_1.PublicKey(requestState.recipientTokenAccount);
364
+ const [vaultInfo, recipientInfo] = await connection.getMultipleAccountsInfo([vaultState, recipientTokenAccount]);
365
+ const vault = decodeOrThrow(vaultCoder(provider), 'vaultState', vaultState, vaultInfo, `vaultRedeem: the request's vault ${vaultState.toBase58()} does not exist.`);
366
+ const depositMint = new web3_js_1.PublicKey(queueState.depositMint);
367
+ const feeRecipient = new web3_js_1.PublicKey(vault.feeRecipient);
368
+ // Off-curve allowed: a multisig vault or program PDA can be the fee recipient.
369
+ const feeRecipientAccount = (0, spl_token_1.getAssociatedTokenAddressSync)(depositMint, feeRecipient, true);
370
+ const preInstructions = [
371
+ (0, spl_token_1.createAssociatedTokenAccountIdempotentInstruction)(finalizer, feeRecipientAccount, feeRecipient, depositMint),
372
+ ];
373
+ // The payout account is fixed by the request, and may have been closed
374
+ // since: a wallet's cleanup closes an empty token account. The holder's own
375
+ // ATA can be recreated here, as the fee recipient's is; any other account
376
+ // cannot be rebuilt from its address, so say so before signing.
377
+ const requestOwner = new web3_js_1.PublicKey(requestState.owner);
378
+ if (!recipientInfo) {
379
+ const ownerAta = (0, spl_token_1.getAssociatedTokenAddressSync)(depositMint, requestOwner, true);
380
+ if (!recipientTokenAccount.equals(ownerAta)) {
381
+ throw new core_1.AugustValidationError('INVALID_INPUT', `vaultRedeem: this request pays ${recipientTokenAccount.toBase58()}, which no longer exists; it must be recreated before the request can be claimed, or the holder can cancel it.`);
382
+ }
383
+ preInstructions.push((0, spl_token_1.createAssociatedTokenAccountIdempotentInstruction)(finalizer, ownerAta, requestOwner, depositMint));
384
+ }
385
+ else if ((0, spl_token_1.unpackAccount)(recipientTokenAccount, recipientInfo, recipientInfo.owner)
386
+ .isFrozen) {
387
+ throw new core_1.AugustValidationError('INVALID_INPUT', `vaultRedeem: this request pays ${recipientTokenAccount.toBase58()}, which is frozen by the token issuer, so the claim would fail; the holder can cancel the request.`);
388
+ }
389
+ const commitment = commitmentOf(connection);
390
+ const signature = await queueMethods(program)
391
+ .finalizeWithdrawal(requestState.sequence)
392
+ .accountsPartial({
393
+ queue,
394
+ vaultState,
395
+ vaultDepositAta: utils_1.SolanaUtils.deriveVaultTokenAtaPda(vaultProgramId, depositMint, vault.vaultVersion[0]),
396
+ feeRecipientAccount,
397
+ escrowShares: queueState.escrowShares,
398
+ escrowAssets: queueState.escrowAssets,
399
+ shareMint: queueState.shareMint,
400
+ depositMint,
401
+ finalizer,
402
+ request,
403
+ owner: requestOwner,
404
+ recipientTokenAccount,
405
+ vaultProgram: vaultProgramId,
406
+ tokenProgram: spl_token_1.TOKEN_PROGRAM_ID,
407
+ })
408
+ .preInstructions(preInstructions)
409
+ .rpc({
410
+ skipPreflight: false,
411
+ preflightCommitment: commitment,
412
+ commitment,
413
+ });
414
+ core_1.Logger.log.info('vaultRedeem', 'Request claimed:', signature);
415
+ return signature;
416
+ }
417
+ catch (e) {
418
+ wrapFailure('withdrawal claim', e, context);
419
+ }
420
+ }
421
+ /**
422
+ * Cancel a pending request: `cancel_withdrawal` returns its shares to the
423
+ * holder's share account (created in the same transaction when they closed
424
+ * theirs) and closes the request, refunding its rent. Allowed at any time
425
+ * while the request exists, cooldown or not, expired or not, paused or not.
426
+ *
427
+ * RPC: two reads in turn (the request, then its queue), one
428
+ * `getParsedTokenAccountsByOwner`, then the send.
429
+ *
430
+ * @returns The confirmed transaction's signature.
431
+ * @throws AugustValidationError for a missing request or one `wallet` did not
432
+ * make, before signing.
433
+ * @throws AugustSDKError when the transaction fails.
434
+ */
435
+ async function handleSolanaCancelRedeemRequest({ provider, connection, queueProgramId, params, }) {
436
+ const context = { request: String(params.request) };
437
+ try {
438
+ if (!params.wallet)
439
+ throw new core_1.AugustValidationError('INVALID_INPUT', 'vaultCancelRedeemRequest: wallet not connected');
440
+ if (!params.request)
441
+ throw new core_1.AugustValidationError('INVALID_INPUT', 'vaultCancelRedeemRequest: request is required');
442
+ const owner = new web3_js_1.PublicKey(params.wallet);
443
+ const request = new web3_js_1.PublicKey(params.request);
444
+ const { program, requestState, queue, queueState } = await loadRequest({
445
+ provider,
446
+ connection,
447
+ queueProgramId,
448
+ request,
449
+ operation: 'vaultCancelRedeemRequest',
450
+ });
451
+ if (!owner.equals(new web3_js_1.PublicKey(requestState.owner))) {
452
+ throw new core_1.AugustValidationError('INVALID_INPUT', 'vaultCancelRedeemRequest: only the holder who made this request may cancel it.');
453
+ }
454
+ const shareMint = new web3_js_1.PublicKey(queueState.shareMint);
455
+ const destination = await (0, token_reads_1.resolveUserTokenAccount)({
456
+ connection,
457
+ owner,
458
+ mint: shareMint,
459
+ });
460
+ if (destination.frozen) {
461
+ throw new core_1.AugustValidationError('INVALID_INPUT', 'Your share account is frozen by the token issuer, so it cannot take the shares back.');
462
+ }
463
+ const commitment = commitmentOf(connection);
464
+ const signature = await queueMethods(program)
465
+ .cancelWithdrawal(requestState.sequence)
466
+ .accountsPartial({
467
+ queue,
468
+ owner,
469
+ request,
470
+ escrowShares: queueState.escrowShares,
471
+ shareMint,
472
+ destinationShareAccount: destination.address,
473
+ tokenProgram: spl_token_1.TOKEN_PROGRAM_ID,
474
+ })
475
+ .preInstructions(destination.createIx ? [destination.createIx] : [])
476
+ .rpc({
477
+ skipPreflight: false,
478
+ preflightCommitment: commitment,
479
+ commitment,
480
+ });
481
+ core_1.Logger.log.info('vaultCancelRedeemRequest', 'Request cancelled:', signature);
482
+ return signature;
483
+ }
484
+ catch (e) {
485
+ wrapFailure('withdrawal cancel', e, context);
486
+ }
487
+ }
488
+ //# sourceMappingURL=queue.actions.js.map
@@ -0,0 +1,108 @@
1
+ import type { AccountInfo, Connection, PublicKey, TransactionInstruction } from '@solana/web3.js';
2
+ /**
3
+ * The vault state and share-mint supply as of a **single slot**.
4
+ *
5
+ * Both figures price the quote, so they have to come from one snapshot. Read
6
+ * independently — even concurrently, as two RPC requests — a deposit or
7
+ * redemption landing in between combines a supply and an asset total that
8
+ * never coexisted, and the quote is wrong in whichever direction the
9
+ * interleaving fell. Seeing a 10% deposit's new assets against the old supply
10
+ * quotes ~10% high, and the floor then rejects a redemption whose share price
11
+ * never moved; the opposite ordering quotes low and silently weakens the
12
+ * protection. `getMultipleAccountsInfo` is served from one slot, so the pair
13
+ * is always coherent — and it costs one round trip instead of two.
14
+ *
15
+ * @param program - Anchor program, used only for its account coder; the
16
+ * provider it was built with is irrelevant, so no read-only program is needed.
17
+ * @param vaultStatePda - Vault state account to decode.
18
+ * @param shareMintAddr - Share mint whose supply and decimals are read.
19
+ * @param extraAccounts - Further accounts to read in the same call and slot,
20
+ * returned raw and in order (`null` for an absent one). Lets a caller that
21
+ * also needs, say, token accounts avoid a second round trip.
22
+ * @returns The decoded `VaultState`, the share mint's raw `supply`, its
23
+ * `decimals`, and `extraAccountInfos` — all from the same slot.
24
+ * @throws {@link AugustSDKError} when either account is absent, or is not the
25
+ * account type it should be.
26
+ */
27
+ export declare function readPricingSnapshot({ connection, program, vaultStatePda, shareMintAddr, extraAccounts, }: {
28
+ connection: Connection;
29
+ program: any;
30
+ vaultStatePda: PublicKey;
31
+ shareMintAddr: PublicKey;
32
+ extraAccounts?: PublicKey[];
33
+ }): Promise<{
34
+ vaultStateData: any;
35
+ supply: bigint;
36
+ shareDecimals: number;
37
+ extraAccountInfos: (AccountInfo<Buffer> | null)[];
38
+ }>;
39
+ /**
40
+ * The token account a vault instruction should use for `mint`, its balance,
41
+ * and the instruction (if any) needed to bring it into existence.
42
+ *
43
+ * `createIx` is present iff the owner holds no *account* for `mint`. It says
44
+ * nothing about balance — an existing account may hold zero, which is a
45
+ * distinct state. Callers that need funds must check {@link balance}.
46
+ */
47
+ export interface IResolvedTokenAccount {
48
+ /** Account to pass to the program as `sender_token_account` / `sender_share_account`. */
49
+ address: PublicKey;
50
+ /** Raw on-chain balance of {@link address}; `0n` when the account is absent. */
51
+ balance: bigint;
52
+ /**
53
+ * True when every account the wallet holds for this mint is frozen. Tokens
54
+ * can move neither out of nor into a frozen account, so this is fatal for
55
+ * funding, burning *and* payout — reject before requesting a signature.
56
+ */
57
+ frozen: boolean;
58
+ /**
59
+ * Idempotent ATA-creation instruction, or `undefined` when an account
60
+ * already exists. Prepend to the vault instruction — never send on its own.
61
+ */
62
+ createIx?: TransactionInstruction;
63
+ }
64
+ /**
65
+ * Pick which token account to hand the vault program for a given mint, and
66
+ * describe how to create it when the user has none.
67
+ *
68
+ * The vault program constrains these accounts by `token::mint` +
69
+ * `token::authority` (not `associated_token::…`), so any account the signer
70
+ * owns qualifies. We therefore keep an existing account when there is one —
71
+ * that is where the user's balance already lives — and fall back to the
72
+ * associated token account otherwise.
73
+ *
74
+ * Two deliberate choices:
75
+ *
76
+ * - **The create is returned, not sent.** Prepended to the deposit/redeem via
77
+ * `.preInstructions()` it is atomic with the transfer, so there is no window
78
+ * in which the vault instruction can reference an account that has not
79
+ * landed. Sent as its own transaction — as this module used to — it raced
80
+ * its own confirmation. Observed in production (2026-08-11):
81
+ * a deposit failed with `Simulation failed … Logs: []` while the share-ATA
82
+ * create was still in flight, then succeeded 17s later on retry. The empty
83
+ * log array means the transaction never reached program execution, so there
84
+ * was nothing on-chain to debug from.
85
+ * - **Idempotent.** `createAssociatedTokenAccountIdempotentInstruction` is a
86
+ * no-op when the account already exists, so a concurrent create — another
87
+ * tab, a wallet's auto-provisioning — cannot turn into an
88
+ * `SystemError::AccountAlreadyInUse` ("already in use") failure between our
89
+ * read and our submit.
90
+ *
91
+ * Which account wins when several exist is decided by
92
+ * {@link selectRichestTokenAccount} — the same rule every balance *read* in
93
+ * this adapter uses, so the account a UI reports and the account a transfer
94
+ * names are never different accounts.
95
+ *
96
+ * Classic SPL Token only: the ATA is derived under `TOKEN_PROGRAM_ID`, matching
97
+ * the `tokenProgram` both handlers pass, even though the program's
98
+ * `TokenInterface` would also accept Token-2022.
99
+ *
100
+ * @param owner - Wallet that owns the account and pays rent for any creation.
101
+ * @throws AugustSDKError when a candidate account's balance cannot be read
102
+ * (propagated from the shared selector).
103
+ */
104
+ export declare function resolveUserTokenAccount({ connection, owner, mint, }: {
105
+ connection: Connection;
106
+ owner: PublicKey;
107
+ mint: PublicKey;
108
+ }): Promise<IResolvedTokenAccount>;