@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.
@@ -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 vault.depositUserList.some(a => a.equals(owner));
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 (vault.withdrawalUserList.length === 0 ||
311
- vault.withdrawalUserList.some(a => a.equals(owner)));
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. */
@@ -0,0 +1 @@
1
+ export * from "./solana-signer.js";
@@ -0,0 +1 @@
1
+ export * from "./solana-signer.js";
@@ -0,0 +1,44 @@
1
+ import { Keypair, PublicKey, type Transaction } from "@solana/web3.js";
2
+ /**
3
+ * The browser-wallet shape: Phantom, Solflare, Backpack and the
4
+ * `@solana/wallet-adapter` wallets all satisfy this.
5
+ *
6
+ * Deliberately structural rather than an import of the wallet-adapter types:
7
+ * taking that dependency would pull a React-oriented package into a library
8
+ * that otherwise needs only web3.js, and every adapter already matches this.
9
+ */
10
+ export interface ISolanaWalletAdapter {
11
+ /** Null while the wallet is disconnected. */
12
+ publicKey: PublicKey | null;
13
+ signTransaction(tx: Transaction): Promise<Transaction>;
14
+ /** Present on most adapters; unused here, declared so they still match. */
15
+ signAllTransactions?(txs: Transaction[]): Promise<Transaction[]>;
16
+ }
17
+ /**
18
+ * Either shape works wherever the SDK signs: a raw `Keypair` for scripts, ops
19
+ * tooling and tests, or a connected wallet for a browser app.
20
+ */
21
+ export type SolanaSigner = Keypair | ISolanaWalletAdapter;
22
+ /** What the call layer actually uses, once the two shapes are collapsed. */
23
+ export interface INormalisedSolanaSigner {
24
+ publicKey: PublicKey;
25
+ signTransaction(tx: Transaction): Promise<Transaction>;
26
+ }
27
+ /**
28
+ * Duck-typed, NOT `instanceof Keypair`.
29
+ *
30
+ * A dApp frequently ends up with two copies of @solana/web3.js in its tree —
31
+ * its own and the SDK's, or two versions hoisted side by side — and a `Keypair`
32
+ * built against one copy fails `instanceof` against the other. That failure
33
+ * mode is silent and maddening to debug, so the check is on shape instead:
34
+ * only an adapter has `signTransaction`.
35
+ */
36
+ export declare function isWalletAdapter(signer: SolanaSigner): signer is ISolanaWalletAdapter;
37
+ /**
38
+ * Collapses either shape into one signer.
39
+ *
40
+ * A disconnected wallet is rejected here rather than later: its `publicKey` is
41
+ * null, which would otherwise surface as a confusing failure deep inside
42
+ * instruction building, after the caller has already paid for account reads.
43
+ */
44
+ export declare function normaliseSigner(signer: SolanaSigner): INormalisedSolanaSigner;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Duck-typed, NOT `instanceof Keypair`.
3
+ *
4
+ * A dApp frequently ends up with two copies of @solana/web3.js in its tree —
5
+ * its own and the SDK's, or two versions hoisted side by side — and a `Keypair`
6
+ * built against one copy fails `instanceof` against the other. That failure
7
+ * mode is silent and maddening to debug, so the check is on shape instead:
8
+ * only an adapter has `signTransaction`.
9
+ */
10
+ export function isWalletAdapter(signer) {
11
+ return typeof signer.signTransaction === "function";
12
+ }
13
+ /**
14
+ * The inverse check, structural throughout.
15
+ *
16
+ * `publicKey instanceof PublicKey` would defeat the whole point of the check
17
+ * above: a Keypair from another copy of web3.js carries THAT copy's PublicKey,
18
+ * so the duplicate-package hazard would simply move here and reject a perfectly
19
+ * good signer. It is checked by the methods this SDK actually calls on it.
20
+ */
21
+ function isKeypairLike(signer) {
22
+ const candidate = signer;
23
+ const key = candidate.publicKey;
24
+ return (candidate.secretKey instanceof Uint8Array &&
25
+ key !== null &&
26
+ key !== undefined &&
27
+ typeof key.toBase58 === "function" &&
28
+ typeof key.toBuffer === "function");
29
+ }
30
+ /**
31
+ * Collapses either shape into one signer.
32
+ *
33
+ * A disconnected wallet is rejected here rather than later: its `publicKey` is
34
+ * null, which would otherwise surface as a confusing failure deep inside
35
+ * instruction building, after the caller has already paid for account reads.
36
+ */
37
+ export function normaliseSigner(signer) {
38
+ if (isWalletAdapter(signer)) {
39
+ const { publicKey } = signer;
40
+ if (publicKey === null || publicKey === undefined) {
41
+ throw new Error("wallet is not connected: its publicKey is null. Connect it before signing.");
42
+ }
43
+ return {
44
+ publicKey,
45
+ signTransaction: (tx) => signer.signTransaction(tx)
46
+ };
47
+ }
48
+ if (isKeypairLike(signer)) {
49
+ return {
50
+ publicKey: signer.publicKey,
51
+ // partialSign rather than sign: a transaction may legitimately need
52
+ // other signatures too (a fresh mint keypair, a fee payer), and
53
+ // `sign` would discard any already collected.
54
+ signTransaction: async (tx) => {
55
+ tx.partialSign(signer);
56
+ return tx;
57
+ }
58
+ };
59
+ }
60
+ throw new Error("unsupported signer: pass a Keypair, or a wallet exposing publicKey and signTransaction");
61
+ }
@@ -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
- const depositUserList = c.vec(x => x.pubkey());
132
- const withdrawalUserList = c.vec(x => x.pubkey());
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
- depositUserList,
165
- withdrawalUserList,
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 {
@@ -2,6 +2,15 @@ import { PublicKey } from "@solana/web3.js";
2
2
  import { type ISolanaVaultPdas } from "../interfaces/index.js";
3
3
  /** Little-endian u64, for the nonce and sequence-number seeds. */
4
4
  export declare function u64le(v: bigint | number): Buffer;
5
+ /**
6
+ * A u128 as 16 little-endian bytes: the program's sequence numbers.
7
+ *
8
+ * Range-checked, because the masking loop truncates silently otherwise:
9
+ * `(1n << 128n) + 6n` and `6n` encode to identical bytes, so an out-of-range
10
+ * sequence number would cancel request 6 rather than fail. `u64le` gets this
11
+ * for free from `writeBigUInt64LE`, which throws.
12
+ */
13
+ export declare function u128le(v: bigint | number): Buffer;
5
14
  /** The protocol-wide config. One per program id, and it can never be closed. */
6
15
  export declare const deriveProtocolConfig: (programId: PublicKey) => PublicKey;
7
16
  /**
@@ -23,6 +32,67 @@ export declare const deriveVaultAccount: (programId: PublicKey, vault: PublicKey
23
32
  * nonce 0.
24
33
  */
25
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;
26
96
  /**
27
97
  * The event authority `emit_cpi!` signs with. Anchor requires it and the
28
98
  * program id as the last two accounts of every instruction that emits.
@@ -20,6 +20,27 @@ export function u64le(v) {
20
20
  b.writeBigUInt64LE(BigInt(v));
21
21
  return b;
22
22
  }
23
+ const U128_MAX = (1n << 128n) - 1n;
24
+ /**
25
+ * A u128 as 16 little-endian bytes: the program's sequence numbers.
26
+ *
27
+ * Range-checked, because the masking loop truncates silently otherwise:
28
+ * `(1n << 128n) + 6n` and `6n` encode to identical bytes, so an out-of-range
29
+ * sequence number would cancel request 6 rather than fail. `u64le` gets this
30
+ * for free from `writeBigUInt64LE`, which throws.
31
+ */
32
+ export function u128le(v) {
33
+ let value = BigInt(v);
34
+ if (value < 0n || value > U128_MAX) {
35
+ throw new RangeError(`value ${value} is out of range for a u128`);
36
+ }
37
+ const buf = Buffer.alloc(16);
38
+ for (let i = 0; i < 16; i++) {
39
+ buf[i] = Number(value & 0xffn);
40
+ value >>= 8n;
41
+ }
42
+ return buf;
43
+ }
23
44
  /** The protocol-wide config. One per program id, and it can never be closed. */
24
45
  export const deriveProtocolConfig = (programId) => PublicKey.findProgramAddressSync([seed(SOLANA_SEEDS.protocolConfig)], programId)[0];
25
46
  /**
@@ -55,11 +76,86 @@ export const deriveWithdrawalRequest = (programId, vault, owner, nonce) => Publi
55
76
  owner.toBuffer(),
56
77
  u64le(nonce)
57
78
  ], programId)[0];
58
- // The parking lot's PDAs - the lot, its escrow and a deposit request - are
59
- // derived in the phase that adds decoders for DepositParkingLot and
60
- // DepositRequest. Deriving an address nothing can read is a surface that
61
- // looks usable and is not. Note for then: a request's seed is the lot's
62
- // sequence_number + 1, the slot the next park will take.
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];
63
159
  /**
64
160
  * The event authority `emit_cpi!` signs with. Anchor requires it and the
65
161
  * program id as the last two accounts of every instruction that emits.