@exponent-labs/exponent-sdk 0.9.1 → 0.9.2

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 (26) hide show
  1. package/build/client/vaults/types/kaminoObligationEntry.d.ts +21 -21
  2. package/build/client/vaults/types/obligationType.d.ts +1 -1
  3. package/build/client/vaults/types/proposalAction.d.ts +54 -54
  4. package/build/client/vaults/types/reserveFarmMapping.d.ts +3 -3
  5. package/build/client/vaults/types/strategyPosition.d.ts +1 -1
  6. package/build/exponentVaults/index.d.ts +1 -1
  7. package/build/exponentVaults/index.js +3 -2
  8. package/build/exponentVaults/index.js.map +1 -1
  9. package/build/exponentVaults/vault-instruction-types.d.ts +1 -1
  10. package/build/exponentVaults/vault-interaction.d.ts +58 -41
  11. package/build/exponentVaults/vault-interaction.js +294 -54
  12. package/build/exponentVaults/vault-interaction.js.map +1 -1
  13. package/build/exponentVaults/vault-interaction.kamino-vault.test.d.ts +1 -0
  14. package/build/exponentVaults/vault-interaction.kamino-vault.test.js +143 -0
  15. package/build/exponentVaults/vault-interaction.kamino-vault.test.js.map +1 -0
  16. package/build/exponentVaults/vaultTransactionBuilder.js +35 -30
  17. package/build/exponentVaults/vaultTransactionBuilder.js.map +1 -1
  18. package/build/exponentVaults/vaultTransactionBuilder.test.js +84 -1
  19. package/build/exponentVaults/vaultTransactionBuilder.test.js.map +1 -1
  20. package/package.json +34 -32
  21. package/src/exponentVaults/index.ts +1 -0
  22. package/src/exponentVaults/vault-instruction-types.ts +1 -1
  23. package/src/exponentVaults/vault-interaction.kamino-vault.test.ts +149 -0
  24. package/src/exponentVaults/vault-interaction.ts +514 -86
  25. package/src/exponentVaults/vaultTransactionBuilder.test.ts +93 -0
  26. package/src/exponentVaults/vaultTransactionBuilder.ts +47 -41
@@ -1,5 +1,7 @@
1
- import { Reserve } from "@exponent-labs/kamino-reserve-deserializer"
1
+ import { BorshCoder, Idl } from "@coral-xyz/anchor"
2
+ import { Fraction, Reserve } from "@exponent-labs/kamino-reserve-deserializer"
2
3
  import { fetchKaminoVaultIndex } from "@exponent-labs/exponent-fetcher"
4
+ import { IDL as KaminoVaultIdl } from "@exponent-labs/kamino-vault-idl"
3
5
  import { KAMINO_MARKETS, KAMINO_RESERVES, KaminoMarket } from "./kamino-markets"
4
6
  import Decimal from "decimal.js"
5
7
  import {
@@ -50,6 +52,7 @@ import {
50
52
  refreshObligation,
51
53
  } from "@exponent-labs/klend-idl/instructions"
52
54
  import {
55
+ AccountLayout,
53
56
  TOKEN_PROGRAM_ID,
54
57
  NATIVE_MINT,
55
58
  getAssociatedTokenAddressSync,
@@ -63,7 +66,18 @@ import {
63
66
  } from "./policyMatcher"
64
67
  import { buildScopeRefreshInstructions } from "./scope-refresh"
65
68
  import * as web3 from "@solana/web3.js"
66
- import { AccountMeta, Connection, PublicKey, SystemProgram, SYSVAR_INSTRUCTIONS_PUBKEY, SYSVAR_RENT_PUBKEY, TransactionInstruction } from "@solana/web3.js"
69
+ import {
70
+ AccountMeta,
71
+ AddressLookupTableAccount,
72
+ Connection,
73
+ PublicKey,
74
+ SystemProgram,
75
+ SYSVAR_INSTRUCTIONS_PUBKEY,
76
+ SYSVAR_RENT_PUBKEY,
77
+ TransactionInstruction,
78
+ TransactionMessage,
79
+ VersionedTransaction,
80
+ } from "@solana/web3.js"
67
81
  import BN from "bn.js"
68
82
 
69
83
  const KAMINO_FARMS_PROGRAM_ID = new PublicKey("FarmsPZpWu9i7Kky8tPN37rs2TpmMrAZrC7S7vJa91Hr")
@@ -73,6 +87,7 @@ const KAMINO_VAULT_ALLOCATION_STRATEGY_OFFSET = 304
73
87
  const KAMINO_VAULT_ALLOCATION_SIZE = 2160
74
88
  const KAMINO_VAULT_ALLOCATION_CTOKEN_VAULT_OFFSET = 32
75
89
  const KAMINO_VAULT_GLOBAL_CONFIG_SEED = Buffer.from("global_config")
90
+ const KAMINO_VAULT_CODER = new BorshCoder(KaminoVaultIdl as Idl)
76
91
 
77
92
  // ============================================================================
78
93
  // Vault Instruction Types (re-exported from vault-instruction-types.ts)
@@ -306,8 +321,8 @@ export const kaminoVaultAction = {
306
321
 
307
322
  /**
308
323
  * Withdraw Kamino Vault shares back into the vault-owned token account.
309
- * When the Kamino Vault currently allocates across multiple reserves,
310
- * specify the reserve to disinvest from.
324
+ * `reserve` is an optional override. When omitted, the SDK plans the
325
+ * withdraw across the vault's active reserves automatically.
311
326
  */
312
327
  withdraw(params: {
313
328
  vault: PublicKey
@@ -403,54 +418,7 @@ export interface VaultSyncTransactionResult {
403
418
  addressLookupTableAddresses: PublicKey[]
404
419
  }
405
420
 
406
- /**
407
- * Build vault instructions and wrap them in a Squads sync transaction.
408
- *
409
- * Takes high-level `VaultInstruction` descriptors (built with `kamino.*`),
410
- * resolves them to raw Solana instructions, then separates them:
411
- * - Permissionless refresh instructions → `preInstructions` (top-level)
412
- * - Vault-signed instructions → `instruction` (Squads sync transaction)
413
- *
414
- * KLend's `check_refresh` requires refreshReserve to be a top-level instruction
415
- * so it can be found via the instruction sysvar.
416
- *
417
- * @returns `{ setupInstructions, preInstructions, instruction, postInstructions, signers, addressLookupTableAddresses }`
418
- *
419
- * @example
420
- * ```ts
421
- * const { setupInstructions, preInstructions, instruction, postInstructions, signers, addressLookupTableAddresses } = await createVaultSyncTransaction({
422
- * instructions: [
423
- * kamino.initUserMetadata(KaminoMarket.MAIN),
424
- * kamino.initObligation(KaminoMarket.MAIN),
425
- * kamino.deposit(KaminoMarket.MAIN, "USDC", new BN(1_000_000)),
426
- * ],
427
- * owner: vaultPda,
428
- * connection,
429
- * policyPda,
430
- * vaultPda,
431
- * signer: wallet.publicKey,
432
- * vaultAddress: VAULT_ADDRESS,
433
- * })
434
- * // Send: [...setupInstructions, ...preInstructions, instruction, ...postInstructions]
435
- * ```
436
- */
437
- export async function createVaultSyncTransaction({
438
- instructions,
439
- owner,
440
- connection,
441
- policyPda,
442
- vaultPda,
443
- signer,
444
- accountIndex = 0,
445
- constraintIndices,
446
- vaultAddress,
447
- leadingAccounts,
448
- preHookAccounts,
449
- postHookAccounts,
450
- squadsProgram = SQUADS_PROGRAM_ID,
451
- autoManagePositions = false,
452
- setupContext,
453
- }: {
421
+ type CreateVaultSyncTransactionParams = {
454
422
  instructions: VaultInstruction[]
455
423
  owner: PublicKey
456
424
  connection: Connection
@@ -469,8 +437,66 @@ export async function createVaultSyncTransaction({
469
437
  autoManagePositions?: boolean
470
438
  /** Optional shared setup context — when provided, setup state (tracked accounts, positions) is shared across calls, preventing duplicate setup instructions. */
471
439
  setupContext?: StrategySetupContext
472
- }): Promise<VaultSyncTransactionResult> {
440
+ }
441
+
442
+ /**
443
+ * Build one or more vault sync transactions from high-level instruction descriptors.
444
+ *
445
+ * This is the plural companion to {@link createVaultSyncTransaction}. Most
446
+ * calls return a single result. Smart Kamino Vault withdraws can expand into
447
+ * multiple reserve-specific sync transactions when the wrapper needs to split
448
+ * an oversized withdraw across sequential chunks.
449
+ */
450
+ export async function createVaultSyncTransactions(
451
+ params: CreateVaultSyncTransactionParams,
452
+ ): Promise<VaultSyncTransactionResult[]> {
453
+ return buildVaultSyncTransactionResults(params, true)
454
+ }
455
+
456
+ /**
457
+ * Build exactly one vault sync transaction and return its wrapped instruction set.
458
+ *
459
+ * Takes high-level `VaultInstruction` descriptors (built with `kamino.*`),
460
+ * resolves them to raw Solana instructions, then separates them:
461
+ * - Permissionless refresh instructions → `preInstructions` (top-level)
462
+ * - Vault-signed instructions → `instruction` (Squads sync transaction)
463
+ *
464
+ * KLend's `check_refresh` requires refreshReserve to be a top-level instruction
465
+ * so it can be found via the instruction sysvar.
466
+ *
467
+ * When a smart Kamino Vault withdraw expands beyond one sync transaction, this
468
+ * singular helper throws and asks the caller to use
469
+ * {@link createVaultSyncTransactions} or {@link VaultTransactionBuilder}.
470
+ */
471
+ export async function createVaultSyncTransaction(
472
+ params: CreateVaultSyncTransactionParams,
473
+ ): Promise<VaultSyncTransactionResult> {
474
+ const results = await buildVaultSyncTransactionResults(params, false)
475
+ return results[0]!
476
+ }
477
+
478
+ async function buildVaultSyncTransactionResults(
479
+ {
480
+ instructions,
481
+ owner,
482
+ connection,
483
+ policyPda,
484
+ vaultPda,
485
+ signer,
486
+ accountIndex = 0,
487
+ constraintIndices,
488
+ vaultAddress,
489
+ leadingAccounts,
490
+ preHookAccounts,
491
+ postHookAccounts,
492
+ squadsProgram = SQUADS_PROGRAM_ID,
493
+ autoManagePositions = false,
494
+ setupContext,
495
+ }: CreateVaultSyncTransactionParams,
496
+ splitOversizedKaminoVaultWithdraw: boolean,
497
+ ): Promise<VaultSyncTransactionResult[]> {
473
498
  vaultPda ??= owner
499
+
474
500
  const resolvedSetupContext = setupContext ?? createStrategySetupContext({
475
501
  connection,
476
502
  env: LOCAL_ENV,
@@ -486,7 +512,8 @@ export async function createVaultSyncTransaction({
486
512
  postHookAccounts,
487
513
  autoManagePositions,
488
514
  })
489
- const { setupInstructions, syncInstructions, preInstructions, postInstructions, signers, addressLookupTableAddresses } = await buildVaultInstructions(
515
+
516
+ const buckets = await buildVaultInstructions(
490
517
  instructions,
491
518
  owner,
492
519
  connection,
@@ -502,10 +529,149 @@ export async function createVaultSyncTransaction({
502
529
  autoManagePositions,
503
530
  resolvedSetupContext,
504
531
  )
532
+
505
533
  const setupStatePriceRefreshInstructions = setupContext
506
534
  ? []
507
535
  : await buildSetupStatePriceRefreshInstructions(resolvedSetupContext)
508
536
 
537
+ const fullResult = await buildWrappedVaultSyncTransactionResult({
538
+ connection,
539
+ policyPda,
540
+ vaultPda,
541
+ signer,
542
+ accountIndex,
543
+ constraintIndices,
544
+ vaultAddress,
545
+ leadingAccounts,
546
+ preHookAccounts,
547
+ postHookAccounts,
548
+ squadsProgram,
549
+ syncInstructions: buckets.syncInstructions,
550
+ setupInstructions: buckets.setupInstructions,
551
+ preInstructions: [...setupStatePriceRefreshInstructions, ...buckets.preInstructions],
552
+ postInstructions: buckets.postInstructions,
553
+ signers: buckets.signers,
554
+ addressLookupTableAddresses: buckets.addressLookupTableAddresses,
555
+ })
556
+
557
+ const isPlannerExpandedKaminoVaultWithdraw =
558
+ instructions.length === 1
559
+ && instructions[0]?.action === KaminoVaultAction.WITHDRAW
560
+ && !(instructions[0] as KaminoVaultWithdrawInstruction).reserve
561
+ && buckets.syncInstructions.length > 1
562
+
563
+ if (!isPlannerExpandedKaminoVaultWithdraw) {
564
+ return [fullResult]
565
+ }
566
+
567
+ if (await vaultSyncResultFitsInSingleTransaction({
568
+ connection,
569
+ payer: signer,
570
+ result: fullResult,
571
+ })) {
572
+ return [fullResult]
573
+ }
574
+
575
+ if (!splitOversizedKaminoVaultWithdraw) {
576
+ throw new Error(
577
+ "Kamino Vault withdraw expands beyond one sync transaction; use createVaultSyncTransactions() or VaultTransactionBuilder.",
578
+ )
579
+ }
580
+
581
+ if (buckets.preInstructions.length > 0 || buckets.postInstructions.length > 0) {
582
+ throw new Error(
583
+ "Kamino Vault withdraw chunking only supports sync-only actions; use explicit reserve overrides if you need custom refresh handling.",
584
+ )
585
+ }
586
+
587
+ const splitResults: VaultSyncTransactionResult[] = []
588
+ for (const [index, syncInstruction] of buckets.syncInstructions.entries()) {
589
+ const chunkConstraintIndices = constraintIndices
590
+ ? constraintIndices.length === buckets.syncInstructions.length
591
+ ? [constraintIndices[index]!]
592
+ : constraintIndices.length === 1
593
+ ? constraintIndices
594
+ : (() => {
595
+ throw new Error(
596
+ "constraintIndices must contain either one entry or one entry per generated sync instruction when splitting a Kamino Vault withdraw.",
597
+ )
598
+ })()
599
+ : undefined
600
+
601
+ const chunkResult = await buildWrappedVaultSyncTransactionResult({
602
+ connection,
603
+ policyPda,
604
+ vaultPda,
605
+ signer,
606
+ accountIndex,
607
+ constraintIndices: chunkConstraintIndices,
608
+ vaultAddress,
609
+ leadingAccounts,
610
+ preHookAccounts,
611
+ postHookAccounts,
612
+ squadsProgram,
613
+ syncInstructions: [syncInstruction],
614
+ setupInstructions: index === 0 ? buckets.setupInstructions : [],
615
+ preInstructions: index === 0 ? [...setupStatePriceRefreshInstructions] : [],
616
+ postInstructions: [],
617
+ signers: buckets.signers,
618
+ addressLookupTableAddresses: buckets.addressLookupTableAddresses,
619
+ })
620
+
621
+ const chunkFits = await vaultSyncResultFitsInSingleTransaction({
622
+ connection,
623
+ payer: signer,
624
+ result: chunkResult,
625
+ })
626
+ if (!chunkFits) {
627
+ throw new Error(
628
+ `Kamino Vault withdraw chunk ${index + 1} still exceeds one sync transaction; use explicit reserve overrides or smaller steps.`,
629
+ )
630
+ }
631
+
632
+ splitResults.push(chunkResult)
633
+ }
634
+
635
+ return splitResults
636
+ }
637
+
638
+ async function buildWrappedVaultSyncTransactionResult({
639
+ connection,
640
+ policyPda,
641
+ vaultPda,
642
+ signer,
643
+ accountIndex,
644
+ constraintIndices,
645
+ vaultAddress,
646
+ leadingAccounts,
647
+ preHookAccounts,
648
+ postHookAccounts,
649
+ squadsProgram,
650
+ syncInstructions,
651
+ setupInstructions,
652
+ preInstructions,
653
+ postInstructions,
654
+ signers,
655
+ addressLookupTableAddresses,
656
+ }: {
657
+ connection: Connection
658
+ policyPda?: PublicKey
659
+ vaultPda: PublicKey
660
+ signer: PublicKey
661
+ accountIndex: number
662
+ constraintIndices?: number[]
663
+ vaultAddress?: PublicKey
664
+ leadingAccounts?: PublicKey[] | AccountMeta[]
665
+ preHookAccounts?: PublicKey[] | AccountMeta[]
666
+ postHookAccounts?: PublicKey[] | AccountMeta[]
667
+ squadsProgram: PublicKey
668
+ syncInstructions: TransactionInstruction[]
669
+ setupInstructions: TransactionInstruction[]
670
+ preInstructions: TransactionInstruction[]
671
+ postInstructions: TransactionInstruction[]
672
+ signers: web3.Signer[]
673
+ addressLookupTableAddresses: PublicKey[]
674
+ }): Promise<VaultSyncTransactionResult> {
509
675
  let resolvedPolicyPda = policyPda
510
676
  let resolvedConstraintIndices = constraintIndices
511
677
  if (!resolvedPolicyPda) {
@@ -517,14 +683,12 @@ export async function createVaultSyncTransaction({
517
683
  resolvedConstraintIndices ??= match.constraintIndices
518
684
  }
519
685
 
520
- // Auto-resolve constraint indices when not explicitly provided
521
686
  resolvedConstraintIndices ??= await resolveConstraintIndices(
522
687
  connection,
523
688
  resolvedPolicyPda,
524
689
  syncInstructions,
525
690
  )
526
691
 
527
- // Auto-resolve policy prefix and hook accounts when not explicitly provided
528
692
  let resolvedLeadingAccounts = leadingAccounts
529
693
  let resolvedPreHookAccounts = preHookAccounts
530
694
  let resolvedPostHookAccounts = postHookAccounts
@@ -550,7 +714,7 @@ export async function createVaultSyncTransaction({
550
714
 
551
715
  return {
552
716
  setupInstructions,
553
- preInstructions: [...setupStatePriceRefreshInstructions, ...preInstructions],
717
+ preInstructions,
554
718
  instruction,
555
719
  postInstructions,
556
720
  signers,
@@ -558,6 +722,38 @@ export async function createVaultSyncTransaction({
558
722
  }
559
723
  }
560
724
 
725
+ async function vaultSyncResultFitsInSingleTransaction(params: {
726
+ connection: Connection
727
+ payer: PublicKey
728
+ result: VaultSyncTransactionResult
729
+ }): Promise<boolean> {
730
+ try {
731
+ const altAccounts = await resolveVaultSyncAltAccounts(
732
+ params.connection,
733
+ params.result.addressLookupTableAddresses,
734
+ )
735
+ const message = new TransactionMessage({
736
+ payerKey: params.payer,
737
+ recentBlockhash: PublicKey.default.toBase58(),
738
+ instructions: [...params.result.preInstructions, params.result.instruction, ...params.result.postInstructions],
739
+ }).compileToV0Message(altAccounts)
740
+
741
+ return new VersionedTransaction(message).serialize().length <= 1232
742
+ } catch {
743
+ return false
744
+ }
745
+ }
746
+
747
+ async function resolveVaultSyncAltAccounts(
748
+ connection: Connection,
749
+ addresses: PublicKey[],
750
+ ): Promise<AddressLookupTableAccount[]> {
751
+ const lookupTables = await Promise.all(addresses.map((address) => connection.getAddressLookupTable(address)))
752
+ return lookupTables
753
+ .map((entry) => entry.value)
754
+ .filter((entry): entry is AddressLookupTableAccount => entry !== null)
755
+ }
756
+
561
757
  // ============================================================================
562
758
  // Internal: Instruction Assembly
563
759
  // ============================================================================
@@ -1698,10 +1894,32 @@ const KAMINO_VAULT_EVENT_AUTHORITY = emitEventAuthority(KAMINO_VAULT_PROGRAM_ID)
1698
1894
  const KAMINO_FARM_USER_STATE_SIZE = 920
1699
1895
  const KAMINO_STAKE_ALL_AMOUNT = new BN("18446744073709551615")
1700
1896
 
1897
+ type KaminoVaultIndex = Awaited<ReturnType<typeof fetchKaminoVaultIndex>>
1898
+
1899
+ type KaminoVaultDecodedState = {
1900
+ tokenAvailable: BN
1901
+ sharesIssued: BN
1902
+ pendingFeesSf: BN
1903
+ allocations: Array<{
1904
+ reserve: PublicKey
1905
+ ctokenAllocation: BN
1906
+ }>
1907
+ }
1908
+
1909
+ type KaminoVaultResolvedReserve = KaminoVaultIndex["reserves"][number] & {
1910
+ account: Reserve
1911
+ investedLiquidityAmount: Decimal
1912
+ availableLiquidityToWithdraw: Decimal
1913
+ }
1914
+
1701
1915
  type KaminoVaultContext = {
1702
- index: Awaited<ReturnType<typeof fetchKaminoVaultIndex>>
1916
+ index: Omit<KaminoVaultIndex, "reserves"> & {
1917
+ reserves: KaminoVaultResolvedReserve[]
1918
+ }
1703
1919
  tokenAta: PublicKey
1704
1920
  sharesAta: PublicKey
1921
+ sharesBalance: BN
1922
+ state: KaminoVaultDecodedState
1705
1923
  }
1706
1924
 
1707
1925
  type KaminoFarmContext = {
@@ -1720,6 +1938,10 @@ function toBn(value: BN | bigint | number): BN {
1720
1938
  return new BN(value.toString())
1721
1939
  }
1722
1940
 
1941
+ function minBn(lhs: BN, rhs: BN): BN {
1942
+ return lhs.lte(rhs) ? lhs : rhs
1943
+ }
1944
+
1723
1945
  function encodeU64InstructionData(discriminator: Buffer, value: BN | bigint | number): Buffer {
1724
1946
  return Buffer.concat([discriminator, toBn(value).toArrayLike(Buffer, "le", 8)])
1725
1947
  }
@@ -1814,6 +2036,151 @@ function resolveKaminoVaultTrackedPriceId(params: {
1814
2036
  return { __kind: "Multiply", priceIds: [...reservePriceIds, params.sharePriceId] }
1815
2037
  }
1816
2038
 
2039
+ type KaminoVaultWithdrawPlanningSnapshot = {
2040
+ sharesBalance: BN
2041
+ tokenAvailable: BN
2042
+ sharesIssued: BN
2043
+ pendingFeesSf: BN
2044
+ reserves: Array<{
2045
+ reserveAddress: PublicKey
2046
+ investedLiquidityAmount: Decimal
2047
+ availableLiquidityToWithdraw: Decimal
2048
+ }>
2049
+ }
2050
+
2051
+ type KaminoVaultWithdrawPlanningLeg = {
2052
+ reserveAddress: PublicKey
2053
+ sharesAmount: BN
2054
+ }
2055
+
2056
+ function decodeKaminoVaultState(data: Buffer): KaminoVaultDecodedState {
2057
+ const decoded = KAMINO_VAULT_CODER.accounts.decode("VaultState", data) as {
2058
+ token_available: BN
2059
+ shares_issued: BN
2060
+ pending_fees_sf: BN
2061
+ vault_allocation_strategy: Array<{
2062
+ reserve: PublicKey
2063
+ ctoken_allocation: BN
2064
+ }>
2065
+ }
2066
+
2067
+ return {
2068
+ tokenAvailable: decoded.token_available,
2069
+ sharesIssued: decoded.shares_issued,
2070
+ pendingFeesSf: decoded.pending_fees_sf,
2071
+ allocations: decoded.vault_allocation_strategy
2072
+ .filter((allocation) => !allocation.reserve.equals(PublicKey.default))
2073
+ .map((allocation) => ({
2074
+ reserve: allocation.reserve,
2075
+ ctokenAllocation: allocation.ctoken_allocation,
2076
+ })),
2077
+ }
2078
+ }
2079
+
2080
+ function calculateKaminoVaultTokensPerShareFromSnapshot(
2081
+ snapshot: KaminoVaultWithdrawPlanningSnapshot,
2082
+ ): Decimal {
2083
+ if (snapshot.sharesIssued.isZero()) {
2084
+ return new Decimal(0)
2085
+ }
2086
+
2087
+ const investedTotal = snapshot.reserves.reduce(
2088
+ (total, reserve) => total.add(reserve.investedLiquidityAmount),
2089
+ new Decimal(0),
2090
+ )
2091
+ const pendingFees = new Fraction(snapshot.pendingFeesSf).toDecimal()
2092
+ const currentVaultAum = new Decimal(snapshot.tokenAvailable.toString())
2093
+ .add(investedTotal)
2094
+ .sub(pendingFees)
2095
+
2096
+ if (currentVaultAum.lte(0)) {
2097
+ return new Decimal(0)
2098
+ }
2099
+
2100
+ return currentVaultAum.div(snapshot.sharesIssued.toString())
2101
+ }
2102
+
2103
+ function planKaminoVaultWithdrawLegsFromSnapshot(params: {
2104
+ sharesAmount: BN
2105
+ reserve?: PublicKey
2106
+ snapshot: KaminoVaultWithdrawPlanningSnapshot
2107
+ }): KaminoVaultWithdrawPlanningLeg[] {
2108
+ if (params.reserve) {
2109
+ return [{ reserveAddress: params.reserve, sharesAmount: params.sharesAmount }]
2110
+ }
2111
+
2112
+ const actualSharesToWithdraw = minBn(params.sharesAmount, params.snapshot.sharesBalance)
2113
+ if (actualSharesToWithdraw.isZero()) {
2114
+ throw new Error("Cannot withdraw zero Kamino Vault shares")
2115
+ }
2116
+
2117
+ if (params.snapshot.reserves.length === 0) {
2118
+ throw new Error("Kamino Vault has no active reserves to anchor a reserve-specific withdraw instruction")
2119
+ }
2120
+
2121
+ const withdrawAllShares = params.sharesAmount.gte(params.snapshot.sharesBalance)
2122
+ const tokensPerShare = calculateKaminoVaultTokensPerShareFromSnapshot(params.snapshot)
2123
+ if (tokensPerShare.lte(0)) {
2124
+ throw new Error("Kamino Vault has zero share price; cannot plan a withdraw")
2125
+ }
2126
+
2127
+ const tokensToWithdraw = new Decimal(actualSharesToWithdraw.toString()).mul(tokensPerShare)
2128
+ const availableTokens = new Decimal(params.snapshot.tokenAvailable.toString())
2129
+
2130
+ if (tokensToWithdraw.lte(availableTokens)) {
2131
+ return [{
2132
+ reserveAddress: params.snapshot.reserves[0]!.reserveAddress,
2133
+ sharesAmount: withdrawAllShares ? params.snapshot.sharesBalance : actualSharesToWithdraw,
2134
+ }]
2135
+ }
2136
+
2137
+ let tokensRemaining = tokensToWithdraw
2138
+ const plannedLegs: KaminoVaultWithdrawPlanningLeg[] = []
2139
+ const sortedReserves = [...params.snapshot.reserves].sort((lhs, rhs) =>
2140
+ rhs.availableLiquidityToWithdraw.comparedTo(lhs.availableLiquidityToWithdraw),
2141
+ )
2142
+
2143
+ for (const [index, reserve] of sortedReserves.entries()) {
2144
+ const legCapacity = reserve.availableLiquidityToWithdraw.add(index === 0 ? availableTokens : 0)
2145
+ if (legCapacity.lte(0)) {
2146
+ continue
2147
+ }
2148
+
2149
+ const tokensForLeg = Decimal.min(tokensRemaining, legCapacity)
2150
+ if (tokensForLeg.lte(0)) {
2151
+ continue
2152
+ }
2153
+
2154
+ const sharesForLeg = withdrawAllShares
2155
+ ? params.snapshot.sharesBalance
2156
+ : new BN(tokensForLeg.div(tokensPerShare).floor().toFixed(0))
2157
+ if (sharesForLeg.isZero()) {
2158
+ continue
2159
+ }
2160
+
2161
+ plannedLegs.push({
2162
+ reserveAddress: reserve.reserveAddress,
2163
+ sharesAmount: sharesForLeg,
2164
+ })
2165
+
2166
+ tokensRemaining = tokensRemaining.sub(tokensForLeg)
2167
+ if (tokensRemaining.lte(0)) {
2168
+ break
2169
+ }
2170
+ }
2171
+
2172
+ if (plannedLegs.length === 0) {
2173
+ throw new Error("Unable to plan a Kamino Vault withdraw across the vault's active reserves")
2174
+ }
2175
+
2176
+ return plannedLegs
2177
+ }
2178
+
2179
+ export const __kaminoVaultTesting = {
2180
+ calculateKaminoVaultTokensPerShareFromSnapshot,
2181
+ planKaminoVaultWithdrawLegsFromSnapshot,
2182
+ }
2183
+
1817
2184
  async function resolveKaminoVaultContext(
1818
2185
  vaultAddress: PublicKey,
1819
2186
  setupContext: StrategySetupContext,
@@ -1833,6 +2200,7 @@ async function resolveKaminoVaultContext(
1833
2200
  throw new Error(`Kamino vault account not found: ${vaultAddress.toBase58()}`)
1834
2201
  }
1835
2202
 
2203
+ const decodedVaultState = decodeKaminoVaultState(Buffer.from(vaultInfo.data))
1836
2204
  const decodedReserves = reserveInfos.map((reserveInfo, index) => {
1837
2205
  if (!reserveInfo?.data) {
1838
2206
  throw new Error(`Missing Kamino reserve account ${reserveAddresses[index]!.toBase58()}`)
@@ -1844,6 +2212,7 @@ async function resolveKaminoVaultContext(
1844
2212
  )
1845
2213
  const normalizedReserves = reserveAddresses.map((reserveAddress, index) => {
1846
2214
  const reserveAccount = decodedReserves[index]!
2215
+ const allocation = decodedVaultState.allocations[index]
1847
2216
  const rawReserve = rawIndex.reserves[index] as typeof rawIndex.reserves[number] & {
1848
2217
  reserve?: PublicKey
1849
2218
  marketAddress?: PublicKey
@@ -1866,6 +2235,13 @@ async function resolveKaminoVaultContext(
1866
2235
  [Buffer.from("lma"), reserveAccount.lendingMarket.toBuffer()],
1867
2236
  KAMINO_LENDING_PROGRAM_ID,
1868
2237
  )
2238
+ const investedLiquidityAmount = allocation
2239
+ ? new Decimal(allocation.ctokenAllocation.toString()).mul(reserveAccount.getCollateralExchangeRate())
2240
+ : new Decimal(0)
2241
+ const availableLiquidityToWithdraw = Decimal.min(
2242
+ investedLiquidityAmount,
2243
+ reserveAccount.getLiquidityAvailableAmount(),
2244
+ )
1869
2245
 
1870
2246
  return {
1871
2247
  reserveAddress,
@@ -1886,6 +2262,9 @@ async function resolveKaminoVaultContext(
1886
2262
  reserveCollateralMint: rawReserve.reserveCollateralMint ?? reserveAccount.collateral.mintPubkey,
1887
2263
  reserveCollateralTokenProgram:
1888
2264
  rawReserve.reserveCollateralTokenProgram ?? collateralMintInfos[index]?.owner ?? PublicKey.default,
2265
+ account: reserveAccount,
2266
+ investedLiquidityAmount,
2267
+ availableLiquidityToWithdraw,
1889
2268
  }
1890
2269
  })
1891
2270
  const index = {
@@ -1901,7 +2280,11 @@ async function resolveKaminoVaultContext(
1901
2280
  }
1902
2281
  const tokenAta = getAssociatedTokenAddressSync(index.tokenMint, setupContext.owner, true, index.tokenProgram)
1903
2282
  const sharesAta = getAssociatedTokenAddressSync(index.sharesMint, setupContext.owner, true, index.sharesTokenProgram)
1904
- return { index, tokenAta, sharesAta }
2283
+ const sharesAtaInfo = await setupContext.connection.getAccountInfo(sharesAta)
2284
+ const sharesBalance = sharesAtaInfo?.data
2285
+ ? new BN(AccountLayout.decode(sharesAtaInfo.data).amount.toString())
2286
+ : new BN(0)
2287
+ return { index, tokenAta, sharesAta, sharesBalance, state: decodedVaultState }
1905
2288
  }
1906
2289
 
1907
2290
  async function queueKaminoVaultSharesTracking(params: {
@@ -2060,36 +2443,76 @@ function buildKaminoVaultDepositInstruction(params: {
2060
2443
  })
2061
2444
  }
2062
2445
 
2063
- function resolveKaminoVaultWithdrawReserve(
2446
+ function resolveExplicitKaminoVaultWithdrawReserve(
2064
2447
  ix: KaminoVaultWithdrawInstruction,
2065
2448
  vaultContext: KaminoVaultContext,
2066
2449
  ): KaminoVaultContext["index"]["reserves"][number] {
2067
- if (ix.reserve) {
2068
- const reserve = vaultContext.index.reserves.find((entry) => entry.reserveAddress.equals(ix.reserve!))
2069
- if (!reserve) {
2070
- throw new Error(
2071
- `Kamino Vault ${ix.vault.toBase58()} does not use reserve ${ix.reserve.toBase58()}`,
2072
- )
2073
- }
2074
- return reserve
2450
+ if (!ix.reserve) {
2451
+ throw new Error("reserve is required when resolving an explicit Kamino Vault withdraw reserve")
2075
2452
  }
2076
2453
 
2077
- if (vaultContext.index.reserves.length !== 1) {
2454
+ const reserve = vaultContext.index.reserves.find((entry) => entry.reserveAddress.equals(ix.reserve))
2455
+ if (!reserve) {
2078
2456
  throw new Error(
2079
- `Kamino Vault ${ix.vault.toBase58()} uses ${vaultContext.index.reserves.length} reserves; specify withdraw.reserve`,
2457
+ `Kamino Vault ${ix.vault.toBase58()} does not use reserve ${ix.reserve.toBase58()}`,
2080
2458
  )
2081
2459
  }
2082
2460
 
2083
- return vaultContext.index.reserves[0]!
2461
+ return reserve
2462
+ }
2463
+
2464
+ function planKaminoVaultWithdrawLegs(
2465
+ ix: KaminoVaultWithdrawInstruction,
2466
+ vaultContext: KaminoVaultContext,
2467
+ ): Array<{
2468
+ reserve: KaminoVaultContext["index"]["reserves"][number]
2469
+ sharesAmount: BN
2470
+ }> {
2471
+ if (ix.reserve) {
2472
+ return [{
2473
+ reserve: resolveExplicitKaminoVaultWithdrawReserve(ix, vaultContext),
2474
+ sharesAmount: ix.sharesAmount,
2475
+ }]
2476
+ }
2477
+
2478
+ const plannedLegs = planKaminoVaultWithdrawLegsFromSnapshot({
2479
+ sharesAmount: ix.sharesAmount,
2480
+ snapshot: {
2481
+ sharesBalance: vaultContext.sharesBalance,
2482
+ tokenAvailable: vaultContext.state.tokenAvailable,
2483
+ sharesIssued: vaultContext.state.sharesIssued,
2484
+ pendingFeesSf: vaultContext.state.pendingFeesSf,
2485
+ reserves: vaultContext.index.reserves.map((reserve) => ({
2486
+ reserveAddress: reserve.reserveAddress,
2487
+ investedLiquidityAmount: reserve.investedLiquidityAmount,
2488
+ availableLiquidityToWithdraw: reserve.availableLiquidityToWithdraw,
2489
+ })),
2490
+ },
2491
+ })
2492
+
2493
+ return plannedLegs.map((leg) => {
2494
+ const reserve = vaultContext.index.reserves.find((entry) => entry.reserveAddress.equals(leg.reserveAddress))
2495
+ if (!reserve) {
2496
+ throw new Error(
2497
+ `Kamino Vault ${ix.vault.toBase58()} does not use reserve ${leg.reserveAddress.toBase58()}`,
2498
+ )
2499
+ }
2500
+
2501
+ return {
2502
+ reserve,
2503
+ sharesAmount: leg.sharesAmount,
2504
+ }
2505
+ })
2084
2506
  }
2085
2507
 
2086
2508
  function buildKaminoVaultWithdrawInstruction(params: {
2087
2509
  owner: PublicKey
2088
2510
  ix: KaminoVaultWithdrawInstruction
2511
+ reserve: KaminoVaultContext["index"]["reserves"][number]
2512
+ sharesAmount: BN
2089
2513
  vaultContext: KaminoVaultContext
2090
2514
  validationAccounts?: AccountMeta[]
2091
2515
  }): TransactionInstruction {
2092
- const reserve = resolveKaminoVaultWithdrawReserve(params.ix, params.vaultContext)
2093
2516
  const [globalConfig] = PublicKey.findProgramAddressSync(
2094
2517
  [KAMINO_VAULT_GLOBAL_CONFIG_SEED],
2095
2518
  KAMINO_VAULT_PROGRAM_ID,
@@ -2112,13 +2535,13 @@ function buildKaminoVaultWithdrawInstruction(params: {
2112
2535
  { pubkey: KAMINO_VAULT_EVENT_AUTHORITY, isSigner: false, isWritable: false },
2113
2536
  { pubkey: KAMINO_VAULT_PROGRAM_ID, isSigner: false, isWritable: false },
2114
2537
  { pubkey: params.ix.vault, isSigner: false, isWritable: true },
2115
- { pubkey: reserve.reserveAddress, isSigner: false, isWritable: true },
2116
- { pubkey: reserve.ctokenVault, isSigner: false, isWritable: true },
2117
- { pubkey: reserve.marketAddress, isSigner: false, isWritable: false },
2118
- { pubkey: reserve.lendingMarketAuthority, isSigner: false, isWritable: false },
2119
- { pubkey: reserve.reserveLiquiditySupply, isSigner: false, isWritable: true },
2120
- { pubkey: reserve.reserveCollateralMint, isSigner: false, isWritable: true },
2121
- { pubkey: reserve.reserveCollateralTokenProgram, isSigner: false, isWritable: false },
2538
+ { pubkey: params.reserve.reserveAddress, isSigner: false, isWritable: true },
2539
+ { pubkey: params.reserve.ctokenVault, isSigner: false, isWritable: true },
2540
+ { pubkey: params.reserve.marketAddress, isSigner: false, isWritable: false },
2541
+ { pubkey: params.reserve.lendingMarketAuthority, isSigner: false, isWritable: false },
2542
+ { pubkey: params.reserve.reserveLiquiditySupply, isSigner: false, isWritable: true },
2543
+ { pubkey: params.reserve.reserveCollateralMint, isSigner: false, isWritable: true },
2544
+ { pubkey: params.reserve.reserveCollateralTokenProgram, isSigner: false, isWritable: false },
2122
2545
  { pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, isSigner: false, isWritable: false },
2123
2546
  { pubkey: KAMINO_VAULT_EVENT_AUTHORITY, isSigner: false, isWritable: false },
2124
2547
  { pubkey: KAMINO_VAULT_PROGRAM_ID, isSigner: false, isWritable: false },
@@ -2129,7 +2552,7 @@ function buildKaminoVaultWithdrawInstruction(params: {
2129
2552
  })),
2130
2553
  ...(params.validationAccounts ?? []),
2131
2554
  ],
2132
- data: encodeU64InstructionData(KAMINO_VAULT_DISCRIMINATORS.withdraw, params.ix.sharesAmount),
2555
+ data: encodeU64InstructionData(KAMINO_VAULT_DISCRIMINATORS.withdraw, params.sharesAmount),
2133
2556
  })
2134
2557
  }
2135
2558
 
@@ -2193,12 +2616,17 @@ async function buildKaminoVaultInstruction(
2193
2616
  return
2194
2617
  }
2195
2618
 
2196
- buckets.syncInstructions.push(buildKaminoVaultWithdrawInstruction({
2197
- owner: setupContext.owner,
2198
- ix,
2199
- vaultContext,
2200
- validationAccounts,
2201
- }))
2619
+ const withdrawLegs = planKaminoVaultWithdrawLegs(ix, vaultContext)
2620
+ for (const leg of withdrawLegs) {
2621
+ buckets.syncInstructions.push(buildKaminoVaultWithdrawInstruction({
2622
+ owner: setupContext.owner,
2623
+ ix,
2624
+ reserve: leg.reserve,
2625
+ sharesAmount: leg.sharesAmount,
2626
+ vaultContext,
2627
+ validationAccounts,
2628
+ }))
2629
+ }
2202
2630
  }
2203
2631
 
2204
2632
  function getOptionalReadonlyAccountMeta(account: PublicKey | null, placeholderProgram: PublicKey): AccountMeta {