@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
|
@@ -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. */
|
|
@@ -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
|
-
|
|
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 {
|
|
@@ -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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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.
|