@ember-finance/sdk 2.40.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/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/tx-builder.d.ts +73 -0
- package/dist/src/solana-vaults/on-chain-calls/tx-builder.js +277 -1
- package/dist/src/solana-vaults/on-chain-calls/user.d.ts +51 -1
- package/dist/src/solana-vaults/on-chain-calls/user.js +94 -2
- 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/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 +61 -0
- package/dist/src/solana-vaults/utils/pdas.js +80 -5
- package/package.json +1 -1
|
@@ -32,6 +32,9 @@ export declare const SOLANA_SEEDS: {
|
|
|
32
32
|
readonly parkingLot: "parking_lot";
|
|
33
33
|
readonly parkingLotEscrow: "parking_lot_escrow";
|
|
34
34
|
readonly depositRequest: "deposit_request";
|
|
35
|
+
readonly depositUser: "deposit_user";
|
|
36
|
+
readonly withdrawalUser: "withdrawal_user";
|
|
37
|
+
readonly parkingLotUser: "parking_lot_user";
|
|
35
38
|
};
|
|
36
39
|
/** A cluster the deployment manifest can describe. */
|
|
37
40
|
export type SolanaCluster = "localnet" | "devnet" | "testnet" | "mainnet";
|
|
@@ -204,8 +207,13 @@ export interface ISolanaVaultState {
|
|
|
204
207
|
sequenceNumber: bigint;
|
|
205
208
|
/** `PublicKey.default` when no lot is bound. */
|
|
206
209
|
parkingLot: PublicKey;
|
|
207
|
-
|
|
208
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Sizes of the two allow lists, not their members: each list is unbounded
|
|
212
|
+
* and lives in one PDA per account. 0 means open to all. Test one account
|
|
213
|
+
* with `deriveDepositUser` / `deriveWithdrawalUser`.
|
|
214
|
+
*/
|
|
215
|
+
depositUserCount: bigint;
|
|
216
|
+
withdrawalUserCount: bigint;
|
|
209
217
|
withdrawalFee: ISolanaWithdrawalFee;
|
|
210
218
|
}
|
|
211
219
|
/** `state::WithdrawalRequest`, the entry stored in the queue. */
|
|
@@ -241,6 +249,62 @@ export interface ISolanaWithdrawalQueue {
|
|
|
241
249
|
head: bigint;
|
|
242
250
|
tail: bigint;
|
|
243
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* `state::DepositParkingLot`: where async deposits wait to be forwarded.
|
|
254
|
+
*
|
|
255
|
+
* A FIFO addressed by sequence number rather than by position: `sequenceNumber`
|
|
256
|
+
* is the last handed out (the tail) and `queueStartIndex` the last settled (the
|
|
257
|
+
* head). Neither is ever reset, so `head === tail` means drained, not empty.
|
|
258
|
+
*/
|
|
259
|
+
export interface ISolanaParkingLotState {
|
|
260
|
+
address: PublicKey;
|
|
261
|
+
bump: number;
|
|
262
|
+
/** The bound vault. Immutable after construction. */
|
|
263
|
+
vault: PublicKey;
|
|
264
|
+
/** Runs `process_deposits` and the allow list. Rotated by the vault admin. */
|
|
265
|
+
operator: PublicKey;
|
|
266
|
+
/** Blocks new parks. Processing and cancellation stay live. */
|
|
267
|
+
depositsPaused: boolean;
|
|
268
|
+
/** Blocks cancellation. Parking and processing stay live. */
|
|
269
|
+
cancellationsPaused: boolean;
|
|
270
|
+
/** FIFO tail: the last sequence number handed out. */
|
|
271
|
+
sequenceNumber: bigint;
|
|
272
|
+
/** FIFO head: the last sequence number settled. Never reset. */
|
|
273
|
+
queueStartIndex: bigint;
|
|
274
|
+
/** Collateral escrowed for requests still pending. */
|
|
275
|
+
totalPendingAssets: bigint;
|
|
276
|
+
/**
|
|
277
|
+
* Solana-only: collateral owed to depositors whose refund could not be
|
|
278
|
+
* delivered, awaiting `claim_refund`. The escrow token account holds
|
|
279
|
+
* `totalPendingAssets + totalClaimableAssets`.
|
|
280
|
+
*/
|
|
281
|
+
totalClaimableAssets: bigint;
|
|
282
|
+
/** Empty means open to all; otherwise only these may park. */
|
|
283
|
+
/**
|
|
284
|
+
* How many accounts the deposit allow list holds, not the accounts
|
|
285
|
+
* themselves: the list is unbounded and lives in one PDA per member, so
|
|
286
|
+
* there is nothing bounded to put here. 0 means the lot is open to all.
|
|
287
|
+
* Use `deriveParkingLotUser` to test one account.
|
|
288
|
+
*/
|
|
289
|
+
depositAllowListCount: bigint;
|
|
290
|
+
}
|
|
291
|
+
/** `state::DepositRequest`: one parked deposit. */
|
|
292
|
+
export interface ISolanaDepositRequest {
|
|
293
|
+
address: PublicKey;
|
|
294
|
+
bump: number;
|
|
295
|
+
parkingLot: PublicKey;
|
|
296
|
+
/** Its place in the lot's FIFO, and the seed of this account. */
|
|
297
|
+
sequenceNumber: bigint;
|
|
298
|
+
/** Escrowed the collateral; receives refunds and the rent. */
|
|
299
|
+
depositor: PublicKey;
|
|
300
|
+
/** Receives the minted vault shares. */
|
|
301
|
+
receiver: PublicKey;
|
|
302
|
+
assets: bigint;
|
|
303
|
+
/** Park time in ms. Informational, as in Move. */
|
|
304
|
+
timestamp: bigint;
|
|
305
|
+
/** Settled, but its refund is waiting to be claimed. */
|
|
306
|
+
claimable: boolean;
|
|
307
|
+
}
|
|
244
308
|
/** `state::ProtocolConfig`, decoded. */
|
|
245
309
|
export interface ISolanaProtocolConfigState {
|
|
246
310
|
admin: PublicKey;
|
|
@@ -30,7 +30,13 @@ export const SOLANA_SEEDS = {
|
|
|
30
30
|
withdrawalRequest: "withdrawal_request",
|
|
31
31
|
parkingLot: "parking_lot",
|
|
32
32
|
parkingLotEscrow: "parking_lot_escrow",
|
|
33
|
-
depositRequest: "deposit_request"
|
|
33
|
+
depositRequest: "deposit_request",
|
|
34
|
+
// One entry PDA per allow-listed account. The lists are unbounded, so
|
|
35
|
+
// membership is the existence of this account rather than a Vec on the
|
|
36
|
+
// vault; every gated instruction takes the caller's entry, present or not.
|
|
37
|
+
depositUser: "deposit_user",
|
|
38
|
+
withdrawalUser: "withdrawal_user",
|
|
39
|
+
parkingLotUser: "parking_lot_user"
|
|
34
40
|
};
|
|
35
41
|
// Call options and response types ship with the builders that take them, in
|
|
36
42
|
// the phase that adds writes. Declaring them here first would be a public
|
|
@@ -59,4 +59,77 @@ export declare class SolanaTxBuilder {
|
|
|
59
59
|
* that `vault` and `owner` are both read-only.
|
|
60
60
|
*/
|
|
61
61
|
static cancelPendingWithdrawalRequest(ctx: ISolanaVaultContext, owner: PublicKey, sequenceNumber: bigint): TransactionInstruction;
|
|
62
|
+
/** The lot and its escrow, derived from the vault's share mint. */
|
|
63
|
+
private static lotAccounts;
|
|
64
|
+
/**
|
|
65
|
+
* `parking_lot_deposit`: escrows collateral and queues a request.
|
|
66
|
+
*
|
|
67
|
+
* `sequenceNumber` MUST be the lot's current `sequence_number + 1`, since
|
|
68
|
+
* the program increments before it writes. That makes this racy by
|
|
69
|
+
* construction — if another park lands first the address is taken and the
|
|
70
|
+
* transaction fails — so it is read fresh in SolanaUserCalls rather than
|
|
71
|
+
* cached.
|
|
72
|
+
*
|
|
73
|
+
* `receiver` takes the minted shares when the request is forwarded; the
|
|
74
|
+
* depositor keeps the refund and the rent.
|
|
75
|
+
*/
|
|
76
|
+
static parkingLotDeposit(ctx: ISolanaVaultContext, depositor: PublicKey, assets: bigint, sequenceNumber: bigint, receiver: PublicKey): TransactionInstruction;
|
|
77
|
+
/**
|
|
78
|
+
* `parking_lot_cancel_deposit`: withdraws a request before it settles and
|
|
79
|
+
* returns the escrowed collateral, closing the account to refund its rent.
|
|
80
|
+
*
|
|
81
|
+
* Takes the request's OWN sequence number, not the lot's next one.
|
|
82
|
+
*/
|
|
83
|
+
static parkingLotCancelDeposit(ctx: ISolanaVaultContext, depositor: PublicKey, sequenceNumber: bigint): TransactionInstruction;
|
|
84
|
+
/**
|
|
85
|
+
* `parking_lot_claim_refund`: collects a refund that settlement could not
|
|
86
|
+
* deliver, for a request whose `claimable` flag is set.
|
|
87
|
+
*
|
|
88
|
+
* Identical account list to cancel — same four accounts, same order — so
|
|
89
|
+
* the two differ only in their discriminator and in what the program will
|
|
90
|
+
* accept.
|
|
91
|
+
*/
|
|
92
|
+
static parkingLotClaimRefund(ctx: ISolanaVaultContext, depositor: PublicKey, sequenceNumber: bigint): TransactionInstruction;
|
|
93
|
+
private static refundLike;
|
|
94
|
+
/**
|
|
95
|
+
* `parking_lot_process_deposits`: forwards up to `numRequests` from the
|
|
96
|
+
* head of the queue into the vault, minting shares to each receiver.
|
|
97
|
+
*
|
|
98
|
+
* Operator-signed. The per-request accounts are not listed here: the
|
|
99
|
+
* program walks the queue from `queue_start_index` and reads each request
|
|
100
|
+
* through remaining_accounts, which the caller appends.
|
|
101
|
+
*/
|
|
102
|
+
static parkingLotProcessDeposits(ctx: ISolanaVaultContext, operator: PublicKey, numRequests: bigint, remaining?: TransactionInstruction["keys"]): TransactionInstruction;
|
|
103
|
+
/** `parking_lot_process_deposits_up_to`: the same, bounded by sequence number. */
|
|
104
|
+
static parkingLotProcessDepositsUpTo(ctx: ISolanaVaultContext, operator: PublicKey, upToSequenceNumber: bigint, remaining?: TransactionInstruction["keys"]): TransactionInstruction;
|
|
105
|
+
private static processLike;
|
|
106
|
+
/**
|
|
107
|
+
* The four admin settings, which share one account list: config, vault,
|
|
108
|
+
* the lot, and the signer. They do NOT share one authority — the program
|
|
109
|
+
* decides, and the shapes being identical hides that:
|
|
110
|
+
*
|
|
111
|
+
* - `set_operator` — the vault admin.
|
|
112
|
+
* - `set_deposits_paused`, `set_cancellations_paused` — the GUARDIAN, via
|
|
113
|
+
* `verify_guardian`, which also fails closed while no guardian is set.
|
|
114
|
+
* - `set_deposit_allow_list` — the lot's operator.
|
|
115
|
+
*
|
|
116
|
+
* Signing a pause with the vault admin key is rejected on chain.
|
|
117
|
+
*/
|
|
118
|
+
private static lotSetting;
|
|
119
|
+
/** `parking_lot_set_operator`. */
|
|
120
|
+
static parkingLotSetOperator(ctx: ISolanaVaultContext, signer: PublicKey, newOperator: PublicKey): TransactionInstruction;
|
|
121
|
+
/** `parking_lot_set_deposits_paused`: blocks new parks only. */
|
|
122
|
+
static parkingLotSetDepositsPaused(ctx: ISolanaVaultContext, signer: PublicKey, paused: boolean): TransactionInstruction;
|
|
123
|
+
/** `parking_lot_set_cancellations_paused`: blocks cancellation only. */
|
|
124
|
+
static parkingLotSetCancellationsPaused(ctx: ISolanaVaultContext, signer: PublicKey, paused: boolean): TransactionInstruction;
|
|
125
|
+
/**
|
|
126
|
+
* `parking_lot_set_deposit_allow_list`: adds or removes one account.
|
|
127
|
+
* An empty list means the lot is open to everyone.
|
|
128
|
+
*
|
|
129
|
+
* Not built through `lotSetting`, unlike its three siblings: the list is
|
|
130
|
+
* unbounded, so adding an account CREATES its entry PDA and removing one
|
|
131
|
+
* closes it. That needs the entry and the system program, and makes the
|
|
132
|
+
* signer writable - it pays the rent, and is refunded on removal.
|
|
133
|
+
*/
|
|
134
|
+
static parkingLotSetDepositAllowList(ctx: ISolanaVaultContext, signer: PublicKey, account: PublicKey, allowed: boolean): TransactionInstruction;
|
|
62
135
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SystemProgram, TransactionInstruction } from "@solana/web3.js";
|
|
2
|
-
import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, associatedTokenAddress, deriveProtocolConfig, deriveVaultAccount, deriveVaultPdas, deriveWithdrawalRequest, eventCpiAccounts, instructionDiscriminator, u128le, u64le } from "../utils/pdas.js";
|
|
2
|
+
import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, associatedTokenAddress, deriveDepositRequest, deriveDepositUser, deriveParkingLot, deriveParkingLotEscrow, deriveParkingLotUser, deriveProtocolConfig, deriveVaultAccount, deriveVaultPdas, deriveWithdrawalRequest, deriveWithdrawalUser, eventCpiAccounts, instructionDiscriminator, u128le, u64le } from "../utils/pdas.js";
|
|
3
3
|
/**
|
|
4
4
|
* Builds the program's user instructions. Pure: it derives addresses and
|
|
5
5
|
* encodes data, and never touches the network or signs.
|
|
@@ -63,6 +63,15 @@ export class SolanaTxBuilder {
|
|
|
63
63
|
isSigner: false,
|
|
64
64
|
isWritable: true
|
|
65
65
|
},
|
|
66
|
+
{
|
|
67
|
+
// The depositor's deposit-allow-list entry, passed whether
|
|
68
|
+
// or not it exists: the list is unbounded and lives in one
|
|
69
|
+
// PDA per member, so the program reads this account to
|
|
70
|
+
// decide. Read-only, and never created here.
|
|
71
|
+
pubkey: deriveDepositUser(ctx.programId, pdas.vault, depositor),
|
|
72
|
+
isSigner: false,
|
|
73
|
+
isWritable: false
|
|
74
|
+
},
|
|
66
75
|
{
|
|
67
76
|
pubkey: associatedTokenAddress(depositor, ctx.collateralMint, ctx.collateralProgram),
|
|
68
77
|
isSigner: false,
|
|
@@ -136,6 +145,19 @@ export class SolanaTxBuilder {
|
|
|
136
145
|
isSigner: false,
|
|
137
146
|
isWritable: true
|
|
138
147
|
},
|
|
148
|
+
{
|
|
149
|
+
// Both ends of the withdrawal allow list. Settlement
|
|
150
|
+
// re-checks them, so a request whose owner or receiver was
|
|
151
|
+
// de-listed meanwhile is skipped rather than paid.
|
|
152
|
+
pubkey: deriveWithdrawalUser(ctx.programId, pdas.vault, owner),
|
|
153
|
+
isSigner: false,
|
|
154
|
+
isWritable: false
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
pubkey: deriveWithdrawalUser(ctx.programId, pdas.vault, receiver),
|
|
158
|
+
isSigner: false,
|
|
159
|
+
isWritable: false
|
|
160
|
+
},
|
|
139
161
|
{ pubkey: ctx.shareProgram, isSigner: false, isWritable: false },
|
|
140
162
|
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
141
163
|
...eventCpiAccounts(ctx.programId)
|
|
@@ -187,4 +209,258 @@ export class SolanaTxBuilder {
|
|
|
187
209
|
])
|
|
188
210
|
});
|
|
189
211
|
}
|
|
212
|
+
// --- parking lot ------------------------------------------------------
|
|
213
|
+
//
|
|
214
|
+
// All nine instructions build here. The three user ones are wired into
|
|
215
|
+
// SolanaUserCalls; the operator and admin ones have no call class yet
|
|
216
|
+
// (phases 3-4) but are pure instructions, so a caller can sign and send
|
|
217
|
+
// them today rather than wait for a surface that only wraps them.
|
|
218
|
+
/** The lot and its escrow, derived from the vault's share mint. */
|
|
219
|
+
static lotAccounts(ctx) {
|
|
220
|
+
const { vault } = deriveVaultPdas(ctx.programId, ctx.shareMint);
|
|
221
|
+
const parkingLot = deriveParkingLot(ctx.programId, vault);
|
|
222
|
+
return {
|
|
223
|
+
vault,
|
|
224
|
+
parkingLot,
|
|
225
|
+
escrow: deriveParkingLotEscrow(ctx.programId, parkingLot)
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* `parking_lot_deposit`: escrows collateral and queues a request.
|
|
230
|
+
*
|
|
231
|
+
* `sequenceNumber` MUST be the lot's current `sequence_number + 1`, since
|
|
232
|
+
* the program increments before it writes. That makes this racy by
|
|
233
|
+
* construction — if another park lands first the address is taken and the
|
|
234
|
+
* transaction fails — so it is read fresh in SolanaUserCalls rather than
|
|
235
|
+
* cached.
|
|
236
|
+
*
|
|
237
|
+
* `receiver` takes the minted shares when the request is forwarded; the
|
|
238
|
+
* depositor keeps the refund and the rent.
|
|
239
|
+
*/
|
|
240
|
+
static parkingLotDeposit(ctx, depositor, assets, sequenceNumber, receiver) {
|
|
241
|
+
const { vault, parkingLot, escrow } = SolanaTxBuilder.lotAccounts(ctx);
|
|
242
|
+
return new TransactionInstruction({
|
|
243
|
+
programId: ctx.programId,
|
|
244
|
+
keys: [
|
|
245
|
+
{
|
|
246
|
+
pubkey: deriveProtocolConfig(ctx.programId),
|
|
247
|
+
isSigner: false,
|
|
248
|
+
isWritable: false
|
|
249
|
+
},
|
|
250
|
+
{ pubkey: vault, isSigner: false, isWritable: false },
|
|
251
|
+
{ pubkey: parkingLot, isSigner: false, isWritable: true },
|
|
252
|
+
{
|
|
253
|
+
// The lot keeps its OWN allow list, separate from the
|
|
254
|
+
// vault's, but the entry is seeded by the vault.
|
|
255
|
+
pubkey: deriveParkingLotUser(ctx.programId, vault, depositor),
|
|
256
|
+
isSigner: false,
|
|
257
|
+
isWritable: false
|
|
258
|
+
},
|
|
259
|
+
{ pubkey: escrow, isSigner: false, isWritable: true },
|
|
260
|
+
{ pubkey: ctx.collateralMint, isSigner: false, isWritable: false },
|
|
261
|
+
{
|
|
262
|
+
pubkey: deriveDepositRequest(ctx.programId, parkingLot, sequenceNumber),
|
|
263
|
+
isSigner: false,
|
|
264
|
+
isWritable: true
|
|
265
|
+
},
|
|
266
|
+
{ pubkey: depositor, isSigner: true, isWritable: true },
|
|
267
|
+
{
|
|
268
|
+
pubkey: deriveVaultAccount(ctx.programId, vault, depositor),
|
|
269
|
+
isSigner: false,
|
|
270
|
+
isWritable: true
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
pubkey: associatedTokenAddress(depositor, ctx.collateralMint, ctx.collateralProgram),
|
|
274
|
+
isSigner: false,
|
|
275
|
+
isWritable: true
|
|
276
|
+
},
|
|
277
|
+
{ pubkey: ctx.collateralProgram, isSigner: false, isWritable: false },
|
|
278
|
+
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
279
|
+
...eventCpiAccounts(ctx.programId)
|
|
280
|
+
],
|
|
281
|
+
data: Buffer.concat([
|
|
282
|
+
instructionDiscriminator("parking_lot_deposit"),
|
|
283
|
+
u64le(assets),
|
|
284
|
+
receiver.toBuffer()
|
|
285
|
+
])
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* `parking_lot_cancel_deposit`: withdraws a request before it settles and
|
|
290
|
+
* returns the escrowed collateral, closing the account to refund its rent.
|
|
291
|
+
*
|
|
292
|
+
* Takes the request's OWN sequence number, not the lot's next one.
|
|
293
|
+
*/
|
|
294
|
+
static parkingLotCancelDeposit(ctx, depositor, sequenceNumber) {
|
|
295
|
+
return SolanaTxBuilder.refundLike(ctx, depositor, sequenceNumber, "parking_lot_cancel_deposit");
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* `parking_lot_claim_refund`: collects a refund that settlement could not
|
|
299
|
+
* deliver, for a request whose `claimable` flag is set.
|
|
300
|
+
*
|
|
301
|
+
* Identical account list to cancel — same four accounts, same order — so
|
|
302
|
+
* the two differ only in their discriminator and in what the program will
|
|
303
|
+
* accept.
|
|
304
|
+
*/
|
|
305
|
+
static parkingLotClaimRefund(ctx, depositor, sequenceNumber) {
|
|
306
|
+
return SolanaTxBuilder.refundLike(ctx, depositor, sequenceNumber, "parking_lot_claim_refund");
|
|
307
|
+
}
|
|
308
|
+
static refundLike(ctx, depositor, sequenceNumber, instruction) {
|
|
309
|
+
const { parkingLot, escrow } = SolanaTxBuilder.lotAccounts(ctx);
|
|
310
|
+
return new TransactionInstruction({
|
|
311
|
+
programId: ctx.programId,
|
|
312
|
+
keys: [
|
|
313
|
+
{
|
|
314
|
+
pubkey: deriveProtocolConfig(ctx.programId),
|
|
315
|
+
isSigner: false,
|
|
316
|
+
isWritable: false
|
|
317
|
+
},
|
|
318
|
+
{ pubkey: parkingLot, isSigner: false, isWritable: true },
|
|
319
|
+
{ pubkey: escrow, isSigner: false, isWritable: true },
|
|
320
|
+
{ pubkey: ctx.collateralMint, isSigner: false, isWritable: false },
|
|
321
|
+
{
|
|
322
|
+
pubkey: deriveDepositRequest(ctx.programId, parkingLot, sequenceNumber),
|
|
323
|
+
isSigner: false,
|
|
324
|
+
isWritable: true
|
|
325
|
+
},
|
|
326
|
+
{ pubkey: depositor, isSigner: true, isWritable: true },
|
|
327
|
+
{
|
|
328
|
+
pubkey: associatedTokenAddress(depositor, ctx.collateralMint, ctx.collateralProgram),
|
|
329
|
+
isSigner: false,
|
|
330
|
+
isWritable: true
|
|
331
|
+
},
|
|
332
|
+
{ pubkey: ctx.collateralProgram, isSigner: false, isWritable: false },
|
|
333
|
+
...eventCpiAccounts(ctx.programId)
|
|
334
|
+
],
|
|
335
|
+
data: Buffer.concat([
|
|
336
|
+
instructionDiscriminator(instruction),
|
|
337
|
+
u128le(sequenceNumber)
|
|
338
|
+
])
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* `parking_lot_process_deposits`: forwards up to `numRequests` from the
|
|
343
|
+
* head of the queue into the vault, minting shares to each receiver.
|
|
344
|
+
*
|
|
345
|
+
* Operator-signed. The per-request accounts are not listed here: the
|
|
346
|
+
* program walks the queue from `queue_start_index` and reads each request
|
|
347
|
+
* through remaining_accounts, which the caller appends.
|
|
348
|
+
*/
|
|
349
|
+
static parkingLotProcessDeposits(ctx, operator, numRequests, remaining = []) {
|
|
350
|
+
return SolanaTxBuilder.processLike(ctx, operator, "parking_lot_process_deposits", u64le(numRequests), remaining);
|
|
351
|
+
}
|
|
352
|
+
/** `parking_lot_process_deposits_up_to`: the same, bounded by sequence number. */
|
|
353
|
+
static parkingLotProcessDepositsUpTo(ctx, operator, upToSequenceNumber, remaining = []) {
|
|
354
|
+
return SolanaTxBuilder.processLike(ctx, operator, "parking_lot_process_deposits_up_to", u128le(upToSequenceNumber), remaining);
|
|
355
|
+
}
|
|
356
|
+
static processLike(ctx, operator, instruction, arg, remaining) {
|
|
357
|
+
const { vault, parkingLot, escrow } = SolanaTxBuilder.lotAccounts(ctx);
|
|
358
|
+
const pdas = deriveVaultPdas(ctx.programId, ctx.shareMint);
|
|
359
|
+
return new TransactionInstruction({
|
|
360
|
+
programId: ctx.programId,
|
|
361
|
+
keys: [
|
|
362
|
+
{
|
|
363
|
+
pubkey: deriveProtocolConfig(ctx.programId),
|
|
364
|
+
isSigner: false,
|
|
365
|
+
isWritable: false
|
|
366
|
+
},
|
|
367
|
+
{ pubkey: vault, isSigner: false, isWritable: true },
|
|
368
|
+
{ pubkey: parkingLot, isSigner: false, isWritable: true },
|
|
369
|
+
{ pubkey: escrow, isSigner: false, isWritable: true },
|
|
370
|
+
{ pubkey: ctx.collateralMint, isSigner: false, isWritable: false },
|
|
371
|
+
{ pubkey: ctx.shareMint, isSigner: false, isWritable: true },
|
|
372
|
+
{ pubkey: pdas.vaultCollateral, isSigner: false, isWritable: true },
|
|
373
|
+
{ pubkey: operator, isSigner: true, isWritable: false },
|
|
374
|
+
{ pubkey: ctx.collateralProgram, isSigner: false, isWritable: false },
|
|
375
|
+
{ pubkey: ctx.shareProgram, isSigner: false, isWritable: false },
|
|
376
|
+
...eventCpiAccounts(ctx.programId),
|
|
377
|
+
...remaining
|
|
378
|
+
],
|
|
379
|
+
data: Buffer.concat([instructionDiscriminator(instruction), arg])
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* The four admin settings, which share one account list: config, vault,
|
|
384
|
+
* the lot, and the signer. They do NOT share one authority — the program
|
|
385
|
+
* decides, and the shapes being identical hides that:
|
|
386
|
+
*
|
|
387
|
+
* - `set_operator` — the vault admin.
|
|
388
|
+
* - `set_deposits_paused`, `set_cancellations_paused` — the GUARDIAN, via
|
|
389
|
+
* `verify_guardian`, which also fails closed while no guardian is set.
|
|
390
|
+
* - `set_deposit_allow_list` — the lot's operator.
|
|
391
|
+
*
|
|
392
|
+
* Signing a pause with the vault admin key is rejected on chain.
|
|
393
|
+
*/
|
|
394
|
+
static lotSetting(ctx, signer, instruction, arg) {
|
|
395
|
+
const { vault, parkingLot } = SolanaTxBuilder.lotAccounts(ctx);
|
|
396
|
+
return new TransactionInstruction({
|
|
397
|
+
programId: ctx.programId,
|
|
398
|
+
keys: [
|
|
399
|
+
{
|
|
400
|
+
pubkey: deriveProtocolConfig(ctx.programId),
|
|
401
|
+
isSigner: false,
|
|
402
|
+
isWritable: false
|
|
403
|
+
},
|
|
404
|
+
{ pubkey: vault, isSigner: false, isWritable: false },
|
|
405
|
+
{ pubkey: parkingLot, isSigner: false, isWritable: true },
|
|
406
|
+
{ pubkey: signer, isSigner: true, isWritable: false },
|
|
407
|
+
...eventCpiAccounts(ctx.programId)
|
|
408
|
+
],
|
|
409
|
+
data: Buffer.concat([instructionDiscriminator(instruction), arg])
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
/** `parking_lot_set_operator`. */
|
|
413
|
+
static parkingLotSetOperator(ctx, signer, newOperator) {
|
|
414
|
+
return SolanaTxBuilder.lotSetting(ctx, signer, "parking_lot_set_operator", newOperator.toBuffer());
|
|
415
|
+
}
|
|
416
|
+
/** `parking_lot_set_deposits_paused`: blocks new parks only. */
|
|
417
|
+
static parkingLotSetDepositsPaused(ctx, signer, paused) {
|
|
418
|
+
return SolanaTxBuilder.lotSetting(ctx, signer, "parking_lot_set_deposits_paused", Buffer.from([paused ? 1 : 0]));
|
|
419
|
+
}
|
|
420
|
+
/** `parking_lot_set_cancellations_paused`: blocks cancellation only. */
|
|
421
|
+
static parkingLotSetCancellationsPaused(ctx, signer, paused) {
|
|
422
|
+
return SolanaTxBuilder.lotSetting(ctx, signer, "parking_lot_set_cancellations_paused", Buffer.from([paused ? 1 : 0]));
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* `parking_lot_set_deposit_allow_list`: adds or removes one account.
|
|
426
|
+
* An empty list means the lot is open to everyone.
|
|
427
|
+
*
|
|
428
|
+
* Not built through `lotSetting`, unlike its three siblings: the list is
|
|
429
|
+
* unbounded, so adding an account CREATES its entry PDA and removing one
|
|
430
|
+
* closes it. That needs the entry and the system program, and makes the
|
|
431
|
+
* signer writable - it pays the rent, and is refunded on removal.
|
|
432
|
+
*/
|
|
433
|
+
static parkingLotSetDepositAllowList(ctx, signer, account, allowed) {
|
|
434
|
+
const { vault, parkingLot } = SolanaTxBuilder.lotAccounts(ctx);
|
|
435
|
+
return new TransactionInstruction({
|
|
436
|
+
programId: ctx.programId,
|
|
437
|
+
keys: [
|
|
438
|
+
{
|
|
439
|
+
pubkey: deriveProtocolConfig(ctx.programId),
|
|
440
|
+
isSigner: false,
|
|
441
|
+
isWritable: false
|
|
442
|
+
},
|
|
443
|
+
{ pubkey: vault, isSigner: false, isWritable: false },
|
|
444
|
+
{ pubkey: parkingLot, isSigner: false, isWritable: true },
|
|
445
|
+
{
|
|
446
|
+
// The entry for the account being listed, not the signer.
|
|
447
|
+
pubkey: deriveParkingLotUser(ctx.programId, vault, account),
|
|
448
|
+
isSigner: false,
|
|
449
|
+
isWritable: true
|
|
450
|
+
},
|
|
451
|
+
{ pubkey: signer, isSigner: true, isWritable: true },
|
|
452
|
+
{
|
|
453
|
+
pubkey: SystemProgram.programId,
|
|
454
|
+
isSigner: false,
|
|
455
|
+
isWritable: false
|
|
456
|
+
},
|
|
457
|
+
...eventCpiAccounts(ctx.programId)
|
|
458
|
+
],
|
|
459
|
+
data: Buffer.concat([
|
|
460
|
+
instructionDiscriminator("parking_lot_set_deposit_allow_list"),
|
|
461
|
+
account.toBuffer(),
|
|
462
|
+
Buffer.from([allowed ? 1 : 0])
|
|
463
|
+
])
|
|
464
|
+
});
|
|
465
|
+
}
|
|
190
466
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Connection, PublicKey, type TransactionInstruction } from "@solana/web3.js";
|
|
2
|
-
import { type ISolanaWithdrawalRequest } from "../interfaces/index.js";
|
|
2
|
+
import { type ISolanaDepositRequest, type ISolanaWithdrawalRequest } from "../interfaces/index.js";
|
|
3
3
|
import { type SolanaSigner } from "../signers/solana-signer.js";
|
|
4
4
|
import { SolanaDeploymentParser } from "../utils/deployment-parser.js";
|
|
5
5
|
/** What a write returns. */
|
|
@@ -113,6 +113,56 @@ export declare class SolanaUserCalls {
|
|
|
113
113
|
* on every repeat deposit for nothing.
|
|
114
114
|
*/
|
|
115
115
|
private ensureShareAccount;
|
|
116
|
+
/**
|
|
117
|
+
* Parks a deposit: escrows collateral and queues a request for the
|
|
118
|
+
* operator to forward into the vault later. No shares are minted here.
|
|
119
|
+
*
|
|
120
|
+
* THIS CALL IS RACY BY CONSTRUCTION. The request's account address is
|
|
121
|
+
* seeded with the lot's `sequence_number + 1`, and the program increments
|
|
122
|
+
* before it writes, so two parks landing in the same slot collide and the
|
|
123
|
+
* loser fails its address check. The lot is read immediately before
|
|
124
|
+
* building to keep the window small, but it cannot be closed from here —
|
|
125
|
+
* on `ParkingLotSequenceTaken` the caller should simply call again, which
|
|
126
|
+
* re-reads and takes the next slot.
|
|
127
|
+
*
|
|
128
|
+
* No automatic retry: a resend after a failed SEND (rather than a failed
|
|
129
|
+
* simulation) could double-park if the first transaction actually landed.
|
|
130
|
+
*
|
|
131
|
+
* `receiver` takes the minted shares when the request is forwarded. The
|
|
132
|
+
* depositor keeps the refund and the account's rent either way.
|
|
133
|
+
*/
|
|
134
|
+
parkDeposit(shareMint: PublicKey | string, assets: SolanaAmount, options?: ISolanaRedeemOptions): Promise<ISolanaTxResponse & {
|
|
135
|
+
sequenceNumber: bigint;
|
|
136
|
+
}>;
|
|
137
|
+
/**
|
|
138
|
+
* Cancels a parked deposit before it settles, returning the escrowed
|
|
139
|
+
* collateral and the account's rent.
|
|
140
|
+
*
|
|
141
|
+
* Takes the request's own sequence number — from `getParkedDeposits` —
|
|
142
|
+
* not the lot's next one.
|
|
143
|
+
*/
|
|
144
|
+
cancelParkedDeposit(shareMint: PublicKey | string, sequenceNumber: SolanaAmount, options?: ISolanaWriteOptions): Promise<ISolanaTxResponse>;
|
|
145
|
+
/**
|
|
146
|
+
* Claims a refund that settlement could not deliver.
|
|
147
|
+
*
|
|
148
|
+
* Only for a request whose `claimable` flag is set — the request settled
|
|
149
|
+
* but the collateral could not be returned, so it waits in escrow under
|
|
150
|
+
* the lot's `totalClaimableAssets`. `getParkedDeposits` separates these
|
|
151
|
+
* from the ones still pending.
|
|
152
|
+
*/
|
|
153
|
+
claimParkedRefund(shareMint: PublicKey | string, sequenceNumber: SolanaAmount, options?: ISolanaWriteOptions): Promise<ISolanaTxResponse>;
|
|
154
|
+
/**
|
|
155
|
+
* This depositor's parked deposits, split into those still queued and
|
|
156
|
+
* those whose refund is waiting to be claimed.
|
|
157
|
+
*
|
|
158
|
+
* Found by filtering on the request accounts themselves rather than by
|
|
159
|
+
* walking the queue: a claimable request has already settled, so it sits
|
|
160
|
+
* BELOW the lot's head and no scan of the pending window would see it.
|
|
161
|
+
*/
|
|
162
|
+
getParkedDeposits(shareMint: PublicKey | string, owner?: PublicKey | string): Promise<{
|
|
163
|
+
pending: ISolanaDepositRequest[];
|
|
164
|
+
claimable: ISolanaDepositRequest[];
|
|
165
|
+
}>;
|
|
116
166
|
/**
|
|
117
167
|
* Queues a withdrawal. This does NOT return collateral: the shares are
|
|
118
168
|
* escrowed and a request is appended to the vault's queue, which the
|
|
@@ -205,15 +205,107 @@ export class SolanaUserCalls {
|
|
|
205
205
|
* missing: including it unconditionally costs a few hundred compute units
|
|
206
206
|
* on every repeat deposit for nothing.
|
|
207
207
|
*/
|
|
208
|
-
async ensureShareAccount(ctx, owner) {
|
|
208
|
+
async ensureShareAccount(ctx, owner, payer = owner) {
|
|
209
209
|
const ata = associatedTokenAddress(owner, ctx.shareMint, ctx.shareProgram);
|
|
210
210
|
const existing = await this.connection.getAccountInfo(ata);
|
|
211
211
|
if (existing !== null)
|
|
212
212
|
return [];
|
|
213
213
|
return [
|
|
214
|
-
SolanaTxBuilder.createAtaIdempotent(
|
|
214
|
+
SolanaTxBuilder.createAtaIdempotent(payer, owner, ctx.shareMint, ctx.shareProgram)
|
|
215
215
|
];
|
|
216
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
|
+
}
|
|
217
309
|
// --- withdrawals ---
|
|
218
310
|
/**
|
|
219
311
|
* Queues a withdrawal. This does NOT return collateral: the shares are
|
|
@@ -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. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
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";
|
|
2
|
+
import { decodeDepositParkingLot, decodeDepositRequest, decodeMint, decodeProtocolConfig, decodeVault, decodeVaultAccount, decodeWithdrawalQueue, decodeWithdrawalRequestAccount } from "../utils/borsh.js";
|
|
3
|
+
import { DEPOSIT_REQUEST_OFFSETS, DEPOSIT_REQUEST_SIZE, MAX_MULTIPLE_ACCOUNTS, MAX_QUEUE_SCAN, accountDiscriminator, associatedTokenAddress, deriveDepositRequest, deriveDepositUser, deriveParkingLot, deriveParkingLotEscrow, deriveVaultAccount, deriveWithdrawalUser } from "../utils/pdas.js";
|
|
4
4
|
import { amountFromShares, amountFromSharesFloor, maxDepositAmount, maxMintShares, sharesFromAmount, sharesFromAmountCeil, vaultTvl, withdrawalFee } from "../utils/math.js";
|
|
5
5
|
/**
|
|
6
6
|
* Reads vault state from the chain.
|
|
@@ -29,6 +29,22 @@ export class SolanaVaultReader {
|
|
|
29
29
|
key(value) {
|
|
30
30
|
return typeof value === "string" ? new PublicKey(value) : value;
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Whether an account is on one of the unbounded allow lists.
|
|
34
|
+
*
|
|
35
|
+
* The program's own test is `UserListEntry::exists`: owned by the program
|
|
36
|
+
* AND non-empty. A bare "the account exists" is weaker and wrong here -
|
|
37
|
+
* these addresses are public and derivable, so anyone can make
|
|
38
|
+
* `getAccountInfo` return non-null for one by sending it lamports. That
|
|
39
|
+
* system-owned, dataless account would read as listed and then be rejected
|
|
40
|
+
* on chain, which is a confusing false positive on a path users hit
|
|
41
|
+
* directly.
|
|
42
|
+
*/
|
|
43
|
+
async isListed(derive, shareMint, account) {
|
|
44
|
+
const entry = derive(this.programId, this.getVaultPdas(shareMint).vault, this.key(account));
|
|
45
|
+
const info = await this.connection.getAccountInfo(entry);
|
|
46
|
+
return info !== null && info.owner.equals(this.programId) && info.data.length > 0;
|
|
47
|
+
}
|
|
32
48
|
// --- raw state ---
|
|
33
49
|
/** The protocol config, or null when the protocol is not initialised. */
|
|
34
50
|
async getProtocolConfig() {
|
|
@@ -238,32 +254,35 @@ export class SolanaVaultReader {
|
|
|
238
254
|
return sharesFromAmountCeil(assets, (await this.getVaultState(shareMint)).rate.value);
|
|
239
255
|
}
|
|
240
256
|
async maxDeposit(shareMint, owner) {
|
|
241
|
-
const [vault, config, supply] = await Promise.all([
|
|
257
|
+
const [vault, config, supply, listed] = await Promise.all([
|
|
242
258
|
this.getVaultState(shareMint),
|
|
243
259
|
this.getProtocolConfig(),
|
|
244
|
-
this.getTotalSupply(shareMint)
|
|
260
|
+
this.getTotalSupply(shareMint),
|
|
261
|
+
this.isListed(deriveDepositUser, shareMint, owner)
|
|
245
262
|
]);
|
|
246
|
-
if (!this.canDeposit(vault, config, this.key(owner)))
|
|
263
|
+
if (!this.canDeposit(vault, config, this.key(owner), listed))
|
|
247
264
|
return 0n;
|
|
248
265
|
return maxDepositAmount(supply, vault.rate.value, vault.maxTvl);
|
|
249
266
|
}
|
|
250
267
|
async maxMint(shareMint, owner) {
|
|
251
|
-
const [vault, config, supply] = await Promise.all([
|
|
268
|
+
const [vault, config, supply, listed] = await Promise.all([
|
|
252
269
|
this.getVaultState(shareMint),
|
|
253
270
|
this.getProtocolConfig(),
|
|
254
|
-
this.getTotalSupply(shareMint)
|
|
271
|
+
this.getTotalSupply(shareMint),
|
|
272
|
+
this.isListed(deriveDepositUser, shareMint, owner)
|
|
255
273
|
]);
|
|
256
|
-
if (!this.canDeposit(vault, config, this.key(owner)))
|
|
274
|
+
if (!this.canDeposit(vault, config, this.key(owner), listed))
|
|
257
275
|
return 0n;
|
|
258
276
|
return maxMintShares(supply, vault.rate.value, vault.maxTvl);
|
|
259
277
|
}
|
|
260
278
|
async maxRedeem(shareMint, owner) {
|
|
261
|
-
const [vault, config, balance] = await Promise.all([
|
|
279
|
+
const [vault, config, balance, listed] = await Promise.all([
|
|
262
280
|
this.getVaultState(shareMint),
|
|
263
281
|
this.getProtocolConfig(),
|
|
264
|
-
this.balanceOf(shareMint, owner)
|
|
282
|
+
this.balanceOf(shareMint, owner),
|
|
283
|
+
this.isListed(deriveWithdrawalUser, shareMint, owner)
|
|
265
284
|
]);
|
|
266
|
-
if (!this.canWithdraw(vault, config, this.key(owner)))
|
|
285
|
+
if (!this.canWithdraw(vault, config, this.key(owner), listed))
|
|
267
286
|
return 0n;
|
|
268
287
|
// Below the minimum the whole balance is unredeemable, not merely capped.
|
|
269
288
|
return balance < vault.minWithdrawalShares ? 0n : balance;
|
|
@@ -282,7 +301,7 @@ export class SolanaVaultReader {
|
|
|
282
301
|
: amountFromSharesFloor(redeemable, vault.rate.value);
|
|
283
302
|
}
|
|
284
303
|
/** `quote.rs::can_deposit`. */
|
|
285
|
-
canDeposit(vault, config, owner) {
|
|
304
|
+
canDeposit(vault, config, owner, listed) {
|
|
286
305
|
if (config?.pauseNonAdminOperations)
|
|
287
306
|
return false;
|
|
288
307
|
if (vault.paused || vault.pausedDeposits)
|
|
@@ -293,22 +312,164 @@ export class SolanaVaultReader {
|
|
|
293
312
|
return false;
|
|
294
313
|
// A bound parking lot closes the direct path to everyone not listed;
|
|
295
314
|
// with no lot, an empty list is open to all.
|
|
296
|
-
if (!vault.parkingLot.equals(PublicKey.default))
|
|
297
|
-
return
|
|
298
|
-
|
|
299
|
-
return (vault.depositUserList.length === 0 ||
|
|
300
|
-
vault.depositUserList.some(a => a.equals(owner)));
|
|
315
|
+
if (!vault.parkingLot.equals(PublicKey.default))
|
|
316
|
+
return listed;
|
|
317
|
+
return vault.depositUserCount === 0n || listed;
|
|
301
318
|
}
|
|
302
319
|
/** `quote.rs::can_withdraw`. */
|
|
303
|
-
canWithdraw(vault, config, owner) {
|
|
320
|
+
canWithdraw(vault, config, owner, listed) {
|
|
304
321
|
if (config?.pauseNonAdminOperations)
|
|
305
322
|
return false;
|
|
306
323
|
if (vault.paused || vault.pausedWithdrawals)
|
|
307
324
|
return false;
|
|
308
325
|
if (vault.blacklisted.some(a => a.equals(owner)))
|
|
309
326
|
return false;
|
|
310
|
-
return
|
|
311
|
-
|
|
327
|
+
return vault.withdrawalUserCount === 0n || listed;
|
|
328
|
+
}
|
|
329
|
+
// --- parking lot ---
|
|
330
|
+
/** The lot's address for a vault, whether or not one exists. */
|
|
331
|
+
parkingLotAddress(shareMint) {
|
|
332
|
+
return deriveParkingLot(this.programId, this.getVaultPdas(shareMint).vault);
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* A vault's parking lot, or null when it has none.
|
|
336
|
+
*
|
|
337
|
+
* Null is the ordinary case: a lot is opt-in per vault, so most vaults have
|
|
338
|
+
* no account at this address at all.
|
|
339
|
+
*/
|
|
340
|
+
async getParkingLot(shareMint) {
|
|
341
|
+
const address = this.parkingLotAddress(shareMint);
|
|
342
|
+
const account = await this.connection.getAccountInfo(address);
|
|
343
|
+
return account === null ? null : decodeDepositParkingLot(account.data, address);
|
|
344
|
+
}
|
|
345
|
+
/** The lot's escrow token account. */
|
|
346
|
+
parkingLotEscrowAddress(shareMint) {
|
|
347
|
+
return deriveParkingLotEscrow(this.programId, this.parkingLotAddress(shareMint));
|
|
348
|
+
}
|
|
349
|
+
/** One parked deposit by its sequence number, or null if it is gone. */
|
|
350
|
+
async getDepositRequest(shareMint, sequenceNumber) {
|
|
351
|
+
const address = deriveDepositRequest(this.programId, this.parkingLotAddress(shareMint), sequenceNumber);
|
|
352
|
+
const account = await this.connection.getAccountInfo(address);
|
|
353
|
+
return account === null ? null : decodeDepositRequest(account.data, address);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* A page of the lot's queue, oldest first, with an explicit cursor.
|
|
357
|
+
*
|
|
358
|
+
* `limit` counts SLOTS SCANNED, not requests returned, because a slot may
|
|
359
|
+
* be empty: a cancelled request's account is closed while its number stays
|
|
360
|
+
* in the range. `scannedTo` is therefore returned separately and is always
|
|
361
|
+
* advanced — a page of zero requests still tells the caller where to
|
|
362
|
+
* resume, which an empty array alone could not.
|
|
363
|
+
*
|
|
364
|
+
* Reads are batched at `MAX_MULTIPLE_ACCOUNTS`: `getMultipleAccounts`
|
|
365
|
+
* refuses more than 100 pubkeys per call, so a larger window is split
|
|
366
|
+
* rather than failing.
|
|
367
|
+
*
|
|
368
|
+
* This window is `(queueStartIndex, sequenceNumber]` — the UNSETTLED part
|
|
369
|
+
* of the queue. Requests whose refund is waiting to be claimed sit BELOW
|
|
370
|
+
* the head and are not here; use `getRequestsByDepositor` for those.
|
|
371
|
+
*/
|
|
372
|
+
async getPendingDeposits(shareMint, options) {
|
|
373
|
+
const lot = await this.getParkingLot(shareMint);
|
|
374
|
+
if (lot === null) {
|
|
375
|
+
return { requests: [], scannedTo: 0n, hasMore: false };
|
|
376
|
+
}
|
|
377
|
+
// NaN survives trunc/max/min, and `slots.length < NaN` is false, so the
|
|
378
|
+
// scan loop would never run and `slots[-1].seq` would throw below.
|
|
379
|
+
const rawLimit = options?.limit;
|
|
380
|
+
const limit = Number.isFinite(rawLimit)
|
|
381
|
+
? Math.min(Math.max(Math.trunc(rawLimit), 1), MAX_QUEUE_SCAN)
|
|
382
|
+
: MAX_MULTIPLE_ACCOUNTS;
|
|
383
|
+
// Clamp up to the head. Below it only CLAIMABLE requests survive (the
|
|
384
|
+
// program settles the rest and the head only moves forward), so an
|
|
385
|
+
// `after` behind the head would walk settled slots and report their
|
|
386
|
+
// requests as pending. Clamping also keeps the cursor monotonic: an
|
|
387
|
+
// `after` past the tail resumes at itself rather than being dragged
|
|
388
|
+
// back to `sequenceNumber`.
|
|
389
|
+
const after = options?.after ?? lot.queueStartIndex;
|
|
390
|
+
const resume = after > lot.queueStartIndex ? after : lot.queueStartIndex;
|
|
391
|
+
const from = resume + 1n;
|
|
392
|
+
if (from > lot.sequenceNumber) {
|
|
393
|
+
return { requests: [], scannedTo: resume, hasMore: false };
|
|
394
|
+
}
|
|
395
|
+
const slots = [];
|
|
396
|
+
for (let seq = from; seq <= lot.sequenceNumber && slots.length < limit; seq++) {
|
|
397
|
+
slots.push({
|
|
398
|
+
seq,
|
|
399
|
+
address: deriveDepositRequest(this.programId, lot.address, seq)
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
const requests = [];
|
|
403
|
+
for (let i = 0; i < slots.length; i += MAX_MULTIPLE_ACCOUNTS) {
|
|
404
|
+
const batch = slots.slice(i, i + MAX_MULTIPLE_ACCOUNTS);
|
|
405
|
+
const accounts = await this.connection.getMultipleAccountsInfo(batch.map(b => b.address));
|
|
406
|
+
accounts.forEach((account, j) => {
|
|
407
|
+
if (account === null)
|
|
408
|
+
return; // cancelled or settled: slot closed
|
|
409
|
+
requests.push(decodeDepositRequest(account.data, batch[j].address));
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
const scannedTo = slots[slots.length - 1].seq;
|
|
413
|
+
return { requests, scannedTo, hasMore: scannedTo < lot.sequenceNumber };
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Every surviving request belonging to one depositor, wherever it sits in
|
|
417
|
+
* the queue.
|
|
418
|
+
*
|
|
419
|
+
* Position-independent on purpose. A CLAIMABLE request — one that settled
|
|
420
|
+
* but whose refund could not be delivered — lives BELOW `queueStartIndex`
|
|
421
|
+
* (see parking_lot.rs: "below the head only claimable requests remain"),
|
|
422
|
+
* so no scan of the pending window can ever find one. Walking downwards
|
|
423
|
+
* instead is unbounded and mostly hits closed accounts.
|
|
424
|
+
*
|
|
425
|
+
* So this filters on chain by the account's own bytes: the request's size,
|
|
426
|
+
* its parking lot at offset 9 and its depositor at offset 57. Those
|
|
427
|
+
* offsets track `DepositRequest` in state.rs and are pinned by test.
|
|
428
|
+
*
|
|
429
|
+
* `getProgramAccounts` is an indexed scan and some public RPCs restrict
|
|
430
|
+
* it; a provider endpoint is the right place to run this.
|
|
431
|
+
*/
|
|
432
|
+
async getRequestsByDepositor(shareMint, depositor) {
|
|
433
|
+
const lot = await this.getParkingLot(shareMint);
|
|
434
|
+
if (lot === null)
|
|
435
|
+
return [];
|
|
436
|
+
const accounts = await this.connection.getProgramAccounts(this.programId, {
|
|
437
|
+
filters: [
|
|
438
|
+
{ dataSize: DEPOSIT_REQUEST_SIZE },
|
|
439
|
+
{
|
|
440
|
+
memcmp: {
|
|
441
|
+
offset: DEPOSIT_REQUEST_OFFSETS.parkingLot,
|
|
442
|
+
bytes: lot.address.toBase58()
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
memcmp: {
|
|
447
|
+
offset: DEPOSIT_REQUEST_OFFSETS.depositor,
|
|
448
|
+
bytes: this.key(depositor).toBase58()
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
]
|
|
452
|
+
});
|
|
453
|
+
return accounts
|
|
454
|
+
.map(({ pubkey, account }) => decodeDepositRequest(account.data, pubkey))
|
|
455
|
+
.sort((a, b) => a.sequenceNumber < b.sequenceNumber
|
|
456
|
+
? -1
|
|
457
|
+
: a.sequenceNumber > b.sequenceNumber
|
|
458
|
+
? 1
|
|
459
|
+
: 0);
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* A depositor's requests, split by what they can do with them.
|
|
463
|
+
*
|
|
464
|
+
* `pending` is still queued and can be cancelled; `claimable` has settled
|
|
465
|
+
* with an undeliverable refund and is collected with `claim_refund`.
|
|
466
|
+
*/
|
|
467
|
+
async getDepositsFor(shareMint, depositor) {
|
|
468
|
+
const mine = await this.getRequestsByDepositor(shareMint, depositor);
|
|
469
|
+
return {
|
|
470
|
+
pending: mine.filter(r => !r.claimable),
|
|
471
|
+
claimable: mine.filter(r => r.claimable)
|
|
472
|
+
};
|
|
312
473
|
}
|
|
313
474
|
// --- withdrawals ---
|
|
314
475
|
/** An owner's vault account, or null before their first deposit or redeem. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PublicKey } from "@solana/web3.js";
|
|
2
|
-
import type { ISolanaProtocolConfigState, ISolanaVaultAccount, ISolanaVaultState, ISolanaWithdrawalQueue, ISolanaWithdrawalRequestAccount } from "../interfaces/index.js";
|
|
2
|
+
import type { ISolanaDepositRequest, ISolanaParkingLotState, ISolanaProtocolConfigState, ISolanaVaultAccount, ISolanaVaultState, ISolanaWithdrawalQueue, ISolanaWithdrawalRequestAccount } from "../interfaces/index.js";
|
|
3
3
|
/**
|
|
4
4
|
* Borsh decoding for `ember_vaults` accounts.
|
|
5
5
|
*
|
|
@@ -62,4 +62,15 @@ export interface ISplMint {
|
|
|
62
62
|
* The classic 82-byte SPL mint layout. The first 82 bytes of a Token-2022
|
|
63
63
|
* mint are identical, so this reads both; it just ignores any extensions.
|
|
64
64
|
*/
|
|
65
|
+
/**
|
|
66
|
+
* `state::DepositParkingLot`.
|
|
67
|
+
*
|
|
68
|
+
* Field order tracks the Rust struct and is load-bearing: a transposition
|
|
69
|
+
* decodes into plausible numbers rather than failing. `sequence_number` and
|
|
70
|
+
* `queue_start_index` are u128 - sixteen bytes each, not eight - so reading
|
|
71
|
+
* either as a u64 would silently shift every field after it.
|
|
72
|
+
*/
|
|
73
|
+
export declare function decodeDepositParkingLot(data: Buffer, address: PublicKey): ISolanaParkingLotState;
|
|
74
|
+
/** `state::DepositRequest`, one parked deposit. */
|
|
75
|
+
export declare function decodeDepositRequest(data: Buffer, address: PublicKey): ISolanaDepositRequest;
|
|
65
76
|
export declare function decodeMint(data: Buffer): ISplMint;
|
|
@@ -128,8 +128,11 @@ function decodeVaultInner(data) {
|
|
|
128
128
|
const maxTvl = c.u64();
|
|
129
129
|
const sequenceNumber = c.u128();
|
|
130
130
|
const parkingLot = c.pubkey();
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
// Counts, not members: both lists became one PDA per account, so the vault
|
|
132
|
+
// carries only their size. 0 means open to all; use deriveDepositUser /
|
|
133
|
+
// deriveWithdrawalUser to test one account.
|
|
134
|
+
const depositUserCount = c.u64();
|
|
135
|
+
const withdrawalUserCount = c.u64();
|
|
133
136
|
// The tail the parking-lot change appended. Decoded rather than skipped:
|
|
134
137
|
// a vault with fees configured would otherwise be indistinguishable from a
|
|
135
138
|
// fee-free one.
|
|
@@ -161,8 +164,8 @@ function decodeVaultInner(data) {
|
|
|
161
164
|
maxTvl,
|
|
162
165
|
sequenceNumber,
|
|
163
166
|
parkingLot,
|
|
164
|
-
|
|
165
|
-
|
|
167
|
+
depositUserCount,
|
|
168
|
+
withdrawalUserCount,
|
|
166
169
|
withdrawalFee
|
|
167
170
|
};
|
|
168
171
|
return { vault, position: c.position() };
|
|
@@ -225,6 +228,45 @@ export function decodeProtocolConfig(data) {
|
|
|
225
228
|
* The classic 82-byte SPL mint layout. The first 82 bytes of a Token-2022
|
|
226
229
|
* mint are identical, so this reads both; it just ignores any extensions.
|
|
227
230
|
*/
|
|
231
|
+
/**
|
|
232
|
+
* `state::DepositParkingLot`.
|
|
233
|
+
*
|
|
234
|
+
* Field order tracks the Rust struct and is load-bearing: a transposition
|
|
235
|
+
* decodes into plausible numbers rather than failing. `sequence_number` and
|
|
236
|
+
* `queue_start_index` are u128 - sixteen bytes each, not eight - so reading
|
|
237
|
+
* either as a u64 would silently shift every field after it.
|
|
238
|
+
*/
|
|
239
|
+
export function decodeDepositParkingLot(data, address) {
|
|
240
|
+
const c = expect(data, "DepositParkingLot");
|
|
241
|
+
return {
|
|
242
|
+
address,
|
|
243
|
+
bump: c.u8(),
|
|
244
|
+
vault: c.pubkey(),
|
|
245
|
+
operator: c.pubkey(),
|
|
246
|
+
depositsPaused: c.bool(),
|
|
247
|
+
cancellationsPaused: c.bool(),
|
|
248
|
+
sequenceNumber: c.u128(),
|
|
249
|
+
queueStartIndex: c.u128(),
|
|
250
|
+
totalPendingAssets: c.u64(),
|
|
251
|
+
totalClaimableAssets: c.u64(),
|
|
252
|
+
depositAllowListCount: c.u64()
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/** `state::DepositRequest`, one parked deposit. */
|
|
256
|
+
export function decodeDepositRequest(data, address) {
|
|
257
|
+
const c = expect(data, "DepositRequest");
|
|
258
|
+
return {
|
|
259
|
+
address,
|
|
260
|
+
bump: c.u8(),
|
|
261
|
+
parkingLot: c.pubkey(),
|
|
262
|
+
sequenceNumber: c.u128(),
|
|
263
|
+
depositor: c.pubkey(),
|
|
264
|
+
receiver: c.pubkey(),
|
|
265
|
+
assets: c.u64(),
|
|
266
|
+
timestamp: c.u64(),
|
|
267
|
+
claimable: c.bool()
|
|
268
|
+
};
|
|
269
|
+
}
|
|
228
270
|
export function decodeMint(data) {
|
|
229
271
|
const option = (o) => data.readUInt32LE(o) === 1 ? new PublicKey(data.subarray(o + 4, o + 36)) : null;
|
|
230
272
|
return {
|
|
@@ -32,6 +32,67 @@ export declare const deriveVaultAccount: (programId: PublicKey, vault: PublicKey
|
|
|
32
32
|
* nonce 0.
|
|
33
33
|
*/
|
|
34
34
|
export declare const deriveWithdrawalRequest: (programId: PublicKey, vault: PublicKey, owner: PublicKey, nonce: bigint) => PublicKey;
|
|
35
|
+
/**
|
|
36
|
+
* `getMultipleAccounts` refuses more than 100 pubkeys per call, so any wider
|
|
37
|
+
* window has to be split rather than sent whole.
|
|
38
|
+
*/
|
|
39
|
+
export declare const MAX_MULTIPLE_ACCOUNTS = 100;
|
|
40
|
+
/** An upper bound on one queue scan, so a caller cannot ask for an unbounded walk. */
|
|
41
|
+
export declare const MAX_QUEUE_SCAN = 1000;
|
|
42
|
+
/** `DepositRequest` is fixed width, which lets getProgramAccounts filter on it. */
|
|
43
|
+
export declare const DEPOSIT_REQUEST_SIZE = 138;
|
|
44
|
+
/**
|
|
45
|
+
* Byte offsets into a `DepositRequest` account, discriminator included.
|
|
46
|
+
*
|
|
47
|
+
* Used as memcmp filters, so they track state.rs exactly: an offset that
|
|
48
|
+
* drifts returns nothing rather than failing, which looks like "no requests".
|
|
49
|
+
*/
|
|
50
|
+
export declare const DEPOSIT_REQUEST_OFFSETS: {
|
|
51
|
+
/** after discriminator(8) + bump(1) */
|
|
52
|
+
readonly parkingLot: 9;
|
|
53
|
+
/** after parking_lot(32) + sequence_number(16) */
|
|
54
|
+
readonly depositor: 57;
|
|
55
|
+
/** after receiver(32) + assets(8) + timestamp(8) */
|
|
56
|
+
readonly claimable: 137;
|
|
57
|
+
};
|
|
58
|
+
/** Membership of a vault's deposit allow list. Scoped to the vault. */
|
|
59
|
+
export declare const deriveDepositUser: (programId: PublicKey, vault: PublicKey, account: PublicKey) => PublicKey;
|
|
60
|
+
/** Membership of a vault's withdrawal allow list. Scoped to the vault. */
|
|
61
|
+
export declare const deriveWithdrawalUser: (programId: PublicKey, vault: PublicKey, account: PublicKey) => PublicKey;
|
|
62
|
+
/**
|
|
63
|
+
* Membership of a parking lot's own deposit allow list.
|
|
64
|
+
*
|
|
65
|
+
* Scoped to the VAULT, not the lot, matching the program's
|
|
66
|
+
* `[PARKING_LOT_USER_SEED, vault, account]`.
|
|
67
|
+
*/
|
|
68
|
+
export declare const deriveParkingLotUser: (programId: PublicKey, vault: PublicKey, account: PublicKey) => PublicKey;
|
|
69
|
+
/**
|
|
70
|
+
* A vault's deposit parking lot: where async deposits wait to be forwarded.
|
|
71
|
+
*
|
|
72
|
+
* One lot per vault, so this is derivable from the vault alone. A vault with
|
|
73
|
+
* no lot simply has no account at this address.
|
|
74
|
+
*/
|
|
75
|
+
export declare const deriveParkingLot: (programId: PublicKey, vault: PublicKey) => PublicKey;
|
|
76
|
+
/**
|
|
77
|
+
* The lot's escrow token account, holding collateral for pending requests.
|
|
78
|
+
*
|
|
79
|
+
* It holds `total_pending_assets + total_claimable_assets`: the second is
|
|
80
|
+
* Solana-only, owed to depositors whose refund could not be delivered.
|
|
81
|
+
*/
|
|
82
|
+
export declare const deriveParkingLotEscrow: (programId: PublicKey, parkingLot: PublicKey) => PublicKey;
|
|
83
|
+
/**
|
|
84
|
+
* One parked deposit, keyed by its queue sequence number.
|
|
85
|
+
*
|
|
86
|
+
* The seed is a u128 (16 bytes LE), not the u64 the withdrawal request uses.
|
|
87
|
+
*
|
|
88
|
+
* WHEN PARKING, the sequence number is the lot's `sequence_number` PLUS ONE -
|
|
89
|
+
* the slot the next park will take, since the program increments before it
|
|
90
|
+
* writes. That makes a park racy by construction: if another lands first this
|
|
91
|
+
* address is already in use and the transaction fails its address check, and
|
|
92
|
+
* must be rebuilt against a fresh read. Cancelling or claiming an EXISTING
|
|
93
|
+
* request uses that request's own recorded sequence number instead.
|
|
94
|
+
*/
|
|
95
|
+
export declare const deriveDepositRequest: (programId: PublicKey, parkingLot: PublicKey, sequenceNumber: bigint) => PublicKey;
|
|
35
96
|
/**
|
|
36
97
|
* The event authority `emit_cpi!` signs with. Anchor requires it and the
|
|
37
98
|
* program id as the last two accounts of every instruction that emits.
|
|
@@ -76,11 +76,86 @@ export const deriveWithdrawalRequest = (programId, vault, owner, nonce) => Publi
|
|
|
76
76
|
owner.toBuffer(),
|
|
77
77
|
u64le(nonce)
|
|
78
78
|
], programId)[0];
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
/**
|
|
80
|
+
* `getMultipleAccounts` refuses more than 100 pubkeys per call, so any wider
|
|
81
|
+
* window has to be split rather than sent whole.
|
|
82
|
+
*/
|
|
83
|
+
export const MAX_MULTIPLE_ACCOUNTS = 100;
|
|
84
|
+
/** An upper bound on one queue scan, so a caller cannot ask for an unbounded walk. */
|
|
85
|
+
export const MAX_QUEUE_SCAN = 1000;
|
|
86
|
+
/** `DepositRequest` is fixed width, which lets getProgramAccounts filter on it. */
|
|
87
|
+
export const DEPOSIT_REQUEST_SIZE = 138;
|
|
88
|
+
/**
|
|
89
|
+
* Byte offsets into a `DepositRequest` account, discriminator included.
|
|
90
|
+
*
|
|
91
|
+
* Used as memcmp filters, so they track state.rs exactly: an offset that
|
|
92
|
+
* drifts returns nothing rather than failing, which looks like "no requests".
|
|
93
|
+
*/
|
|
94
|
+
export const DEPOSIT_REQUEST_OFFSETS = {
|
|
95
|
+
/** after discriminator(8) + bump(1) */
|
|
96
|
+
parkingLot: 9,
|
|
97
|
+
/** after parking_lot(32) + sequence_number(16) */
|
|
98
|
+
depositor: 57,
|
|
99
|
+
/** after receiver(32) + assets(8) + timestamp(8) */
|
|
100
|
+
claimable: 137
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* An allow-list entry: one account per listed member.
|
|
104
|
+
*
|
|
105
|
+
* The three lists are unbounded, so membership is not a `Vec` on the vault or
|
|
106
|
+
* the lot but the EXISTENCE of this PDA. The program's test is
|
|
107
|
+
* `UserListEntry::exists` - owned by the program and non-empty - which is
|
|
108
|
+
* stricter than "the account exists": these addresses are public and
|
|
109
|
+
* derivable, so anyone can make a bare `getAccountInfo` return non-null by
|
|
110
|
+
* sending lamports to one. A system-owned account with no data reads as
|
|
111
|
+
* listed to a naive check and is rejected on chain.
|
|
112
|
+
*
|
|
113
|
+
* Every gated instruction takes the caller's entry whether or not it exists,
|
|
114
|
+
* so these are always derivable without a network read.
|
|
115
|
+
*/
|
|
116
|
+
const deriveUserEntry = (programId, seedName, scope, account) => PublicKey.findProgramAddressSync([seed(seedName), scope.toBuffer(), account.toBuffer()], programId)[0];
|
|
117
|
+
/** Membership of a vault's deposit allow list. Scoped to the vault. */
|
|
118
|
+
export const deriveDepositUser = (programId, vault, account) => deriveUserEntry(programId, SOLANA_SEEDS.depositUser, vault, account);
|
|
119
|
+
/** Membership of a vault's withdrawal allow list. Scoped to the vault. */
|
|
120
|
+
export const deriveWithdrawalUser = (programId, vault, account) => deriveUserEntry(programId, SOLANA_SEEDS.withdrawalUser, vault, account);
|
|
121
|
+
/**
|
|
122
|
+
* Membership of a parking lot's own deposit allow list.
|
|
123
|
+
*
|
|
124
|
+
* Scoped to the VAULT, not the lot, matching the program's
|
|
125
|
+
* `[PARKING_LOT_USER_SEED, vault, account]`.
|
|
126
|
+
*/
|
|
127
|
+
export const deriveParkingLotUser = (programId, vault, account) => deriveUserEntry(programId, SOLANA_SEEDS.parkingLotUser, vault, account);
|
|
128
|
+
/**
|
|
129
|
+
* A vault's deposit parking lot: where async deposits wait to be forwarded.
|
|
130
|
+
*
|
|
131
|
+
* One lot per vault, so this is derivable from the vault alone. A vault with
|
|
132
|
+
* no lot simply has no account at this address.
|
|
133
|
+
*/
|
|
134
|
+
export const deriveParkingLot = (programId, vault) => PublicKey.findProgramAddressSync([seed(SOLANA_SEEDS.parkingLot), vault.toBuffer()], programId)[0];
|
|
135
|
+
/**
|
|
136
|
+
* The lot's escrow token account, holding collateral for pending requests.
|
|
137
|
+
*
|
|
138
|
+
* It holds `total_pending_assets + total_claimable_assets`: the second is
|
|
139
|
+
* Solana-only, owed to depositors whose refund could not be delivered.
|
|
140
|
+
*/
|
|
141
|
+
export const deriveParkingLotEscrow = (programId, parkingLot) => PublicKey.findProgramAddressSync([seed(SOLANA_SEEDS.parkingLotEscrow), parkingLot.toBuffer()], programId)[0];
|
|
142
|
+
/**
|
|
143
|
+
* One parked deposit, keyed by its queue sequence number.
|
|
144
|
+
*
|
|
145
|
+
* The seed is a u128 (16 bytes LE), not the u64 the withdrawal request uses.
|
|
146
|
+
*
|
|
147
|
+
* WHEN PARKING, the sequence number is the lot's `sequence_number` PLUS ONE -
|
|
148
|
+
* the slot the next park will take, since the program increments before it
|
|
149
|
+
* writes. That makes a park racy by construction: if another lands first this
|
|
150
|
+
* address is already in use and the transaction fails its address check, and
|
|
151
|
+
* must be rebuilt against a fresh read. Cancelling or claiming an EXISTING
|
|
152
|
+
* request uses that request's own recorded sequence number instead.
|
|
153
|
+
*/
|
|
154
|
+
export const deriveDepositRequest = (programId, parkingLot, sequenceNumber) => PublicKey.findProgramAddressSync([
|
|
155
|
+
seed(SOLANA_SEEDS.depositRequest),
|
|
156
|
+
parkingLot.toBuffer(),
|
|
157
|
+
u128le(sequenceNumber)
|
|
158
|
+
], programId)[0];
|
|
84
159
|
/**
|
|
85
160
|
* The event authority `emit_cpi!` signs with. Anchor requires it and the
|
|
86
161
|
* program id as the last two accounts of every instruction that emits.
|