@augustdigital/sdk 9.11.0 → 9.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/modules/vaults/utils.d.ts +8 -0
- package/lib/modules/vaults/utils.js +19 -0
- package/lib/sdk.d.ts +2406 -1723
- package/lib/types/vaults.d.ts +55 -1
- package/lib/types/vaults.js +5 -0
- package/lib/types/webserver.d.ts +8 -0
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.describeSolanaError = describeSolanaError;
|
|
4
4
|
exports.slippageHint = slippageHint;
|
|
5
|
+
exports.validateExplicitFloor = validateExplicitFloor;
|
|
5
6
|
exports.handleSolanaDeposit = handleSolanaDeposit;
|
|
6
7
|
exports.handleSolanaRedeem = handleSolanaRedeem;
|
|
7
8
|
const anchor_1 = require("@coral-xyz/anchor");
|
|
@@ -11,6 +12,7 @@ const constants_1 = require("./constants");
|
|
|
11
12
|
const pricing_1 = require("./pricing");
|
|
12
13
|
const utils_1 = require("./utils");
|
|
13
14
|
const core_1 = require("../../core");
|
|
15
|
+
const token_reads_1 = require("./token-reads");
|
|
14
16
|
/**
|
|
15
17
|
* User-facing fallback for a throwable that carries no extractable reason.
|
|
16
18
|
* Surfaces as e.g. `"Solana deposit failed: please try again"` — actionable,
|
|
@@ -145,128 +147,6 @@ function sendsCheckedInstruction({ program, checkedMethod, floor, slippageBps, h
|
|
|
145
147
|
'enable slippage protection.');
|
|
146
148
|
return false;
|
|
147
149
|
}
|
|
148
|
-
/**
|
|
149
|
-
* The vault state and share-mint supply as of a **single slot**.
|
|
150
|
-
*
|
|
151
|
-
* Both figures price the quote, so they have to come from one snapshot. Read
|
|
152
|
-
* independently — even concurrently, as two RPC requests — a deposit or
|
|
153
|
-
* redemption landing in between combines a supply and an asset total that
|
|
154
|
-
* never coexisted, and the quote is wrong in whichever direction the
|
|
155
|
-
* interleaving fell. Seeing a 10% deposit's new assets against the old supply
|
|
156
|
-
* quotes ~10% high, and the floor then rejects a redemption whose share price
|
|
157
|
-
* never moved; the opposite ordering quotes low and silently weakens the
|
|
158
|
-
* protection. `getMultipleAccountsInfo` is served from one slot, so the pair
|
|
159
|
-
* is always coherent — and it costs one round trip instead of two.
|
|
160
|
-
*
|
|
161
|
-
* @param program - Anchor program, used only for its account coder; the
|
|
162
|
-
* provider it was built with is irrelevant, so no read-only program is needed.
|
|
163
|
-
* @param vaultStatePda - Vault state account to decode.
|
|
164
|
-
* @param shareMintAddr - Share mint whose supply and decimals are read.
|
|
165
|
-
* @returns The decoded `VaultState`, the share mint's raw `supply`, and its
|
|
166
|
-
* `decimals` — all from the same slot.
|
|
167
|
-
* @throws {@link AugustSDKError} when either account is absent, or is not the
|
|
168
|
-
* account type it should be.
|
|
169
|
-
*/
|
|
170
|
-
async function readPricingSnapshot({ connection, program, vaultStatePda, shareMintAddr, }) {
|
|
171
|
-
const [vaultStateInfo, shareMintInfo] = await connection.getMultipleAccountsInfo([vaultStatePda, shareMintAddr]);
|
|
172
|
-
if (!vaultStateInfo) {
|
|
173
|
-
throw new core_1.AugustSDKError('UNKNOWN', `Vault state account ${vaultStatePda.toBase58()} does not exist; it cannot be priced.`);
|
|
174
|
-
}
|
|
175
|
-
if (!shareMintInfo) {
|
|
176
|
-
throw new core_1.AugustSDKError('UNKNOWN', `Share mint ${shareMintAddr.toBase58()} does not exist; it cannot be priced.`);
|
|
177
|
-
}
|
|
178
|
-
// `vaultState`, not the IDL's own `VaultState`: `Program` runs the IDL
|
|
179
|
-
// through `convertIdlToCamelCase`, so its coder only answers to the
|
|
180
|
-
// camelCase name and the IDL spelling throws `Account not found`. (A coder
|
|
181
|
-
// built directly from the raw IDL behaves the opposite way — which is why
|
|
182
|
-
// this has to be checked against a real `Program`, as its test does.)
|
|
183
|
-
//
|
|
184
|
-
// Decoded by name rather than `decodeAny`, so a `vaultAddress` pointing at
|
|
185
|
-
// some other account of this program fails here instead of being decoded as
|
|
186
|
-
// whatever it happens to be.
|
|
187
|
-
let vaultStateData;
|
|
188
|
-
try {
|
|
189
|
-
vaultStateData = program.coder.accounts.decode('vaultState', vaultStateInfo.data);
|
|
190
|
-
}
|
|
191
|
-
catch (e) {
|
|
192
|
-
throw new core_1.AugustSDKError('UNKNOWN', `Account ${vaultStatePda.toBase58()} is not a vault state account for this program.`, { cause: e });
|
|
193
|
-
}
|
|
194
|
-
let mint;
|
|
195
|
-
try {
|
|
196
|
-
mint = (0, spl_token_1.unpackMint)(shareMintAddr, shareMintInfo, spl_token_1.TOKEN_PROGRAM_ID);
|
|
197
|
-
}
|
|
198
|
-
catch (e) {
|
|
199
|
-
throw new core_1.AugustSDKError('UNKNOWN', `Account ${shareMintAddr.toBase58()} is not an SPL Token mint.`, { cause: e });
|
|
200
|
-
}
|
|
201
|
-
return {
|
|
202
|
-
vaultStateData,
|
|
203
|
-
supply: mint.supply,
|
|
204
|
-
shareDecimals: mint.decimals,
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
/**
|
|
208
|
-
* Pick which token account to hand the vault program for a given mint, and
|
|
209
|
-
* describe how to create it when the user has none.
|
|
210
|
-
*
|
|
211
|
-
* The vault program constrains these accounts by `token::mint` +
|
|
212
|
-
* `token::authority` (not `associated_token::…`), so any account the signer
|
|
213
|
-
* owns qualifies. We therefore keep an existing account when there is one —
|
|
214
|
-
* that is where the user's balance already lives — and fall back to the
|
|
215
|
-
* associated token account otherwise.
|
|
216
|
-
*
|
|
217
|
-
* Two deliberate choices:
|
|
218
|
-
*
|
|
219
|
-
* - **The create is returned, not sent.** Prepended to the deposit/redeem via
|
|
220
|
-
* `.preInstructions()` it is atomic with the transfer, so there is no window
|
|
221
|
-
* in which the vault instruction can reference an account that has not
|
|
222
|
-
* landed. Sent as its own transaction — as this module used to — it raced
|
|
223
|
-
* its own confirmation. Observed in production (2026-08-11):
|
|
224
|
-
* a deposit failed with `Simulation failed … Logs: []` while the share-ATA
|
|
225
|
-
* create was still in flight, then succeeded 17s later on retry. The empty
|
|
226
|
-
* log array means the transaction never reached program execution, so there
|
|
227
|
-
* was nothing on-chain to debug from.
|
|
228
|
-
* - **Idempotent.** `createAssociatedTokenAccountIdempotentInstruction` is a
|
|
229
|
-
* no-op when the account already exists, so a concurrent create — another
|
|
230
|
-
* tab, a wallet's auto-provisioning — cannot turn into an
|
|
231
|
-
* `SystemError::AccountAlreadyInUse` ("already in use") failure between our
|
|
232
|
-
* read and our submit.
|
|
233
|
-
*
|
|
234
|
-
* Which account wins when several exist is decided by
|
|
235
|
-
* {@link selectRichestTokenAccount} — the same rule every balance *read* in
|
|
236
|
-
* this adapter uses, so the account a UI reports and the account a transfer
|
|
237
|
-
* names are never different accounts.
|
|
238
|
-
*
|
|
239
|
-
* Classic SPL Token only: the ATA is derived under `TOKEN_PROGRAM_ID`, matching
|
|
240
|
-
* the `tokenProgram` both handlers pass, even though the program's
|
|
241
|
-
* `TokenInterface` would also accept Token-2022.
|
|
242
|
-
*
|
|
243
|
-
* @param owner - Wallet that owns the account and pays rent for any creation.
|
|
244
|
-
* @throws AugustSDKError when a candidate account's balance cannot be read
|
|
245
|
-
* (propagated from the shared selector), and `TokenOwnerOffCurveError` when
|
|
246
|
-
* `owner` is off-curve and an ATA has to be derived.
|
|
247
|
-
*/
|
|
248
|
-
async function resolveUserTokenAccount({ connection, owner, mint, }) {
|
|
249
|
-
const existing = await connection.getParsedTokenAccountsByOwner(owner, {
|
|
250
|
-
mint,
|
|
251
|
-
});
|
|
252
|
-
const richest = (0, utils_1.selectRichestTokenAccount)(existing.value);
|
|
253
|
-
if (richest)
|
|
254
|
-
return {
|
|
255
|
-
address: richest.pubkey,
|
|
256
|
-
balance: richest.amount,
|
|
257
|
-
frozen: richest.frozen,
|
|
258
|
-
};
|
|
259
|
-
const ata = await (0, spl_token_1.getAssociatedTokenAddress)(mint, owner);
|
|
260
|
-
return {
|
|
261
|
-
address: ata,
|
|
262
|
-
balance: BigInt(0),
|
|
263
|
-
frozen: false,
|
|
264
|
-
createIx: (0, spl_token_1.createAssociatedTokenAccountIdempotentInstruction)(owner, // payer
|
|
265
|
-
ata, // associated token account
|
|
266
|
-
owner, // owner
|
|
267
|
-
mint),
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
150
|
/**
|
|
271
151
|
* Resolve the two token accounts a vault instruction needs, concurrently.
|
|
272
152
|
*
|
|
@@ -283,7 +163,7 @@ async function resolveUserTokenAccount({ connection, owner, mint, }) {
|
|
|
283
163
|
* @param mints - `[first, second]`; the returned tuple matches this order.
|
|
284
164
|
*/
|
|
285
165
|
async function resolveUserTokenAccountPair({ connection, owner, mints, }) {
|
|
286
|
-
const settled = await Promise.allSettled(mints.map((mint) => resolveUserTokenAccount({ connection, owner, mint })));
|
|
166
|
+
const settled = await Promise.allSettled(mints.map((mint) => (0, token_reads_1.resolveUserTokenAccount)({ connection, owner, mint })));
|
|
287
167
|
const resolved = settled.map((outcome) => {
|
|
288
168
|
if (outcome.status === 'rejected')
|
|
289
169
|
throw outcome.reason;
|
|
@@ -466,7 +346,7 @@ async function handleSolanaDeposit({ provider, connection, network = constants_1
|
|
|
466
346
|
// pricing against a mismatched pair is worse than not quoting at all —
|
|
467
347
|
// see `readPricingSnapshot`. A caller already holding a quote can pass
|
|
468
348
|
// `minSharesOut` and skip it.
|
|
469
|
-
const { vaultStateData, supply } = await readPricingSnapshot({
|
|
349
|
+
const { vaultStateData, supply } = await (0, token_reads_1.readPricingSnapshot)({
|
|
470
350
|
connection,
|
|
471
351
|
program,
|
|
472
352
|
vaultStatePda,
|
|
@@ -613,12 +493,21 @@ async function handleSolanaRedeem({ provider, connection, vaultProgramId, vaultA
|
|
|
613
493
|
// replaces a `getParsedAccountInfo` here and a `vaultState.fetch` further
|
|
614
494
|
// down — one round trip rather than two, and supply and assets can no
|
|
615
495
|
// longer be read either side of someone else's deposit.
|
|
616
|
-
const { vaultStateData, supply: shareSupply, shareDecimals, } = await readPricingSnapshot({
|
|
496
|
+
const { vaultStateData, supply: shareSupply, shareDecimals, } = await (0, token_reads_1.readPricingSnapshot)({
|
|
617
497
|
connection,
|
|
618
498
|
program,
|
|
619
499
|
vaultStatePda,
|
|
620
500
|
shareMintAddr,
|
|
621
501
|
});
|
|
502
|
+
// A vault with a queue attached accepts a redeem from the queue alone, so
|
|
503
|
+
// a holder's direct redeem can only revert with `WithdrawalQueueRequired`.
|
|
504
|
+
// Say so before the wallet is asked to sign.
|
|
505
|
+
const gate = vaultStateData.withdrawalQueueAuthority;
|
|
506
|
+
if (gate && !new web3_js_1.PublicKey(gate).equals(web3_js_1.PublicKey.default)) {
|
|
507
|
+
throw new core_1.AugustValidationError('INVALID_INPUT', 'This vault redeems through its withdrawal queue: request with ' +
|
|
508
|
+
'`vaultRequestRedeem`, then claim the request with `vaultRedeem` and ' +
|
|
509
|
+
'`options.request` once its cooldown has run.');
|
|
510
|
+
}
|
|
622
511
|
let redeemSharesRaw;
|
|
623
512
|
if (typeof redeemShares === 'bigint') {
|
|
624
513
|
redeemSharesRaw = new anchor_1.BN(redeemShares.toString());
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { type Connection, PublicKey } from '@solana/web3.js';
|
|
2
|
+
import type { IAddress, ISolanaNetwork, IVaultAvailableRedemption } from '../../types';
|
|
3
|
+
import { VaultRedemptionStatus } from '../../types/vaults';
|
|
4
|
+
import type { IVaultWithdrawalQueue } from '../../types/vaults';
|
|
5
|
+
import type { ISolanaVaultState, ISolanaWithdrawalRequestState } from './types';
|
|
6
|
+
/**
|
|
7
|
+
* The queue PDA for a vault: `["withdrawal_queue", vault_state]` under the queue
|
|
8
|
+
* program. The vault only ever attaches this address.
|
|
9
|
+
*
|
|
10
|
+
* @param vaultState - The vault's `VaultState` account.
|
|
11
|
+
* @param queueProgramId - The august_withdrawal_queue program id for the network.
|
|
12
|
+
*/
|
|
13
|
+
export declare function deriveWithdrawalQueuePda(vaultState: PublicKey, queueProgramId: PublicKey): PublicKey;
|
|
14
|
+
/**
|
|
15
|
+
* Where a request stands at `nowSeconds`, by the program's own rules
|
|
16
|
+
* (`WithdrawalRequest::is_expired` / `is_eligible`): a non-zero `expiresAt` at
|
|
17
|
+
* or before now is expired, and that is checked first because an expired
|
|
18
|
+
* request can only be cancelled; otherwise it is claimable from `eligibleAt`.
|
|
19
|
+
*
|
|
20
|
+
* @param request - Decoded request; `eligibleAt` / `expiresAt` in Unix seconds.
|
|
21
|
+
* @param nowSeconds - Unix seconds. Wall-clock time stands in for the chain's
|
|
22
|
+
* clock, so a request within seconds of a boundary may be classified a slot
|
|
23
|
+
* early or late.
|
|
24
|
+
*/
|
|
25
|
+
export declare function classifyWithdrawalRequest(request: Pick<ISolanaWithdrawalRequestState, 'eligibleAt' | 'expiresAt'>, nowSeconds: number): VaultRedemptionStatus;
|
|
26
|
+
/**
|
|
27
|
+
* What finalizing `shares` would pay the recipient at the vault's current
|
|
28
|
+
* state: the offset-aware `assets_for_redeem`, less the ceil-rounded
|
|
29
|
+
* withdrawal fee. Finalize prices at execution, so this is an estimate.
|
|
30
|
+
*
|
|
31
|
+
* @returns Raw deposit-mint units, net of fee.
|
|
32
|
+
* @throws AugustValidationError when the vault has no share price.
|
|
33
|
+
*/
|
|
34
|
+
export declare function estimateRequestPayout(input: {
|
|
35
|
+
vaultState: Pick<ISolanaVaultState, 'localAum' | 'deployedAum' | 'withdrawalFee' | 'shareOffset'>;
|
|
36
|
+
supply: bigint;
|
|
37
|
+
shares: bigint;
|
|
38
|
+
}): bigint;
|
|
39
|
+
/**
|
|
40
|
+
* The withdrawal queue program id a Solana service names, or `undefined` when
|
|
41
|
+
* it names none: the queue is not deployed on its network (mainnet-beta until
|
|
42
|
+
* its upgrade), or the service has neither `network` nor `getProgramId`. Read
|
|
43
|
+
* paths treat that as "no queue" rather than a failed read, so they make no RPC
|
|
44
|
+
* call and log nothing.
|
|
45
|
+
*
|
|
46
|
+
* Prefers `network`, read straight from `programIds`: `getProgramId` is an
|
|
47
|
+
* instrumented public method, and calling it where it throws by design would
|
|
48
|
+
* record an error breadcrumb on every vault read.
|
|
49
|
+
*
|
|
50
|
+
* @param service - The SolanaAdapter (or a stand-in) the read was given.
|
|
51
|
+
* @returns The queue program id, or `undefined`.
|
|
52
|
+
* @throws When the service names a malformed id; callers read inside their
|
|
53
|
+
* own `try`, so that is logged as a failed queue read.
|
|
54
|
+
*/
|
|
55
|
+
export declare function resolveQueueProgramId(service: {
|
|
56
|
+
network?: ISolanaNetwork;
|
|
57
|
+
getProgramId?: (type: 'vault' | 'withdrawalQueue') => string;
|
|
58
|
+
}): PublicKey | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* The vault's withdrawal queue summary for `IVault.withdrawalQueue`, or
|
|
61
|
+
* `undefined` when the vault has no queue.
|
|
62
|
+
*
|
|
63
|
+
* `enabled` is whether the vault's redeem gate points at this queue, read from
|
|
64
|
+
* the vault state in the same call as the queue, so an attach or release shows
|
|
65
|
+
* at once rather than when a cached vault state expires. A queue that was
|
|
66
|
+
* released keeps its account and may still hold pending requests, so it is
|
|
67
|
+
* reported with `enabled: false`.
|
|
68
|
+
*
|
|
69
|
+
* RPC: one `getMultipleAccountsInfo` (the queue and the vault state). Not
|
|
70
|
+
* cached: the counters change with every request.
|
|
71
|
+
*
|
|
72
|
+
* @param input - The vault and where to read.
|
|
73
|
+
*/
|
|
74
|
+
export declare function getSolanaWithdrawalQueueSummary(input: {
|
|
75
|
+
connection: Connection;
|
|
76
|
+
queueProgramId: PublicKey;
|
|
77
|
+
/** The vault's `VaultState` account. */
|
|
78
|
+
vaultState: PublicKey;
|
|
79
|
+
/** Decimals of the vault's share mint, for `pendingShares`. */
|
|
80
|
+
shareDecimals: number;
|
|
81
|
+
}): Promise<IVaultWithdrawalQueue | undefined>;
|
|
82
|
+
/**
|
|
83
|
+
* `owner`'s pending requests on a vault's queue, split the way the EVM path
|
|
84
|
+
* splits them: `availableRedemptions` holds requests still waiting
|
|
85
|
+
* (`AWAITING_COOLDOWN`) or past their window (`EXPIRED`, cancel-only);
|
|
86
|
+
* `pendingRedemptions` holds requests finalizable now (`READY_TO_CLAIM`).
|
|
87
|
+
*
|
|
88
|
+
* Each entry's `amount` is the escrowed shares and `estimatedAssets` the net
|
|
89
|
+
* payout at the current share price (see {@link estimateRequestPayout}),
|
|
90
|
+
* priced from the vault state and share supply of a single slot, never from a
|
|
91
|
+
* cached state. `receiver` is the wallet that owns the request's payout token
|
|
92
|
+
* account, which need not be the requester; if that account is gone it is the
|
|
93
|
+
* account address itself. `id` and `hash` are the request account's address:
|
|
94
|
+
* the program keeps no creation transaction to point at. `date` and
|
|
95
|
+
* `day` / `month` / `year` are the eligibility time (UTC); `timestamp` is when
|
|
96
|
+
* the request was made.
|
|
97
|
+
*
|
|
98
|
+
* RPC, worst case: one `getAccountInfo` (skipped once the queue is known to
|
|
99
|
+
* exist), one `getProgramAccounts` filtered on queue and owner, and, when the
|
|
100
|
+
* owner has any request, one `getMultipleAccountsInfo` carrying the vault
|
|
101
|
+
* state, share mint and up to 98 distinct payout accounts. Each further 100
|
|
102
|
+
* distinct payout accounts cost one more `getMultipleAccountsInfo`, at most
|
|
103
|
+
* four in flight. A vault without a queue costs the single `getAccountInfo`.
|
|
104
|
+
*
|
|
105
|
+
* @param input - The vault, the owner, and where to read.
|
|
106
|
+
*/
|
|
107
|
+
export declare function getSolanaWithdrawalRedemptions(input: {
|
|
108
|
+
connection: Connection;
|
|
109
|
+
queueProgramId: PublicKey;
|
|
110
|
+
/** The vault's `VaultState` account, read fresh for pricing. */
|
|
111
|
+
vaultStateAddress: PublicKey;
|
|
112
|
+
/** The vault's share mint, read in the same call for the supply. */
|
|
113
|
+
shareMint: PublicKey;
|
|
114
|
+
/** The holder whose requests to list. */
|
|
115
|
+
owner: PublicKey;
|
|
116
|
+
/** The vault address to stamp on each entry. */
|
|
117
|
+
vault: IAddress;
|
|
118
|
+
/** Deposit mint decimals, for `estimatedAssets`. */
|
|
119
|
+
depositDecimals: number;
|
|
120
|
+
/** Unix seconds to classify against; defaults to now. */
|
|
121
|
+
nowSeconds?: number;
|
|
122
|
+
}): Promise<{
|
|
123
|
+
availableRedemptions: IVaultAvailableRedemption[];
|
|
124
|
+
pendingRedemptions: IVaultAvailableRedemption[];
|
|
125
|
+
}>;
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.deriveWithdrawalQueuePda = deriveWithdrawalQueuePda;
|
|
4
|
+
exports.classifyWithdrawalRequest = classifyWithdrawalRequest;
|
|
5
|
+
exports.estimateRequestPayout = estimateRequestPayout;
|
|
6
|
+
exports.resolveQueueProgramId = resolveQueueProgramId;
|
|
7
|
+
exports.getSolanaWithdrawalQueueSummary = getSolanaWithdrawalQueueSummary;
|
|
8
|
+
exports.getSolanaWithdrawalRedemptions = getSolanaWithdrawalRedemptions;
|
|
9
|
+
/**
|
|
10
|
+
* Read paths for the august_withdrawal_queue program: the per-vault queue
|
|
11
|
+
* summary and a holder's pending withdrawal requests.
|
|
12
|
+
*
|
|
13
|
+
* Internal to the SDK. `getSolanaVault` uses {@link getSolanaWithdrawalQueueSummary};
|
|
14
|
+
* `getVaultPositions` and `getVaultAvailableRedemptions` use
|
|
15
|
+
* {@link getSolanaWithdrawalRedemptions}. Nothing here signs or writes.
|
|
16
|
+
*/
|
|
17
|
+
const anchor_1 = require("@coral-xyz/anchor");
|
|
18
|
+
const spl_token_1 = require("@solana/spl-token");
|
|
19
|
+
const web3_js_1 = require("@solana/web3.js");
|
|
20
|
+
const core_1 = require("../../core");
|
|
21
|
+
const cache_1 = require("../../core/cache");
|
|
22
|
+
const concurrency_1 = require("../../core/helpers/concurrency");
|
|
23
|
+
const vaults_1 = require("../../types/vaults");
|
|
24
|
+
const vault_idl_1 = require("./idl/vault-idl");
|
|
25
|
+
const withdrawal_queue_idl_1 = require("./idl/withdrawal-queue-idl");
|
|
26
|
+
const pricing_1 = require("./pricing");
|
|
27
|
+
const constants_1 = require("./constants");
|
|
28
|
+
const utils_1 = require("./utils");
|
|
29
|
+
const token_reads_1 = require("./token-reads");
|
|
30
|
+
const WITHDRAWAL_QUEUE_SEED = 'withdrawal_queue';
|
|
31
|
+
/** Byte offsets of `queue` and `owner` in a `WithdrawalRequest`, after the 8-byte discriminator. */
|
|
32
|
+
const REQUEST_QUEUE_OFFSET = 8;
|
|
33
|
+
const REQUEST_OWNER_OFFSET = 40;
|
|
34
|
+
/** `getMultipleAccountsInfo` accepts at most this many accounts per call. */
|
|
35
|
+
const MAX_ACCOUNTS_PER_CALL = 100;
|
|
36
|
+
/** Payout-account batches beyond the first, in flight at once. */
|
|
37
|
+
const PAYOUT_BATCH_CONCURRENCY = 4;
|
|
38
|
+
/**
|
|
39
|
+
* The queue PDA for a vault: `["withdrawal_queue", vault_state]` under the queue
|
|
40
|
+
* program. The vault only ever attaches this address.
|
|
41
|
+
*
|
|
42
|
+
* @param vaultState - The vault's `VaultState` account.
|
|
43
|
+
* @param queueProgramId - The august_withdrawal_queue program id for the network.
|
|
44
|
+
*/
|
|
45
|
+
function deriveWithdrawalQueuePda(vaultState, queueProgramId) {
|
|
46
|
+
return web3_js_1.PublicKey.findProgramAddressSync([Buffer.from(WITHDRAWAL_QUEUE_SEED), vaultState.toBuffer()], queueProgramId)[0];
|
|
47
|
+
}
|
|
48
|
+
/** A read-only queue program client on `connection`. No wallet, no RPC on construction. */
|
|
49
|
+
function queueProgram(connection, queueProgramId) {
|
|
50
|
+
return new anchor_1.Program({ ...withdrawal_queue_idl_1.withdrawalQueueIdl, address: queueProgramId.toBase58() }, utils_1.SolanaUtils.getReadOnlyProvider({ connection }));
|
|
51
|
+
}
|
|
52
|
+
/** A read-only vault program client, used only for its account coder. */
|
|
53
|
+
function vaultProgram(connection) {
|
|
54
|
+
return new anchor_1.Program(vault_idl_1.vaultIdl, utils_1.SolanaUtils.getReadOnlyProvider({ connection }));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Where a request stands at `nowSeconds`, by the program's own rules
|
|
58
|
+
* (`WithdrawalRequest::is_expired` / `is_eligible`): a non-zero `expiresAt` at
|
|
59
|
+
* or before now is expired, and that is checked first because an expired
|
|
60
|
+
* request can only be cancelled; otherwise it is claimable from `eligibleAt`.
|
|
61
|
+
*
|
|
62
|
+
* @param request - Decoded request; `eligibleAt` / `expiresAt` in Unix seconds.
|
|
63
|
+
* @param nowSeconds - Unix seconds. Wall-clock time stands in for the chain's
|
|
64
|
+
* clock, so a request within seconds of a boundary may be classified a slot
|
|
65
|
+
* early or late.
|
|
66
|
+
*/
|
|
67
|
+
function classifyWithdrawalRequest(request, nowSeconds) {
|
|
68
|
+
const expiresAt = BigInt(request.expiresAt.toString());
|
|
69
|
+
const now = BigInt(nowSeconds);
|
|
70
|
+
if (expiresAt !== 0n && now >= expiresAt) {
|
|
71
|
+
return vaults_1.VaultRedemptionStatus.EXPIRED;
|
|
72
|
+
}
|
|
73
|
+
if (now >= BigInt(request.eligibleAt.toString())) {
|
|
74
|
+
return vaults_1.VaultRedemptionStatus.READY_TO_CLAIM;
|
|
75
|
+
}
|
|
76
|
+
return vaults_1.VaultRedemptionStatus.AWAITING_COOLDOWN;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* What finalizing `shares` would pay the recipient at the vault's current
|
|
80
|
+
* state: the offset-aware `assets_for_redeem`, less the ceil-rounded
|
|
81
|
+
* withdrawal fee. Finalize prices at execution, so this is an estimate.
|
|
82
|
+
*
|
|
83
|
+
* @returns Raw deposit-mint units, net of fee.
|
|
84
|
+
* @throws AugustValidationError when the vault has no share price.
|
|
85
|
+
*/
|
|
86
|
+
function estimateRequestPayout(input) {
|
|
87
|
+
const { vaultState, supply, shares } = input;
|
|
88
|
+
const gross = (0, pricing_1.assetsForRedeem)({
|
|
89
|
+
supply,
|
|
90
|
+
totalAssets: (0, pricing_1.totalAssetsOf)(vaultState),
|
|
91
|
+
shares,
|
|
92
|
+
offset: (0, pricing_1.resolveShareOffset)(vaultState.shareOffset),
|
|
93
|
+
});
|
|
94
|
+
return (gross -
|
|
95
|
+
(0, pricing_1.withdrawalFee)(gross, (0, pricing_1.toBigInt)(vaultState.withdrawalFee, 'withdrawalFee')));
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Whether the vault's queue account exists, remembering a yes. A queue is never
|
|
99
|
+
* closed once created (release keeps it so the vault can re-attach), so a
|
|
100
|
+
* positive answer is safe to cache; a negative one is not, and costs one
|
|
101
|
+
* `getAccountInfo` each time.
|
|
102
|
+
*/
|
|
103
|
+
async function queueExists(connection, queue) {
|
|
104
|
+
const cacheKey = `withdrawalQueueExists-${queue.toBase58()}`;
|
|
105
|
+
if (await (0, cache_1.getSafeCache)(cacheKey))
|
|
106
|
+
return true;
|
|
107
|
+
const exists = (await connection.getAccountInfo(queue)) !== null;
|
|
108
|
+
if (exists)
|
|
109
|
+
await (0, cache_1.setSafeCache)(cacheKey, true);
|
|
110
|
+
return exists;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The wallet a request pays: the owner (authority) of its payout token account.
|
|
114
|
+
* A request may pay someone other than its requester, so this is read from the
|
|
115
|
+
* account rather than assumed. If the account is gone or is not a token
|
|
116
|
+
* account, its address is the best identification there is, and is returned
|
|
117
|
+
* as is; finalizing such a request would fail until the account is restored.
|
|
118
|
+
*/
|
|
119
|
+
function payoutWallet(tokenAccount, info) {
|
|
120
|
+
if (!info)
|
|
121
|
+
return tokenAccount.toBase58();
|
|
122
|
+
try {
|
|
123
|
+
return (0, spl_token_1.unpackAccount)(tokenAccount, info, info.owner).owner.toBase58();
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return tokenAccount.toBase58();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The withdrawal queue program id a Solana service names, or `undefined` when
|
|
131
|
+
* it names none: the queue is not deployed on its network (mainnet-beta until
|
|
132
|
+
* its upgrade), or the service has neither `network` nor `getProgramId`. Read
|
|
133
|
+
* paths treat that as "no queue" rather than a failed read, so they make no RPC
|
|
134
|
+
* call and log nothing.
|
|
135
|
+
*
|
|
136
|
+
* Prefers `network`, read straight from `programIds`: `getProgramId` is an
|
|
137
|
+
* instrumented public method, and calling it where it throws by design would
|
|
138
|
+
* record an error breadcrumb on every vault read.
|
|
139
|
+
*
|
|
140
|
+
* @param service - The SolanaAdapter (or a stand-in) the read was given.
|
|
141
|
+
* @returns The queue program id, or `undefined`.
|
|
142
|
+
* @throws When the service names a malformed id; callers read inside their
|
|
143
|
+
* own `try`, so that is logged as a failed queue read.
|
|
144
|
+
*/
|
|
145
|
+
function resolveQueueProgramId(service) {
|
|
146
|
+
if (service.network) {
|
|
147
|
+
const id = (0, constants_1.getDeployedProgramIds)(service.network)?.withdrawalQueue;
|
|
148
|
+
return id ? new web3_js_1.PublicKey(id) : undefined;
|
|
149
|
+
}
|
|
150
|
+
if (!service.getProgramId)
|
|
151
|
+
return undefined;
|
|
152
|
+
try {
|
|
153
|
+
return new web3_js_1.PublicKey(service.getProgramId('withdrawalQueue'));
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (error instanceof core_1.AugustValidationError)
|
|
157
|
+
return undefined;
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The vault's withdrawal queue summary for `IVault.withdrawalQueue`, or
|
|
163
|
+
* `undefined` when the vault has no queue.
|
|
164
|
+
*
|
|
165
|
+
* `enabled` is whether the vault's redeem gate points at this queue, read from
|
|
166
|
+
* the vault state in the same call as the queue, so an attach or release shows
|
|
167
|
+
* at once rather than when a cached vault state expires. A queue that was
|
|
168
|
+
* released keeps its account and may still hold pending requests, so it is
|
|
169
|
+
* reported with `enabled: false`.
|
|
170
|
+
*
|
|
171
|
+
* RPC: one `getMultipleAccountsInfo` (the queue and the vault state). Not
|
|
172
|
+
* cached: the counters change with every request.
|
|
173
|
+
*
|
|
174
|
+
* @param input - The vault and where to read.
|
|
175
|
+
*/
|
|
176
|
+
async function getSolanaWithdrawalQueueSummary(input) {
|
|
177
|
+
const queue = deriveWithdrawalQueuePda(input.vaultState, input.queueProgramId);
|
|
178
|
+
const [queueInfo, vaultStateInfo] = await input.connection.getMultipleAccountsInfo([queue, input.vaultState]);
|
|
179
|
+
if (!queueInfo || !vaultStateInfo)
|
|
180
|
+
return undefined;
|
|
181
|
+
// Decoded by name, so an account of another type at either address throws
|
|
182
|
+
// rather than being read as this one.
|
|
183
|
+
const state = queueProgram(input.connection, input.queueProgramId).coder.accounts.decode('withdrawalQueue', queueInfo.data);
|
|
184
|
+
const gate = vaultProgram(input.connection).coder.accounts.decode('vaultState', vaultStateInfo.data).withdrawalQueueAuthority;
|
|
185
|
+
return {
|
|
186
|
+
address: queue.toBase58(),
|
|
187
|
+
enabled: gate ? new web3_js_1.PublicKey(gate).equals(queue) : false,
|
|
188
|
+
cooldownSeconds: state.cooldownSeconds.toNumber(),
|
|
189
|
+
fulfillmentWindowSeconds: state.fulfillmentWindowSeconds.toNumber(),
|
|
190
|
+
pendingCount: state.pendingRequests.toNumber(),
|
|
191
|
+
pendingShares: (0, core_1.toNormalizedBn)(BigInt(state.pendingShares.toString()), input.shareDecimals),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* `owner`'s pending requests on a vault's queue, split the way the EVM path
|
|
196
|
+
* splits them: `availableRedemptions` holds requests still waiting
|
|
197
|
+
* (`AWAITING_COOLDOWN`) or past their window (`EXPIRED`, cancel-only);
|
|
198
|
+
* `pendingRedemptions` holds requests finalizable now (`READY_TO_CLAIM`).
|
|
199
|
+
*
|
|
200
|
+
* Each entry's `amount` is the escrowed shares and `estimatedAssets` the net
|
|
201
|
+
* payout at the current share price (see {@link estimateRequestPayout}),
|
|
202
|
+
* priced from the vault state and share supply of a single slot, never from a
|
|
203
|
+
* cached state. `receiver` is the wallet that owns the request's payout token
|
|
204
|
+
* account, which need not be the requester; if that account is gone it is the
|
|
205
|
+
* account address itself. `id` and `hash` are the request account's address:
|
|
206
|
+
* the program keeps no creation transaction to point at. `date` and
|
|
207
|
+
* `day` / `month` / `year` are the eligibility time (UTC); `timestamp` is when
|
|
208
|
+
* the request was made.
|
|
209
|
+
*
|
|
210
|
+
* RPC, worst case: one `getAccountInfo` (skipped once the queue is known to
|
|
211
|
+
* exist), one `getProgramAccounts` filtered on queue and owner, and, when the
|
|
212
|
+
* owner has any request, one `getMultipleAccountsInfo` carrying the vault
|
|
213
|
+
* state, share mint and up to 98 distinct payout accounts. Each further 100
|
|
214
|
+
* distinct payout accounts cost one more `getMultipleAccountsInfo`, at most
|
|
215
|
+
* four in flight. A vault without a queue costs the single `getAccountInfo`.
|
|
216
|
+
*
|
|
217
|
+
* @param input - The vault, the owner, and where to read.
|
|
218
|
+
*/
|
|
219
|
+
async function getSolanaWithdrawalRedemptions(input) {
|
|
220
|
+
const availableRedemptions = [];
|
|
221
|
+
const pendingRedemptions = [];
|
|
222
|
+
const queue = deriveWithdrawalQueuePda(input.vaultStateAddress, input.queueProgramId);
|
|
223
|
+
if (!(await queueExists(input.connection, queue))) {
|
|
224
|
+
return { availableRedemptions, pendingRedemptions };
|
|
225
|
+
}
|
|
226
|
+
const requests = await queueProgram(input.connection, input.queueProgramId).account.withdrawalRequest.all([
|
|
227
|
+
{ memcmp: { offset: REQUEST_QUEUE_OFFSET, bytes: queue.toBase58() } },
|
|
228
|
+
{ memcmp: { offset: REQUEST_OWNER_OFFSET, bytes: input.owner.toBase58() } },
|
|
229
|
+
]);
|
|
230
|
+
if (requests.length === 0) {
|
|
231
|
+
return { availableRedemptions, pendingRedemptions };
|
|
232
|
+
}
|
|
233
|
+
// Payout accounts, deduplicated: requests often share one. The first batch
|
|
234
|
+
// rides with the vault state and share mint, which must share a slot to
|
|
235
|
+
// price coherently; the rest only name receivers, so they need not.
|
|
236
|
+
const payoutAccounts = [
|
|
237
|
+
...new Map(requests.map(({ account }) => [
|
|
238
|
+
account.recipientTokenAccount.toBase58(),
|
|
239
|
+
account.recipientTokenAccount,
|
|
240
|
+
])).values(),
|
|
241
|
+
];
|
|
242
|
+
const firstBatch = payoutAccounts.slice(0, MAX_ACCOUNTS_PER_CALL - 2);
|
|
243
|
+
const laterBatches = [];
|
|
244
|
+
for (let i = firstBatch.length; i < payoutAccounts.length; i += MAX_ACCOUNTS_PER_CALL) {
|
|
245
|
+
laterBatches.push(payoutAccounts.slice(i, i + MAX_ACCOUNTS_PER_CALL));
|
|
246
|
+
}
|
|
247
|
+
const [snapshot, laterInfos] = await Promise.all([
|
|
248
|
+
(0, token_reads_1.readPricingSnapshot)({
|
|
249
|
+
connection: input.connection,
|
|
250
|
+
program: vaultProgram(input.connection),
|
|
251
|
+
vaultStatePda: input.vaultStateAddress,
|
|
252
|
+
shareMintAddr: input.shareMint,
|
|
253
|
+
extraAccounts: firstBatch,
|
|
254
|
+
}),
|
|
255
|
+
(0, concurrency_1.mapWithConcurrency)(laterBatches, PAYOUT_BATCH_CONCURRENCY, (batch) => input.connection.getMultipleAccountsInfo(batch)),
|
|
256
|
+
]);
|
|
257
|
+
const { vaultStateData, supply, shareDecimals } = snapshot;
|
|
258
|
+
const payoutInfos = [...snapshot.extraAccountInfos, ...laterInfos.flat()];
|
|
259
|
+
const payoutInfoOf = new Map(payoutAccounts.map((address, i) => [address.toBase58(), payoutInfos[i]]));
|
|
260
|
+
const now = input.nowSeconds ?? Math.floor(Date.now() / 1000);
|
|
261
|
+
for (const { publicKey, account } of requests) {
|
|
262
|
+
const shares = BigInt(account.shares.toString());
|
|
263
|
+
const eligibleAt = account.eligibleAt.toNumber();
|
|
264
|
+
const date = new Date(eligibleAt * 1000);
|
|
265
|
+
const status = classifyWithdrawalRequest(account, now);
|
|
266
|
+
let estimatedAssets;
|
|
267
|
+
try {
|
|
268
|
+
estimatedAssets = (0, core_1.toNormalizedBn)(estimateRequestPayout({ vaultState: vaultStateData, supply, shares }), input.depositDecimals);
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
// A vault with no share price (assets zero against outstanding shares)
|
|
272
|
+
// has nothing to quote; the request itself is still listed.
|
|
273
|
+
core_1.Logger.log.warn('getSolanaWithdrawalRedemptions', 'No payout estimate', {
|
|
274
|
+
request: publicKey.toBase58(),
|
|
275
|
+
error,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
const entry = {
|
|
279
|
+
id: publicKey.toBase58(),
|
|
280
|
+
hash: publicKey.toBase58(),
|
|
281
|
+
timestamp: account.requestedAt.toNumber(),
|
|
282
|
+
receiver: payoutWallet(account.recipientTokenAccount, payoutInfoOf.get(account.recipientTokenAccount.toBase58()) ?? null),
|
|
283
|
+
day: (0, core_1.toNormalizedBn)(date.getUTCDate(), 0),
|
|
284
|
+
month: (0, core_1.toNormalizedBn)(date.getUTCMonth() + 1, 0),
|
|
285
|
+
year: (0, core_1.toNormalizedBn)(date.getUTCFullYear(), 0),
|
|
286
|
+
amount: (0, core_1.toNormalizedBn)(shares, shareDecimals),
|
|
287
|
+
date,
|
|
288
|
+
vault: input.vault,
|
|
289
|
+
status,
|
|
290
|
+
...(estimatedAssets && { estimatedAssets }),
|
|
291
|
+
};
|
|
292
|
+
if (status === vaults_1.VaultRedemptionStatus.READY_TO_CLAIM) {
|
|
293
|
+
pendingRedemptions.push(entry);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
availableRedemptions.push(entry);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return { availableRedemptions, pendingRedemptions };
|
|
300
|
+
}
|
|
301
|
+
//# sourceMappingURL=withdrawal-queue.js.map
|
|
@@ -141,6 +141,8 @@ exports.METHOD_CATEGORIES = {
|
|
|
141
141
|
vaultRedeem: 'write.redeem',
|
|
142
142
|
claimWithdrawal: 'write.redeem',
|
|
143
143
|
vaultRequestRedeem: 'write.redeem',
|
|
144
|
+
// Solana withdrawal queue: cancelling a request is part of the redeem flow.
|
|
145
|
+
vaultCancelRedeemRequest: 'write.redeem',
|
|
144
146
|
rwaRedeemAsset: 'write.redeem',
|
|
145
147
|
vaultApprove: 'write.approve',
|
|
146
148
|
approve: 'write.approve',
|
package/lib/main.d.ts
CHANGED
|
@@ -206,8 +206,24 @@ export declare class AugustSDK extends AugustBase {
|
|
|
206
206
|
}): Promise<number>;
|
|
207
207
|
/**
|
|
208
208
|
* Get redemption requests that are ready to be claimed.
|
|
209
|
+
*
|
|
210
|
+
* For a Solana vault, `wallet` is the holder's base58 address and the result
|
|
211
|
+
* lists their withdrawal-queue requests: waiting (`AWAITING_COOLDOWN`) and
|
|
212
|
+
* past-window (`EXPIRED`, cancel-only) ones in `availableRedemptions`, and
|
|
213
|
+
* claimable ones (`READY_TO_CLAIM`) in `pendingRedemptions`, each with
|
|
214
|
+
* `estimatedAssets`, the net payout at the current share price. Without a
|
|
215
|
+
* Solana wallet, or for a vault with no queue, both lists are empty. Solana
|
|
216
|
+
* RPC: the vault state, one `getAccountInfo` for the queue, and, when it
|
|
217
|
+
* exists, one `getProgramAccounts` and one `getMultipleAccountsInfo` (the
|
|
218
|
+
* vault state, share mint and payout accounts, read together).
|
|
219
|
+
*
|
|
209
220
|
* @param props - Vault address, chain ID, and optional wallet filter
|
|
210
221
|
* @returns Array of available redemption requests with amounts and timestamps
|
|
222
|
+
* @example
|
|
223
|
+
* ```ts
|
|
224
|
+
* const { availableRedemptions, pendingRedemptions } =
|
|
225
|
+
* await sdk.getVaultAvailableRedemptions({ vault: solanaVault, wallet: holder });
|
|
226
|
+
* ```
|
|
211
227
|
*/
|
|
212
228
|
getVaultAvailableRedemptions(props: {
|
|
213
229
|
vault: IAddress;
|