@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.
- package/lib/adapters/solana/constants.d.ts +12 -4
- package/lib/adapters/solana/constants.js +8 -3
- package/lib/adapters/solana/getters.d.ts +5 -0
- package/lib/adapters/solana/getters.js +35 -1
- package/lib/adapters/solana/idl/vault-idl.d.ts +334 -7
- package/lib/adapters/solana/idl/vault-idl.js +600 -59
- package/lib/adapters/solana/idl/withdrawal-queue-idl.d.ts +790 -0
- package/lib/adapters/solana/idl/withdrawal-queue-idl.js +1868 -0
- package/lib/adapters/solana/index.d.ts +108 -10
- package/lib/adapters/solana/index.js +137 -12
- package/lib/adapters/solana/pricing.d.ts +11 -0
- package/lib/adapters/solana/pricing.js +24 -8
- package/lib/adapters/solana/queue.actions.d.ts +98 -0
- package/lib/adapters/solana/queue.actions.js +488 -0
- package/lib/adapters/solana/token-reads.d.ts +108 -0
- package/lib/adapters/solana/token-reads.js +145 -0
- package/lib/adapters/solana/types.d.ts +200 -1
- package/lib/adapters/solana/utils.d.ts +16 -0
- package/lib/adapters/solana/utils.js +41 -4
- package/lib/adapters/solana/vault.actions.d.ts +17 -0
- package/lib/adapters/solana/vault.actions.js +14 -125
- package/lib/adapters/solana/withdrawal-queue.d.ts +125 -0
- package/lib/adapters/solana/withdrawal-queue.js +301 -0
- package/lib/core/analytics/method-taxonomy.js +2 -0
- package/lib/core/analytics/version.d.ts +1 -1
- package/lib/core/analytics/version.js +1 -1
- package/lib/main.d.ts +16 -0
- package/lib/main.js +16 -0
- package/lib/modules/vaults/getters.d.ts +2 -2
- package/lib/modules/vaults/getters.js +103 -3
- package/lib/modules/vaults/main.d.ts +12 -0
- package/lib/modules/vaults/main.js +17 -1
- package/lib/modules/vaults/types.d.ts +14 -1
- package/lib/sdk.d.ts +2389 -1723
- package/lib/types/vaults.d.ts +47 -1
- package/lib/types/vaults.js +5 -0
- package/lib/types/webserver.d.ts +8 -0
- 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>;
|