@ember-finance/sdk 2.39.0 → 2.42.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/ember-vaults.d.ts +14 -6
- package/dist/src/ember-vaults.js +20 -3
- package/dist/src/solana-vaults/index.d.ts +1 -0
- package/dist/src/solana-vaults/index.js +1 -0
- package/dist/src/solana-vaults/interfaces/index.d.ts +66 -2
- package/dist/src/solana-vaults/interfaces/index.js +7 -1
- package/dist/src/solana-vaults/on-chain-calls/index.d.ts +2 -0
- package/dist/src/solana-vaults/on-chain-calls/index.js +2 -0
- package/dist/src/solana-vaults/on-chain-calls/tx-builder.d.ts +135 -0
- package/dist/src/solana-vaults/on-chain-calls/tx-builder.js +466 -0
- package/dist/src/solana-vaults/on-chain-calls/user.d.ts +200 -0
- package/dist/src/solana-vaults/on-chain-calls/user.js +380 -0
- package/dist/src/solana-vaults/on-chain-calls/vault-reader.d.ts +80 -1
- package/dist/src/solana-vaults/on-chain-calls/vault-reader.js +181 -20
- 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 +12 -1
- package/dist/src/solana-vaults/utils/borsh.js +46 -4
- package/dist/src/solana-vaults/utils/pdas.d.ts +70 -0
- package/dist/src/solana-vaults/utils/pdas.js +101 -5
- package/package.json +1 -1
|
@@ -0,0 +1,380 @@
|
|
|
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, payer = 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(payer, owner, ctx.shareMint, ctx.shareProgram)
|
|
215
|
+
];
|
|
216
|
+
}
|
|
217
|
+
// --- parking lot ---
|
|
218
|
+
/**
|
|
219
|
+
* Parks a deposit: escrows collateral and queues a request for the
|
|
220
|
+
* operator to forward into the vault later. No shares are minted here.
|
|
221
|
+
*
|
|
222
|
+
* THIS CALL IS RACY BY CONSTRUCTION. The request's account address is
|
|
223
|
+
* seeded with the lot's `sequence_number + 1`, and the program increments
|
|
224
|
+
* before it writes, so two parks landing in the same slot collide and the
|
|
225
|
+
* loser fails its address check. The lot is read immediately before
|
|
226
|
+
* building to keep the window small, but it cannot be closed from here —
|
|
227
|
+
* on `ParkingLotSequenceTaken` the caller should simply call again, which
|
|
228
|
+
* re-reads and takes the next slot.
|
|
229
|
+
*
|
|
230
|
+
* No automatic retry: a resend after a failed SEND (rather than a failed
|
|
231
|
+
* simulation) could double-park if the first transaction actually landed.
|
|
232
|
+
*
|
|
233
|
+
* `receiver` takes the minted shares when the request is forwarded. The
|
|
234
|
+
* depositor keeps the refund and the account's rent either way.
|
|
235
|
+
*/
|
|
236
|
+
async parkDeposit(shareMint, assets, options) {
|
|
237
|
+
const assetsValue = toBigInt(assets, "assets");
|
|
238
|
+
const signer = this.resolveSigner(options?.signer);
|
|
239
|
+
const ctx = await this.vaultContext(shareMint);
|
|
240
|
+
const lot = await this.reader.getParkingLot(ctx.shareMint);
|
|
241
|
+
if (lot === null) {
|
|
242
|
+
throw new Error(`vault ${ctx.shareMint.toBase58()} has no parking lot; deposit directly instead`);
|
|
243
|
+
}
|
|
244
|
+
if (lot.depositsPaused) {
|
|
245
|
+
throw new Error("the parking lot is not accepting deposits");
|
|
246
|
+
}
|
|
247
|
+
// +1: the slot this park will take, because the program increments
|
|
248
|
+
// before it writes.
|
|
249
|
+
const sequenceNumber = lot.sequenceNumber + 1n;
|
|
250
|
+
const receiver = options?.receiver
|
|
251
|
+
? this.key(options.receiver)
|
|
252
|
+
: signer.publicKey;
|
|
253
|
+
// The program only checks the receiver's share account when the request
|
|
254
|
+
// is FORWARDED, and a missing one makes it skip and refund instead
|
|
255
|
+
// (`parking_lot.rs` can_receive_mint / should_skip). A first-time
|
|
256
|
+
// receiver's park would otherwise succeed here and silently become a
|
|
257
|
+
// refund later, long after the caller stopped watching. Idempotent, so
|
|
258
|
+
// it is a no-op when the account already exists, and the signer pays
|
|
259
|
+
// because the receiver may never have signed anything.
|
|
260
|
+
const response = await this.send([
|
|
261
|
+
...(await this.ensureShareAccount(ctx, receiver, signer.publicKey)),
|
|
262
|
+
SolanaTxBuilder.parkingLotDeposit(ctx, signer.publicKey, assetsValue, sequenceNumber, receiver)
|
|
263
|
+
], signer, options);
|
|
264
|
+
return { ...response, sequenceNumber };
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Cancels a parked deposit before it settles, returning the escrowed
|
|
268
|
+
* collateral and the account's rent.
|
|
269
|
+
*
|
|
270
|
+
* Takes the request's own sequence number — from `getParkedDeposits` —
|
|
271
|
+
* not the lot's next one.
|
|
272
|
+
*/
|
|
273
|
+
async cancelParkedDeposit(shareMint, sequenceNumber, options) {
|
|
274
|
+
rejectReceiver(options, "cancelParkedDeposit");
|
|
275
|
+
const sequenceValue = toBigInt(sequenceNumber, "sequenceNumber");
|
|
276
|
+
const signer = this.resolveSigner(options?.signer);
|
|
277
|
+
const ctx = await this.vaultContext(shareMint);
|
|
278
|
+
return this.send([
|
|
279
|
+
SolanaTxBuilder.parkingLotCancelDeposit(ctx, signer.publicKey, sequenceValue)
|
|
280
|
+
], signer, options);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Claims a refund that settlement could not deliver.
|
|
284
|
+
*
|
|
285
|
+
* Only for a request whose `claimable` flag is set — the request settled
|
|
286
|
+
* but the collateral could not be returned, so it waits in escrow under
|
|
287
|
+
* the lot's `totalClaimableAssets`. `getParkedDeposits` separates these
|
|
288
|
+
* from the ones still pending.
|
|
289
|
+
*/
|
|
290
|
+
async claimParkedRefund(shareMint, sequenceNumber, options) {
|
|
291
|
+
rejectReceiver(options, "claimParkedRefund");
|
|
292
|
+
const sequenceValue = toBigInt(sequenceNumber, "sequenceNumber");
|
|
293
|
+
const signer = this.resolveSigner(options?.signer);
|
|
294
|
+
const ctx = await this.vaultContext(shareMint);
|
|
295
|
+
return this.send([SolanaTxBuilder.parkingLotClaimRefund(ctx, signer.publicKey, sequenceValue)], signer, options);
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* This depositor's parked deposits, split into those still queued and
|
|
299
|
+
* those whose refund is waiting to be claimed.
|
|
300
|
+
*
|
|
301
|
+
* Found by filtering on the request accounts themselves rather than by
|
|
302
|
+
* walking the queue: a claimable request has already settled, so it sits
|
|
303
|
+
* BELOW the lot's head and no scan of the pending window would see it.
|
|
304
|
+
*/
|
|
305
|
+
async getParkedDeposits(shareMint, owner) {
|
|
306
|
+
const address = owner ? this.key(owner) : this.resolveSigner().publicKey;
|
|
307
|
+
return this.reader.getDepositsFor(shareMint, address);
|
|
308
|
+
}
|
|
309
|
+
// --- withdrawals ---
|
|
310
|
+
/**
|
|
311
|
+
* Queues a withdrawal. This does NOT return collateral: the shares are
|
|
312
|
+
* escrowed and a request is appended to the vault's queue, which the
|
|
313
|
+
* operator settles later. Watch it with `getPendingWithdrawals`.
|
|
314
|
+
*
|
|
315
|
+
* The request's account is seeded by the owner's next nonce, which is read
|
|
316
|
+
* here — a caller passing a stale one would collide with their own earlier
|
|
317
|
+
* request.
|
|
318
|
+
*/
|
|
319
|
+
async redeemShares(shareMint, shares, options) {
|
|
320
|
+
const sharesValue = toBigInt(shares, "shares");
|
|
321
|
+
const signer = this.resolveSigner(options?.signer);
|
|
322
|
+
const ctx = await this.vaultContext(shareMint);
|
|
323
|
+
const account = await this.reader.getAccountState(ctx.shareMint, signer.publicKey);
|
|
324
|
+
// Null before the owner's first deposit or redeem, where the program
|
|
325
|
+
// creates the account and starts the nonce at 0.
|
|
326
|
+
const nonce = account?.nextRequestNonce ?? 0n;
|
|
327
|
+
const receiver = options?.receiver
|
|
328
|
+
? this.key(options.receiver)
|
|
329
|
+
: signer.publicKey;
|
|
330
|
+
return this.send([
|
|
331
|
+
SolanaTxBuilder.redeemShares(ctx, signer.publicKey, sharesValue, nonce, receiver)
|
|
332
|
+
], signer, options);
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Cancels a queued withdrawal by its queue `sequenceNumber` — not the
|
|
336
|
+
* request nonce that seeds the request account. Take it from
|
|
337
|
+
* `getPendingWithdrawals`.
|
|
338
|
+
*/
|
|
339
|
+
async cancelPendingWithdrawalRequest(shareMint, sequenceNumber, options) {
|
|
340
|
+
const sequenceValue = toBigInt(sequenceNumber, "sequenceNumber");
|
|
341
|
+
const signer = this.resolveSigner(options?.signer);
|
|
342
|
+
const ctx = await this.vaultContext(shareMint);
|
|
343
|
+
return this.send([
|
|
344
|
+
SolanaTxBuilder.cancelPendingWithdrawalRequest(ctx, signer.publicKey, sequenceValue)
|
|
345
|
+
], signer, options);
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* An owner's live queued withdrawals, each carrying the `sequenceNumber`
|
|
349
|
+
* that `cancelPendingWithdrawalRequest` takes.
|
|
350
|
+
*
|
|
351
|
+
* CANCELLED REQUESTS ARE EXCLUDED, and that filter is not cosmetic.
|
|
352
|
+
* `cancel_pending_withdrawal_request` does not remove the entry: it pushes
|
|
353
|
+
* the sequence number onto the account's `cancel_withdraw_request` list and
|
|
354
|
+
* leaves the request in place, for the operator to skip when it settles
|
|
355
|
+
* (see withdraw.rs). Returning the raw list would therefore show a
|
|
356
|
+
* withdrawal the user already cancelled as still pending, indefinitely.
|
|
357
|
+
*/
|
|
358
|
+
async getPendingWithdrawals(shareMint, owner) {
|
|
359
|
+
const account = await this.accountState(shareMint, owner);
|
|
360
|
+
if (!account)
|
|
361
|
+
return [];
|
|
362
|
+
const cancelled = new Set(account.cancelWithdrawRequest.map(String));
|
|
363
|
+
return account.pendingWithdrawalRequests.filter(request => !cancelled.has(String(request.sequenceNumber)));
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* The requests this owner has cancelled that the operator has not yet
|
|
367
|
+
* settled. They still occupy a slot on the account until it does.
|
|
368
|
+
*/
|
|
369
|
+
async getCancelledWithdrawals(shareMint, owner) {
|
|
370
|
+
const account = await this.accountState(shareMint, owner);
|
|
371
|
+
if (!account)
|
|
372
|
+
return [];
|
|
373
|
+
const cancelled = new Set(account.cancelWithdrawRequest.map(String));
|
|
374
|
+
return account.pendingWithdrawalRequests.filter(request => cancelled.has(String(request.sequenceNumber)));
|
|
375
|
+
}
|
|
376
|
+
async accountState(shareMint, owner) {
|
|
377
|
+
const address = owner ? this.key(owner) : this.resolveSigner().publicKey;
|
|
378
|
+
return this.reader.getAccountState(shareMint, address);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
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";
|
|
2
|
+
import { type ISolanaDepositRequest, type ISolanaParkingLotState, type ISolanaProtocolConfigState, type ISolanaVaultAccount, type ISolanaVaultPdas, type ISolanaVaultState, type ISolanaWithdrawalRequestAccount } from "../interfaces/index.js";
|
|
3
3
|
import { type IWithdrawalFeeBreakdown } from "../utils/math.js";
|
|
4
4
|
import { SolanaDeploymentParser } from "../utils/deployment-parser.js";
|
|
5
5
|
/**
|
|
@@ -22,6 +22,18 @@ export declare class SolanaVaultReader {
|
|
|
22
22
|
updateDeployment(parser: SolanaDeploymentParser): void;
|
|
23
23
|
get programId(): PublicKey;
|
|
24
24
|
private key;
|
|
25
|
+
/**
|
|
26
|
+
* Whether an account is on one of the unbounded allow lists.
|
|
27
|
+
*
|
|
28
|
+
* The program's own test is `UserListEntry::exists`: owned by the program
|
|
29
|
+
* AND non-empty. A bare "the account exists" is weaker and wrong here -
|
|
30
|
+
* these addresses are public and derivable, so anyone can make
|
|
31
|
+
* `getAccountInfo` return non-null for one by sending it lamports. That
|
|
32
|
+
* system-owned, dataless account would read as listed and then be rejected
|
|
33
|
+
* on chain, which is a confusing false positive on a path users hit
|
|
34
|
+
* directly.
|
|
35
|
+
*/
|
|
36
|
+
private isListed;
|
|
25
37
|
/** The protocol config, or null when the protocol is not initialised. */
|
|
26
38
|
getProtocolConfig(): Promise<ISolanaProtocolConfigState | null>;
|
|
27
39
|
isProtocolPaused(): Promise<boolean>;
|
|
@@ -139,6 +151,73 @@ export declare class SolanaVaultReader {
|
|
|
139
151
|
private canDeposit;
|
|
140
152
|
/** `quote.rs::can_withdraw`. */
|
|
141
153
|
private canWithdraw;
|
|
154
|
+
/** The lot's address for a vault, whether or not one exists. */
|
|
155
|
+
parkingLotAddress(shareMint: PublicKey | string): PublicKey;
|
|
156
|
+
/**
|
|
157
|
+
* A vault's parking lot, or null when it has none.
|
|
158
|
+
*
|
|
159
|
+
* Null is the ordinary case: a lot is opt-in per vault, so most vaults have
|
|
160
|
+
* no account at this address at all.
|
|
161
|
+
*/
|
|
162
|
+
getParkingLot(shareMint: PublicKey | string): Promise<ISolanaParkingLotState | null>;
|
|
163
|
+
/** The lot's escrow token account. */
|
|
164
|
+
parkingLotEscrowAddress(shareMint: PublicKey | string): PublicKey;
|
|
165
|
+
/** One parked deposit by its sequence number, or null if it is gone. */
|
|
166
|
+
getDepositRequest(shareMint: PublicKey | string, sequenceNumber: bigint): Promise<ISolanaDepositRequest | null>;
|
|
167
|
+
/**
|
|
168
|
+
* A page of the lot's queue, oldest first, with an explicit cursor.
|
|
169
|
+
*
|
|
170
|
+
* `limit` counts SLOTS SCANNED, not requests returned, because a slot may
|
|
171
|
+
* be empty: a cancelled request's account is closed while its number stays
|
|
172
|
+
* in the range. `scannedTo` is therefore returned separately and is always
|
|
173
|
+
* advanced — a page of zero requests still tells the caller where to
|
|
174
|
+
* resume, which an empty array alone could not.
|
|
175
|
+
*
|
|
176
|
+
* Reads are batched at `MAX_MULTIPLE_ACCOUNTS`: `getMultipleAccounts`
|
|
177
|
+
* refuses more than 100 pubkeys per call, so a larger window is split
|
|
178
|
+
* rather than failing.
|
|
179
|
+
*
|
|
180
|
+
* This window is `(queueStartIndex, sequenceNumber]` — the UNSETTLED part
|
|
181
|
+
* of the queue. Requests whose refund is waiting to be claimed sit BELOW
|
|
182
|
+
* the head and are not here; use `getRequestsByDepositor` for those.
|
|
183
|
+
*/
|
|
184
|
+
getPendingDeposits(shareMint: PublicKey | string, options?: {
|
|
185
|
+
limit?: number;
|
|
186
|
+
after?: bigint;
|
|
187
|
+
}): Promise<{
|
|
188
|
+
requests: ISolanaDepositRequest[];
|
|
189
|
+
/** The last slot examined. Pass back as `after` to continue. */
|
|
190
|
+
scannedTo: bigint;
|
|
191
|
+
hasMore: boolean;
|
|
192
|
+
}>;
|
|
193
|
+
/**
|
|
194
|
+
* Every surviving request belonging to one depositor, wherever it sits in
|
|
195
|
+
* the queue.
|
|
196
|
+
*
|
|
197
|
+
* Position-independent on purpose. A CLAIMABLE request — one that settled
|
|
198
|
+
* but whose refund could not be delivered — lives BELOW `queueStartIndex`
|
|
199
|
+
* (see parking_lot.rs: "below the head only claimable requests remain"),
|
|
200
|
+
* so no scan of the pending window can ever find one. Walking downwards
|
|
201
|
+
* instead is unbounded and mostly hits closed accounts.
|
|
202
|
+
*
|
|
203
|
+
* So this filters on chain by the account's own bytes: the request's size,
|
|
204
|
+
* its parking lot at offset 9 and its depositor at offset 57. Those
|
|
205
|
+
* offsets track `DepositRequest` in state.rs and are pinned by test.
|
|
206
|
+
*
|
|
207
|
+
* `getProgramAccounts` is an indexed scan and some public RPCs restrict
|
|
208
|
+
* it; a provider endpoint is the right place to run this.
|
|
209
|
+
*/
|
|
210
|
+
getRequestsByDepositor(shareMint: PublicKey | string, depositor: PublicKey | string): Promise<ISolanaDepositRequest[]>;
|
|
211
|
+
/**
|
|
212
|
+
* A depositor's requests, split by what they can do with them.
|
|
213
|
+
*
|
|
214
|
+
* `pending` is still queued and can be cancelled; `claimable` has settled
|
|
215
|
+
* with an undeliverable refund and is collected with `claim_refund`.
|
|
216
|
+
*/
|
|
217
|
+
getDepositsFor(shareMint: PublicKey | string, depositor: PublicKey | string): Promise<{
|
|
218
|
+
pending: ISolanaDepositRequest[];
|
|
219
|
+
claimable: ISolanaDepositRequest[];
|
|
220
|
+
}>;
|
|
142
221
|
/** An owner's vault account, or null before their first deposit or redeem. */
|
|
143
222
|
getAccountState(shareMint: PublicKey | string, owner: PublicKey | string): Promise<ISolanaVaultAccount | null>;
|
|
144
223
|
/** Queue head and tail. Equal means drained. */
|