@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.
@@ -22,6 +22,8 @@ import type { Connection as SolanaConnection } from "@solana/web3.js";
22
22
  import type { ISolanaDeployment } from "./solana-vaults/interfaces/index.js";
23
23
  import { SolanaDeploymentParser } from "./solana-vaults/utils/deployment-parser.js";
24
24
  import { SolanaVaultReader } from "./solana-vaults/on-chain-calls/vault-reader.js";
25
+ import { SolanaUserCalls } from "./solana-vaults/on-chain-calls/user.js";
26
+ import type { SolanaSigner } from "./solana-vaults/signers/solana-signer.js";
25
27
  import { ChainType, PlatformEnv } from "./common/constants.js";
26
28
  /**
27
29
  * SUI-specific initialization options
@@ -54,13 +56,16 @@ export interface EvmInitOptions {
54
56
  * Solana-specific initialization options.
55
57
  *
56
58
  * `connection` rather than a provider: web3.js has no provider abstraction,
57
- * and every read below goes through it. No signer yet - this slice is
58
- * read-only, and the signer shape (wallet adapter vs Keypair) is settled when
59
- * the write builders land.
59
+ * and every read goes through it.
60
+ *
61
+ * `signer` takes either shape - a raw Keypair for scripts and ops tooling, or
62
+ * a connected browser wallet - and is optional so a page can read before
63
+ * anyone connects. A write with no signer anywhere throws.
60
64
  */
61
65
  export interface SolanaInitOptions {
62
66
  chainType: "solana";
63
67
  connection: SolanaConnection;
68
+ signer?: SolanaSigner;
64
69
  /**
65
70
  * Unused on Solana and accepted only so the constructor can read it
66
71
  * uniformly. The chain identifier is "solana" on every cluster; which
@@ -76,8 +81,8 @@ export type EmberVaultsInitOptions = SuiInitOptions | EvmInitOptions | SolanaIni
76
81
  /**
77
82
  * Solana-specific EmberVaults instance type.
78
83
  *
79
- * Narrower than its Sui and EVM counterparts on purpose: reads only. The
80
- * admin, operator, vaultAdmin, user and txBuilder surfaces follow, and the
84
+ * Narrower than its Sui and EVM counterparts: reads plus the user writes.
85
+ * The admin, operator, vaultAdmin and txBuilder surfaces follow, and the
81
86
  * getters for them throw rather than returning undefined until they do.
82
87
  */
83
88
  export interface SolanaEmberVaults {
@@ -86,6 +91,7 @@ export interface SolanaEmberVaults {
86
91
  parser: SolanaDeploymentParser;
87
92
  walletAddress?: string;
88
93
  reader: SolanaVaultReader;
94
+ user: SolanaUserCalls;
89
95
  deployment: ISolanaDeployment;
90
96
  }
91
97
  /**
@@ -178,6 +184,8 @@ export declare class EmberVaults {
178
184
  private _solanaParser?;
179
185
  private _solanaDeployment?;
180
186
  private _solanaVaultReader?;
187
+ private _solanaUser?;
188
+ private _solanaSigner?;
181
189
  private _walletAddress?;
182
190
  private _chainIdentifier?;
183
191
  constructor(options: EmberVaultsInitOptions);
@@ -221,7 +229,7 @@ export declare class EmberVaults {
221
229
  get admin(): SuiAdminCalls | EVMAdminCalls;
222
230
  get operator(): SuiOperatorCalls | EVMOperatorCalls;
223
231
  get vaultAdmin(): SuiVaultAdminCalls | EVMVaultAdminCalls;
224
- get user(): SuiUserCalls | EVMUserCalls;
232
+ get user(): SuiUserCalls | EVMUserCalls | SolanaUserCalls;
225
233
  get deployment(): ISuiDeployment | EvmProtocolInfo | ISolanaDeployment;
226
234
  /**
227
235
  * Check if this is a SUI instance
@@ -16,6 +16,7 @@ import { AccountsApi, Configuration, VaultsApi } from "./api/index.js";
16
16
  import { AccountsApi as AccountsV2Api, Configuration as ConfigurationV2, VaultsApi as VaultsV2Api } from "./api/v2/index.js";
17
17
  import { SolanaDeploymentParser } from "./solana-vaults/utils/deployment-parser.js";
18
18
  import { SolanaVaultReader } from "./solana-vaults/on-chain-calls/vault-reader.js";
19
+ import { SolanaUserCalls } from "./solana-vaults/on-chain-calls/user.js";
19
20
  import { environmentConfig } from "./common/constants.js";
20
21
  /**
21
22
  * EmberVaults - Unified interface for both SUI and EVM chains
@@ -73,10 +74,12 @@ export class EmberVaults {
73
74
  _initSolana(options) {
74
75
  this._solanaConnection = options.connection;
75
76
  this._walletAddress = options.walletAddress;
77
+ this._solanaSigner = options.signer;
76
78
  if (options.deployment) {
77
79
  this._solanaDeployment = options.deployment;
78
80
  this._solanaParser = new SolanaDeploymentParser(options.deployment);
79
81
  this._solanaVaultReader = new SolanaVaultReader(options.connection, this._solanaParser);
82
+ this._solanaUser = new SolanaUserCalls(options.connection, this._solanaParser, options.signer);
80
83
  }
81
84
  }
82
85
  _initSui(options) {
@@ -157,6 +160,12 @@ export class EmberVaults {
157
160
  // so it is rebuilt rather than mutated - a deployment pointing at a
158
161
  // different program is a different parser.
159
162
  this._solanaParser = new SolanaDeploymentParser(solanaDeployment);
163
+ if (this._solanaUser) {
164
+ this._solanaUser.updateDeployment(this._solanaParser);
165
+ }
166
+ else {
167
+ this._solanaUser = new SolanaUserCalls(this._solanaConnection, this._solanaParser, this._solanaSigner);
168
+ }
160
169
  if (this._solanaVaultReader) {
161
170
  this._solanaVaultReader.updateDeployment(this._solanaParser);
162
171
  }
@@ -234,7 +243,8 @@ export class EmberVaults {
234
243
  */
235
244
  notOnSolana(surface) {
236
245
  throw new Error(`${surface} is not available for Solana yet. ` +
237
- `The Solana module currently exposes reads only: use .reader, .parser or .deployment.`);
246
+ `The Solana module exposes reads and the user writes: ` +
247
+ `use .reader, .user, .parser or .deployment.`);
238
248
  }
239
249
  // ============================================
240
250
  // Solana Getters
@@ -276,8 +286,11 @@ export class EmberVaults {
276
286
  return this.chainType === "sui" ? this._suiVaultAdmin : this._evmVaultAdmin;
277
287
  }
278
288
  get user() {
279
- if (this.chainType === "solana")
280
- this.notOnSolana("user");
289
+ if (this.chainType === "solana") {
290
+ if (!this._solanaUser)
291
+ throw new Error("No Solana deployment loaded. Pass one, or call init().");
292
+ return this._solanaUser;
293
+ }
281
294
  return this.chainType === "sui" ? this._suiUser : this._evmUser;
282
295
  }
283
296
  get deployment() {
@@ -360,12 +373,16 @@ export class EmberVaults {
360
373
  if (!this._solanaParser || !this._solanaVaultReader || !this._solanaDeployment) {
361
374
  throw new Error("No Solana deployment loaded. Pass one, or call init().");
362
375
  }
376
+ if (!this._solanaUser) {
377
+ throw new Error("No Solana deployment loaded. Pass one, or call init().");
378
+ }
363
379
  return {
364
380
  chainType: "solana",
365
381
  connection: this._solanaConnection,
366
382
  parser: this._solanaParser,
367
383
  walletAddress: this._walletAddress,
368
384
  reader: this._solanaVaultReader,
385
+ user: this._solanaUser,
369
386
  deployment: this._solanaDeployment
370
387
  };
371
388
  }
@@ -1,3 +1,4 @@
1
1
  export * from "./interfaces/index.js";
2
2
  export * from "./on-chain-calls/index.js";
3
+ export * from "./signers/index.js";
3
4
  export * from "./utils/index.js";
@@ -1,3 +1,4 @@
1
1
  export * from "./interfaces/index.js";
2
2
  export * from "./on-chain-calls/index.js";
3
+ export * from "./signers/index.js";
3
4
  export * from "./utils/index.js";
@@ -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
- depositUserList: PublicKey[];
208
- withdrawalUserList: PublicKey[];
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
@@ -1 +1,3 @@
1
+ export * from "./tx-builder.js";
2
+ export * from "./user.js";
1
3
  export * from "./vault-reader.js";
@@ -1 +1,3 @@
1
+ export * from "./tx-builder.js";
2
+ export * from "./user.js";
1
3
  export * from "./vault-reader.js";
@@ -0,0 +1,135 @@
1
+ import { PublicKey, TransactionInstruction } from "@solana/web3.js";
2
+ /** Everything an instruction needs that cannot be derived from the share mint. */
3
+ export interface ISolanaVaultContext {
4
+ programId: PublicKey;
5
+ shareMint: PublicKey;
6
+ collateralMint: PublicKey;
7
+ /** The owning program of each mint; they need not be the same one. */
8
+ shareProgram: PublicKey;
9
+ collateralProgram: PublicKey;
10
+ }
11
+ /**
12
+ * Builds the program's user instructions. Pure: it derives addresses and
13
+ * encodes data, and never touches the network or signs.
14
+ *
15
+ * Kept separate from SolanaUserCalls so a caller can batch several of these
16
+ * into one transaction, add their own instructions around them, or hand the
17
+ * result to a different sender — the same split the EVM side has.
18
+ */
19
+ export declare class SolanaTxBuilder {
20
+ /**
21
+ * Creates an associated token account only if it is missing.
22
+ *
23
+ * `tokenProgram` is a parameter rather than a constant: an ATA's address
24
+ * derives from it, so a Token-2022 mint resolves somewhere else entirely
25
+ * and passing the SPL Token default would create an account at an address
26
+ * the program will not accept.
27
+ */
28
+ static createAtaIdempotent(payer: PublicKey, owner: PublicKey, mint: PublicKey, tokenProgram?: PublicKey): TransactionInstruction;
29
+ /**
30
+ * `deposit_asset`: moves collateral in and mints shares at the live rate.
31
+ *
32
+ * `minShares` is the slippage floor and is enforced on chain. It matters
33
+ * only if the rate manager moves the rate between quoting and landing.
34
+ */
35
+ static depositAsset(ctx: ISolanaVaultContext, depositor: PublicKey, amount: bigint, minShares: bigint): TransactionInstruction;
36
+ /**
37
+ * `mint_shares`: the same movement expressed the other way round — an exact
38
+ * share count out, with `maxAmount` capping the collateral it may consume.
39
+ */
40
+ static mintShares(ctx: ISolanaVaultContext, depositor: PublicKey, shares: bigint, maxAmount: bigint): TransactionInstruction;
41
+ /**
42
+ * `redeem_shares`: burns nothing yet — it escrows the shares into the
43
+ * vault's pending account and appends a request to the withdrawal queue.
44
+ * The operator settles it later, so this returns no collateral.
45
+ *
46
+ * `requestIndex` identifies the request account and must be the owner's
47
+ * next index; SolanaUserCalls reads it so callers do not have to.
48
+ */
49
+ static redeemShares(ctx: ISolanaVaultContext, owner: PublicKey, shares: bigint, requestNonce: bigint, receiver: PublicKey): TransactionInstruction;
50
+ /**
51
+ * `cancel_pending_withdrawal_request`: withdraws the request from the
52
+ * queue, identified by its `sequenceNumber`.
53
+ *
54
+ * Only four accounts, none of them token accounts. The escrowed shares are
55
+ * not moved here — the request is marked cancelled on the owner's
56
+ * VaultAccount and the operator returns the shares when it next settles —
57
+ * so neither the share mint, the pending-shares escrow nor the request's
58
+ * own account appear. Matches CancelWithdrawalRequest in withdraw.rs: note
59
+ * that `vault` and `owner` are both read-only.
60
+ */
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;
135
+ }