@perena/vault-sdk 1.0.18

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.
Files changed (4) hide show
  1. package/README.md +569 -0
  2. package/dist/index.d.ts +13974 -0
  3. package/dist/index.js +17968 -0
  4. package/package.json +64 -0
package/README.md ADDED
@@ -0,0 +1,569 @@
1
+ # Vault SDK
2
+
3
+ TypeScript helpers for the Bankineco vault program. The SDK wraps PDA derivation,
4
+ account fetching, and instruction building behind a single `VaultClient`. Every
5
+ on-chain instruction has a **transaction builder** on `client.tx` that returns a
6
+ `VaultTransactionPlan` you can sign and send.
7
+
8
+ For runnable CLI examples, see [`scripts/README.md`](scripts/README.md).
9
+
10
+ ## Install
11
+
12
+ From the monorepo (workspace package name is `vault`):
13
+
14
+ ```bash
15
+ pnpm install
16
+ pnpm --filter vault run build
17
+ ```
18
+
19
+ In another workspace package:
20
+
21
+ ```json
22
+ {
23
+ "dependencies": {
24
+ "vault": "workspace:*"
25
+ }
26
+ }
27
+ ```
28
+
29
+ ## Quick start
30
+
31
+ ### Option A — `createVaultClient` (scripts and backend services)
32
+
33
+ Use this when targeting deployed `test` or `prod` networks. It wires RPC,
34
+ keypair, and program id from env vars (see [`.env.example`](.env.example)).
35
+
36
+ ```typescript
37
+ import { createVaultClient } from "vault";
38
+
39
+ const { client, payer, signer } = createVaultClient("test", {
40
+ role: "vault", // default keypair: ~/.config/solana/vault-test.json
41
+ });
42
+
43
+ const vault = "..." as Address; // vault PDA
44
+ const vaultAccount = await client.account.fetchVault(vault);
45
+
46
+ const plan = await client.tx.executeDeposit.getTx({
47
+ user: signer,
48
+ vault,
49
+ assetMint: vaultAccount.config.baseAssetMint,
50
+ shareMint: vaultAccount.config.shareMint,
51
+ amount: 1_000_000n, // base units (6 decimals → 1.0 UI)
52
+ });
53
+
54
+ const signature = await client.sendTransaction(payer, plan);
55
+ console.log(signature);
56
+ ```
57
+
58
+ ### Option B — manual `VaultClient` (tests, custom integrations)
59
+
60
+ Use this when you already have a `Connection` and signer:
61
+
62
+ ```typescript
63
+ import { AnchorProvider } from "@anchor-lang/core";
64
+ import { Connection, Keypair } from "@solana/web3.js";
65
+ import { fromWeb3Pk } from "common";
66
+ import { VaultClient, makeProvider } from "vault";
67
+
68
+ const connection = new Connection("http://127.0.0.1:8899", "confirmed");
69
+ const payer = Keypair.generate();
70
+ const client = new VaultClient(makeProvider(connection, payer));
71
+
72
+ const [vault] = await client.pda.deriveVaultPda(0); // vault id
73
+ ```
74
+
75
+ ## Creating transactions
76
+
77
+ ### The builder pattern
78
+
79
+ Every instruction lives on `client.tx.<builderName>`. Builders follow the same
80
+ contract:
81
+
82
+ | Method | Returns | When to use |
83
+ | --------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
84
+ | `getTx(txArgs)` | `VaultTransactionPlan` | **Preferred.** Derives PDAs/ATAs, prepends setup ixs (e.g. idempotent ATA creates), and attaches cache invalidation metadata. |
85
+ | `getIx(ixArgs)` | `Instruction` | Low-level. You supply every account address yourself. |
86
+
87
+ `TxArgs` types are the high-level inputs (curator, vault, amounts as `bigint`,
88
+ optional PDAs). `IxArgs` types are the fully-resolved account sets the program
89
+ expects. Type definitions live in
90
+ [`src/client/builders/args.ts`](src/client/builders/args.ts).
91
+
92
+ A typical flow:
93
+
94
+ ```typescript
95
+ // 1. Build a plan
96
+ const plan = await client.tx.executeDeposit.getTx({
97
+ user: signer,
98
+ vault,
99
+ assetMint,
100
+ shareMint,
101
+ amount: 500_000n,
102
+ });
103
+
104
+ // 2. Send it (signs, confirms, clears relevant account cache)
105
+ const sig = await client.sendTransaction(payer, plan);
106
+ ```
107
+
108
+ `VaultTransactionPlan` contains:
109
+
110
+ - `instructions` — ordered `@solana/kit` instructions (preamble + program ix)
111
+ - `lookupTables?` — optional address lookup tables for v0 transactions
112
+ - `postSuccessCacheInvalidations?` — vault cache keys to clear after success
113
+
114
+ ### Direct Rust CPI integrations
115
+
116
+ For integrations that build a Rust CPI into the vault program, some builders also
117
+ expose the resolved Anchor account map and method arguments separately:
118
+
119
+ - `getIxAccounts(ixArgs)` — named accounts passed to Anchor's `accountsPartial`
120
+ - `getIxData(ixArgs)` — instruction method arguments, such as amounts and
121
+ tranche kind
122
+
123
+ These helpers are currently available on:
124
+
125
+ - `client.tx.executeDeposit`
126
+ - `client.tx.executeWithdraw`
127
+ - `client.tx.executeTrancheDeposit`
128
+ - `client.tx.executeTrancheWithdraw`
129
+
130
+ Use these when a TypeScript integration wants the SDK to stay the source of
131
+ truth for the account layout and argument shape, but your on-chain Rust program
132
+ performs the final CPI. They accept fully-resolved `IxArgs`, so derive PDAs/ATAs
133
+ with `client.pda`, account fetches, or `getAtaAddress(...)` before calling them.
134
+ The returned data is not serialized instruction bytes; it is the same account
135
+ map and argument values that `getIx(...)` uses internally.
136
+
137
+ ```typescript
138
+ import { BN } from "@anchor-lang/core";
139
+
140
+ const ixArgs = {
141
+ user,
142
+ vault,
143
+ vaultOracle,
144
+ vaultTrancheState: null,
145
+ assetMint,
146
+ shareMint,
147
+ amount: new BN("1000000"),
148
+ assetTokenProgram,
149
+ shareTokenProgram,
150
+ userAssetAta,
151
+ vaultAssetAta,
152
+ feeVault,
153
+ feeVaultAta,
154
+ userShareAta,
155
+ };
156
+
157
+ const accounts = client.tx.executeDeposit.getIxAccounts(ixArgs);
158
+ const data = client.tx.executeDeposit.getIxData(ixArgs);
159
+
160
+ // Example payload to hand to your Rust integration layer.
161
+ const cpiPayload = {
162
+ accounts: Object.fromEntries(
163
+ Object.entries(accounts).map(([name, pubkey]) => [
164
+ name,
165
+ pubkey?.toString() ?? null,
166
+ ])
167
+ ),
168
+ data: {
169
+ amount: data.amount.toString(),
170
+ },
171
+ };
172
+ ```
173
+
174
+ In Rust, map those account addresses into the matching Anchor CPI account struct
175
+ and pass the data fields into the generated CPI method. For example,
176
+ `executeDeposit.getIxData(...)` returns `{ amount }`, which corresponds to the
177
+ `amount` argument on the vault program's `execute_deposit` CPI.
178
+
179
+ ## Quoting conversions
180
+
181
+ Use `client.quote.quote(...)` to calculate expected output amounts before
182
+ building a transaction. Quotes fetch the current vault/tranche state, calculate
183
+ using the same fee formulas as the program, and require a `signer` address so
184
+ the SDK can check whether the signer is owned by a fee-exempt program.
185
+
186
+ All amounts are base units. `expectedAmountOut` is the net amount after fees;
187
+ `grossAmountOut`, `feeAmount`, `feeBps`, and `feeExempt` explain the quote.
188
+
189
+ ### Regular share mint
190
+
191
+ ```typescript
192
+ // Asset -> regular vault shares
193
+ const depositQuote = await client.quote.quote({
194
+ shareClass: "regular",
195
+ direction: "deposit",
196
+ signer,
197
+ vault,
198
+ assetMint,
199
+ amount: 1_000_000n,
200
+ });
201
+
202
+ console.log(depositQuote.expectedAmountOut); // regular shares to receive
203
+ console.log(depositQuote.feeExempt); // true when signer owner is whitelisted
204
+
205
+ // Regular vault shares -> asset
206
+ const withdrawQuote = await client.quote.quote({
207
+ shareClass: "regular",
208
+ direction: "withdraw",
209
+ signer,
210
+ vault,
211
+ assetMint,
212
+ amount: 500_000n, // share amount to burn
213
+ });
214
+
215
+ console.log(withdrawQuote.expectedAmountOut); // asset tokens to receive
216
+ ```
217
+
218
+ ### Junior and senior tranches
219
+
220
+ ```typescript
221
+ // Asset -> junior tranche shares
222
+ const juniorDepositQuote = await client.quote.quote({
223
+ shareClass: "junior",
224
+ direction: "deposit",
225
+ signer,
226
+ vault,
227
+ assetMint,
228
+ amount: 1_000_000n,
229
+ });
230
+
231
+ // Senior works the same way.
232
+ const seniorDepositQuote = await client.quote.quote({
233
+ shareClass: "senior",
234
+ direction: "deposit",
235
+ signer,
236
+ vault,
237
+ assetMint,
238
+ amount: 1_000_000n,
239
+ });
240
+ ```
241
+
242
+ For junior withdrawals, pass `withdrawalMode: "instant"` to quote
243
+ `executeTrancheWithdraw`, or `withdrawalMode: "queued"` to quote
244
+ `fulfillJuniorTrancheWithdraw` after lockup. Senior withdrawals are already
245
+ fee-free, but still support the same quote shape.
246
+
247
+ ```typescript
248
+ // Junior tranche shares -> asset through instant withdrawal.
249
+ const instantJuniorWithdrawQuote = await client.quote.quote({
250
+ shareClass: "junior",
251
+ direction: "withdraw",
252
+ withdrawalMode: "instant",
253
+ signer,
254
+ vault,
255
+ assetMint,
256
+ amount: 100_000n, // junior shares to burn
257
+ });
258
+
259
+ // Junior tranche shares -> asset through queued fulfillment.
260
+ const queuedJuniorWithdrawQuote = await client.quote.quote({
261
+ shareClass: "junior",
262
+ direction: "withdraw",
263
+ withdrawalMode: "queued",
264
+ signer,
265
+ vault,
266
+ assetMint,
267
+ amount: 100_000n,
268
+ });
269
+
270
+ // Senior tranche shares -> asset.
271
+ const seniorWithdrawQuote = await client.quote.quote({
272
+ shareClass: "senior",
273
+ direction: "withdraw",
274
+ signer,
275
+ vault,
276
+ assetMint,
277
+ amount: 100_000n,
278
+ });
279
+ ```
280
+
281
+ If you already derived the tranche state PDA, pass `vaultTrancheState` to avoid
282
+ an extra PDA derivation/fetch. Pass `fresh: true` when quoting immediately after
283
+ another transaction and you want to bypass the SDK account cache.
284
+
285
+ ### Example: deposit
286
+
287
+ ```typescript
288
+ const plan = await client.tx.executeDeposit.getTx({
289
+ user: signer,
290
+ vault,
291
+ assetMint,
292
+ shareMint,
293
+ amount: 1_000_000n,
294
+ });
295
+ await client.sendTransaction(payer, plan);
296
+ ```
297
+
298
+ The builder automatically:
299
+
300
+ - Derives `vaultOracle` from the vault PDA
301
+ - Computes user/vault ATAs
302
+ - Prepends idempotent ATA-create instructions where needed
303
+
304
+ ### Example: withdraw
305
+
306
+ ```typescript
307
+ const plan = await client.tx.executeWithdraw.getTx({
308
+ user: signer,
309
+ vault,
310
+ assetMint,
311
+ shareMint,
312
+ shareAmount: 500_000n,
313
+ });
314
+ await client.sendTransaction(payer, plan);
315
+ ```
316
+
317
+ For withdrawals routed through external liquidity (CPI), pass
318
+ `externalWithdrawIxRefs`, `externalWithdrawAccounts`, and optionally
319
+ `lookupTables` — see `ExecuteWithdrawTxArgs` in `args.ts`.
320
+
321
+ ### Example: vault setup (multi-step)
322
+
323
+ Deploy flows compose several builders in sequence:
324
+
325
+ ```typescript
326
+ const curator = fromWeb3Pk(payer.publicKey);
327
+
328
+ await client.sendTransaction(
329
+ payer,
330
+ await client.tx.createVault.getTx({
331
+ curator,
332
+ shareMint,
333
+ strictAssetMint: assetMint,
334
+ assetDecimals: 6,
335
+ })
336
+ );
337
+
338
+ await client.sendTransaction(
339
+ payer,
340
+ await client.tx.createTrancheState.getTx({
341
+ curator,
342
+ vault,
343
+ juniorShareMint,
344
+ seniorShareMint,
345
+ seniorFixedApyBps: 1_000,
346
+ })
347
+ );
348
+
349
+ await client.sendTransaction(
350
+ payer,
351
+ await client.tx.updateConsensusSigners.getTx({
352
+ curator,
353
+ vault,
354
+ signers: [curator],
355
+ })
356
+ );
357
+ ```
358
+
359
+ ### Example: tranche deposit
360
+
361
+ ```typescript
362
+ const plan = await client.tx.executeTrancheDeposit.getTx({
363
+ user: signer,
364
+ vault,
365
+ assetMint,
366
+ trancheShareMint: juniorMint,
367
+ kind: { junior: {} }, // or { senior: {} }
368
+ amount: 1_000_000n,
369
+ });
370
+ await client.sendTransaction(payer, plan);
371
+ ```
372
+
373
+ ### Example: junior withdrawal queue
374
+
375
+ ```typescript
376
+ // User queues a locked junior withdraw
377
+ await client.sendTransaction(
378
+ payer,
379
+ await client.tx.requestJuniorTrancheWithdraw.getTx({
380
+ owner: signer,
381
+ vault,
382
+ juniorMint,
383
+ queueId: 0,
384
+ shareAmount: 100_000n,
385
+ })
386
+ );
387
+
388
+ // Fulfiller pays out after lockup (see WithdrawalQueueService for batch logic)
389
+ await client.sendTransaction(
390
+ fulfiller,
391
+ await client.tx.fulfillJuniorTrancheWithdraw.getTx({
392
+ fulfiller: fromWeb3Pk(fulfiller.publicKey),
393
+ owner,
394
+ vault,
395
+ queueId: 0,
396
+ })
397
+ );
398
+ ```
399
+
400
+ ## Sending transactions
401
+
402
+ ### Built-in helper
403
+
404
+ `VaultClient.sendTransaction` builds a legacy `Transaction`, signs with the
405
+ fee payer, sends, confirms, and applies cache invalidations from the plan:
406
+
407
+ ```typescript
408
+ const signature = await client.sendTransaction(payer, plan);
409
+ ```
410
+
411
+ ### Versioned transactions (lookup tables)
412
+
413
+ Some builders (e.g. `executeWithdraw`, `protocolInteraction`, `jupiterSwap`)
414
+ return `lookupTables` on the plan. `sendTransaction` does **not** compile v0
415
+ messages — use `sendVersionedTransaction` from `common` instead:
416
+
417
+ ```typescript
418
+ import { fromKitInstruction, sendVersionedTransaction } from "common";
419
+
420
+ const plan = await client.tx.protocolInteraction.getTx({
421
+ /* ... */
422
+ });
423
+
424
+ const signature = await sendVersionedTransaction({
425
+ connection: client.provider.connection,
426
+ payer,
427
+ instructions: plan.instructions.map(fromKitInstruction),
428
+ lookupTables: plan.lookupTables ?? [],
429
+ });
430
+
431
+ client.applyCacheInvalidations(plan);
432
+ ```
433
+
434
+ ### Composing your own transaction
435
+
436
+ You can merge instructions from multiple builders or mix in custom ixs:
437
+
438
+ ```typescript
439
+ const depositPlan = await client.tx.executeDeposit.getTx({ /* ... */ });
440
+ const customIx = /* your instruction */;
441
+
442
+ const combined: VaultTransactionPlan = {
443
+ instructions: [...depositPlan.instructions, customIx],
444
+ postSuccessCacheInvalidations: depositPlan.postSuccessCacheInvalidations,
445
+ };
446
+ ```
447
+
448
+ Or call `getIx` directly when you only need the program instruction:
449
+
450
+ ```typescript
451
+ const ix = await client.tx.executeDeposit.getIx({
452
+ user: signer,
453
+ vault,
454
+ vaultOracle,
455
+ vaultTrancheState: null,
456
+ assetMint,
457
+ shareMint,
458
+ amount: new BN("1000000"),
459
+ assetTokenProgram: TOKEN_PROGRAM_ID,
460
+ shareTokenProgram: TOKEN_PROGRAM_ID,
461
+ userAssetAta,
462
+ vaultAssetAta,
463
+ userShareAta,
464
+ });
465
+ ```
466
+
467
+ ## Reading on-chain state
468
+
469
+ Use `client.account` to fetch decoded vault accounts (cached by default, TTL
470
+ 60s):
471
+
472
+ ```typescript
473
+ const vaultAccount = await client.account.fetchVault(vault);
474
+ const oracle = await client.account.fetchVaultOracle(vaultOraclePda);
475
+ const trancheState = await client.account.fetchVaultTrancheState(tranchePda);
476
+
477
+ // Force a fresh read after an external tx
478
+ const fresh = await client.account.fetchVault(vault, { fresh: true });
479
+ client.clearAllCache();
480
+ ```
481
+
482
+ Derive PDAs with `client.pda`:
483
+
484
+ ```typescript
485
+ const [vault] = await client.pda.deriveVaultPda(vaultId);
486
+ const [vaultOracle] = await client.pda.deriveVaultOraclePda(vault);
487
+ const [trancheState] = await client.pda.deriveVaultTrancheStatePda(vault);
488
+ const [queue] = await client.pda.deriveWithdrawalQueuePda(
489
+ vault,
490
+ owner,
491
+ queueId
492
+ );
493
+ ```
494
+
495
+ ## Available transaction builders
496
+
497
+ All builders are on `client.tx`:
498
+
499
+ | Builder | Program instruction | Typical signer |
500
+ | ------------------------------ | --------------------------------- | ---------------- |
501
+ | `createVault` | `create_vault` | Curator |
502
+ | `createAssetHolding` | `create_asset_holding` | HW manager |
503
+ | `removeAssetHolding` | `remove_asset_holding` | HW manager |
504
+ | `updateConsensusSigners` | `update_consensus_signers` | Curator |
505
+ | `createTrancheState` | `create_tranche_state` | Curator |
506
+ | `setVaultConfig` | `set_vault_config` | Curator |
507
+ | `updateTrancheConfig` | `update_tranche_config` | Curator |
508
+ | `activateCircuitBreaker` | `activate_circuit_breaker` | CB trigger |
509
+ | `disableCircuitBreaker` | `disable_circuit_breaker` | Curator |
510
+ | `updateConsensusOracle` | `update_consensus_oracle` | Consensus signer |
511
+ | `executeDeposit` | `execute_deposit` | User |
512
+ | `executeTrancheDeposit` | `execute_tranche_deposit` | User |
513
+ | `executeTrancheWithdraw` | `execute_tranche_withdraw` | User |
514
+ | `requestJuniorTrancheWithdraw` | `request_junior_tranche_withdraw` | Owner |
515
+ | `cancelJuniorTrancheWithdraw` | `cancel_junior_tranche_withdraw` | Owner |
516
+ | `fulfillJuniorTrancheWithdraw` | `fulfill_junior_tranche_withdraw` | Fulfiller |
517
+ | `executeWithdraw` | `execute_withdraw` | User |
518
+ | `managerWithdrawAsset` | `manager_withdraw_asset` | HW manager |
519
+ | `managerRedepositAsset` | `manager_redeposit_asset` | HW manager |
520
+ | `protocolInteraction` | `protocol_interaction` | HW manager |
521
+ | `jupiterSwap` | `jupiter_swap` | HW manager |
522
+ | `setExternalLiquidity` | `set_external_liquidity` | HW manager |
523
+ | `setAssetPriceOracle` | `set_asset_price_oracle` | Curator |
524
+ | `updateAssetPrice` | `update_asset_price` | Oracle keeper |
525
+ | `vaultReallocation` | `vault_reallocation` | HW manager |
526
+ | `withdrawProtocolFees` | `withdraw_protocol_fees` | Curator |
527
+ | `withdrawTrancheFees` | `withdraw_tranche_fees` | Curator |
528
+
529
+ Argument shapes for each builder: [`src/client/builders/args.ts`](src/client/builders/args.ts).
530
+
531
+ ## Higher-level services
532
+
533
+ For recurring backend work, prefer the services layer over reimplementing
534
+ discovery logic in your app:
535
+
536
+ - **`WithdrawalQueueService`** — scan and fulfill eligible junior withdrawal
537
+ queues (`src/services/withdrawalQueueService.ts`)
538
+ - **`OracleService`** — consensus oracle / asset price updates
539
+ (`src/services/oracleService.ts`)
540
+
541
+ These services call the same `client.tx.*.getTx` builders internally.
542
+
543
+ ## Environment and RPC
544
+
545
+ Copy [`.env.example`](.env.example) to `.env`. RPC resolution order:
546
+
547
+ 1. `VAULT_RPC_URL_TEST` / `VAULT_RPC_URL_PROD`
548
+ 2. `RPC_URL`
549
+ 3. `HELIUS_API_KEY` (auto-built Helius URL)
550
+
551
+ Program ids per environment: `getVaultProgramId("test" | "prod")` in
552
+ [`src/env.ts`](src/env.ts).
553
+
554
+ ## CLI scripts
555
+
556
+ Thin wrappers around the SDK live in [`scripts/`](scripts/). From the repo root:
557
+
558
+ ```bash
559
+ pnpm deposit test <VAULT_PDA> 100
560
+ pnpm inspect prod <VAULT_PDA>
561
+ ```
562
+
563
+ See [`scripts/README.md`](scripts/README.md) for the full command list.
564
+
565
+ ## Types and addresses
566
+
567
+ - Public keys use `@solana/kit` `Address` strings in builder args.
568
+ - Convert from web3.js: `fromWeb3Pk(keypair.publicKey)` (from `common`).
569
+ - Amounts in `TxArgs` are `bigint` base units unless noted otherwise.