@ember-finance/sdk 2.38.0 → 2.39.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.
@@ -0,0 +1,190 @@
1
+ import { type Connection, PublicKey } from "@solana/web3.js";
2
+ import { type ISolanaProtocolConfigState, type ISolanaVaultAccount, type ISolanaVaultPdas, type ISolanaVaultState, type ISolanaWithdrawalRequestAccount } from "../interfaces/index.js";
3
+ import { type IWithdrawalFeeBreakdown } from "../utils/math.js";
4
+ import { SolanaDeploymentParser } from "../utils/deployment-parser.js";
5
+ /**
6
+ * Reads vault state from the chain.
7
+ *
8
+ * The quote methods answer locally from decoded state rather than simulating
9
+ * the program's `quote.rs` view instructions. A simulation costs a round trip
10
+ * each and the public RPCs throttle hard, so a page rendering a dozen quotes
11
+ * would stall; the arithmetic is transcribed in `utils/math.ts`, rounding
12
+ * included, and pinned against the program's own values in the tests.
13
+ *
14
+ * Everything else decodes accounts directly. Solana has no view functions in
15
+ * the EVM sense, so there is nothing cheaper to call.
16
+ */
17
+ export declare class SolanaVaultReader {
18
+ private readonly connection;
19
+ private parser;
20
+ constructor(connection: Connection, parser: SolanaDeploymentParser);
21
+ /** Swap in a newer deployment without rebuilding the reader. */
22
+ updateDeployment(parser: SolanaDeploymentParser): void;
23
+ get programId(): PublicKey;
24
+ private key;
25
+ /** The protocol config, or null when the protocol is not initialised. */
26
+ getProtocolConfig(): Promise<ISolanaProtocolConfigState | null>;
27
+ isProtocolPaused(): Promise<boolean>;
28
+ /** The vault account, by share mint. Throws if it does not exist. */
29
+ getVaultState(shareMint: PublicKey | string): Promise<ISolanaVaultState>;
30
+ /**
31
+ * Delegated to the parser rather than derived here, so the manifest's
32
+ * recorded vault address is still checked against what this program id
33
+ * produces. Deriving directly would skip that and go on to read a
34
+ * different deployment, surfacing as a confusing missing-vault error.
35
+ */
36
+ getVaultPdas(shareMint: PublicKey | string): ISolanaVaultPdas;
37
+ /** Every vault the deployment knows about, keyed by share mint. */
38
+ getAllVaultAddresses(): string[];
39
+ getVaultRoles(shareMint: PublicKey | string): Promise<{
40
+ admin: PublicKey;
41
+ operator: PublicKey;
42
+ rateManager: PublicKey | null;
43
+ subAccounts: PublicKey[];
44
+ }>;
45
+ /**
46
+ * The four pause bits. `vault` is the whole-vault switch and ORs with each
47
+ * per-operation bit, so a caller checking only the specific one would miss
48
+ * a globally paused vault.
49
+ */
50
+ getVaultPauseStatus(shareMint: PublicKey | string): Promise<{
51
+ vault: boolean;
52
+ deposits: boolean;
53
+ withdrawals: boolean;
54
+ privileged: boolean;
55
+ }>;
56
+ isBlacklisted(shareMint: PublicKey | string, account: PublicKey | string): Promise<boolean>;
57
+ isSubAccount(shareMint: PublicKey | string, account: PublicKey | string): Promise<boolean>;
58
+ /** Share supply, read from the mint rather than tracked in the vault. */
59
+ getTotalSupply(shareMint: PublicKey | string): Promise<bigint>;
60
+ /** `total_assets`: TVL at the current rate. */
61
+ getTotalAssets(shareMint: PublicKey | string): Promise<bigint>;
62
+ /**
63
+ * What the custody account actually holds, against what the vault believes.
64
+ *
65
+ * Solvent means custody covers what the vault owes, not that the two are
66
+ * equal. Anyone can transfer into the custody account, and an unsolicited
67
+ * single unit would otherwise make a fully backed vault read as
68
+ * insolvent. `drift` carries the difference: positive is a donation or a
69
+ * stuck transfer, negative is a real shortfall.
70
+ */
71
+ getVaultBalances(shareMint: PublicKey | string): Promise<{
72
+ recorded: bigint;
73
+ custody: bigint;
74
+ solvent: boolean;
75
+ drift: bigint;
76
+ }>;
77
+ /**
78
+ * The token program that owns `mint`.
79
+ *
80
+ * Resolved from the mint account itself, not assumed. Associated token
81
+ * addresses derive differently under SPL Token and Token-2022, and
82
+ * `create_vault` takes the two token programs separately, so a vault's
83
+ * collateral and shares need not share one. Guessing wrong derives an
84
+ * address nothing lives at and reports a holder as owning zero.
85
+ *
86
+ * The manifest is consulted first where it has an answer, since that
87
+ * saves a round trip; it only covers collateral mints, and only those it
88
+ * happens to list.
89
+ */
90
+ private resolveTokenProgram;
91
+ /** An owner's share balance. Zero when they hold no token account. */
92
+ balanceOf(shareMint: PublicKey | string, owner: PublicKey | string): Promise<bigint>;
93
+ /** An owner's collateral balance for this vault's deposit token. */
94
+ getUnderlyingTokenBalance(shareMint: PublicKey | string, owner: PublicKey | string): Promise<bigint>;
95
+ private tokenBalance;
96
+ convertToShares(shareMint: PublicKey | string, assets: bigint): Promise<bigint>;
97
+ convertToAssets(shareMint: PublicKey | string, shares: bigint): Promise<bigint>;
98
+ /** Shares minted for `assets`. Floors, so a deposit never over-issues. */
99
+ previewDeposit(shareMint: PublicKey | string, assets: bigint): Promise<bigint>;
100
+ /** Collateral needed to mint `shares`. Ceils, so the vault is never short. */
101
+ previewMint(shareMint: PublicKey | string, shares: bigint): Promise<bigint>;
102
+ /**
103
+ * Collateral for `shares` at the current rate, **before withdrawal fees**.
104
+ *
105
+ * This matches the program's `preview_redeem`, which is also gross — but
106
+ * settlement subtracts the permanent and time-based fees before paying
107
+ * out, so on a fee-bearing vault the holder receives less than this.
108
+ * {@link previewRedeemNet} is what they actually get.
109
+ */
110
+ previewRedeem(shareMint: PublicKey | string, shares: bigint): Promise<bigint>;
111
+ /**
112
+ * What a holder actually receives for `shares`, fees deducted.
113
+ *
114
+ * `nowMs` decides whether the time-based fee applies, since it is charged
115
+ * only inside `timeBasedFeeThreshold` of the holder's last deposit.
116
+ * Defaults to local time, which is close enough except right at the
117
+ * boundary; pass the chain's clock when that matters.
118
+ */
119
+ previewRedeemNet(shareMint: PublicKey | string, shares: bigint, owner: PublicKey | string, nowMs?: bigint): Promise<IWithdrawalFeeBreakdown>;
120
+ /** The vault's configured withdrawal fees, and whether `owner` is exempt. */
121
+ getWithdrawalFee(shareMint: PublicKey | string, owner?: PublicKey | string): Promise<{
122
+ permanentFeePercentage: bigint;
123
+ timeBasedFeePercentage: bigint;
124
+ timeBasedFeeThreshold: bigint;
125
+ feeExempt: PublicKey[];
126
+ exempt: boolean;
127
+ }>;
128
+ /** Shares to give up for `assets`. Ceils, so the quote never understates. */
129
+ previewWithdraw(shareMint: PublicKey | string, assets: bigint): Promise<bigint>;
130
+ maxDeposit(shareMint: PublicKey | string, owner: PublicKey | string): Promise<bigint>;
131
+ maxMint(shareMint: PublicKey | string, owner: PublicKey | string): Promise<bigint>;
132
+ maxRedeem(shareMint: PublicKey | string, owner: PublicKey | string): Promise<bigint>;
133
+ /**
134
+ * Gross, matching the program's `max_withdraw`. Settlement deducts the
135
+ * withdrawal fees from this; see {@link previewRedeemNet}.
136
+ */
137
+ maxWithdraw(shareMint: PublicKey | string, owner: PublicKey | string): Promise<bigint>;
138
+ /** `quote.rs::can_deposit`. */
139
+ private canDeposit;
140
+ /** `quote.rs::can_withdraw`. */
141
+ private canWithdraw;
142
+ /** An owner's vault account, or null before their first deposit or redeem. */
143
+ getAccountState(shareMint: PublicKey | string, owner: PublicKey | string): Promise<ISolanaVaultAccount | null>;
144
+ /** Queue head and tail. Equal means drained. */
145
+ getWithdrawalQueue(shareMint: PublicKey | string): Promise<{
146
+ head: bigint;
147
+ tail: bigint;
148
+ }>;
149
+ /**
150
+ * Every open request, in queue order.
151
+ *
152
+ * Request addresses are keyed by owner and nonce, so the queue cannot be
153
+ * derived — this enumerates the program's accounts. That is a
154
+ * `getProgramAccounts` call, which public RPCs throttle hard; prefer
155
+ * {@link getAccountState} when you only need one owner's requests.
156
+ */
157
+ getAllPendingWithdrawals(shareMint: PublicKey | string): Promise<ISolanaWithdrawalRequestAccount[]>;
158
+ getPendingWithdrawalsCount(shareMint: PublicKey | string): Promise<number>;
159
+ /**
160
+ * Everything a UI needs for one holder, in one pass.
161
+ *
162
+ * Batched rather than composed from the methods above: each of those
163
+ * re-reads the vault, which is four extra round trips on an endpoint that
164
+ * throttles.
165
+ */
166
+ getUserPosition(shareMint: PublicKey | string, owner: PublicKey | string): Promise<{
167
+ shares: bigint;
168
+ assets: bigint;
169
+ collateralBalance: bigint;
170
+ pendingWithdrawalShares: bigint;
171
+ pendingRequests: number;
172
+ lastDepositTs: bigint;
173
+ }>;
174
+ /** Vault state, supply, TVL and solvency in one pass, for a vault page. */
175
+ getCompleteVaultInfo(shareMint: PublicKey | string): Promise<{
176
+ state: ISolanaVaultState;
177
+ totalSupply: bigint;
178
+ totalAssets: bigint;
179
+ custody: bigint;
180
+ solvent: boolean;
181
+ pendingWithdrawals: number;
182
+ }>;
183
+ /** The rate, with what a single share is worth at it. */
184
+ getVaultRate(shareMint: PublicKey | string): Promise<{
185
+ rate: bigint;
186
+ maxChangePerUpdate: bigint;
187
+ updateInterval: bigint;
188
+ lastUpdatedAt: bigint;
189
+ }>;
190
+ }
@@ -0,0 +1,432 @@
1
+ import { PublicKey } from "@solana/web3.js";
2
+ import { decodeMint, decodeProtocolConfig, decodeVault, decodeVaultAccount, decodeWithdrawalQueue, decodeWithdrawalRequestAccount } from "../utils/borsh.js";
3
+ import { accountDiscriminator, associatedTokenAddress, deriveVaultAccount } from "../utils/pdas.js";
4
+ import { amountFromShares, amountFromSharesFloor, maxDepositAmount, maxMintShares, sharesFromAmount, sharesFromAmountCeil, vaultTvl, withdrawalFee } from "../utils/math.js";
5
+ /**
6
+ * Reads vault state from the chain.
7
+ *
8
+ * The quote methods answer locally from decoded state rather than simulating
9
+ * the program's `quote.rs` view instructions. A simulation costs a round trip
10
+ * each and the public RPCs throttle hard, so a page rendering a dozen quotes
11
+ * would stall; the arithmetic is transcribed in `utils/math.ts`, rounding
12
+ * included, and pinned against the program's own values in the tests.
13
+ *
14
+ * Everything else decodes accounts directly. Solana has no view functions in
15
+ * the EVM sense, so there is nothing cheaper to call.
16
+ */
17
+ export class SolanaVaultReader {
18
+ constructor(connection, parser) {
19
+ this.connection = connection;
20
+ this.parser = parser;
21
+ }
22
+ /** Swap in a newer deployment without rebuilding the reader. */
23
+ updateDeployment(parser) {
24
+ this.parser = parser;
25
+ }
26
+ get programId() {
27
+ return this.parser.programId;
28
+ }
29
+ key(value) {
30
+ return typeof value === "string" ? new PublicKey(value) : value;
31
+ }
32
+ // --- raw state ---
33
+ /** The protocol config, or null when the protocol is not initialised. */
34
+ async getProtocolConfig() {
35
+ const account = await this.connection.getAccountInfo(this.parser.getProtocolConfigAddress());
36
+ return account === null ? null : decodeProtocolConfig(account.data);
37
+ }
38
+ async isProtocolPaused() {
39
+ const config = await this.getProtocolConfig();
40
+ return config?.pauseNonAdminOperations ?? false;
41
+ }
42
+ /** The vault account, by share mint. Throws if it does not exist. */
43
+ async getVaultState(shareMint) {
44
+ const { vault } = this.getVaultPdas(shareMint);
45
+ const account = await this.connection.getAccountInfo(vault);
46
+ if (account === null) {
47
+ throw new Error(`no vault at ${vault.toBase58()}`);
48
+ }
49
+ return decodeVault(account.data);
50
+ }
51
+ /**
52
+ * Delegated to the parser rather than derived here, so the manifest's
53
+ * recorded vault address is still checked against what this program id
54
+ * produces. Deriving directly would skip that and go on to read a
55
+ * different deployment, surfacing as a confusing missing-vault error.
56
+ */
57
+ getVaultPdas(shareMint) {
58
+ return this.parser.getVaultPdas(this.key(shareMint));
59
+ }
60
+ /** Every vault the deployment knows about, keyed by share mint. */
61
+ getAllVaultAddresses() {
62
+ return Object.keys(this.parser.getVaults());
63
+ }
64
+ async getVaultRoles(shareMint) {
65
+ const vault = await this.getVaultState(shareMint);
66
+ return {
67
+ admin: vault.admin,
68
+ operator: vault.operator,
69
+ // The program stores "unset" as the default pubkey; null reads better.
70
+ rateManager: vault.rateManager.equals(PublicKey.default)
71
+ ? null
72
+ : vault.rateManager,
73
+ subAccounts: vault.subAccounts
74
+ };
75
+ }
76
+ /**
77
+ * The four pause bits. `vault` is the whole-vault switch and ORs with each
78
+ * per-operation bit, so a caller checking only the specific one would miss
79
+ * a globally paused vault.
80
+ */
81
+ async getVaultPauseStatus(shareMint) {
82
+ const v = await this.getVaultState(shareMint);
83
+ return {
84
+ vault: v.paused,
85
+ deposits: v.paused || v.pausedDeposits,
86
+ withdrawals: v.paused || v.pausedWithdrawals,
87
+ privileged: v.paused || v.pausedPrivileged
88
+ };
89
+ }
90
+ async isBlacklisted(shareMint, account) {
91
+ const vault = await this.getVaultState(shareMint);
92
+ const who = this.key(account);
93
+ return vault.blacklisted.some(a => a.equals(who));
94
+ }
95
+ async isSubAccount(shareMint, account) {
96
+ const vault = await this.getVaultState(shareMint);
97
+ const who = this.key(account);
98
+ return vault.subAccounts.some(a => a.equals(who));
99
+ }
100
+ // --- supply and balances ---
101
+ /** Share supply, read from the mint rather than tracked in the vault. */
102
+ async getTotalSupply(shareMint) {
103
+ const account = await this.connection.getAccountInfo(this.key(shareMint));
104
+ if (account === null)
105
+ throw new Error("share mint not found");
106
+ return decodeMint(account.data).supply;
107
+ }
108
+ /** `total_assets`: TVL at the current rate. */
109
+ async getTotalAssets(shareMint) {
110
+ const [vault, supply] = await Promise.all([
111
+ this.getVaultState(shareMint),
112
+ this.getTotalSupply(shareMint)
113
+ ]);
114
+ return vaultTvl(supply, vault.rate.value);
115
+ }
116
+ /**
117
+ * What the custody account actually holds, against what the vault believes.
118
+ *
119
+ * Solvent means custody covers what the vault owes, not that the two are
120
+ * equal. Anyone can transfer into the custody account, and an unsolicited
121
+ * single unit would otherwise make a fully backed vault read as
122
+ * insolvent. `drift` carries the difference: positive is a donation or a
123
+ * stuck transfer, negative is a real shortfall.
124
+ */
125
+ async getVaultBalances(shareMint) {
126
+ const pdas = this.getVaultPdas(shareMint);
127
+ const [vault, custody] = await Promise.all([
128
+ this.getVaultState(shareMint),
129
+ this.connection.getTokenAccountBalance(pdas.vaultCollateral)
130
+ ]);
131
+ const held = BigInt(custody.value.amount);
132
+ return {
133
+ recorded: vault.balance,
134
+ custody: held,
135
+ solvent: held >= vault.balance,
136
+ drift: held - vault.balance
137
+ };
138
+ }
139
+ /**
140
+ * The token program that owns `mint`.
141
+ *
142
+ * Resolved from the mint account itself, not assumed. Associated token
143
+ * addresses derive differently under SPL Token and Token-2022, and
144
+ * `create_vault` takes the two token programs separately, so a vault's
145
+ * collateral and shares need not share one. Guessing wrong derives an
146
+ * address nothing lives at and reports a holder as owning zero.
147
+ *
148
+ * The manifest is consulted first where it has an answer, since that
149
+ * saves a round trip; it only covers collateral mints, and only those it
150
+ * happens to list.
151
+ */
152
+ async resolveTokenProgram(mint) {
153
+ const recorded = this.parser.getTokenProgram(mint);
154
+ if (recorded)
155
+ return recorded;
156
+ const account = await this.connection.getAccountInfo(mint);
157
+ if (account === null)
158
+ throw new Error(`mint ${mint.toBase58()} not found`);
159
+ return account.owner;
160
+ }
161
+ /** An owner's share balance. Zero when they hold no token account. */
162
+ async balanceOf(shareMint, owner) {
163
+ const mint = this.key(shareMint);
164
+ return this.tokenBalance(mint, this.key(owner), await this.resolveTokenProgram(mint));
165
+ }
166
+ /** An owner's collateral balance for this vault's deposit token. */
167
+ async getUnderlyingTokenBalance(shareMint, owner) {
168
+ const mint = this.parser.getCollateralMint(shareMint);
169
+ return this.tokenBalance(mint, this.key(owner), await this.resolveTokenProgram(mint));
170
+ }
171
+ async tokenBalance(mint, owner, tokenProgram) {
172
+ const ata = associatedTokenAddress(owner, mint, tokenProgram);
173
+ const account = await this.connection.getAccountInfo(ata);
174
+ // No token account is not an error: it is how a wallet that has never
175
+ // held this token looks, and the answer is zero.
176
+ if (account === null)
177
+ return 0n;
178
+ return account.data.readBigUInt64LE(64);
179
+ }
180
+ // --- quotes, mirroring quote.rs ---
181
+ async convertToShares(shareMint, assets) {
182
+ return sharesFromAmount(assets, (await this.getVaultState(shareMint)).rate.value);
183
+ }
184
+ async convertToAssets(shareMint, shares) {
185
+ return amountFromSharesFloor(shares, (await this.getVaultState(shareMint)).rate.value);
186
+ }
187
+ /** Shares minted for `assets`. Floors, so a deposit never over-issues. */
188
+ async previewDeposit(shareMint, assets) {
189
+ return this.convertToShares(shareMint, assets);
190
+ }
191
+ /** Collateral needed to mint `shares`. Ceils, so the vault is never short. */
192
+ async previewMint(shareMint, shares) {
193
+ return amountFromShares(shares, (await this.getVaultState(shareMint)).rate.value);
194
+ }
195
+ /**
196
+ * Collateral for `shares` at the current rate, **before withdrawal fees**.
197
+ *
198
+ * This matches the program's `preview_redeem`, which is also gross — but
199
+ * settlement subtracts the permanent and time-based fees before paying
200
+ * out, so on a fee-bearing vault the holder receives less than this.
201
+ * {@link previewRedeemNet} is what they actually get.
202
+ */
203
+ async previewRedeem(shareMint, shares) {
204
+ return this.convertToAssets(shareMint, shares);
205
+ }
206
+ /**
207
+ * What a holder actually receives for `shares`, fees deducted.
208
+ *
209
+ * `nowMs` decides whether the time-based fee applies, since it is charged
210
+ * only inside `timeBasedFeeThreshold` of the holder's last deposit.
211
+ * Defaults to local time, which is close enough except right at the
212
+ * boundary; pass the chain's clock when that matters.
213
+ */
214
+ async previewRedeemNet(shareMint, shares, owner, nowMs = BigInt(Date.now())) {
215
+ const who = this.key(owner);
216
+ const [vault, account] = await Promise.all([
217
+ this.getVaultState(shareMint),
218
+ this.getAccountState(shareMint, who)
219
+ ]);
220
+ return withdrawalFee(amountFromSharesFloor(shares, vault.rate.value), vault.withdrawalFee, {
221
+ exempt: vault.withdrawalFee.feeExempt.some(a => a.equals(who)),
222
+ lastDepositTs: account?.lastDepositTs ?? 0n,
223
+ nowMs
224
+ });
225
+ }
226
+ /** The vault's configured withdrawal fees, and whether `owner` is exempt. */
227
+ async getWithdrawalFee(shareMint, owner) {
228
+ const vault = await this.getVaultState(shareMint);
229
+ const fee = vault.withdrawalFee;
230
+ const who = owner === undefined ? undefined : this.key(owner);
231
+ return {
232
+ ...fee,
233
+ exempt: who !== undefined && fee.feeExempt.some(a => a.equals(who))
234
+ };
235
+ }
236
+ /** Shares to give up for `assets`. Ceils, so the quote never understates. */
237
+ async previewWithdraw(shareMint, assets) {
238
+ return sharesFromAmountCeil(assets, (await this.getVaultState(shareMint)).rate.value);
239
+ }
240
+ async maxDeposit(shareMint, owner) {
241
+ const [vault, config, supply] = await Promise.all([
242
+ this.getVaultState(shareMint),
243
+ this.getProtocolConfig(),
244
+ this.getTotalSupply(shareMint)
245
+ ]);
246
+ if (!this.canDeposit(vault, config, this.key(owner)))
247
+ return 0n;
248
+ return maxDepositAmount(supply, vault.rate.value, vault.maxTvl);
249
+ }
250
+ async maxMint(shareMint, owner) {
251
+ const [vault, config, supply] = await Promise.all([
252
+ this.getVaultState(shareMint),
253
+ this.getProtocolConfig(),
254
+ this.getTotalSupply(shareMint)
255
+ ]);
256
+ if (!this.canDeposit(vault, config, this.key(owner)))
257
+ return 0n;
258
+ return maxMintShares(supply, vault.rate.value, vault.maxTvl);
259
+ }
260
+ async maxRedeem(shareMint, owner) {
261
+ const [vault, config, balance] = await Promise.all([
262
+ this.getVaultState(shareMint),
263
+ this.getProtocolConfig(),
264
+ this.balanceOf(shareMint, owner)
265
+ ]);
266
+ if (!this.canWithdraw(vault, config, this.key(owner)))
267
+ return 0n;
268
+ // Below the minimum the whole balance is unredeemable, not merely capped.
269
+ return balance < vault.minWithdrawalShares ? 0n : balance;
270
+ }
271
+ /**
272
+ * Gross, matching the program's `max_withdraw`. Settlement deducts the
273
+ * withdrawal fees from this; see {@link previewRedeemNet}.
274
+ */
275
+ async maxWithdraw(shareMint, owner) {
276
+ const [vault, redeemable] = await Promise.all([
277
+ this.getVaultState(shareMint),
278
+ this.maxRedeem(shareMint, owner)
279
+ ]);
280
+ return redeemable === 0n
281
+ ? 0n
282
+ : amountFromSharesFloor(redeemable, vault.rate.value);
283
+ }
284
+ /** `quote.rs::can_deposit`. */
285
+ canDeposit(vault, config, owner) {
286
+ if (config?.pauseNonAdminOperations)
287
+ return false;
288
+ if (vault.paused || vault.pausedDeposits)
289
+ return false;
290
+ if (vault.blacklisted.some(a => a.equals(owner)))
291
+ return false;
292
+ if (vault.subAccounts.some(a => a.equals(owner)))
293
+ return false;
294
+ // A bound parking lot closes the direct path to everyone not listed;
295
+ // with no lot, an empty list is open to all.
296
+ if (!vault.parkingLot.equals(PublicKey.default)) {
297
+ return vault.depositUserList.some(a => a.equals(owner));
298
+ }
299
+ return (vault.depositUserList.length === 0 ||
300
+ vault.depositUserList.some(a => a.equals(owner)));
301
+ }
302
+ /** `quote.rs::can_withdraw`. */
303
+ canWithdraw(vault, config, owner) {
304
+ if (config?.pauseNonAdminOperations)
305
+ return false;
306
+ if (vault.paused || vault.pausedWithdrawals)
307
+ return false;
308
+ if (vault.blacklisted.some(a => a.equals(owner)))
309
+ return false;
310
+ return (vault.withdrawalUserList.length === 0 ||
311
+ vault.withdrawalUserList.some(a => a.equals(owner)));
312
+ }
313
+ // --- withdrawals ---
314
+ /** An owner's vault account, or null before their first deposit or redeem. */
315
+ async getAccountState(shareMint, owner) {
316
+ const { vault } = this.getVaultPdas(shareMint);
317
+ const address = deriveVaultAccount(this.programId, vault, this.key(owner));
318
+ const account = await this.connection.getAccountInfo(address);
319
+ return account === null ? null : decodeVaultAccount(account.data);
320
+ }
321
+ /** Queue head and tail. Equal means drained. */
322
+ async getWithdrawalQueue(shareMint) {
323
+ const { withdrawalQueue } = this.getVaultPdas(shareMint);
324
+ const account = await this.connection.getAccountInfo(withdrawalQueue);
325
+ if (account === null)
326
+ throw new Error("withdrawal queue not found");
327
+ const q = decodeWithdrawalQueue(account.data);
328
+ return { head: q.head, tail: q.tail };
329
+ }
330
+ /**
331
+ * Every open request, in queue order.
332
+ *
333
+ * Request addresses are keyed by owner and nonce, so the queue cannot be
334
+ * derived — this enumerates the program's accounts. That is a
335
+ * `getProgramAccounts` call, which public RPCs throttle hard; prefer
336
+ * {@link getAccountState} when you only need one owner's requests.
337
+ */
338
+ async getAllPendingWithdrawals(shareMint) {
339
+ const { vault } = this.getVaultPdas(shareMint);
340
+ const accounts = await this.connection.getProgramAccounts(this.programId, {
341
+ filters: [
342
+ {
343
+ memcmp: {
344
+ offset: 0,
345
+ bytes: bs58(accountDiscriminator("WithdrawalRequestAccount"))
346
+ }
347
+ },
348
+ // discriminator + bump, then the vault the request belongs to.
349
+ { memcmp: { offset: 9, bytes: vault.toBase58() } }
350
+ ]
351
+ });
352
+ return accounts
353
+ .map(a => decodeWithdrawalRequestAccount(a.pubkey, a.account.data))
354
+ .sort((a, b) => a.queueIndex < b.queueIndex ? -1 : a.queueIndex > b.queueIndex ? 1 : 0);
355
+ }
356
+ async getPendingWithdrawalsCount(shareMint) {
357
+ const { head, tail } = await this.getWithdrawalQueue(shareMint);
358
+ return Number(tail - head);
359
+ }
360
+ // --- composed views ---
361
+ /**
362
+ * Everything a UI needs for one holder, in one pass.
363
+ *
364
+ * Batched rather than composed from the methods above: each of those
365
+ * re-reads the vault, which is four extra round trips on an endpoint that
366
+ * throttles.
367
+ */
368
+ async getUserPosition(shareMint, owner) {
369
+ const who = this.key(owner);
370
+ const [vault, shares, collateralBalance, account] = await Promise.all([
371
+ this.getVaultState(shareMint),
372
+ this.balanceOf(shareMint, who),
373
+ this.getUnderlyingTokenBalance(shareMint, who),
374
+ this.getAccountState(shareMint, who)
375
+ ]);
376
+ return {
377
+ shares,
378
+ assets: amountFromSharesFloor(shares, vault.rate.value),
379
+ collateralBalance,
380
+ pendingWithdrawalShares: account?.totalPendingWithdrawalShares ?? 0n,
381
+ pendingRequests: account?.pendingWithdrawalRequests.length ?? 0,
382
+ lastDepositTs: account?.lastDepositTs ?? 0n
383
+ };
384
+ }
385
+ /** Vault state, supply, TVL and solvency in one pass, for a vault page. */
386
+ async getCompleteVaultInfo(shareMint) {
387
+ const pdas = this.getVaultPdas(shareMint);
388
+ const [state, totalSupply, custody, queue] = await Promise.all([
389
+ this.getVaultState(shareMint),
390
+ this.getTotalSupply(shareMint),
391
+ this.connection.getTokenAccountBalance(pdas.vaultCollateral),
392
+ this.getWithdrawalQueue(shareMint)
393
+ ]);
394
+ const held = BigInt(custody.value.amount);
395
+ return {
396
+ state,
397
+ totalSupply,
398
+ totalAssets: vaultTvl(totalSupply, state.rate.value),
399
+ custody: held,
400
+ solvent: held >= state.balance,
401
+ pendingWithdrawals: Number(queue.tail - queue.head)
402
+ };
403
+ }
404
+ /** The rate, with what a single share is worth at it. */
405
+ async getVaultRate(shareMint) {
406
+ const { rate } = await this.getVaultState(shareMint);
407
+ return {
408
+ rate: rate.value,
409
+ maxChangePerUpdate: rate.maxRateChangePerUpdate,
410
+ updateInterval: rate.rateUpdateInterval,
411
+ lastUpdatedAt: rate.lastUpdatedAt
412
+ };
413
+ }
414
+ }
415
+ /** base58, for `memcmp` filters: web3.js wants a string, not bytes. */
416
+ function bs58(bytes) {
417
+ const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
418
+ let n = 0n;
419
+ for (const b of bytes)
420
+ n = n * 256n + BigInt(b);
421
+ let out = "";
422
+ while (n > 0n) {
423
+ out = ALPHABET[Number(n % 58n)] + out;
424
+ n /= 58n;
425
+ }
426
+ for (const b of bytes) {
427
+ if (b !== 0)
428
+ break;
429
+ out = "1" + out;
430
+ }
431
+ return out;
432
+ }
@@ -0,0 +1,65 @@
1
+ import { PublicKey } from "@solana/web3.js";
2
+ import type { ISolanaProtocolConfigState, ISolanaVaultAccount, ISolanaVaultState, ISolanaWithdrawalQueue, ISolanaWithdrawalRequestAccount } from "../interfaces/index.js";
3
+ /**
4
+ * Borsh decoding for `ember_vaults` accounts.
5
+ *
6
+ * Decoded by hand rather than through Anchor's client. The program's IDL is
7
+ * not published on chain — `anchor idl init` cannot get an IDL this size past
8
+ * the public RPCs — so a client cannot fetch it, and pulling in the whole
9
+ * Anchor runtime to read a few dozen fields is not worth the dependency.
10
+ *
11
+ * Field order here must track `state.rs`. The discriminator check catches a
12
+ * wrong account type; it cannot catch a field reordered within the right one,
13
+ * so `SolanaAccountDecoding.test.ts` pins the offsets against fixtures taken
14
+ * from a real deployment.
15
+ */
16
+ /** A forward-only reader over account data. */
17
+ export declare class Cursor {
18
+ private readonly data;
19
+ private offset;
20
+ constructor(data: Buffer, offset?: number);
21
+ skip(n: number): this;
22
+ u8(): number;
23
+ bool(): boolean;
24
+ u64(): bigint;
25
+ u128(): bigint;
26
+ pubkey(): PublicKey;
27
+ string(): string;
28
+ vec<T>(item: (c: Cursor) => T): T[];
29
+ /** How far in the cursor has read. For the layout tests. */
30
+ position(): number;
31
+ }
32
+ /**
33
+ * `state::Vault`, in declaration order.
34
+ *
35
+ * Returns the cursor's final position alongside the value, so a caller can
36
+ * check it against the account's own length. Stopping early still decodes
37
+ * without error — the withdrawal-fee tail was missed exactly that way — so
38
+ * "it parsed" is not evidence the layout is right.
39
+ */
40
+ export declare function decodeVaultWithPosition(data: Buffer): {
41
+ vault: ISolanaVaultState;
42
+ position: number;
43
+ };
44
+ /** `state::Vault`, in declaration order. */
45
+ export declare function decodeVault(data: Buffer): ISolanaVaultState;
46
+ /** `state::VaultAccount`. */
47
+ export declare function decodeVaultAccount(data: Buffer): ISolanaVaultAccount;
48
+ /** `state::WithdrawalRequestAccount`. */
49
+ export declare function decodeWithdrawalRequestAccount(address: PublicKey, data: Buffer): ISolanaWithdrawalRequestAccount;
50
+ /** `state::WithdrawalQueue`: head and tail, the entries live in their own accounts. */
51
+ export declare function decodeWithdrawalQueue(data: Buffer): ISolanaWithdrawalQueue;
52
+ /** `state::ProtocolConfig`. */
53
+ export declare function decodeProtocolConfig(data: Buffer): ISolanaProtocolConfigState;
54
+ export interface ISplMint {
55
+ mintAuthority: PublicKey | null;
56
+ supply: bigint;
57
+ decimals: number;
58
+ isInitialized: boolean;
59
+ freezeAuthority: PublicKey | null;
60
+ }
61
+ /**
62
+ * The classic 82-byte SPL mint layout. The first 82 bytes of a Token-2022
63
+ * mint are identical, so this reads both; it just ignores any extensions.
64
+ */
65
+ export declare function decodeMint(data: Buffer): ISplMint;