@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,466 @@
|
|
|
1
|
+
import { SystemProgram, TransactionInstruction } from "@solana/web3.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
|
+
/**
|
|
4
|
+
* Builds the program's user instructions. Pure: it derives addresses and
|
|
5
|
+
* encodes data, and never touches the network or signs.
|
|
6
|
+
*
|
|
7
|
+
* Kept separate from SolanaUserCalls so a caller can batch several of these
|
|
8
|
+
* into one transaction, add their own instructions around them, or hand the
|
|
9
|
+
* result to a different sender — the same split the EVM side has.
|
|
10
|
+
*/
|
|
11
|
+
export class SolanaTxBuilder {
|
|
12
|
+
/**
|
|
13
|
+
* Creates an associated token account only if it is missing.
|
|
14
|
+
*
|
|
15
|
+
* `tokenProgram` is a parameter rather than a constant: an ATA's address
|
|
16
|
+
* derives from it, so a Token-2022 mint resolves somewhere else entirely
|
|
17
|
+
* and passing the SPL Token default would create an account at an address
|
|
18
|
+
* the program will not accept.
|
|
19
|
+
*/
|
|
20
|
+
static createAtaIdempotent(payer, owner, mint, tokenProgram = TOKEN_PROGRAM_ID) {
|
|
21
|
+
return new TransactionInstruction({
|
|
22
|
+
programId: ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
23
|
+
keys: [
|
|
24
|
+
{ pubkey: payer, isSigner: true, isWritable: true },
|
|
25
|
+
{
|
|
26
|
+
pubkey: associatedTokenAddress(owner, mint, tokenProgram),
|
|
27
|
+
isSigner: false,
|
|
28
|
+
isWritable: true
|
|
29
|
+
},
|
|
30
|
+
{ pubkey: owner, isSigner: false, isWritable: false },
|
|
31
|
+
{ pubkey: mint, isSigner: false, isWritable: false },
|
|
32
|
+
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
33
|
+
{ pubkey: tokenProgram, isSigner: false, isWritable: false }
|
|
34
|
+
],
|
|
35
|
+
// 1 = CreateIdempotent. Unlike 0 (Create), this succeeds when the
|
|
36
|
+
// account already exists, so a repeat deposit needs no pre-check.
|
|
37
|
+
data: Buffer.from([1])
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* `deposit_asset`: moves collateral in and mints shares at the live rate.
|
|
42
|
+
*
|
|
43
|
+
* `minShares` is the slippage floor and is enforced on chain. It matters
|
|
44
|
+
* only if the rate manager moves the rate between quoting and landing.
|
|
45
|
+
*/
|
|
46
|
+
static depositAsset(ctx, depositor, amount, minShares) {
|
|
47
|
+
const pdas = deriveVaultPdas(ctx.programId, ctx.shareMint);
|
|
48
|
+
return new TransactionInstruction({
|
|
49
|
+
programId: ctx.programId,
|
|
50
|
+
keys: [
|
|
51
|
+
{
|
|
52
|
+
pubkey: deriveProtocolConfig(ctx.programId),
|
|
53
|
+
isSigner: false,
|
|
54
|
+
isWritable: false
|
|
55
|
+
},
|
|
56
|
+
{ pubkey: pdas.vault, isSigner: false, isWritable: true },
|
|
57
|
+
{ pubkey: ctx.collateralMint, isSigner: false, isWritable: false },
|
|
58
|
+
{ pubkey: ctx.shareMint, isSigner: false, isWritable: true },
|
|
59
|
+
{ pubkey: pdas.vaultCollateral, isSigner: false, isWritable: true },
|
|
60
|
+
{ pubkey: depositor, isSigner: true, isWritable: true },
|
|
61
|
+
{
|
|
62
|
+
pubkey: deriveVaultAccount(ctx.programId, pdas.vault, depositor),
|
|
63
|
+
isSigner: false,
|
|
64
|
+
isWritable: true
|
|
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
|
+
},
|
|
75
|
+
{
|
|
76
|
+
pubkey: associatedTokenAddress(depositor, ctx.collateralMint, ctx.collateralProgram),
|
|
77
|
+
isSigner: false,
|
|
78
|
+
isWritable: true
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
pubkey: associatedTokenAddress(depositor, ctx.shareMint, ctx.shareProgram),
|
|
82
|
+
isSigner: false,
|
|
83
|
+
isWritable: true
|
|
84
|
+
},
|
|
85
|
+
{ pubkey: ctx.collateralProgram, isSigner: false, isWritable: false },
|
|
86
|
+
{ pubkey: ctx.shareProgram, isSigner: false, isWritable: false },
|
|
87
|
+
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
88
|
+
...eventCpiAccounts(ctx.programId)
|
|
89
|
+
],
|
|
90
|
+
data: Buffer.concat([
|
|
91
|
+
instructionDiscriminator("deposit_asset"),
|
|
92
|
+
u64le(amount),
|
|
93
|
+
u64le(minShares)
|
|
94
|
+
])
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* `mint_shares`: the same movement expressed the other way round — an exact
|
|
99
|
+
* share count out, with `maxAmount` capping the collateral it may consume.
|
|
100
|
+
*/
|
|
101
|
+
static mintShares(ctx, depositor, shares, maxAmount) {
|
|
102
|
+
const deposit = SolanaTxBuilder.depositAsset(ctx, depositor, shares, maxAmount);
|
|
103
|
+
return new TransactionInstruction({
|
|
104
|
+
programId: deposit.programId,
|
|
105
|
+
keys: deposit.keys,
|
|
106
|
+
data: Buffer.concat([
|
|
107
|
+
instructionDiscriminator("mint_shares"),
|
|
108
|
+
u64le(shares),
|
|
109
|
+
u64le(maxAmount)
|
|
110
|
+
])
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* `redeem_shares`: burns nothing yet — it escrows the shares into the
|
|
115
|
+
* vault's pending account and appends a request to the withdrawal queue.
|
|
116
|
+
* The operator settles it later, so this returns no collateral.
|
|
117
|
+
*
|
|
118
|
+
* `requestIndex` identifies the request account and must be the owner's
|
|
119
|
+
* next index; SolanaUserCalls reads it so callers do not have to.
|
|
120
|
+
*/
|
|
121
|
+
static redeemShares(ctx, owner, shares, requestNonce, receiver) {
|
|
122
|
+
const pdas = deriveVaultPdas(ctx.programId, ctx.shareMint);
|
|
123
|
+
const vaultAccount = deriveVaultAccount(ctx.programId, pdas.vault, owner);
|
|
124
|
+
return new TransactionInstruction({
|
|
125
|
+
programId: ctx.programId,
|
|
126
|
+
keys: [
|
|
127
|
+
{
|
|
128
|
+
pubkey: deriveProtocolConfig(ctx.programId),
|
|
129
|
+
isSigner: false,
|
|
130
|
+
isWritable: false
|
|
131
|
+
},
|
|
132
|
+
{ pubkey: pdas.vault, isSigner: false, isWritable: true },
|
|
133
|
+
{ pubkey: ctx.shareMint, isSigner: false, isWritable: false },
|
|
134
|
+
{ pubkey: pdas.vaultPendingShares, isSigner: false, isWritable: true },
|
|
135
|
+
{ pubkey: pdas.withdrawalQueue, isSigner: false, isWritable: true },
|
|
136
|
+
{ pubkey: vaultAccount, isSigner: false, isWritable: true },
|
|
137
|
+
{ pubkey: owner, isSigner: true, isWritable: true },
|
|
138
|
+
{
|
|
139
|
+
pubkey: deriveWithdrawalRequest(ctx.programId, pdas.vault, owner, requestNonce),
|
|
140
|
+
isSigner: false,
|
|
141
|
+
isWritable: true
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
pubkey: associatedTokenAddress(owner, ctx.shareMint, ctx.shareProgram),
|
|
145
|
+
isSigner: false,
|
|
146
|
+
isWritable: true
|
|
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
|
+
},
|
|
161
|
+
{ pubkey: ctx.shareProgram, isSigner: false, isWritable: false },
|
|
162
|
+
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
163
|
+
...eventCpiAccounts(ctx.programId)
|
|
164
|
+
],
|
|
165
|
+
// `receiver` is an argument, not an account: the collateral is paid
|
|
166
|
+
// out later by the operator, so there is nothing to write to now.
|
|
167
|
+
data: Buffer.concat([
|
|
168
|
+
instructionDiscriminator("redeem_shares"),
|
|
169
|
+
u64le(shares),
|
|
170
|
+
receiver.toBuffer()
|
|
171
|
+
])
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* `cancel_pending_withdrawal_request`: withdraws the request from the
|
|
176
|
+
* queue, identified by its `sequenceNumber`.
|
|
177
|
+
*
|
|
178
|
+
* Only four accounts, none of them token accounts. The escrowed shares are
|
|
179
|
+
* not moved here — the request is marked cancelled on the owner's
|
|
180
|
+
* VaultAccount and the operator returns the shares when it next settles —
|
|
181
|
+
* so neither the share mint, the pending-shares escrow nor the request's
|
|
182
|
+
* own account appear. Matches CancelWithdrawalRequest in withdraw.rs: note
|
|
183
|
+
* that `vault` and `owner` are both read-only.
|
|
184
|
+
*/
|
|
185
|
+
static cancelPendingWithdrawalRequest(ctx, owner, sequenceNumber) {
|
|
186
|
+
const pdas = deriveVaultPdas(ctx.programId, ctx.shareMint);
|
|
187
|
+
return new TransactionInstruction({
|
|
188
|
+
programId: ctx.programId,
|
|
189
|
+
keys: [
|
|
190
|
+
{
|
|
191
|
+
pubkey: deriveProtocolConfig(ctx.programId),
|
|
192
|
+
isSigner: false,
|
|
193
|
+
isWritable: false
|
|
194
|
+
},
|
|
195
|
+
{ pubkey: pdas.vault, isSigner: false, isWritable: false },
|
|
196
|
+
{
|
|
197
|
+
pubkey: deriveVaultAccount(ctx.programId, pdas.vault, owner),
|
|
198
|
+
isSigner: false,
|
|
199
|
+
isWritable: true
|
|
200
|
+
},
|
|
201
|
+
{ pubkey: owner, isSigner: true, isWritable: false },
|
|
202
|
+
...eventCpiAccounts(ctx.programId)
|
|
203
|
+
],
|
|
204
|
+
// u128, not u64: a queue sequence number, not the request nonce
|
|
205
|
+
// that seeds the request PDA.
|
|
206
|
+
data: Buffer.concat([
|
|
207
|
+
instructionDiscriminator("cancel_pending_withdrawal_request"),
|
|
208
|
+
u128le(sequenceNumber)
|
|
209
|
+
])
|
|
210
|
+
});
|
|
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
|
+
}
|
|
466
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { type Connection, PublicKey, type TransactionInstruction } from "@solana/web3.js";
|
|
2
|
+
import { type ISolanaDepositRequest, type ISolanaWithdrawalRequest } from "../interfaces/index.js";
|
|
3
|
+
import { type SolanaSigner } from "../signers/solana-signer.js";
|
|
4
|
+
import { SolanaDeploymentParser } from "../utils/deployment-parser.js";
|
|
5
|
+
/** What a write returns. */
|
|
6
|
+
export interface ISolanaTxResponse {
|
|
7
|
+
/** The transaction signature, base58. Also its explorer id. */
|
|
8
|
+
signature: string;
|
|
9
|
+
/** Compute units the simulation reported, when one ran. */
|
|
10
|
+
unitsConsumed?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface ISolanaWriteOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Simulate before sending. On by default: a failed simulation costs one
|
|
15
|
+
* round trip and reports the program's error, where a failed send costs a
|
|
16
|
+
* fee and reports a bare "custom program error".
|
|
17
|
+
*/
|
|
18
|
+
simulate?: boolean;
|
|
19
|
+
/** Extra instructions to run before the vault call, e.g. a compute budget. */
|
|
20
|
+
preInstructions?: TransactionInstruction[];
|
|
21
|
+
/** Overrides the signer supplied to the constructor, for this call only. */
|
|
22
|
+
signer?: SolanaSigner;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Redemptions, and only redemptions, take a receiver.
|
|
26
|
+
*
|
|
27
|
+
* `deposit_asset` and `mint_shares` have no receiver argument and mint into the
|
|
28
|
+
* depositor's own associated token account, so a `receiver` on those options
|
|
29
|
+
* would be accepted and silently ignored. It is kept off their type rather than
|
|
30
|
+
* rejected at runtime, so the mistake does not compile.
|
|
31
|
+
*/
|
|
32
|
+
export interface ISolanaRedeemOptions extends ISolanaWriteOptions {
|
|
33
|
+
/** Who receives the collateral at settlement. Defaults to the signer. */
|
|
34
|
+
receiver?: PublicKey | string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Amounts accept the shapes the Sui and EVM surfaces accept, not just bigint.
|
|
38
|
+
*
|
|
39
|
+
* `EmberVaults.user` is a union of the three chains' call classes, so a shared
|
|
40
|
+
* call site resolves each parameter to the INTERSECTION of their types. Sui's
|
|
41
|
+
* `NumStr` is `number | string` and EVM's is `string | number | bigint`;
|
|
42
|
+
* intersected with a bigint-only Solana that is `never`, so before this type
|
|
43
|
+
* existed `ember.user.redeemShares(vault, x)` failed to compile for every x —
|
|
44
|
+
* `100`, `"100"` and `100n` alike.
|
|
45
|
+
*
|
|
46
|
+
* `100n` through `ember.user` is still rejected, because Sui's `NumStr` has no
|
|
47
|
+
* bigint. That is pre-existing and unrelated to Solana; a bigint works on a
|
|
48
|
+
* narrowed instance (`asSolana().user`, or a `SolanaUserCalls` directly).
|
|
49
|
+
*/
|
|
50
|
+
export type SolanaAmount = bigint | number | string;
|
|
51
|
+
/**
|
|
52
|
+
* The user-facing writes: deposit, mint, redeem and cancel.
|
|
53
|
+
*
|
|
54
|
+
* Each method reads only what it needs to build a correct instruction — the
|
|
55
|
+
* mints' token programs and, for a redeem, the owner's request nonce — then
|
|
56
|
+
* signs and sends. The instruction building itself lives in SolanaTxBuilder,
|
|
57
|
+
* so a caller who wants to batch or route differently can use that directly.
|
|
58
|
+
*/
|
|
59
|
+
export declare class SolanaUserCalls {
|
|
60
|
+
private readonly connection;
|
|
61
|
+
/**
|
|
62
|
+
* Optional so one instance can serve a page that has not connected a
|
|
63
|
+
* wallet yet; every write then takes its own signer.
|
|
64
|
+
*/
|
|
65
|
+
private readonly signer?;
|
|
66
|
+
private parser;
|
|
67
|
+
private readonly reader;
|
|
68
|
+
constructor(connection: Connection, parser: SolanaDeploymentParser,
|
|
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?: SolanaSigner);
|
|
74
|
+
/** Swap in a newer deployment without rebuilding the caller. */
|
|
75
|
+
updateDeployment(parser: SolanaDeploymentParser): void;
|
|
76
|
+
private key;
|
|
77
|
+
private resolveSigner;
|
|
78
|
+
/**
|
|
79
|
+
* The mints and their owning token programs.
|
|
80
|
+
*
|
|
81
|
+
* The programs come from the deployment manifest when it records them and
|
|
82
|
+
* from the mint accounts otherwise. They are not assumed equal: a vault's
|
|
83
|
+
* collateral and share mints may sit under SPL Token and Token-2022
|
|
84
|
+
* respectively, and the ATA addresses differ between the two.
|
|
85
|
+
*/
|
|
86
|
+
private vaultContext;
|
|
87
|
+
private tokenProgram;
|
|
88
|
+
/**
|
|
89
|
+
* Signs, sends and confirms. Simulation runs first unless turned off, and
|
|
90
|
+
* its error is surfaced in place of the send's: the program's own message
|
|
91
|
+
* is in the simulation logs, while a failed send gives only a code.
|
|
92
|
+
*/
|
|
93
|
+
private send;
|
|
94
|
+
/**
|
|
95
|
+
* Deposits collateral and mints shares at the vault's live rate.
|
|
96
|
+
*
|
|
97
|
+
* `minShares` is the on-chain slippage floor; it bites only if the rate
|
|
98
|
+
* manager moves the rate between quoting and landing. Quote it with
|
|
99
|
+
* `SolanaVaultReader.previewDeposit`.
|
|
100
|
+
*
|
|
101
|
+
* The depositor's share account is created here when missing, so a first
|
|
102
|
+
* deposit is still a single transaction.
|
|
103
|
+
*/
|
|
104
|
+
depositAsset(shareMint: PublicKey | string, amount: SolanaAmount, minShares: SolanaAmount, options?: ISolanaWriteOptions): Promise<ISolanaTxResponse>;
|
|
105
|
+
/**
|
|
106
|
+
* Mints an exact share count, spending at most `maxAmount` of collateral.
|
|
107
|
+
* The mirror of depositAsset, for a caller who wants a round share figure.
|
|
108
|
+
*/
|
|
109
|
+
mintShares(shareMint: PublicKey | string, shares: SolanaAmount, maxAmount: SolanaAmount, options?: ISolanaWriteOptions): Promise<ISolanaTxResponse>;
|
|
110
|
+
/**
|
|
111
|
+
* The idempotent ATA instruction, but only when the account is actually
|
|
112
|
+
* missing: including it unconditionally costs a few hundred compute units
|
|
113
|
+
* on every repeat deposit for nothing.
|
|
114
|
+
*/
|
|
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
|
+
}>;
|
|
166
|
+
/**
|
|
167
|
+
* Queues a withdrawal. This does NOT return collateral: the shares are
|
|
168
|
+
* escrowed and a request is appended to the vault's queue, which the
|
|
169
|
+
* operator settles later. Watch it with `getPendingWithdrawals`.
|
|
170
|
+
*
|
|
171
|
+
* The request's account is seeded by the owner's next nonce, which is read
|
|
172
|
+
* here — a caller passing a stale one would collide with their own earlier
|
|
173
|
+
* request.
|
|
174
|
+
*/
|
|
175
|
+
redeemShares(shareMint: PublicKey | string, shares: SolanaAmount, options?: ISolanaRedeemOptions): Promise<ISolanaTxResponse>;
|
|
176
|
+
/**
|
|
177
|
+
* Cancels a queued withdrawal by its queue `sequenceNumber` — not the
|
|
178
|
+
* request nonce that seeds the request account. Take it from
|
|
179
|
+
* `getPendingWithdrawals`.
|
|
180
|
+
*/
|
|
181
|
+
cancelPendingWithdrawalRequest(shareMint: PublicKey | string, sequenceNumber: SolanaAmount, options?: ISolanaWriteOptions): Promise<ISolanaTxResponse>;
|
|
182
|
+
/**
|
|
183
|
+
* An owner's live queued withdrawals, each carrying the `sequenceNumber`
|
|
184
|
+
* that `cancelPendingWithdrawalRequest` takes.
|
|
185
|
+
*
|
|
186
|
+
* CANCELLED REQUESTS ARE EXCLUDED, and that filter is not cosmetic.
|
|
187
|
+
* `cancel_pending_withdrawal_request` does not remove the entry: it pushes
|
|
188
|
+
* the sequence number onto the account's `cancel_withdraw_request` list and
|
|
189
|
+
* leaves the request in place, for the operator to skip when it settles
|
|
190
|
+
* (see withdraw.rs). Returning the raw list would therefore show a
|
|
191
|
+
* withdrawal the user already cancelled as still pending, indefinitely.
|
|
192
|
+
*/
|
|
193
|
+
getPendingWithdrawals(shareMint: PublicKey | string, owner?: PublicKey | string): Promise<ISolanaWithdrawalRequest[]>;
|
|
194
|
+
/**
|
|
195
|
+
* The requests this owner has cancelled that the operator has not yet
|
|
196
|
+
* settled. They still occupy a slot on the account until it does.
|
|
197
|
+
*/
|
|
198
|
+
getCancelledWithdrawals(shareMint: PublicKey | string, owner?: PublicKey | string): Promise<ISolanaWithdrawalRequest[]>;
|
|
199
|
+
private accountState;
|
|
200
|
+
}
|