@exponent-labs/exponent-sdk 0.9.0 → 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 (155) hide show
  1. package/build/client/vaults/index.d.ts +2 -0
  2. package/build/client/vaults/index.js +2 -0
  3. package/build/client/vaults/index.js.map +1 -1
  4. package/build/client/vaults/types/index.d.ts +2 -0
  5. package/build/client/vaults/types/index.js +2 -0
  6. package/build/client/vaults/types/index.js.map +1 -1
  7. package/build/client/vaults/types/kaminoFarmEntry.d.ts +15 -0
  8. package/build/client/vaults/types/kaminoFarmEntry.js +17 -0
  9. package/build/client/vaults/types/kaminoFarmEntry.js.map +1 -0
  10. package/build/client/vaults/types/kaminoObligationEntry.d.ts +21 -4
  11. package/build/client/vaults/types/kaminoObligationEntry.js +2 -1
  12. package/build/client/vaults/types/kaminoObligationEntry.js.map +1 -1
  13. package/build/client/vaults/types/positionUpdate.d.ts +9 -0
  14. package/build/client/vaults/types/positionUpdate.js +23 -0
  15. package/build/client/vaults/types/positionUpdate.js.map +1 -1
  16. package/build/client/vaults/types/proposalAction.js +0 -3
  17. package/build/client/vaults/types/proposalAction.js.map +1 -1
  18. package/build/client/vaults/types/reserveFarmMapping.d.ts +19 -0
  19. package/build/client/vaults/types/reserveFarmMapping.js +18 -0
  20. package/build/client/vaults/types/reserveFarmMapping.js.map +1 -0
  21. package/build/client/vaults/types/strategyPosition.d.ts +5 -0
  22. package/build/client/vaults/types/strategyPosition.js +5 -0
  23. package/build/client/vaults/types/strategyPosition.js.map +1 -1
  24. package/build/exponentVaults/aumCalculator.d.ts +25 -4
  25. package/build/exponentVaults/aumCalculator.js +236 -15
  26. package/build/exponentVaults/aumCalculator.js.map +1 -1
  27. package/build/exponentVaults/fetcher.d.ts +52 -0
  28. package/build/exponentVaults/fetcher.js +199 -0
  29. package/build/exponentVaults/fetcher.js.map +1 -0
  30. package/build/exponentVaults/index.d.ts +10 -9
  31. package/build/exponentVaults/index.js +26 -8
  32. package/build/exponentVaults/index.js.map +1 -1
  33. package/build/exponentVaults/kamino-farms.d.ts +144 -0
  34. package/build/exponentVaults/kamino-farms.js +396 -0
  35. package/build/exponentVaults/kamino-farms.js.map +1 -0
  36. package/build/exponentVaults/loopscale/client.d.ts +240 -0
  37. package/build/exponentVaults/loopscale/client.js +590 -0
  38. package/build/exponentVaults/loopscale/client.js.map +1 -0
  39. package/build/exponentVaults/loopscale/client.test.d.ts +1 -0
  40. package/build/exponentVaults/loopscale/client.test.js +183 -0
  41. package/build/exponentVaults/loopscale/client.test.js.map +1 -0
  42. package/build/exponentVaults/loopscale/helpers.d.ts +29 -0
  43. package/build/exponentVaults/loopscale/helpers.js +119 -0
  44. package/build/exponentVaults/loopscale/helpers.js.map +1 -0
  45. package/build/exponentVaults/loopscale/index.d.ts +3 -0
  46. package/build/exponentVaults/loopscale/index.js +12 -0
  47. package/build/exponentVaults/loopscale/index.js.map +1 -0
  48. package/build/exponentVaults/loopscale/prepared-transactions.d.ts +13 -0
  49. package/build/exponentVaults/loopscale/prepared-transactions.js +271 -0
  50. package/build/exponentVaults/loopscale/prepared-transactions.js.map +1 -0
  51. package/build/exponentVaults/loopscale/prepared-transactions.test.d.ts +1 -0
  52. package/build/exponentVaults/loopscale/prepared-transactions.test.js +400 -0
  53. package/build/exponentVaults/loopscale/prepared-transactions.test.js.map +1 -0
  54. package/build/exponentVaults/loopscale/prepared-types.d.ts +62 -0
  55. package/build/exponentVaults/loopscale/prepared-types.js +3 -0
  56. package/build/exponentVaults/loopscale/prepared-types.js.map +1 -0
  57. package/build/exponentVaults/loopscale/response-plan.d.ts +69 -0
  58. package/build/exponentVaults/loopscale/response-plan.js +141 -0
  59. package/build/exponentVaults/loopscale/response-plan.js.map +1 -0
  60. package/build/exponentVaults/loopscale/response-plan.test.d.ts +1 -0
  61. package/build/exponentVaults/loopscale/response-plan.test.js +139 -0
  62. package/build/exponentVaults/loopscale/response-plan.test.js.map +1 -0
  63. package/build/exponentVaults/loopscale/send-plan.d.ts +75 -0
  64. package/build/exponentVaults/loopscale/send-plan.js +235 -0
  65. package/build/exponentVaults/loopscale/send-plan.js.map +1 -0
  66. package/build/exponentVaults/loopscale/types.d.ts +443 -0
  67. package/build/exponentVaults/loopscale/types.js +3 -0
  68. package/build/exponentVaults/loopscale/types.js.map +1 -0
  69. package/build/exponentVaults/loopscale-client.d.ts +113 -524
  70. package/build/exponentVaults/loopscale-client.js +296 -539
  71. package/build/exponentVaults/loopscale-client.js.map +1 -1
  72. package/build/exponentVaults/loopscale-client.test.d.ts +1 -0
  73. package/build/exponentVaults/loopscale-client.test.js +162 -0
  74. package/build/exponentVaults/loopscale-client.test.js.map +1 -0
  75. package/build/exponentVaults/loopscale-client.types.d.ts +425 -0
  76. package/build/exponentVaults/loopscale-client.types.js +3 -0
  77. package/build/exponentVaults/loopscale-client.types.js.map +1 -0
  78. package/build/exponentVaults/loopscale-execution.d.ts +125 -0
  79. package/build/exponentVaults/loopscale-execution.js +341 -0
  80. package/build/exponentVaults/loopscale-execution.js.map +1 -0
  81. package/build/exponentVaults/loopscale-execution.test.d.ts +1 -0
  82. package/build/exponentVaults/loopscale-execution.test.js +139 -0
  83. package/build/exponentVaults/loopscale-execution.test.js.map +1 -0
  84. package/build/exponentVaults/loopscale-vault.d.ts +115 -0
  85. package/build/exponentVaults/loopscale-vault.js +275 -0
  86. package/build/exponentVaults/loopscale-vault.js.map +1 -0
  87. package/build/exponentVaults/loopscale-vault.test.d.ts +1 -0
  88. package/build/exponentVaults/loopscale-vault.test.js +102 -0
  89. package/build/exponentVaults/loopscale-vault.test.js.map +1 -0
  90. package/build/exponentVaults/policyBuilders.d.ts +62 -0
  91. package/build/exponentVaults/policyBuilders.js +119 -2
  92. package/build/exponentVaults/policyBuilders.js.map +1 -1
  93. package/build/exponentVaults/pricePathResolver.d.ts +45 -0
  94. package/build/exponentVaults/pricePathResolver.js +198 -0
  95. package/build/exponentVaults/pricePathResolver.js.map +1 -0
  96. package/build/exponentVaults/pricePathResolver.test.d.ts +1 -0
  97. package/build/exponentVaults/pricePathResolver.test.js +369 -0
  98. package/build/exponentVaults/pricePathResolver.test.js.map +1 -0
  99. package/build/exponentVaults/syncTransaction.js +4 -1
  100. package/build/exponentVaults/syncTransaction.js.map +1 -1
  101. package/build/exponentVaults/titan-quote.js +170 -36
  102. package/build/exponentVaults/titan-quote.js.map +1 -1
  103. package/build/exponentVaults/vault-instruction-types.d.ts +363 -0
  104. package/build/exponentVaults/vault-instruction-types.js +128 -0
  105. package/build/exponentVaults/vault-instruction-types.js.map +1 -0
  106. package/build/exponentVaults/vault-interaction.d.ts +203 -343
  107. package/build/exponentVaults/vault-interaction.js +1894 -426
  108. package/build/exponentVaults/vault-interaction.js.map +1 -1
  109. package/build/exponentVaults/vault-interaction.kamino-vault.test.d.ts +1 -0
  110. package/build/exponentVaults/vault-interaction.kamino-vault.test.js +143 -0
  111. package/build/exponentVaults/vault-interaction.kamino-vault.test.js.map +1 -0
  112. package/build/exponentVaults/vault.d.ts +51 -2
  113. package/build/exponentVaults/vault.js +324 -48
  114. package/build/exponentVaults/vault.js.map +1 -1
  115. package/build/exponentVaults/vaultTransactionBuilder.d.ts +100 -134
  116. package/build/exponentVaults/vaultTransactionBuilder.js +383 -285
  117. package/build/exponentVaults/vaultTransactionBuilder.js.map +1 -1
  118. package/build/exponentVaults/vaultTransactionBuilder.test.d.ts +1 -0
  119. package/build/exponentVaults/vaultTransactionBuilder.test.js +297 -0
  120. package/build/exponentVaults/vaultTransactionBuilder.test.js.map +1 -0
  121. package/build/marketThree.d.ts +6 -2
  122. package/build/marketThree.js +10 -8
  123. package/build/marketThree.js.map +1 -1
  124. package/package.json +34 -32
  125. package/src/client/vaults/index.ts +2 -0
  126. package/src/client/vaults/types/index.ts +2 -0
  127. package/src/client/vaults/types/kaminoFarmEntry.ts +32 -0
  128. package/src/client/vaults/types/kaminoObligationEntry.ts +6 -3
  129. package/src/client/vaults/types/positionUpdate.ts +62 -0
  130. package/src/client/vaults/types/proposalAction.ts +0 -3
  131. package/src/client/vaults/types/reserveFarmMapping.ts +35 -0
  132. package/src/client/vaults/types/strategyPosition.ts +18 -1
  133. package/src/exponentVaults/aumCalculator.ts +353 -16
  134. package/src/exponentVaults/fetcher.ts +257 -0
  135. package/src/exponentVaults/index.ts +65 -40
  136. package/src/exponentVaults/kamino-farms.ts +538 -0
  137. package/src/exponentVaults/loopscale/client.ts +808 -0
  138. package/src/exponentVaults/loopscale/helpers.ts +172 -0
  139. package/src/exponentVaults/loopscale/index.ts +57 -0
  140. package/src/exponentVaults/loopscale/prepared-transactions.ts +435 -0
  141. package/src/exponentVaults/loopscale/prepared-types.ts +73 -0
  142. package/src/exponentVaults/loopscale/types.ts +466 -0
  143. package/src/exponentVaults/policyBuilders.ts +170 -0
  144. package/src/exponentVaults/pricePathResolver.test.ts +466 -0
  145. package/src/exponentVaults/pricePathResolver.ts +273 -0
  146. package/src/exponentVaults/syncTransaction.ts +6 -1
  147. package/src/exponentVaults/titan-quote.ts +231 -45
  148. package/src/exponentVaults/vault-instruction-types.ts +493 -0
  149. package/src/exponentVaults/vault-interaction.kamino-vault.test.ts +149 -0
  150. package/src/exponentVaults/vault-interaction.ts +2818 -799
  151. package/src/exponentVaults/vault.ts +474 -63
  152. package/src/exponentVaults/vaultTransactionBuilder.test.ts +349 -0
  153. package/src/exponentVaults/vaultTransactionBuilder.ts +581 -433
  154. package/src/marketThree.ts +14 -6
  155. package/src/exponentVaults/loopscale-client.ts +0 -1373
@@ -7,6 +7,8 @@ import { AccountConstraint, DataConstraint, InstructionConstraint, PolicyConfig,
7
7
  // ============================================================================
8
8
 
9
9
  export const KAMINO_LENDING_PROGRAM_ID = new PublicKey("KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD")
10
+ export const KAMINO_VAULT_PROGRAM_ID = new PublicKey("KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd")
11
+ export const KAMINO_FARMS_PROGRAM_ID = new PublicKey("FarmsPZpWu9i7Kky8tPN37rs2TpmMrAZrC7S7vJa91Hr")
10
12
 
11
13
  // ============================================================================
12
14
  // Instruction Discriminators (first 8 bytes of sha256("global:<method_name>"))
@@ -33,6 +35,19 @@ export const KAMINO_DISCRIMINATORS = {
33
35
  refreshObligation: Buffer.from([33, 132, 147, 228, 151, 192, 72, 89]),
34
36
  }
35
37
 
38
+ export const KAMINO_VAULT_DISCRIMINATORS = {
39
+ deposit: Buffer.from([242, 35, 198, 137, 82, 225, 242, 182]),
40
+ withdraw: Buffer.from([183, 18, 70, 156, 148, 109, 161, 34]),
41
+ }
42
+
43
+ export const KAMINO_FARM_DISCRIMINATORS = {
44
+ initializeUser: Buffer.from([111, 17, 185, 250, 60, 122, 38, 254]),
45
+ stake: Buffer.from([206, 176, 202, 18, 200, 209, 179, 108]),
46
+ unstake: Buffer.from([90, 95, 107, 42, 205, 124, 50, 225]),
47
+ withdrawUnstakedDeposits: Buffer.from([36, 102, 187, 49, 220, 36, 132, 67]),
48
+ harvestReward: Buffer.from([68, 200, 228, 233, 184, 32, 226, 188]),
49
+ }
50
+
36
51
  // ============================================================================
37
52
  // Policy Builder Functions
38
53
  // ============================================================================
@@ -672,6 +687,161 @@ export function createKaminoPolicy(params: {
672
687
  })
673
688
  }
674
689
 
690
+ export const KAMINO_VAULT_ACCOUNT_INDICES = {
691
+ deposit: {
692
+ vaultState: 1,
693
+ tokenMint: 3,
694
+ },
695
+ withdraw: {
696
+ vaultState: 1,
697
+ tokenMint: 6,
698
+ reserve: 15,
699
+ },
700
+ }
701
+
702
+ export type KaminoVaultPolicyAction = "deposit" | "withdraw"
703
+
704
+ export function createKaminoVaultPolicy(params: {
705
+ allowedVaults?: PublicKey[]
706
+ allowedDepositMints?: PublicKey[]
707
+ actions: KaminoVaultPolicyAction[]
708
+ spendingLimits?: SpendingLimit[]
709
+ threshold?: number
710
+ timeLock?: number
711
+ }): PolicyConfig {
712
+ const instructionsConstraints: InstructionConstraint[] = []
713
+
714
+ for (const action of params.actions) {
715
+ const accountConstraints: AccountConstraint[] = []
716
+ const accountIndices =
717
+ action === "deposit" ? KAMINO_VAULT_ACCOUNT_INDICES.deposit : KAMINO_VAULT_ACCOUNT_INDICES.withdraw
718
+
719
+ if (params.allowedVaults && params.allowedVaults.length > 0) {
720
+ accountConstraints.push(createAccountConstraint(accountIndices.vaultState, params.allowedVaults))
721
+ }
722
+ if (params.allowedDepositMints && params.allowedDepositMints.length > 0) {
723
+ accountConstraints.push(createAccountConstraint(accountIndices.tokenMint, params.allowedDepositMints))
724
+ }
725
+
726
+ instructionsConstraints.push(
727
+ createInstructionConstraint({
728
+ programId: KAMINO_VAULT_PROGRAM_ID,
729
+ accountConstraints,
730
+ dataConstraints: [
731
+ createDiscriminatorConstraint(
732
+ action === "deposit"
733
+ ? KAMINO_VAULT_DISCRIMINATORS.deposit
734
+ : KAMINO_VAULT_DISCRIMINATORS.withdraw,
735
+ ),
736
+ ],
737
+ }),
738
+ )
739
+ }
740
+
741
+ return createProgramInteractionPolicy({
742
+ instructionsConstraints,
743
+ spendingLimits: params.spendingLimits,
744
+ threshold: params.threshold,
745
+ timeLock: params.timeLock,
746
+ })
747
+ }
748
+
749
+ export const KAMINO_FARM_ACCOUNT_INDICES = {
750
+ initializeUser: {
751
+ farmState: 5,
752
+ },
753
+ stake: {
754
+ farmState: 2,
755
+ tokenMint: 5,
756
+ },
757
+ unstake: {
758
+ farmState: 2,
759
+ },
760
+ withdrawUnstakedDeposits: {
761
+ farmState: 2,
762
+ },
763
+ harvestReward: {
764
+ farmState: 2,
765
+ rewardMint: 4,
766
+ },
767
+ }
768
+
769
+ export type KaminoFarmPolicyAction =
770
+ | "initializeUser"
771
+ | "stake"
772
+ | "unstake"
773
+ | "withdrawUnstakedDeposits"
774
+ | "harvestReward"
775
+
776
+ export function createKaminoFarmPolicy(params: {
777
+ allowedFarmStates?: PublicKey[]
778
+ allowedUnderlyingMints?: PublicKey[]
779
+ allowedRewardMints?: PublicKey[]
780
+ actions: KaminoFarmPolicyAction[]
781
+ spendingLimits?: SpendingLimit[]
782
+ threshold?: number
783
+ timeLock?: number
784
+ }): PolicyConfig {
785
+ const instructionsConstraints: InstructionConstraint[] = []
786
+
787
+ for (const action of params.actions) {
788
+ const accountConstraints: AccountConstraint[] = []
789
+
790
+ const farmIndex =
791
+ action === "initializeUser"
792
+ ? KAMINO_FARM_ACCOUNT_INDICES.initializeUser.farmState
793
+ : action === "stake"
794
+ ? KAMINO_FARM_ACCOUNT_INDICES.stake.farmState
795
+ : action === "unstake"
796
+ ? KAMINO_FARM_ACCOUNT_INDICES.unstake.farmState
797
+ : action === "withdrawUnstakedDeposits"
798
+ ? KAMINO_FARM_ACCOUNT_INDICES.withdrawUnstakedDeposits.farmState
799
+ : KAMINO_FARM_ACCOUNT_INDICES.harvestReward.farmState
800
+
801
+ if (params.allowedFarmStates && params.allowedFarmStates.length > 0) {
802
+ accountConstraints.push(createAccountConstraint(farmIndex, params.allowedFarmStates))
803
+ }
804
+
805
+ if (action === "stake" && params.allowedUnderlyingMints && params.allowedUnderlyingMints.length > 0) {
806
+ accountConstraints.push(
807
+ createAccountConstraint(KAMINO_FARM_ACCOUNT_INDICES.stake.tokenMint, params.allowedUnderlyingMints),
808
+ )
809
+ }
810
+
811
+ if (action === "harvestReward" && params.allowedRewardMints && params.allowedRewardMints.length > 0) {
812
+ accountConstraints.push(
813
+ createAccountConstraint(KAMINO_FARM_ACCOUNT_INDICES.harvestReward.rewardMint, params.allowedRewardMints),
814
+ )
815
+ }
816
+
817
+ const discriminator =
818
+ action === "initializeUser"
819
+ ? KAMINO_FARM_DISCRIMINATORS.initializeUser
820
+ : action === "stake"
821
+ ? KAMINO_FARM_DISCRIMINATORS.stake
822
+ : action === "unstake"
823
+ ? KAMINO_FARM_DISCRIMINATORS.unstake
824
+ : action === "withdrawUnstakedDeposits"
825
+ ? KAMINO_FARM_DISCRIMINATORS.withdrawUnstakedDeposits
826
+ : KAMINO_FARM_DISCRIMINATORS.harvestReward
827
+
828
+ instructionsConstraints.push(
829
+ createInstructionConstraint({
830
+ programId: KAMINO_FARMS_PROGRAM_ID,
831
+ accountConstraints,
832
+ dataConstraints: [createDiscriminatorConstraint(discriminator)],
833
+ }),
834
+ )
835
+ }
836
+
837
+ return createProgramInteractionPolicy({
838
+ instructionsConstraints,
839
+ spendingLimits: params.spendingLimits,
840
+ threshold: params.threshold,
841
+ timeLock: params.timeLock,
842
+ })
843
+ }
844
+
675
845
  // ============================================================================
676
846
  // Exponent Core Policy Builders (Strip/Merge)
677
847
  // ============================================================================
@@ -0,0 +1,466 @@
1
+ import { describe, expect, it } from "@jest/globals"
2
+ import { PublicKey } from "@solana/web3.js"
3
+ import type { ExponentPrice, ExponentPrices } from "@exponent-labs/exponent-vaults-fetcher"
4
+ import {
5
+ extractPriceIds,
6
+ resolveExponentPricePath,
7
+ resolveBestKaminoQuotePath,
8
+ resolveKaminoReservePriceIdOrThrow,
9
+ resolvePriceIdFromMintToUnderlying,
10
+ resolvePriceIdFromMintToUnderlyingOrThrow,
11
+ getPriceInputMintFromPriceId,
12
+ } from "./pricePathResolver"
13
+
14
+ // ============================================================================
15
+ // Test helpers
16
+ // ============================================================================
17
+
18
+ /** Create a deterministic PublicKey from a seed byte. */
19
+ function pk(seed: number): PublicKey {
20
+ return new PublicKey(Uint8Array.from({ length: 32 }, () => seed))
21
+ }
22
+
23
+ /** Named mints for readability. */
24
+ const USDC = pk(1)
25
+ const SOL = pk(2)
26
+ const PT_SOL = pk(3)
27
+ const USDT = pk(6)
28
+ const BSOL = pk(8)
29
+ const PT_BSOL = pk(9)
30
+
31
+ function makePriceEntry(params: {
32
+ priceId: bigint
33
+ priceMint: PublicKey
34
+ underlyingMint: PublicKey
35
+ }): ExponentPrice {
36
+ return {
37
+ priceId: params.priceId,
38
+ priceMint: params.priceMint,
39
+ underlyingMint: params.underlyingMint,
40
+ price: [[0n, 0n, 0n, 0n]],
41
+ positionsAmount: 0n,
42
+ lastUpdatedAt: 0n,
43
+ lastUpdatedSlot: 0n,
44
+ priceType: 0,
45
+ impliedApyBps: null,
46
+ impliedApy: null,
47
+ priceInterfaceAccounts: PublicKey.default,
48
+ interfaceAccounts: [],
49
+ } as unknown as ExponentPrice
50
+ }
51
+
52
+ function makePrices(entries: ExponentPrice[]): ExponentPrices {
53
+ return {
54
+ managers: [],
55
+ prices: entries,
56
+ } as unknown as ExponentPrices
57
+ }
58
+
59
+ // ============================================================================
60
+ // resolveExponentPricePath — BFS core
61
+ // ============================================================================
62
+
63
+ describe("resolveExponentPricePath", () => {
64
+ it("resolves a direct 1-hop path (PT_SOL → SOL)", () => {
65
+ const prices = makePrices([
66
+ makePriceEntry({ priceId: 1n, priceMint: PT_SOL, underlyingMint: SOL }),
67
+ ])
68
+
69
+ const path = resolveExponentPricePath(prices, PT_SOL, SOL)
70
+
71
+ expect(path).not.toBeNull()
72
+ expect(path!.sourceMint.equals(PT_SOL)).toBe(true)
73
+ expect(path!.targetMint.equals(SOL)).toBe(true)
74
+ expect(path!.edges).toHaveLength(1)
75
+ expect(path!.edges[0].priceId).toBe(1n)
76
+ expect(path!.mints).toHaveLength(2)
77
+ })
78
+
79
+ it("resolves a 2-hop path (PT_SOL → SOL → USDC)", () => {
80
+ const prices = makePrices([
81
+ makePriceEntry({ priceId: 1n, priceMint: PT_SOL, underlyingMint: SOL }),
82
+ makePriceEntry({ priceId: 2n, priceMint: SOL, underlyingMint: USDC }),
83
+ ])
84
+
85
+ const path = resolveExponentPricePath(prices, PT_SOL, USDC)
86
+
87
+ expect(path).not.toBeNull()
88
+ expect(path!.edges).toHaveLength(2)
89
+ expect(path!.mints).toHaveLength(3)
90
+ expect(path!.mints[0].equals(PT_SOL)).toBe(true)
91
+ expect(path!.mints[1].equals(SOL)).toBe(true)
92
+ expect(path!.mints[2].equals(USDC)).toBe(true)
93
+ })
94
+
95
+ it("resolves a 3-hop path (PT_BSOL → BSOL → SOL → USDC)", () => {
96
+ const prices = makePrices([
97
+ makePriceEntry({ priceId: 1n, priceMint: PT_BSOL, underlyingMint: BSOL }),
98
+ makePriceEntry({ priceId: 2n, priceMint: BSOL, underlyingMint: SOL }),
99
+ makePriceEntry({ priceId: 3n, priceMint: SOL, underlyingMint: USDC }),
100
+ ])
101
+
102
+ const path = resolveExponentPricePath(prices, PT_BSOL, USDC)
103
+
104
+ expect(path).not.toBeNull()
105
+ expect(path!.edges).toHaveLength(3)
106
+ expect(path!.mints[0].equals(PT_BSOL)).toBe(true)
107
+ expect(path!.mints[1].equals(BSOL)).toBe(true)
108
+ expect(path!.mints[2].equals(SOL)).toBe(true)
109
+ expect(path!.mints[3].equals(USDC)).toBe(true)
110
+ })
111
+
112
+ it("returns null when no path exists", () => {
113
+ const prices = makePrices([
114
+ makePriceEntry({ priceId: 1n, priceMint: PT_SOL, underlyingMint: SOL }),
115
+ // No edge from SOL to USDC — disconnected
116
+ ])
117
+
118
+ const path = resolveExponentPricePath(prices, PT_SOL, USDC)
119
+
120
+ expect(path).toBeNull()
121
+ })
122
+
123
+ it("returns null for empty price graph", () => {
124
+ const prices = makePrices([])
125
+ expect(resolveExponentPricePath(prices, PT_SOL, SOL)).toBeNull()
126
+ })
127
+
128
+ it("finds the shortest path when multiple paths exist", () => {
129
+ const prices = makePrices([
130
+ // Direct: PT_SOL → USDC (1 hop)
131
+ makePriceEntry({ priceId: 10n, priceMint: PT_SOL, underlyingMint: USDC }),
132
+ // Indirect: PT_SOL → SOL → USDC (2 hops)
133
+ makePriceEntry({ priceId: 1n, priceMint: PT_SOL, underlyingMint: SOL }),
134
+ makePriceEntry({ priceId: 2n, priceMint: SOL, underlyingMint: USDC }),
135
+ ])
136
+
137
+ const path = resolveExponentPricePath(prices, PT_SOL, USDC)
138
+
139
+ // BFS finds shortest first
140
+ expect(path).not.toBeNull()
141
+ expect(path!.edges).toHaveLength(1)
142
+ expect(path!.edges[0].priceId).toBe(10n)
143
+ })
144
+
145
+ it("handles cycles in the graph without infinite loop", () => {
146
+ const prices = makePrices([
147
+ makePriceEntry({ priceId: 1n, priceMint: SOL, underlyingMint: USDC }),
148
+ makePriceEntry({ priceId: 2n, priceMint: USDC, underlyingMint: SOL }), // Cycle
149
+ makePriceEntry({ priceId: 3n, priceMint: PT_SOL, underlyingMint: SOL }),
150
+ ])
151
+
152
+ const path = resolveExponentPricePath(prices, PT_SOL, USDC)
153
+
154
+ expect(path).not.toBeNull()
155
+ expect(path!.edges).toHaveLength(2)
156
+ })
157
+ })
158
+
159
+ // ============================================================================
160
+ // resolvePriceIdFromMintToUnderlying — PriceId construction
161
+ // ============================================================================
162
+
163
+ describe("resolvePriceIdFromMintToUnderlying", () => {
164
+ it("returns Simple PriceId for a 1-hop path", () => {
165
+ const prices = makePrices([
166
+ makePriceEntry({ priceId: 7n, priceMint: PT_SOL, underlyingMint: SOL }),
167
+ ])
168
+
169
+ const resolved = resolvePriceIdFromMintToUnderlyingOrThrow({
170
+ prices,
171
+ sourceMint: PT_SOL,
172
+ targetMint: SOL,
173
+ label: "test",
174
+ })
175
+
176
+ expect(resolved.__kind).toBe("Simple")
177
+ expect(extractPriceIds(resolved)).toEqual([7n])
178
+ })
179
+
180
+ it("returns Multiply PriceId for a 2-hop path in underlying-to-input order", () => {
181
+ // Path: PT_SOL → SOL → USDC
182
+ // Edges: [edge(12n, PT_SOL→SOL), edge(11n, SOL→USDC)]
183
+ // On-chain expects: multiply prices from underlying→input, so reversed: [11n, 12n]
184
+ const prices = makePrices([
185
+ makePriceEntry({ priceId: 11n, priceMint: SOL, underlyingMint: USDC }),
186
+ makePriceEntry({ priceId: 12n, priceMint: PT_SOL, underlyingMint: SOL }),
187
+ ])
188
+
189
+ const resolved = resolvePriceIdFromMintToUnderlyingOrThrow({
190
+ prices,
191
+ sourceMint: PT_SOL,
192
+ targetMint: USDC,
193
+ label: "test",
194
+ })
195
+
196
+ expect(resolved.__kind).toBe("Multiply")
197
+ // On-chain validates: first entry's underlying_mint = vault underlying (USDC)
198
+ // Then chains: USDC ← SOL ← PT_SOL
199
+ expect(extractPriceIds(resolved)).toEqual([11n, 12n])
200
+ })
201
+
202
+ it("returns Multiply PriceId for a 3-hop path", () => {
203
+ const prices = makePrices([
204
+ makePriceEntry({ priceId: 1n, priceMint: PT_BSOL, underlyingMint: BSOL }),
205
+ makePriceEntry({ priceId: 2n, priceMint: BSOL, underlyingMint: SOL }),
206
+ makePriceEntry({ priceId: 3n, priceMint: SOL, underlyingMint: USDC }),
207
+ ])
208
+
209
+ const resolved = resolvePriceIdFromMintToUnderlyingOrThrow({
210
+ prices,
211
+ sourceMint: PT_BSOL,
212
+ targetMint: USDC,
213
+ label: "test",
214
+ })
215
+
216
+ expect(resolved.__kind).toBe("Multiply")
217
+ // Reversed edge order: [3n (SOL→USDC), 2n (BSOL→SOL), 1n (PT_BSOL→BSOL)]
218
+ expect(extractPriceIds(resolved)).toEqual([3n, 2n, 1n])
219
+ })
220
+
221
+ it("returns null when no path exists", () => {
222
+ const prices = makePrices([])
223
+ const resolved = resolvePriceIdFromMintToUnderlying(prices, PT_SOL, SOL)
224
+ expect(resolved).toBeNull()
225
+ })
226
+
227
+ it("throws with descriptive error when path is missing (OrThrow variant)", () => {
228
+ const prices = makePrices([])
229
+
230
+ expect(() =>
231
+ resolvePriceIdFromMintToUnderlyingOrThrow({
232
+ prices,
233
+ sourceMint: PT_SOL,
234
+ targetMint: SOL,
235
+ label: "test label",
236
+ })
237
+ ).toThrow(`Missing Exponent price path for test label: ${PT_SOL.toBase58()} -> ${SOL.toBase58()}`)
238
+ })
239
+ })
240
+
241
+ // ============================================================================
242
+ // resolveBestKaminoQuotePath — Kamino quote mint selection
243
+ // ============================================================================
244
+
245
+ describe("resolveBestKaminoQuotePath", () => {
246
+ it("always uses vault underlying as the quote mint", () => {
247
+ // USDC vault — quote should be USDC itself via the One price entry
248
+ const prices = makePrices([
249
+ makePriceEntry({ priceId: 1n, priceMint: USDC, underlyingMint: USDC }),
250
+ makePriceEntry({ priceId: 2n, priceMint: SOL, underlyingMint: USDC }),
251
+ ])
252
+
253
+ const result = resolveBestKaminoQuotePath({
254
+ prices,
255
+ vaultUnderlyingMint: USDC,
256
+ })
257
+
258
+ expect(result.quoteInputMint.equals(USDC)).toBe(true)
259
+ expect(extractPriceIds(result.quotePriceId)).toEqual([1n])
260
+ })
261
+
262
+ it("works for a SOL vault with a SOL→SOL identity entry", () => {
263
+ const prices = makePrices([
264
+ makePriceEntry({ priceId: 3n, priceMint: SOL, underlyingMint: SOL }),
265
+ ])
266
+
267
+ const result = resolveBestKaminoQuotePath({
268
+ prices,
269
+ vaultUnderlyingMint: SOL,
270
+ })
271
+
272
+ expect(result.quoteInputMint.equals(SOL)).toBe(true)
273
+ expect(extractPriceIds(result.quotePriceId)).toEqual([3n])
274
+ })
275
+
276
+ it("throws when no identity price exists for vault underlying", () => {
277
+ const prices = makePrices([])
278
+
279
+ expect(() =>
280
+ resolveBestKaminoQuotePath({
281
+ prices,
282
+ vaultUnderlyingMint: USDC,
283
+ })
284
+ ).toThrow("Missing Exponent price path for Kamino quote resolution")
285
+ })
286
+ })
287
+
288
+ // ============================================================================
289
+ // resolveKaminoReservePriceIdOrThrow — reserve-to-quote mapping
290
+ // ============================================================================
291
+
292
+ describe("resolveKaminoReservePriceIdOrThrow", () => {
293
+ it("returns identity (Simple 0) when reserve mint equals quote mint", () => {
294
+ const prices = makePrices([])
295
+
296
+ const result = resolveKaminoReservePriceIdOrThrow({
297
+ prices,
298
+ reserveMint: USDC,
299
+ quoteInputMint: USDC,
300
+ })
301
+
302
+ expect(result.__kind).toBe("Simple")
303
+ expect(extractPriceIds(result)).toEqual([0n])
304
+ })
305
+
306
+ it("resolves reserve → quote when they differ", () => {
307
+ const prices = makePrices([
308
+ makePriceEntry({ priceId: 5n, priceMint: SOL, underlyingMint: USDC }),
309
+ ])
310
+
311
+ const result = resolveKaminoReservePriceIdOrThrow({
312
+ prices,
313
+ reserveMint: SOL,
314
+ quoteInputMint: USDC,
315
+ })
316
+
317
+ expect(extractPriceIds(result)).toEqual([5n])
318
+ })
319
+
320
+ it("throws when reserve cannot reach quote", () => {
321
+ const prices = makePrices([])
322
+
323
+ expect(() =>
324
+ resolveKaminoReservePriceIdOrThrow({
325
+ prices,
326
+ reserveMint: SOL,
327
+ quoteInputMint: USDC,
328
+ })
329
+ ).toThrow("Missing Exponent price path for Kamino reserve mapping")
330
+ })
331
+ })
332
+
333
+ // ============================================================================
334
+ // Kamino end-to-end: quote selection + reserve mapping
335
+ // ============================================================================
336
+
337
+ describe("Kamino end-to-end price resolution", () => {
338
+ it("resolves a SOL vault with SOL and USDC reserves", () => {
339
+ // SOL vault: underlying = SOL
340
+ // Reserves: SOL (passthrough), USDC (needs USDC→SOL price)
341
+ const prices = makePrices([
342
+ makePriceEntry({ priceId: 1n, priceMint: SOL, underlyingMint: SOL }),
343
+ makePriceEntry({ priceId: 2n, priceMint: USDC, underlyingMint: SOL }),
344
+ ])
345
+
346
+ // Quote is always vault underlying
347
+ const quotePath = resolveBestKaminoQuotePath({
348
+ prices,
349
+ vaultUnderlyingMint: SOL,
350
+ })
351
+ expect(quotePath.quoteInputMint.equals(SOL)).toBe(true)
352
+
353
+ // SOL reserve → SOL quote: passthrough
354
+ const solReservePrice = resolveKaminoReservePriceIdOrThrow({
355
+ prices,
356
+ reserveMint: SOL,
357
+ quoteInputMint: quotePath.quoteInputMint,
358
+ })
359
+ expect(extractPriceIds(solReservePrice)).toEqual([0n])
360
+
361
+ // USDC reserve → SOL quote: needs the USDC→SOL price
362
+ const usdcReservePrice = resolveKaminoReservePriceIdOrThrow({
363
+ prices,
364
+ reserveMint: USDC,
365
+ quoteInputMint: quotePath.quoteInputMint,
366
+ })
367
+ expect(extractPriceIds(usdcReservePrice)).toEqual([2n])
368
+ })
369
+
370
+ it("resolves a USDC vault with BSOL needing a Multiply chain", () => {
371
+ // USDC vault: underlying = USDC
372
+ // BSOL reserve needs BSOL → SOL → USDC (two hops)
373
+ const prices = makePrices([
374
+ makePriceEntry({ priceId: 1n, priceMint: USDC, underlyingMint: USDC }),
375
+ makePriceEntry({ priceId: 2n, priceMint: SOL, underlyingMint: USDC }),
376
+ makePriceEntry({ priceId: 3n, priceMint: BSOL, underlyingMint: SOL }),
377
+ ])
378
+
379
+ const quotePath = resolveBestKaminoQuotePath({
380
+ prices,
381
+ vaultUnderlyingMint: USDC,
382
+ })
383
+ expect(quotePath.quoteInputMint.equals(USDC)).toBe(true)
384
+
385
+ // BSOL → USDC requires chaining: Multiply [SOL→USDC, BSOL→SOL]
386
+ const bsolReservePrice = resolveKaminoReservePriceIdOrThrow({
387
+ prices,
388
+ reserveMint: BSOL,
389
+ quoteInputMint: quotePath.quoteInputMint,
390
+ })
391
+ expect(extractPriceIds(bsolReservePrice)).toEqual([2n, 3n])
392
+
393
+ // USDC → USDC: passthrough
394
+ const usdcReservePrice = resolveKaminoReservePriceIdOrThrow({
395
+ prices,
396
+ reserveMint: USDC,
397
+ quoteInputMint: quotePath.quoteInputMint,
398
+ })
399
+ expect(extractPriceIds(usdcReservePrice)).toEqual([0n])
400
+ })
401
+ })
402
+
403
+ // ============================================================================
404
+ // extractPriceIds — parsing
405
+ // ============================================================================
406
+
407
+ describe("extractPriceIds", () => {
408
+ it("extracts from Simple (__kind format)", () => {
409
+ expect(extractPriceIds({ __kind: "Simple", priceId: 42n })).toEqual([42n])
410
+ })
411
+
412
+ it("extracts from Multiply (__kind format)", () => {
413
+ expect(extractPriceIds({ __kind: "Multiply", priceIds: [1n, 2n, 3n] })).toEqual([1n, 2n, 3n])
414
+ })
415
+
416
+ it("extracts from Simple (nested format)", () => {
417
+ expect(extractPriceIds({ simple: { priceId: 42n } })).toEqual([42n])
418
+ })
419
+
420
+ it("extracts from Multiply (nested format)", () => {
421
+ expect(extractPriceIds({ multiply: { priceIds: [1n, 2n, 3n] } })).toEqual([1n, 2n, 3n])
422
+ })
423
+
424
+ it("handles number priceIds in nested format", () => {
425
+ expect(extractPriceIds({ simple: { priceId: 42 } })).toEqual([42n])
426
+ })
427
+
428
+ it("throws for invalid shapes", () => {
429
+ expect(() => extractPriceIds(null)).toThrow("Invalid PriceId")
430
+ expect(() => extractPriceIds({})).toThrow("Unsupported PriceId shape")
431
+ })
432
+ })
433
+
434
+ // ============================================================================
435
+ // getPriceInputMintFromPriceId — reverse lookup
436
+ // ============================================================================
437
+
438
+ describe("getPriceInputMintFromPriceId", () => {
439
+ it("returns the priceMint of the last price in a Simple PriceId", () => {
440
+ const prices = makePrices([
441
+ makePriceEntry({ priceId: 5n, priceMint: SOL, underlyingMint: USDC }),
442
+ ])
443
+
444
+ const mint = getPriceInputMintFromPriceId(prices, { __kind: "Simple", priceId: 5n })
445
+ expect(mint.equals(SOL)).toBe(true)
446
+ })
447
+
448
+ it("returns the priceMint of the last price in a Multiply PriceId", () => {
449
+ const prices = makePrices([
450
+ makePriceEntry({ priceId: 1n, priceMint: SOL, underlyingMint: USDC }),
451
+ makePriceEntry({ priceId: 2n, priceMint: PT_SOL, underlyingMint: SOL }),
452
+ ])
453
+
454
+ // Multiply [1n, 2n] — last entry is priceId 2n, which has priceMint PT_SOL
455
+ const mint = getPriceInputMintFromPriceId(prices, { __kind: "Multiply", priceIds: [1n, 2n] })
456
+ expect(mint.equals(PT_SOL)).toBe(true)
457
+ })
458
+
459
+ it("throws when price entry not found", () => {
460
+ const prices = makePrices([])
461
+
462
+ expect(() =>
463
+ getPriceInputMintFromPriceId(prices, { __kind: "Simple", priceId: 999n })
464
+ ).toThrow("Price entry not found for id 999")
465
+ })
466
+ })