@ember-finance/sdk 2.38.0 → 2.40.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/dist/src/common/constants.d.ts +1 -1
- package/dist/src/ember-vaults.d.ts +85 -5
- package/dist/src/ember-vaults.js +144 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/solana-vaults/index.d.ts +4 -0
- package/dist/src/solana-vaults/index.js +4 -0
- package/dist/src/solana-vaults/interfaces/index.d.ts +260 -0
- package/dist/src/solana-vaults/interfaces/index.js +37 -0
- package/dist/src/solana-vaults/on-chain-calls/index.d.ts +3 -0
- package/dist/src/solana-vaults/on-chain-calls/index.js +3 -0
- package/dist/src/solana-vaults/on-chain-calls/tx-builder.d.ts +62 -0
- package/dist/src/solana-vaults/on-chain-calls/tx-builder.js +190 -0
- package/dist/src/solana-vaults/on-chain-calls/user.d.ts +150 -0
- package/dist/src/solana-vaults/on-chain-calls/user.js +288 -0
- package/dist/src/solana-vaults/on-chain-calls/vault-reader.d.ts +190 -0
- package/dist/src/solana-vaults/on-chain-calls/vault-reader.js +432 -0
- package/dist/src/solana-vaults/signers/index.d.ts +1 -0
- package/dist/src/solana-vaults/signers/index.js +1 -0
- package/dist/src/solana-vaults/signers/solana-signer.d.ts +44 -0
- package/dist/src/solana-vaults/signers/solana-signer.js +61 -0
- package/dist/src/solana-vaults/utils/borsh.d.ts +65 -0
- package/dist/src/solana-vaults/utils/borsh.js +237 -0
- package/dist/src/solana-vaults/utils/deployment-parser.d.ts +94 -0
- package/dist/src/solana-vaults/utils/deployment-parser.js +174 -0
- package/dist/src/solana-vaults/utils/index.d.ts +3 -0
- package/dist/src/solana-vaults/utils/index.js +3 -0
- package/dist/src/solana-vaults/utils/math.d.ts +77 -0
- package/dist/src/solana-vaults/utils/math.js +111 -0
- package/dist/src/solana-vaults/utils/pdas.d.ts +63 -0
- package/dist/src/solana-vaults/utils/pdas.js +114 -0
- package/package.json +10 -2
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { PublicKey, Transaction } from "@solana/web3.js";
|
|
2
|
+
import { normaliseSigner } from "../signers/solana-signer.js";
|
|
3
|
+
import { associatedTokenAddress } from "../utils/pdas.js";
|
|
4
|
+
import { SolanaTxBuilder } from "./tx-builder.js";
|
|
5
|
+
import { SolanaVaultReader } from "./vault-reader.js";
|
|
6
|
+
/**
|
|
7
|
+
* Normalises an amount to a bigint, rejecting what `BigInt()` would quietly
|
|
8
|
+
* accept.
|
|
9
|
+
*
|
|
10
|
+
* `BigInt()` is far looser than it looks: `BigInt("")` and `BigInt(" ")` are
|
|
11
|
+
* `0n`, `BigInt("0x10")` is `16n` and `BigInt("-5")` is `-5n`. An empty form
|
|
12
|
+
* field would therefore become a zero-amount deposit rather than an error, and
|
|
13
|
+
* a negative would travel as far as `u64le` before failing with a RangeError
|
|
14
|
+
* that names no field. Strings must be plain decimal digits, and every amount
|
|
15
|
+
* here is unsigned on chain.
|
|
16
|
+
*/
|
|
17
|
+
function toBigInt(value, field) {
|
|
18
|
+
let result;
|
|
19
|
+
if (typeof value === "bigint") {
|
|
20
|
+
result = value;
|
|
21
|
+
}
|
|
22
|
+
else if (typeof value === "number") {
|
|
23
|
+
if (!Number.isInteger(value)) {
|
|
24
|
+
throw new TypeError(`${field} must be a whole number, got ${value}`);
|
|
25
|
+
}
|
|
26
|
+
// Past 2^53 a number has already lost the precision an exact token
|
|
27
|
+
// amount needs, so the value here would not be the one the caller meant.
|
|
28
|
+
if (!Number.isSafeInteger(value)) {
|
|
29
|
+
throw new TypeError(`${field} is too large to be exact as a number (${value}); pass a bigint or a string`);
|
|
30
|
+
}
|
|
31
|
+
result = BigInt(value);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
const digits = value.trim();
|
|
35
|
+
if (!/^\d+$/.test(digits)) {
|
|
36
|
+
throw new TypeError(`${field} must be a decimal integer string, got ${JSON.stringify(value)}`);
|
|
37
|
+
}
|
|
38
|
+
result = BigInt(digits);
|
|
39
|
+
}
|
|
40
|
+
if (result < 0n) {
|
|
41
|
+
throw new TypeError(`${field} must not be negative, got ${result}`);
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Rejects a `receiver` on the instructions that have none.
|
|
47
|
+
*
|
|
48
|
+
* The options types already keep it off deposit and mint, but excess-property
|
|
49
|
+
* checking only applies to object LITERALS: a variable typed
|
|
50
|
+
* ISolanaRedeemOptions satisfies ISolanaWriteOptions and passes straight
|
|
51
|
+
* through, where the field would be silently dropped.
|
|
52
|
+
*/
|
|
53
|
+
function rejectReceiver(options, call) {
|
|
54
|
+
if (options && "receiver" in options) {
|
|
55
|
+
throw new TypeError(`${call} mints into the depositor's own token account and takes no receiver; ` +
|
|
56
|
+
`the program's instruction has no such argument`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The user-facing writes: deposit, mint, redeem and cancel.
|
|
61
|
+
*
|
|
62
|
+
* Each method reads only what it needs to build a correct instruction — the
|
|
63
|
+
* mints' token programs and, for a redeem, the owner's request nonce — then
|
|
64
|
+
* signs and sends. The instruction building itself lives in SolanaTxBuilder,
|
|
65
|
+
* so a caller who wants to batch or route differently can use that directly.
|
|
66
|
+
*/
|
|
67
|
+
export class SolanaUserCalls {
|
|
68
|
+
constructor(connection, parser,
|
|
69
|
+
/**
|
|
70
|
+
* Optional so one instance can serve a page that has not connected a
|
|
71
|
+
* wallet yet; every write then takes its own signer.
|
|
72
|
+
*/
|
|
73
|
+
signer) {
|
|
74
|
+
this.connection = connection;
|
|
75
|
+
this.signer = signer;
|
|
76
|
+
this.parser = parser;
|
|
77
|
+
this.reader = new SolanaVaultReader(connection, parser);
|
|
78
|
+
}
|
|
79
|
+
/** Swap in a newer deployment without rebuilding the caller. */
|
|
80
|
+
updateDeployment(parser) {
|
|
81
|
+
this.parser = parser;
|
|
82
|
+
this.reader.updateDeployment(parser);
|
|
83
|
+
}
|
|
84
|
+
key(value) {
|
|
85
|
+
return typeof value === "string" ? new PublicKey(value) : value;
|
|
86
|
+
}
|
|
87
|
+
resolveSigner(override) {
|
|
88
|
+
const signer = override ?? this.signer;
|
|
89
|
+
if (!signer) {
|
|
90
|
+
throw new Error("no signer: pass one to the constructor or to this call");
|
|
91
|
+
}
|
|
92
|
+
return normaliseSigner(signer);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The mints and their owning token programs.
|
|
96
|
+
*
|
|
97
|
+
* The programs come from the deployment manifest when it records them and
|
|
98
|
+
* from the mint accounts otherwise. They are not assumed equal: a vault's
|
|
99
|
+
* collateral and share mints may sit under SPL Token and Token-2022
|
|
100
|
+
* respectively, and the ATA addresses differ between the two.
|
|
101
|
+
*/
|
|
102
|
+
async vaultContext(shareMint) {
|
|
103
|
+
const mint = this.key(shareMint);
|
|
104
|
+
const collateralMint = this.parser.getCollateralMint(mint);
|
|
105
|
+
const [shareProgram, collateralProgram] = await Promise.all([
|
|
106
|
+
this.tokenProgram(mint),
|
|
107
|
+
this.tokenProgram(collateralMint)
|
|
108
|
+
]);
|
|
109
|
+
return {
|
|
110
|
+
programId: this.parser.programId,
|
|
111
|
+
shareMint: mint,
|
|
112
|
+
collateralMint,
|
|
113
|
+
shareProgram,
|
|
114
|
+
collateralProgram
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
async tokenProgram(mint) {
|
|
118
|
+
const recorded = this.parser.getTokenProgram(mint);
|
|
119
|
+
if (recorded)
|
|
120
|
+
return recorded;
|
|
121
|
+
const account = await this.connection.getAccountInfo(mint);
|
|
122
|
+
if (account === null)
|
|
123
|
+
throw new Error(`mint ${mint.toBase58()} not found`);
|
|
124
|
+
return account.owner;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Signs, sends and confirms. Simulation runs first unless turned off, and
|
|
128
|
+
* its error is surfaced in place of the send's: the program's own message
|
|
129
|
+
* is in the simulation logs, while a failed send gives only a code.
|
|
130
|
+
*/
|
|
131
|
+
async send(instructions, signer, options) {
|
|
132
|
+
const tx = new Transaction();
|
|
133
|
+
if (options?.preInstructions?.length)
|
|
134
|
+
tx.add(...options.preInstructions);
|
|
135
|
+
tx.add(...instructions);
|
|
136
|
+
const { blockhash, lastValidBlockHeight } = await this.connection.getLatestBlockhash();
|
|
137
|
+
tx.recentBlockhash = blockhash;
|
|
138
|
+
tx.feePayer = signer.publicKey;
|
|
139
|
+
let unitsConsumed;
|
|
140
|
+
if (options?.simulate !== false) {
|
|
141
|
+
const sim = await this.connection.simulateTransaction(tx);
|
|
142
|
+
if (sim.value.err) {
|
|
143
|
+
const logs = sim.value.logs?.join("\n") ?? "";
|
|
144
|
+
throw new Error(`simulation failed: ${JSON.stringify(sim.value.err)}\n${logs}`);
|
|
145
|
+
}
|
|
146
|
+
unitsConsumed = sim.value.unitsConsumed ?? undefined;
|
|
147
|
+
}
|
|
148
|
+
const signed = await signer.signTransaction(tx);
|
|
149
|
+
const signature = await this.connection.sendRawTransaction(signed.serialize());
|
|
150
|
+
// Confirm at the commitment this Connection READS at, not a hardcoded
|
|
151
|
+
// one. Confirming at "confirmed" under a "finalized" connection returns
|
|
152
|
+
// before the following reads can see the write - two awaited
|
|
153
|
+
// redeemShares calls would then read the same nonce and derive a
|
|
154
|
+
// request account that already exists.
|
|
155
|
+
const confirmation = await this.connection.confirmTransaction({ signature, blockhash, lastValidBlockHeight }, this.connection.commitment ?? "confirmed");
|
|
156
|
+
if (confirmation.value.err) {
|
|
157
|
+
throw new Error(`transaction ${signature} failed: ${JSON.stringify(confirmation.value.err)}`);
|
|
158
|
+
}
|
|
159
|
+
return { signature, unitsConsumed };
|
|
160
|
+
}
|
|
161
|
+
// --- deposits ---
|
|
162
|
+
/**
|
|
163
|
+
* Deposits collateral and mints shares at the vault's live rate.
|
|
164
|
+
*
|
|
165
|
+
* `minShares` is the on-chain slippage floor; it bites only if the rate
|
|
166
|
+
* manager moves the rate between quoting and landing. Quote it with
|
|
167
|
+
* `SolanaVaultReader.previewDeposit`.
|
|
168
|
+
*
|
|
169
|
+
* The depositor's share account is created here when missing, so a first
|
|
170
|
+
* deposit is still a single transaction.
|
|
171
|
+
*/
|
|
172
|
+
async depositAsset(shareMint, amount, minShares, options) {
|
|
173
|
+
// Validated before any round trip: a bad amount should not cost two
|
|
174
|
+
// account reads first, and the error should name the field rather than
|
|
175
|
+
// surface as a failed mint lookup.
|
|
176
|
+
rejectReceiver(options, "depositAsset");
|
|
177
|
+
const amountValue = toBigInt(amount, "amount");
|
|
178
|
+
const minSharesValue = toBigInt(minShares, "minShares");
|
|
179
|
+
const signer = this.resolveSigner(options?.signer);
|
|
180
|
+
const ctx = await this.vaultContext(shareMint);
|
|
181
|
+
const instructions = [
|
|
182
|
+
...(await this.ensureShareAccount(ctx, signer.publicKey)),
|
|
183
|
+
SolanaTxBuilder.depositAsset(ctx, signer.publicKey, amountValue, minSharesValue)
|
|
184
|
+
];
|
|
185
|
+
return this.send(instructions, signer, options);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Mints an exact share count, spending at most `maxAmount` of collateral.
|
|
189
|
+
* The mirror of depositAsset, for a caller who wants a round share figure.
|
|
190
|
+
*/
|
|
191
|
+
async mintShares(shareMint, shares, maxAmount, options) {
|
|
192
|
+
rejectReceiver(options, "mintShares");
|
|
193
|
+
const sharesValue = toBigInt(shares, "shares");
|
|
194
|
+
const maxAmountValue = toBigInt(maxAmount, "maxAmount");
|
|
195
|
+
const signer = this.resolveSigner(options?.signer);
|
|
196
|
+
const ctx = await this.vaultContext(shareMint);
|
|
197
|
+
const instructions = [
|
|
198
|
+
...(await this.ensureShareAccount(ctx, signer.publicKey)),
|
|
199
|
+
SolanaTxBuilder.mintShares(ctx, signer.publicKey, sharesValue, maxAmountValue)
|
|
200
|
+
];
|
|
201
|
+
return this.send(instructions, signer, options);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* The idempotent ATA instruction, but only when the account is actually
|
|
205
|
+
* missing: including it unconditionally costs a few hundred compute units
|
|
206
|
+
* on every repeat deposit for nothing.
|
|
207
|
+
*/
|
|
208
|
+
async ensureShareAccount(ctx, owner) {
|
|
209
|
+
const ata = associatedTokenAddress(owner, ctx.shareMint, ctx.shareProgram);
|
|
210
|
+
const existing = await this.connection.getAccountInfo(ata);
|
|
211
|
+
if (existing !== null)
|
|
212
|
+
return [];
|
|
213
|
+
return [
|
|
214
|
+
SolanaTxBuilder.createAtaIdempotent(owner, owner, ctx.shareMint, ctx.shareProgram)
|
|
215
|
+
];
|
|
216
|
+
}
|
|
217
|
+
// --- withdrawals ---
|
|
218
|
+
/**
|
|
219
|
+
* Queues a withdrawal. This does NOT return collateral: the shares are
|
|
220
|
+
* escrowed and a request is appended to the vault's queue, which the
|
|
221
|
+
* operator settles later. Watch it with `getPendingWithdrawals`.
|
|
222
|
+
*
|
|
223
|
+
* The request's account is seeded by the owner's next nonce, which is read
|
|
224
|
+
* here — a caller passing a stale one would collide with their own earlier
|
|
225
|
+
* request.
|
|
226
|
+
*/
|
|
227
|
+
async redeemShares(shareMint, shares, options) {
|
|
228
|
+
const sharesValue = toBigInt(shares, "shares");
|
|
229
|
+
const signer = this.resolveSigner(options?.signer);
|
|
230
|
+
const ctx = await this.vaultContext(shareMint);
|
|
231
|
+
const account = await this.reader.getAccountState(ctx.shareMint, signer.publicKey);
|
|
232
|
+
// Null before the owner's first deposit or redeem, where the program
|
|
233
|
+
// creates the account and starts the nonce at 0.
|
|
234
|
+
const nonce = account?.nextRequestNonce ?? 0n;
|
|
235
|
+
const receiver = options?.receiver
|
|
236
|
+
? this.key(options.receiver)
|
|
237
|
+
: signer.publicKey;
|
|
238
|
+
return this.send([
|
|
239
|
+
SolanaTxBuilder.redeemShares(ctx, signer.publicKey, sharesValue, nonce, receiver)
|
|
240
|
+
], signer, options);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Cancels a queued withdrawal by its queue `sequenceNumber` — not the
|
|
244
|
+
* request nonce that seeds the request account. Take it from
|
|
245
|
+
* `getPendingWithdrawals`.
|
|
246
|
+
*/
|
|
247
|
+
async cancelPendingWithdrawalRequest(shareMint, sequenceNumber, options) {
|
|
248
|
+
const sequenceValue = toBigInt(sequenceNumber, "sequenceNumber");
|
|
249
|
+
const signer = this.resolveSigner(options?.signer);
|
|
250
|
+
const ctx = await this.vaultContext(shareMint);
|
|
251
|
+
return this.send([
|
|
252
|
+
SolanaTxBuilder.cancelPendingWithdrawalRequest(ctx, signer.publicKey, sequenceValue)
|
|
253
|
+
], signer, options);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* An owner's live queued withdrawals, each carrying the `sequenceNumber`
|
|
257
|
+
* that `cancelPendingWithdrawalRequest` takes.
|
|
258
|
+
*
|
|
259
|
+
* CANCELLED REQUESTS ARE EXCLUDED, and that filter is not cosmetic.
|
|
260
|
+
* `cancel_pending_withdrawal_request` does not remove the entry: it pushes
|
|
261
|
+
* the sequence number onto the account's `cancel_withdraw_request` list and
|
|
262
|
+
* leaves the request in place, for the operator to skip when it settles
|
|
263
|
+
* (see withdraw.rs). Returning the raw list would therefore show a
|
|
264
|
+
* withdrawal the user already cancelled as still pending, indefinitely.
|
|
265
|
+
*/
|
|
266
|
+
async getPendingWithdrawals(shareMint, owner) {
|
|
267
|
+
const account = await this.accountState(shareMint, owner);
|
|
268
|
+
if (!account)
|
|
269
|
+
return [];
|
|
270
|
+
const cancelled = new Set(account.cancelWithdrawRequest.map(String));
|
|
271
|
+
return account.pendingWithdrawalRequests.filter(request => !cancelled.has(String(request.sequenceNumber)));
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* The requests this owner has cancelled that the operator has not yet
|
|
275
|
+
* settled. They still occupy a slot on the account until it does.
|
|
276
|
+
*/
|
|
277
|
+
async getCancelledWithdrawals(shareMint, owner) {
|
|
278
|
+
const account = await this.accountState(shareMint, owner);
|
|
279
|
+
if (!account)
|
|
280
|
+
return [];
|
|
281
|
+
const cancelled = new Set(account.cancelWithdrawRequest.map(String));
|
|
282
|
+
return account.pendingWithdrawalRequests.filter(request => cancelled.has(String(request.sequenceNumber)));
|
|
283
|
+
}
|
|
284
|
+
async accountState(shareMint, owner) {
|
|
285
|
+
const address = owner ? this.key(owner) : this.resolveSigner().publicKey;
|
|
286
|
+
return this.reader.getAccountState(shareMint, address);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -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
|
+
}
|