@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
@@ -1,22 +1,44 @@
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"
3
+ import { fetchKaminoVaultIndex } from "@exponent-labs/exponent-fetcher"
4
+ import { IDL as KaminoVaultIdl } from "@exponent-labs/kamino-vault-idl"
2
5
  import { KAMINO_MARKETS, KAMINO_RESERVES, KaminoMarket } from "./kamino-markets"
3
6
  import Decimal from "decimal.js"
4
- import { getKaminoLendObligation, getKaminoUserMetadata, getKaminoFarmsObligationFarm } from "./../../../kamino-lend-standard/src/constants"
5
- import { KAMINO_LENDING_PROGRAM_ID } from "./policyBuilders"
7
+ import {
8
+ getKaminoFarmsObligationFarm,
9
+ getKaminoFarmsRewardsTreasuryVault,
10
+ getKaminoLendObligation,
11
+ getKaminoUserMetadata,
12
+ } from "./../../../kamino-lend-standard/src/constants"
13
+ import {
14
+ KAMINO_FARM_DISCRIMINATORS,
15
+ KAMINO_LENDING_PROGRAM_ID,
16
+ KAMINO_VAULT_DISCRIMINATORS,
17
+ KAMINO_VAULT_PROGRAM_ID,
18
+ } from "./policyBuilders"
6
19
  import { Obligation } from "@exponent-labs/klend-idl/accounts"
7
- import { bigintU256ToString } from "@exponent-labs/precise-number"
8
20
  import { Orderbook } from "../orderbook/orderbook"
9
21
  import { MarketThree } from "../marketThree"
10
- import { uniqueRemainingAccounts } from "../utils"
22
+ import { emitEventAuthority, uniqueRemainingAccounts } from "../utils"
11
23
  import { SwapDirection } from "../client/clmm"
12
24
  import * as exponentClmm from "../client/clmm"
13
25
  import * as exponentVaults from "../client/vaults"
14
26
  import { OfferType, offerOptions, amount as createAmount } from "../client/orderbook"
15
27
  import { LOCAL_ENV, Environment } from "../environment"
16
28
  import { Vault } from "../vault"
29
+ import { collectTrackedStrategyVaultPriceIds } from "./vault"
17
30
  import { YtPosition } from "../ytPosition"
18
31
  import { ExponentVault as StrategyVault } from "./vault"
19
32
  import type { ExponentPrice, ExponentPrices } from "@exponent-labs/exponent-vaults-fetcher"
33
+ import { decodeKaminoFarmState, getKaminoFarmScopePricesAddress } from "./kamino-farms"
34
+ import type { PriceId as ClientPriceId } from "../client/vaults/types/priceId"
35
+ import {
36
+ extractPriceIds,
37
+ getPriceInputMintFromPriceId,
38
+ resolveBestKaminoQuotePath,
39
+ resolveKaminoReservePriceIdOrThrow,
40
+ resolvePriceIdFromMintToUnderlyingOrThrow,
41
+ } from "./pricePathResolver"
20
42
 
21
43
  import {
22
44
  depositReserveLiquidityAndObligationCollateralV2,
@@ -30,6 +52,7 @@ import {
30
52
  refreshObligation,
31
53
  } from "@exponent-labs/klend-idl/instructions"
32
54
  import {
55
+ AccountLayout,
33
56
  TOKEN_PROGRAM_ID,
34
57
  NATIVE_MINT,
35
58
  getAssociatedTokenAddressSync,
@@ -43,403 +66,146 @@ import {
43
66
  } from "./policyMatcher"
44
67
  import { buildScopeRefreshInstructions } from "./scope-refresh"
45
68
  import * as web3 from "@solana/web3.js"
46
- 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"
47
81
  import BN from "bn.js"
48
82
 
49
83
  const KAMINO_FARMS_PROGRAM_ID = new PublicKey("FarmsPZpWu9i7Kky8tPN37rs2TpmMrAZrC7S7vJa91Hr")
84
+ const KAMINO_VAULT_PRICE_TYPE_WIRE = 14
85
+ const KAMINO_VAULT_ACCOUNT_DISCRIMINATOR_LEN = 8
86
+ const KAMINO_VAULT_ALLOCATION_STRATEGY_OFFSET = 304
87
+ const KAMINO_VAULT_ALLOCATION_SIZE = 2160
88
+ const KAMINO_VAULT_ALLOCATION_CTOKEN_VAULT_OFFSET = 32
89
+ const KAMINO_VAULT_GLOBAL_CONFIG_SEED = Buffer.from("global_config")
90
+ const KAMINO_VAULT_CODER = new BorshCoder(KaminoVaultIdl as Idl)
50
91
 
51
92
  // ============================================================================
52
- // Vault Instruction Types
53
- // ============================================================================
54
-
55
- type KaminoReserves = typeof KAMINO_RESERVES
56
-
57
- /** Actions that can be performed through the vault instruction builder. */
58
- export enum VaultAction {
59
- INIT_USER_METADATA = "INIT_USER_METADATA",
60
- INIT_OBLIGATION = "INIT_OBLIGATION",
61
- DEPOSIT = "DEPOSIT",
62
- WITHDRAW = "WITHDRAW",
63
- BORROW = "BORROW",
64
- REPAY = "REPAY",
65
- }
66
-
67
- /** A market-level instruction (no specific reserve needed). */
68
- export type MarketInstruction = {
69
- action: VaultAction.INIT_USER_METADATA | VaultAction.INIT_OBLIGATION
70
- market: KaminoMarket
71
- }
72
-
73
- /** A reserve-level instruction with an amount. */
74
- export type ReserveInstruction = {
75
- action: VaultAction.DEPOSIT | VaultAction.WITHDRAW | VaultAction.BORROW | VaultAction.REPAY
76
- market: KaminoMarket
77
- asset: string
78
- amount: BN
79
- }
80
-
81
- // ============================================================================
82
- // Orderbook Instruction Types
83
- // ============================================================================
84
-
85
- /** Orderbook trade direction */
86
- export enum OrderbookTradeDirection {
87
- BUY_PT = "BUY_PT",
88
- SELL_PT = "SELL_PT",
89
- BUY_YT = "BUY_YT",
90
- SELL_YT = "SELL_YT",
91
- }
92
-
93
- /** Offer options for limit orders (currently only FillOrKill supported) */
94
- export type OrderbookOfferOption = "FillOrKill"
95
-
96
- /** Actions that can be performed on the Exponent Orderbook */
97
- export enum OrderbookAction {
98
- POST_OFFER = "POST_OFFER",
99
- MARKET_OFFER = "MARKET_OFFER",
100
- REMOVE_OFFER = "REMOVE_OFFER",
101
- WITHDRAW_FUNDS = "WITHDRAW_FUNDS",
102
- }
103
-
104
- export type OrderbookInstructionMode = "wrapper" | "raw"
105
-
106
- /** Base instruction type for all orderbook operations */
107
- interface OrderbookInstructionBase {
108
- action: OrderbookAction
109
- orderbook: PublicKey
110
- mode?: OrderbookInstructionMode
111
- }
112
-
113
- /** Post a limit order on the orderbook */
114
- export interface OrderbookPostOfferInstruction extends OrderbookInstructionBase {
115
- action: OrderbookAction.POST_OFFER
116
- direction: OrderbookTradeDirection
117
- priceApy: number
118
- amount: bigint
119
- offerIdx: number
120
- offerOption?: OrderbookOfferOption
121
- virtualOffer?: boolean
122
- expirySeconds?: number
123
- }
124
-
125
- /** Execute a market order on the orderbook */
126
- export interface OrderbookMarketOfferInstruction extends OrderbookInstructionBase {
127
- action: OrderbookAction.MARKET_OFFER
128
- direction: OrderbookTradeDirection
129
- maxPriceApy: number
130
- amount: bigint
131
- minAmountOut: bigint
132
- virtualOffer?: boolean
133
- }
134
-
135
- /** Cancel an existing limit order */
136
- export interface OrderbookRemoveOfferInstruction extends OrderbookInstructionBase {
137
- action: OrderbookAction.REMOVE_OFFER
138
- offerIdx: number
139
- }
140
-
141
- /** Withdraw funds from user escrow */
142
- export interface OrderbookWithdrawFundsInstruction extends OrderbookInstructionBase {
143
- action: OrderbookAction.WITHDRAW_FUNDS
144
- ptAmount?: bigint | null
145
- ytAmount?: bigint | null
146
- syAmount?: bigint | null
147
- }
148
-
149
- /** A single orderbook instruction */
150
- export type OrderbookInstruction =
151
- | OrderbookPostOfferInstruction
152
- | OrderbookMarketOfferInstruction
153
- | OrderbookRemoveOfferInstruction
154
- | OrderbookWithdrawFundsInstruction
155
-
156
- // ============================================================================
157
- // Core Instruction Types (Strip/Merge)
158
- // ============================================================================
159
-
160
- /** Actions that can be performed on Exponent Core */
161
- export enum CoreAction {
162
- STRIP = "STRIP",
163
- MERGE = "MERGE",
164
- WITHDRAW_YT = "WITHDRAW_YT",
165
- DEPOSIT_YT = "DEPOSIT_YT",
166
- INITIALIZE_YIELD_POSITION = "INITIALIZE_YIELD_POSITION",
167
- }
168
-
169
- /** Base instruction type for all core operations */
170
- interface CoreInstructionBase {
171
- action: CoreAction
172
- vault: PublicKey
173
- }
174
-
175
- /** Strip LST into PT + YT */
176
- export interface CoreStripInstruction extends CoreInstructionBase {
177
- action: CoreAction.STRIP
178
- /** Amount of base token (LST) to strip */
179
- amountBase: bigint
180
- }
181
-
182
- /** Merge PT + YT into LST */
183
- export interface CoreMergeInstruction extends CoreInstructionBase {
184
- action: CoreAction.MERGE
185
- /** Amount of PT/YT to merge (must have equal amounts of both) */
186
- amountPy: bigint
187
- }
188
-
189
- /** Withdraw YT from the tracked yield position back into the YT token account */
190
- export interface CoreWithdrawYtInstruction extends CoreInstructionBase {
191
- action: CoreAction.WITHDRAW_YT
192
- amountYt: bigint
193
- }
194
-
195
- /** Deposit YT from the YT token account back into the tracked yield position */
196
- export interface CoreDepositYtInstruction extends CoreInstructionBase {
197
- action: CoreAction.DEPOSIT_YT
198
- amountYt: bigint
199
- }
200
-
201
- /** Initialize yield position for a vault (owner = Squads vault) */
202
- export interface CoreInitializeYieldPositionInstruction extends CoreInstructionBase {
203
- action: CoreAction.INITIALIZE_YIELD_POSITION
204
- }
205
-
206
- /** A single core instruction */
207
- export type CoreInstruction =
208
- | CoreStripInstruction
209
- | CoreMergeInstruction
210
- | CoreWithdrawYtInstruction
211
- | CoreDepositYtInstruction
212
- | CoreInitializeYieldPositionInstruction
213
-
214
- // ============================================================================
215
- // Standard Program Instruction Types (mint_sy / redeem_sy)
216
- // ============================================================================
217
-
218
- export enum SyAction {
219
- MINT = "MINT_SY",
220
- REDEEM = "REDEEM_SY",
221
- }
222
-
223
- interface SyInstructionBase {
224
- action: SyAction
225
- vault: PublicKey
226
- }
227
-
228
- export interface SyMintInstruction extends SyInstructionBase {
229
- action: SyAction.MINT
230
- amountBase: bigint
231
- }
232
-
233
- export interface SyRedeemInstruction extends SyInstructionBase {
234
- action: SyAction.REDEEM
235
- amountSy: bigint
236
- }
237
-
238
- export type SyInstruction = SyMintInstruction | SyRedeemInstruction
239
-
240
- // ============================================================================
241
- // Titan Instruction Types
242
- // ============================================================================
243
-
244
- export enum TitanAction {
245
- SWAP = "SWAP",
246
- }
247
-
248
- /** A pre-built Titan swap instruction to wrap in a sync transaction. */
249
- export interface TitanSwapInstruction {
250
- action: TitanAction.SWAP
251
- /** The raw Titan SwapRouteV2 TransactionInstruction (from Titan's router API) */
252
- instruction: TransactionInstruction
253
- }
254
-
255
- // ============================================================================
256
- // Loopscale Instruction Types
257
- // ============================================================================
258
-
259
- /** Actions for Loopscale interactions (loans = borrower side, strategies = lender side). */
260
- export enum LoopscaleAction {
261
- CREATE_LOAN = "LOOPSCALE_CREATE_LOAN",
262
- DEPOSIT_COLLATERAL = "LOOPSCALE_DEPOSIT_COLLATERAL",
263
- BORROW_PRINCIPAL = "LOOPSCALE_BORROW_PRINCIPAL",
264
- REPAY_PRINCIPAL = "LOOPSCALE_REPAY_PRINCIPAL",
265
- WITHDRAW_COLLATERAL = "LOOPSCALE_WITHDRAW_COLLATERAL",
266
- CLOSE_LOAN = "LOOPSCALE_CLOSE_LOAN",
267
- UPDATE_WEIGHT_MATRIX = "LOOPSCALE_UPDATE_WEIGHT_MATRIX",
268
- CREATE_STRATEGY = "LOOPSCALE_CREATE_STRATEGY",
269
- DEPOSIT_STRATEGY = "LOOPSCALE_DEPOSIT_STRATEGY",
270
- WITHDRAW_STRATEGY = "LOOPSCALE_WITHDRAW_STRATEGY",
271
- CLOSE_STRATEGY = "LOOPSCALE_CLOSE_STRATEGY",
272
- UPDATE_STRATEGY = "LOOPSCALE_UPDATE_STRATEGY",
273
- LOCK_LOAN = "LOOPSCALE_LOCK_LOAN",
274
- UNLOCK_LOAN = "LOOPSCALE_UNLOCK_LOAN",
275
- REFINANCE_LEDGER = "LOOPSCALE_REFINANCE_LEDGER",
276
- }
277
-
278
- /** A pre-built Loopscale instruction (loan or strategy) to wrap in a sync transaction. */
279
- export interface LoopscaleInstruction {
280
- action: LoopscaleAction
281
- /** The raw Loopscale TransactionInstruction (from Loopscale API or local builder) */
282
- instruction: TransactionInstruction
283
- }
284
-
285
- // ============================================================================
286
- // CLMM Instruction Types
93
+ // Vault Instruction Types (re-exported from vault-instruction-types.ts)
287
94
  // ============================================================================
288
95
 
289
- /** Actions that can be performed on the Exponent CLMM (Concentrated Liquidity Market Maker). */
290
- export enum ClmmAction {
291
- /** Create a new LP position with a specified tick range. Generates keypair internally. */
292
- DEPOSIT_LIQUIDITY = "CLMM_DEPOSIT_LIQUIDITY",
293
- /** Add more liquidity to an existing LP position. */
294
- ADD_LIQUIDITY = "CLMM_ADD_LIQUIDITY",
295
- /** Remove liquidity from an LP position and receive PT + SY. */
296
- WITHDRAW_LIQUIDITY = "CLMM_WITHDRAW_LIQUIDITY",
297
- /** Low-level PT/SY swap. Prefer buyPt/sellPt for directional trades. */
298
- TRADE_PT = "CLMM_TRADE_PT",
299
- /** Buy PT with SY on the CLMM. */
300
- BUY_PT = "CLMM_BUY_PT",
301
- /** Sell PT for SY on the CLMM. */
302
- SELL_PT = "CLMM_SELL_PT",
303
- /** Buy YT with SY on the CLMM. */
304
- BUY_YT = "CLMM_BUY_YT",
305
- /** Sell YT for SY on the CLMM. */
306
- SELL_YT = "CLMM_SELL_YT",
307
- /** Claim farm emissions from an LP position. */
308
- CLAIM_FARM_EMISSION = "CLMM_CLAIM_FARM_EMISSION",
309
- }
310
-
311
- /** Base type for all CLMM instructions. */
312
- interface ClmmInstructionBase {
313
- action: ClmmAction
314
- /** The CLMM MarketThree account address. */
315
- market: PublicKey
316
- }
317
-
318
- /** Create a new LP position on the CLMM. The lpPosition keypair is generated internally for good UX. */
319
- export interface ClmmDepositLiquidityInstruction extends ClmmInstructionBase {
320
- action: ClmmAction.DEPOSIT_LIQUIDITY
321
- /** Maximum amount of PT to deposit. */
322
- ptInIntent: bigint
323
- /** Maximum amount of SY to deposit. */
324
- syInIntent: bigint
325
- /** Lower tick boundary (APY value). */
326
- lowerTickKey: number
327
- /** Upper tick boundary (APY value). */
328
- upperTickKey: number
329
- }
330
-
331
- /** Add liquidity to an existing LP position. */
332
- export interface ClmmAddLiquidityInstruction extends ClmmInstructionBase {
333
- action: ClmmAction.ADD_LIQUIDITY
334
- /** The existing LpPosition account public key. */
335
- lpPosition: PublicKey
336
- /** Maximum amount of PT to add. */
337
- ptInIntent: bigint
338
- /** Maximum amount of SY to add. */
339
- syInIntent: bigint
340
- }
96
+ export {
97
+ VaultAction,
98
+ KaminoVaultAction,
99
+ KaminoFarmAction,
100
+ OrderbookTradeDirection,
101
+ OrderbookAction,
102
+ CoreAction,
103
+ SyAction,
104
+ TitanAction,
105
+ LoopscaleAction,
106
+ ClmmAction,
107
+ } from "./vault-instruction-types"
108
+
109
+ export type {
110
+ MarketInstruction,
111
+ ReserveInstruction,
112
+ KaminoVaultInstruction,
113
+ KaminoVaultDepositInstruction,
114
+ KaminoVaultWithdrawInstruction,
115
+ KaminoFarmInstruction,
116
+ KaminoFarmInitializeUserInstruction,
117
+ KaminoFarmStakeInstruction,
118
+ KaminoFarmUnstakeInstruction,
119
+ KaminoFarmWithdrawUnstakedDepositsInstruction,
120
+ KaminoFarmHarvestRewardInstruction,
121
+ OrderbookOfferOption,
122
+ OrderbookInstructionMode,
123
+ OrderbookPostOfferInstruction,
124
+ OrderbookMarketOfferInstruction,
125
+ OrderbookRemoveOfferInstruction,
126
+ OrderbookWithdrawFundsInstruction,
127
+ OrderbookInstruction,
128
+ CoreStripInstruction,
129
+ CoreMergeInstruction,
130
+ CoreWithdrawYtInstruction,
131
+ CoreDepositYtInstruction,
132
+ CoreInitializeYieldPositionInstruction,
133
+ CoreInstruction,
134
+ SyMintInstruction,
135
+ SyRedeemInstruction,
136
+ SyInstruction,
137
+ TitanSwapInstruction,
138
+ LoopscaleInstruction,
139
+ ClmmDepositLiquidityInstruction,
140
+ ClmmAddLiquidityInstruction,
141
+ ClmmWithdrawLiquidityInstruction,
142
+ ClmmTradePtInstruction,
143
+ ClmmBuyPtInstruction,
144
+ ClmmSellPtInstruction,
145
+ ClmmBuyYtInstruction,
146
+ ClmmSellYtInstruction,
147
+ ClmmClaimFarmEmissionInstruction,
148
+ ClmmInstruction,
149
+ VaultInstruction,
150
+ KaminoReserves,
151
+ } from "./vault-instruction-types"
152
+
153
+ import type {
154
+ MarketInstruction,
155
+ ReserveInstruction,
156
+ KaminoVaultInstruction,
157
+ KaminoVaultDepositInstruction,
158
+ KaminoVaultWithdrawInstruction,
159
+ KaminoFarmInstruction,
160
+ KaminoFarmInitializeUserInstruction,
161
+ KaminoFarmStakeInstruction,
162
+ KaminoFarmUnstakeInstruction,
163
+ KaminoFarmWithdrawUnstakedDepositsInstruction,
164
+ KaminoFarmHarvestRewardInstruction,
165
+ OrderbookOfferOption,
166
+ OrderbookInstructionMode,
167
+ OrderbookInstruction,
168
+ OrderbookPostOfferInstruction,
169
+ OrderbookMarketOfferInstruction,
170
+ OrderbookRemoveOfferInstruction,
171
+ OrderbookWithdrawFundsInstruction,
172
+ CoreInstruction,
173
+ CoreStripInstruction,
174
+ CoreMergeInstruction,
175
+ CoreWithdrawYtInstruction,
176
+ CoreDepositYtInstruction,
177
+ CoreInitializeYieldPositionInstruction,
178
+ SyInstruction,
179
+ TitanSwapInstruction,
180
+ LoopscaleInstruction,
181
+ ClmmInstruction,
182
+ ClmmDepositLiquidityInstruction,
183
+ ClmmAddLiquidityInstruction,
184
+ ClmmWithdrawLiquidityInstruction,
185
+ ClmmTradePtInstruction,
186
+ ClmmBuyPtInstruction,
187
+ ClmmSellPtInstruction,
188
+ ClmmBuyYtInstruction,
189
+ ClmmSellYtInstruction,
190
+ ClmmClaimFarmEmissionInstruction,
191
+ VaultInstruction,
192
+ KaminoReserves,
193
+ SyMintInstruction,
194
+ SyRedeemInstruction,
195
+ } from "./vault-instruction-types"
341
196
 
342
- /** Withdraw liquidity from an LP position. */
343
- export interface ClmmWithdrawLiquidityInstruction extends ClmmInstructionBase {
344
- action: ClmmAction.WITHDRAW_LIQUIDITY
345
- /** The LpPosition account to withdraw from. */
346
- lpPosition: PublicKey
347
- /** Amount of liquidity (LP units) to remove. */
348
- lpIn: bigint
349
- /** Minimum PT to receive (slippage protection). */
350
- minPtOut: bigint
351
- /** Minimum SY to receive (slippage protection). */
352
- minSyOut: bigint
353
- }
354
-
355
- /** Low-level PT/SY swap on the CLMM. Prefer buyPt/sellPt for directional trades. */
356
- export interface ClmmTradePtInstruction extends ClmmInstructionBase {
357
- action: ClmmAction.TRADE_PT
358
- /** Amount of the input token. */
359
- traderAmount: bigint
360
- /** Minimum output amount (slippage protection). */
361
- outConstraint: bigint
362
- /** Swap direction: SyToPt or PtToSy. */
363
- swapDirection: SwapDirection
364
- /** Optional price limit (ln implied APY). */
365
- lnImpliedApyLimit?: number
366
- }
367
-
368
- /** Buy PT with SY on the CLMM. */
369
- export interface ClmmBuyPtInstruction extends ClmmInstructionBase {
370
- action: ClmmAction.BUY_PT
371
- /** Amount of SY to spend. */
372
- amountSy: bigint
373
- /** Minimum PT to receive (slippage protection). */
374
- outConstraint: bigint
375
- /** Optional price limit (ln implied APY). */
376
- lnImpliedApyLimit?: number
377
- }
378
-
379
- /** Sell PT for SY on the CLMM. */
380
- export interface ClmmSellPtInstruction extends ClmmInstructionBase {
381
- action: ClmmAction.SELL_PT
382
- /** Amount of PT to sell. */
383
- amountPt: bigint
384
- /** Minimum SY to receive (slippage protection). */
385
- outConstraint: bigint
386
- /** Optional price limit (ln implied APY). */
387
- lnImpliedApyLimit?: number
388
- }
389
-
390
- /** Buy YT with SY on the CLMM. */
391
- export interface ClmmBuyYtInstruction extends ClmmInstructionBase {
392
- action: ClmmAction.BUY_YT
393
- /** Minimum amount of YT to receive. */
394
- ytOut: bigint
395
- /** Maximum amount of SY to spend. */
396
- maxSyIn: bigint
397
- /** Optional price limit (ln implied APY). */
398
- lnImpliedApyLimit?: number
399
- }
400
-
401
- /** Sell YT for SY on the CLMM. */
402
- export interface ClmmSellYtInstruction extends ClmmInstructionBase {
403
- action: ClmmAction.SELL_YT
404
- /** Amount of YT to sell. */
405
- ytIn: bigint
406
- /** Minimum SY to receive (slippage protection). */
407
- minSyOut: bigint
408
- /** Optional price limit (ln implied APY). */
409
- lnImpliedApyLimit?: number
410
- }
411
-
412
- /** Claim farm emissions from an LP position. */
413
- export interface ClmmClaimFarmEmissionInstruction extends ClmmInstructionBase {
414
- action: ClmmAction.CLAIM_FARM_EMISSION
415
- /** The LpPosition account to claim from. */
416
- lpPosition: PublicKey
417
- /** Index of the farm to claim from. */
418
- farmIndex: number
419
- }
420
-
421
- /** A single CLMM instruction. */
422
- export type ClmmInstruction =
423
- | ClmmDepositLiquidityInstruction
424
- | ClmmAddLiquidityInstruction
425
- | ClmmWithdrawLiquidityInstruction
426
- | ClmmTradePtInstruction
427
- | ClmmBuyPtInstruction
428
- | ClmmSellPtInstruction
429
- | ClmmBuyYtInstruction
430
- | ClmmSellYtInstruction
431
- | ClmmClaimFarmEmissionInstruction
432
-
433
- /** A single vault instruction — pass an array of these to `createVaultSyncTransaction`. */
434
- export type VaultInstruction =
435
- | MarketInstruction
436
- | ReserveInstruction
437
- | OrderbookInstruction
438
- | CoreInstruction
439
- | SyInstruction
440
- | TitanSwapInstruction
441
- | ClmmInstruction
442
- | LoopscaleInstruction
197
+ import {
198
+ VaultAction,
199
+ KaminoVaultAction,
200
+ KaminoFarmAction,
201
+ OrderbookAction,
202
+ CoreAction,
203
+ SyAction,
204
+ TitanAction,
205
+ LoopscaleAction,
206
+ ClmmAction,
207
+ OrderbookTradeDirection,
208
+ } from "./vault-instruction-types"
443
209
 
444
210
  // ============================================================================
445
211
  // Kamino Action Builders
@@ -483,7 +249,8 @@ export const kaminoAction = {
483
249
 
484
250
  /**
485
251
  * Initialize a Kamino obligation for a market.
486
- * No-ops if the account already exists on-chain.
252
+ * When `autoManagePositions` is enabled, the SDK also registers the new
253
+ * obligation as a tracked strategy position after the init succeeds.
487
254
  * @param market - The Kamino lending market
488
255
  */
489
256
  initObligation(market: KaminoMarket): MarketInstruction {
@@ -532,6 +299,92 @@ export const kaminoAction = {
532
299
  },
533
300
  }
534
301
 
302
+ /**
303
+ * Builder for direct Kamino Vault action descriptors.
304
+ *
305
+ * These actions move vault-owned tokens into a Kamino Vault and, when
306
+ * `autoManagePositions` is enabled, automatically track the resulting share
307
+ * token account as a managed strategy position.
308
+ */
309
+ export const kaminoVaultAction = {
310
+ /**
311
+ * Deposit the vault-owned token account into a Kamino Vault.
312
+ * @param params.vault - Kamino Vault address
313
+ * @param params.amount - Amount of deposit tokens to move into the vault
314
+ */
315
+ deposit(params: {
316
+ vault: PublicKey
317
+ amount: BN
318
+ }): KaminoVaultDepositInstruction {
319
+ return { action: KaminoVaultAction.DEPOSIT, ...params }
320
+ },
321
+
322
+ /**
323
+ * Withdraw Kamino Vault shares back into the vault-owned token account.
324
+ * `reserve` is an optional override. When omitted, the SDK plans the
325
+ * withdraw across the vault's active reserves automatically.
326
+ */
327
+ withdraw(params: {
328
+ vault: PublicKey
329
+ sharesAmount: BN
330
+ reserve?: PublicKey
331
+ }): KaminoVaultWithdrawInstruction {
332
+ return { action: KaminoVaultAction.WITHDRAW, ...params }
333
+ },
334
+ }
335
+
336
+ /**
337
+ * Builder for direct Kamino Farm action descriptors.
338
+ *
339
+ * These actions operate on a farm `user_state` derived from the managed vault
340
+ * owner by default. Pass `delegatee` when targeting a delegated farm user
341
+ * state, such as a Kamino obligation-owned farm entry.
342
+ */
343
+ export const kaminoFarmAction = {
344
+ /** Initialize the farm `user_state` PDA. */
345
+ initializeUser(params: {
346
+ farmState: PublicKey
347
+ delegatee?: PublicKey
348
+ }): KaminoFarmInitializeUserInstruction {
349
+ return { action: KaminoFarmAction.INITIALIZE_USER, ...params }
350
+ },
351
+
352
+ /** Stake the managed vault's token ATA into the farm. */
353
+ stake(params: {
354
+ farmState: PublicKey
355
+ amount: BN | "ALL"
356
+ delegatee?: PublicKey
357
+ }): KaminoFarmStakeInstruction {
358
+ return { action: KaminoFarmAction.STAKE, ...params }
359
+ },
360
+
361
+ /** Unstake a scaled share amount from the farm. */
362
+ unstake(params: {
363
+ farmState: PublicKey
364
+ stakeSharesScaled: BN
365
+ delegatee?: PublicKey
366
+ }): KaminoFarmUnstakeInstruction {
367
+ return { action: KaminoFarmAction.UNSTAKE, ...params }
368
+ },
369
+
370
+ /** Withdraw matured unstaked deposits back into the managed vault ATA. */
371
+ withdrawUnstakedDeposits(params: {
372
+ farmState: PublicKey
373
+ delegatee?: PublicKey
374
+ }): KaminoFarmWithdrawUnstakedDepositsInstruction {
375
+ return { action: KaminoFarmAction.WITHDRAW_UNSTAKED_DEPOSITS, ...params }
376
+ },
377
+
378
+ /** Harvest a specific reward index into a managed vault reward ATA. */
379
+ harvestReward(params: {
380
+ farmState: PublicKey
381
+ rewardIndex: number
382
+ delegatee?: PublicKey
383
+ }): KaminoFarmHarvestRewardInstruction {
384
+ return { action: KaminoFarmAction.HARVEST_REWARD, ...params }
385
+ },
386
+ }
387
+
535
388
  // ============================================================================
536
389
  // Sync Transaction Builder
537
390
  // ============================================================================
@@ -565,57 +418,12 @@ export interface VaultSyncTransactionResult {
565
418
  addressLookupTableAddresses: PublicKey[]
566
419
  }
567
420
 
568
- /**
569
- * Build vault instructions and wrap them in a Squads sync transaction.
570
- *
571
- * Takes high-level `VaultInstruction` descriptors (built with `kamino.*`),
572
- * resolves them to raw Solana instructions, then separates them:
573
- * - Permissionless refresh instructions → `preInstructions` (top-level)
574
- * - Vault-signed instructions → `instruction` (Squads sync transaction)
575
- *
576
- * KLend's `check_refresh` requires refreshReserve to be a top-level instruction
577
- * so it can be found via the instruction sysvar.
578
- *
579
- * @returns `{ setupInstructions, preInstructions, instruction, postInstructions, signers, addressLookupTableAddresses }`
580
- *
581
- * @example
582
- * ```ts
583
- * const { setupInstructions, preInstructions, instruction, postInstructions, signers, addressLookupTableAddresses } = await createVaultSyncTransaction({
584
- * instructions: [
585
- * kamino.initUserMetadata(KaminoMarket.MAIN),
586
- * kamino.initObligation(KaminoMarket.MAIN),
587
- * kamino.deposit(KaminoMarket.MAIN, "USDC", new BN(1_000_000)),
588
- * ],
589
- * owner: vaultPda,
590
- * connection,
591
- * policyPda,
592
- * vaultPda,
593
- * signer: wallet.publicKey,
594
- * vaultAddress: VAULT_ADDRESS,
595
- * })
596
- * // Send: [...setupInstructions, ...preInstructions, instruction, ...postInstructions]
597
- * ```
598
- */
599
- export async function createVaultSyncTransaction({
600
- instructions,
601
- owner,
602
- connection,
603
- policyPda,
604
- vaultPda,
605
- signer,
606
- accountIndex = 0,
607
- constraintIndices,
608
- vaultAddress,
609
- leadingAccounts,
610
- preHookAccounts,
611
- postHookAccounts,
612
- squadsProgram = SQUADS_PROGRAM_ID,
613
- }: {
421
+ type CreateVaultSyncTransactionParams = {
614
422
  instructions: VaultInstruction[]
615
423
  owner: PublicKey
616
424
  connection: Connection
617
425
  policyPda?: PublicKey
618
- vaultPda: PublicKey
426
+ vaultPda?: PublicKey
619
427
  signer: PublicKey
620
428
  accountIndex?: number
621
429
  constraintIndices?: number[]
@@ -625,8 +433,87 @@ export async function createVaultSyncTransaction({
625
433
  preHookAccounts?: PublicKey[] | AccountMeta[]
626
434
  postHookAccounts?: PublicKey[] | AccountMeta[]
627
435
  squadsProgram?: PublicKey
628
- }): Promise<VaultSyncTransactionResult> {
629
- const { setupInstructions, syncInstructions, preInstructions, postInstructions, signers, addressLookupTableAddresses } = await buildVaultInstructions(
436
+ /** Automatically manage new Kamino/CLMM/yield position tracking for manager flows. Defaults to `false` for this low-level helper. */
437
+ autoManagePositions?: boolean
438
+ /** Optional shared setup context — when provided, setup state (tracked accounts, positions) is shared across calls, preventing duplicate setup instructions. */
439
+ setupContext?: StrategySetupContext
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[]> {
498
+ vaultPda ??= owner
499
+
500
+ const resolvedSetupContext = setupContext ?? createStrategySetupContext({
501
+ connection,
502
+ env: LOCAL_ENV,
503
+ owner,
504
+ signer,
505
+ vaultAddress,
506
+ policyPda,
507
+ vaultPda,
508
+ accountIndex,
509
+ squadsProgram,
510
+ leadingAccounts,
511
+ preHookAccounts,
512
+ postHookAccounts,
513
+ autoManagePositions,
514
+ })
515
+
516
+ const buckets = await buildVaultInstructions(
630
517
  instructions,
631
518
  owner,
632
519
  connection,
@@ -639,55 +526,236 @@ export async function createVaultSyncTransaction({
639
526
  preHookAccounts,
640
527
  postHookAccounts,
641
528
  squadsProgram,
529
+ autoManagePositions,
530
+ resolvedSetupContext,
642
531
  )
643
532
 
644
- let resolvedPolicyPda = policyPda
645
- let resolvedConstraintIndices = constraintIndices
646
- if (!resolvedPolicyPda) {
647
- if (!vaultAddress) {
648
- throw new Error("vaultAddress is required when policyPda is not provided")
649
- }
650
- const match = await resolvePolicyMatchForVault(connection, vaultAddress, signer, syncInstructions)
651
- resolvedPolicyPda = match.policyPda
652
- resolvedConstraintIndices ??= match.constraintIndices
653
- }
533
+ const setupStatePriceRefreshInstructions = setupContext
534
+ ? []
535
+ : await buildSetupStatePriceRefreshInstructions(resolvedSetupContext)
654
536
 
655
- // Auto-resolve constraint indices when not explicitly provided
656
- resolvedConstraintIndices ??= await resolveConstraintIndices(
537
+ const fullResult = await buildWrappedVaultSyncTransactionResult({
657
538
  connection,
658
- resolvedPolicyPda,
659
- syncInstructions,
660
- )
661
-
662
- // Auto-resolve policy prefix and hook accounts when not explicitly provided
663
- let resolvedLeadingAccounts = leadingAccounts
664
- let resolvedPreHookAccounts = preHookAccounts
665
- let resolvedPostHookAccounts = postHookAccounts
666
- if (vaultAddress && (!leadingAccounts || !preHookAccounts || !postHookAccounts)) {
667
- const hooks = await resolveHookAccounts(connection, resolvedPolicyPda, vaultAddress, signer)
668
- resolvedLeadingAccounts ??= hooks.leadingAccounts
669
- resolvedPreHookAccounts ??= hooks.preHookAccounts
670
- resolvedPostHookAccounts ??= hooks.postHookAccounts
671
- }
672
-
673
- const instruction = wrapInstructionsInSyncTransaction({
674
- policyPda: resolvedPolicyPda,
539
+ policyPda,
675
540
  vaultPda,
676
541
  signer,
677
- instructions: syncInstructions,
678
- squadsProgram,
679
542
  accountIndex,
680
- constraintIndices: resolvedConstraintIndices,
681
- leadingAccounts: resolvedLeadingAccounts,
682
- preHookAccounts: resolvedPreHookAccounts,
683
- postHookAccounts: resolvedPostHookAccounts,
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,
684
555
  })
685
556
 
686
- return { setupInstructions, preInstructions, instruction, postInstructions, signers, addressLookupTableAddresses }
687
- }
557
+ const isPlannerExpandedKaminoVaultWithdraw =
558
+ instructions.length === 1
559
+ && instructions[0]?.action === KaminoVaultAction.WITHDRAW
560
+ && !(instructions[0] as KaminoVaultWithdrawInstruction).reserve
561
+ && buckets.syncInstructions.length > 1
688
562
 
689
- // ============================================================================
690
- // Internal: Instruction Assembly
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> {
675
+ let resolvedPolicyPda = policyPda
676
+ let resolvedConstraintIndices = constraintIndices
677
+ if (!resolvedPolicyPda) {
678
+ if (!vaultAddress) {
679
+ throw new Error("vaultAddress is required when policyPda is not provided")
680
+ }
681
+ const match = await resolvePolicyMatchForVault(connection, vaultAddress, signer, syncInstructions)
682
+ resolvedPolicyPda = match.policyPda
683
+ resolvedConstraintIndices ??= match.constraintIndices
684
+ }
685
+
686
+ resolvedConstraintIndices ??= await resolveConstraintIndices(
687
+ connection,
688
+ resolvedPolicyPda,
689
+ syncInstructions,
690
+ )
691
+
692
+ let resolvedLeadingAccounts = leadingAccounts
693
+ let resolvedPreHookAccounts = preHookAccounts
694
+ let resolvedPostHookAccounts = postHookAccounts
695
+ if (vaultAddress && (!leadingAccounts || !preHookAccounts || !postHookAccounts)) {
696
+ const hooks = await resolveHookAccounts(connection, resolvedPolicyPda, vaultAddress, signer)
697
+ resolvedLeadingAccounts ??= hooks.leadingAccounts
698
+ resolvedPreHookAccounts ??= hooks.preHookAccounts
699
+ resolvedPostHookAccounts ??= hooks.postHookAccounts
700
+ }
701
+
702
+ const instruction = wrapInstructionsInSyncTransaction({
703
+ policyPda: resolvedPolicyPda,
704
+ vaultPda,
705
+ signer,
706
+ instructions: syncInstructions,
707
+ squadsProgram,
708
+ accountIndex,
709
+ constraintIndices: resolvedConstraintIndices,
710
+ leadingAccounts: resolvedLeadingAccounts,
711
+ preHookAccounts: resolvedPreHookAccounts,
712
+ postHookAccounts: resolvedPostHookAccounts,
713
+ })
714
+
715
+ return {
716
+ setupInstructions,
717
+ preInstructions,
718
+ instruction,
719
+ postInstructions,
720
+ signers,
721
+ addressLookupTableAddresses,
722
+ }
723
+ }
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
+
757
+ // ============================================================================
758
+ // Internal: Instruction Assembly
691
759
  // ============================================================================
692
760
 
693
761
  /** KLend farm modes — collateral for deposit/withdraw, debt for borrow/repay. */
@@ -704,6 +772,14 @@ interface InstructionBuckets {
704
772
  addressLookupTableAddresses: PublicKey[]
705
773
  }
706
774
 
775
+ function isKaminoVaultInstruction(ix: VaultInstruction): ix is KaminoVaultInstruction {
776
+ return Object.values(KaminoVaultAction).includes(ix.action as KaminoVaultAction)
777
+ }
778
+
779
+ function isKaminoFarmInstruction(ix: VaultInstruction): ix is KaminoFarmInstruction {
780
+ return Object.values(KaminoFarmAction).includes(ix.action as KaminoFarmAction)
781
+ }
782
+
707
783
  function isOrderbookInstruction(ix: VaultInstruction): ix is OrderbookInstruction {
708
784
  return Object.values(OrderbookAction).includes(ix.action as OrderbookAction)
709
785
  }
@@ -740,24 +816,37 @@ const TITAN_OUTPUT_TOKEN_PROGRAM_ACCOUNT_INDEX = 7
740
816
  const YIELD_POSITION_BASE_SIZE = 124
741
817
  const YIELD_POSITION_TRACKER_SIZE = 40
742
818
 
743
- type SimplePriceLookup = {
744
- priceId: bigint
745
- price: Decimal
819
+ type TrackedKaminoObligationState = {
820
+ quotePriceId: ClientPriceId
821
+ quoteInputMint: PublicKey
822
+ mappedReserves: Set<string>
746
823
  }
747
824
 
748
825
  type StrategySetupState = {
749
826
  strategyVault: StrategyVault
750
827
  prices: ExponentPrices
828
+ requiredPriceIds: Set<number>
751
829
  nextStrategyPositionIndex: number
752
830
  trackedOrderbooks: Set<string>
753
831
  trackedYieldVaults: Set<string>
832
+ trackedKaminoObligations: Map<string, TrackedKaminoObligationState>
833
+ trackedKaminoFarms: Set<string>
834
+ trackedClmmPositions: Set<string>
754
835
  tokenEntryAccountByMint: Map<string, string>
755
836
  tokenPositionIndexByMint: Map<string, number>
756
837
  trackedTokenAccounts: Set<string>
838
+ baseAumAccounts: AccountMeta[]
839
+ plannedAumAccounts: AccountMeta[]
757
840
  existingAccounts: Map<string, boolean>
758
841
  }
759
842
 
760
- type StrategySetupContext = {
843
+ type ResolvedHookAccounts = {
844
+ leadingAccounts: PublicKey[] | AccountMeta[]
845
+ preHookAccounts: PublicKey[] | AccountMeta[]
846
+ postHookAccounts: PublicKey[] | AccountMeta[]
847
+ }
848
+
849
+ export type StrategySetupContext = {
761
850
  connection: Connection
762
851
  env: Environment
763
852
  owner: PublicKey
@@ -770,10 +859,14 @@ type StrategySetupContext = {
770
859
  leadingAccounts?: PublicKey[] | AccountMeta[]
771
860
  preHookAccounts?: PublicKey[] | AccountMeta[]
772
861
  postHookAccounts?: PublicKey[] | AccountMeta[]
862
+ autoManagePositions: boolean
863
+ pricesAccount?: ExponentPrices
773
864
  statePromise?: Promise<StrategySetupState | null>
865
+ /** Cached hook resolution promise — avoids redundant RPC calls when wrapping multiple setup instructions. */
866
+ resolvedHooksPromise?: Promise<ResolvedHookAccounts>
774
867
  }
775
868
 
776
- function createStrategySetupContext({
869
+ export function createStrategySetupContext({
777
870
  connection,
778
871
  env,
779
872
  owner,
@@ -786,6 +879,8 @@ function createStrategySetupContext({
786
879
  leadingAccounts,
787
880
  preHookAccounts,
788
881
  postHookAccounts,
882
+ autoManagePositions = true,
883
+ pricesAccount,
789
884
  }: {
790
885
  connection: Connection
791
886
  env: Environment
@@ -799,6 +894,8 @@ function createStrategySetupContext({
799
894
  leadingAccounts?: PublicKey[] | AccountMeta[]
800
895
  preHookAccounts?: PublicKey[] | AccountMeta[]
801
896
  postHookAccounts?: PublicKey[] | AccountMeta[]
897
+ autoManagePositions?: boolean
898
+ pricesAccount?: ExponentPrices
802
899
  }): StrategySetupContext {
803
900
  return {
804
901
  connection,
@@ -813,6 +910,17 @@ function createStrategySetupContext({
813
910
  leadingAccounts,
814
911
  preHookAccounts,
815
912
  postHookAccounts,
913
+ autoManagePositions,
914
+ pricesAccount,
915
+ }
916
+ }
917
+
918
+ function trackRequiredPriceIds(requiredPriceIds: Set<number>, priceIdValue: unknown) {
919
+ for (const id of extractPriceIds(priceIdValue)) {
920
+ const numericId = Number(id)
921
+ if (numericId !== 0) {
922
+ requiredPriceIds.add(numericId)
923
+ }
816
924
  }
817
925
  }
818
926
 
@@ -828,10 +936,13 @@ async function loadStrategySetupState(context: StrategySetupContext): Promise<St
828
936
  connection: context.connection,
829
937
  address: context.vaultAddress!,
830
938
  })
831
- const prices = await strategyVault.fetcher.fetchExponentPrices()
939
+ const prices = context.pricesAccount ?? await strategyVault.fetcher.fetchExponentPrices()
832
940
 
833
941
  const trackedOrderbooks = new Set<string>()
834
942
  const trackedYieldVaults = new Set<string>()
943
+ const trackedKaminoObligations = new Map<string, TrackedKaminoObligationState>()
944
+ const trackedKaminoFarms = new Set<string>()
945
+ const trackedClmmPositions = new Set<string>()
835
946
  const tokenEntryAccountByMint = new Map<string, string>()
836
947
  const tokenPositionIndexByMint = new Map<string, number>()
837
948
  const trackedTokenAccounts = new Set<string>()
@@ -850,6 +961,27 @@ async function loadStrategySetupState(context: StrategySetupContext): Promise<St
850
961
  trackedYieldVaults.add(position.yieldPosition[0].vault.toBase58())
851
962
  continue
852
963
  }
964
+ const kaminoEntry = getTrackedKaminoObligationFromPosition(position)
965
+ if (kaminoEntry) {
966
+ trackedKaminoObligations.set(kaminoEntry.obligation.toBase58(), {
967
+ quotePriceId: kaminoEntry.quotePriceId,
968
+ quoteInputMint: getPriceInputMintFromPriceId(prices, kaminoEntry.quotePriceId),
969
+ mappedReserves: new Set(
970
+ (kaminoEntry.reservePriceMappings ?? []).map((mapping) => mapping.reserve.toBase58()),
971
+ ),
972
+ })
973
+ continue
974
+ }
975
+ const kaminoFarmEntry = getTrackedKaminoFarmFromPosition(position)
976
+ if (kaminoFarmEntry) {
977
+ trackedKaminoFarms.add(kaminoFarmKey(kaminoFarmEntry.farmState, kaminoFarmEntry.userState))
978
+ continue
979
+ }
980
+ const clmmEntry = getTrackedClmmPositionFromPosition(position)
981
+ if (clmmEntry) {
982
+ trackedClmmPositions.add(clmmEntry.lpPosition.toBase58())
983
+ continue
984
+ }
853
985
  if ("tokenAccount" in position && position.tokenAccount?.[0]) {
854
986
  const entry = position.tokenAccount[0]
855
987
  tokenPositionIndexByMint.set(entry.tokenMint.toBase58(), index)
@@ -862,12 +994,21 @@ async function loadStrategySetupState(context: StrategySetupContext): Promise<St
862
994
  return {
863
995
  strategyVault,
864
996
  prices,
997
+ requiredPriceIds: collectTrackedStrategyVaultPriceIds({
998
+ tokenEntries: strategyVault.state.tokenEntries,
999
+ strategyPositions: strategyVault.state.strategyPositions as Array<Record<string, unknown>>,
1000
+ }),
865
1001
  nextStrategyPositionIndex: strategyVault.state.strategyPositions.length,
866
1002
  trackedOrderbooks,
867
1003
  trackedYieldVaults,
1004
+ trackedKaminoObligations,
1005
+ trackedKaminoFarms,
1006
+ trackedClmmPositions,
868
1007
  tokenEntryAccountByMint,
869
1008
  tokenPositionIndexByMint,
870
1009
  trackedTokenAccounts,
1010
+ baseAumAccounts: mutableStrategyVault(strategyVault).aumRemainingAccounts(),
1011
+ plannedAumAccounts: [],
871
1012
  existingAccounts: new Map<string, boolean>(),
872
1013
  }
873
1014
  })()
@@ -876,163 +1017,544 @@ async function loadStrategySetupState(context: StrategySetupContext): Promise<St
876
1017
  return context.statePromise
877
1018
  }
878
1019
 
879
- function findSimplePrice(
880
- prices: ExponentPrices,
881
- priceMint: PublicKey,
882
- underlyingMint: PublicKey,
883
- ): SimplePriceLookup | null {
884
- for (const entry of prices.prices) {
885
- if (!entry) continue
886
- if (!entry.priceMint.equals(priceMint) || !entry.underlyingMint.equals(underlyingMint)) {
1020
+ export async function buildSetupStatePriceRefreshInstructions(
1021
+ setupContext: StrategySetupContext,
1022
+ ): Promise<TransactionInstruction[]> {
1023
+ const state = await loadStrategySetupState(setupContext)
1024
+ if (!state) {
1025
+ return []
1026
+ }
1027
+
1028
+ const refreshInstructions: TransactionInstruction[] = []
1029
+ const reserveAccounts = new Map<string, { account: Reserve }>()
1030
+ for (const priceId of state.requiredPriceIds) {
1031
+ const priceEntry = state.prices.prices[priceId]
1032
+ if (!priceEntry || !isKaminoVaultPriceType(priceEntry.priceType)) {
887
1033
  continue
888
1034
  }
889
1035
 
890
- return {
891
- priceId: entry.priceId,
892
- price: exponentPriceToDecimal(entry),
1036
+ for (const reserveAddress of priceEntry.interfaceAccounts.slice(1)) {
1037
+ const reserveKey = reserveAddress.toBase58()
1038
+ if (reserveAccounts.has(reserveKey)) {
1039
+ continue
1040
+ }
1041
+
1042
+ const reserveAccount = await Reserve.fetch(setupContext.connection, reserveAddress)
1043
+ if (!reserveAccount) {
1044
+ throw new Error(`Missing Kamino reserve account ${reserveKey} required to refresh Kamino vault prices`)
1045
+ }
1046
+
1047
+ reserveAccounts.set(reserveKey, { account: reserveAccount })
1048
+ }
1049
+ }
1050
+
1051
+ if (reserveAccounts.size > 0) {
1052
+ const reserves = [...reserveAccounts.values()]
1053
+ const defaultKey = PublicKey.default
1054
+ const oracleOrSentinel = (key: PublicKey) =>
1055
+ key.equals(defaultKey) ? KAMINO_LENDING_PROGRAM_ID : key
1056
+
1057
+ refreshInstructions.push(...await buildScopeRefreshInstructions(setupContext.connection, reserves))
1058
+
1059
+ for (const [reserveKey, { account }] of reserveAccounts.entries()) {
1060
+ const tokenInfo = account.config.tokenInfo
1061
+ refreshInstructions.push(
1062
+ refreshReserve({
1063
+ reserve: new PublicKey(reserveKey),
1064
+ lendingMarket: account.lendingMarket,
1065
+ pythOracle: oracleOrSentinel(tokenInfo.pythConfiguration.price),
1066
+ switchboardPriceOracle: oracleOrSentinel(tokenInfo.switchboardConfiguration.priceAggregator),
1067
+ switchboardTwapOracle: oracleOrSentinel(tokenInfo.switchboardConfiguration.twapAggregator),
1068
+ scopePrices: oracleOrSentinel(tokenInfo.scopeConfiguration.priceFeed),
1069
+ }),
1070
+ )
893
1071
  }
894
1072
  }
895
1073
 
896
- return null
1074
+ const updatePriceInstructions = await state.strategyVault.ixsUpdateStrategyVaultPrices(state.prices, {
1075
+ manager: setupContext.signer,
1076
+ priceIds: [...state.requiredPriceIds].sort((a, b) => a - b),
1077
+ })
1078
+
1079
+ return [...refreshInstructions, ...updatePriceInstructions]
897
1080
  }
898
1081
 
899
- function exponentPriceToDecimal(entry: ExponentPrice): Decimal {
900
- const raw = entry.price[0]
901
- if (!Array.isArray(raw)) {
902
- throw new Error(`Invalid Exponent price format for ${entry.priceId.toString()}`)
903
- }
904
- return new Decimal(bigintU256ToString(raw.map((value) => BigInt(value.toString()))))
1082
+ function isAutoManagePositionsEnabled(setupContext?: StrategySetupContext): boolean {
1083
+ return setupContext?.autoManagePositions ?? true
905
1084
  }
906
1085
 
907
- async function accountExists(
908
- state: StrategySetupState,
909
- connection: Connection,
910
- address: PublicKey,
911
- ): Promise<boolean> {
912
- const cacheKey = address.toBase58()
913
- const cached = state.existingAccounts.get(cacheKey)
914
- if (cached !== undefined) {
915
- return cached
1086
+ function unwrapTupleLikeValue(value: unknown): unknown {
1087
+ if (Array.isArray(value)) {
1088
+ return value[0]
916
1089
  }
917
-
918
- const exists = (await connection.getAccountInfo(address)) !== null
919
- state.existingAccounts.set(cacheKey, exists)
920
- return exists
1090
+ if (value && typeof value === "object" && "0" in value) {
1091
+ return (value as { 0?: unknown })[0]
1092
+ }
1093
+ return value ?? undefined
921
1094
  }
922
1095
 
923
- async function maybeCreateOwnedAtaSetupInstruction({
924
- state,
925
- connection,
926
- payer,
927
- owner,
928
- mint,
929
- tokenProgram,
930
- tokenAccount,
931
- }: {
932
- state: StrategySetupState
933
- connection: Connection
934
- payer: PublicKey
935
- owner: PublicKey
936
- mint: PublicKey
937
- tokenProgram: PublicKey
938
- tokenAccount: PublicKey
939
- }): Promise<TransactionInstruction | null> {
940
- if (await accountExists(state, connection, tokenAccount)) {
1096
+ function getTrackedKaminoObligationFromPosition(position: unknown): {
1097
+ obligation: PublicKey
1098
+ quotePriceId: ClientPriceId
1099
+ reservePriceMappings: Array<{ reserve: PublicKey; reservePriceId: ClientPriceId }>
1100
+ } | null {
1101
+ if (!position || typeof position !== "object" || !("obligation" in position)) {
941
1102
  return null
942
1103
  }
943
1104
 
944
- const expectedAta = getAssociatedTokenAddressSync(mint, owner, true, tokenProgram)
945
- if (!expectedAta.equals(tokenAccount)) {
1105
+ const obligationContainer = (position as { obligation?: unknown }).obligation
1106
+ const obligationValue = unwrapTupleLikeValue(obligationContainer)
1107
+ const kaminoContainer = (
1108
+ obligationValue
1109
+ && typeof obligationValue === "object"
1110
+ && "kaminoObligation" in (obligationValue as Record<string, unknown>)
1111
+ )
1112
+ ? (obligationValue as { kaminoObligation?: unknown }).kaminoObligation
1113
+ : obligationValue
1114
+ const kaminoEntry = unwrapTupleLikeValue(kaminoContainer)
1115
+
1116
+ if (
1117
+ !kaminoEntry
1118
+ || typeof kaminoEntry !== "object"
1119
+ || !("obligation" in kaminoEntry)
1120
+ || !((kaminoEntry as { obligation?: unknown }).obligation instanceof PublicKey)
1121
+ ) {
946
1122
  return null
947
1123
  }
948
1124
 
949
- state.existingAccounts.set(tokenAccount.toBase58(), true)
950
- return createAssociatedTokenAccountIdempotentInstruction(payer, tokenAccount, owner, mint, tokenProgram)
951
- }
1125
+ const typedKaminoEntry = kaminoEntry as {
1126
+ obligation: PublicKey
1127
+ quotePriceId: ClientPriceId
1128
+ reservePriceMappings?: Array<{ reserve: PublicKey; reservePriceId: ClientPriceId }>
1129
+ }
952
1130
 
953
- async function ensureOrderbookPositionSetup(
954
- orderbook: Orderbook,
955
- buckets: InstructionBuckets,
956
- setupContext: StrategySetupContext,
957
- ) {
958
- const state = await loadStrategySetupState(setupContext)
959
- if (!state) {
960
- return
1131
+ return {
1132
+ obligation: typedKaminoEntry.obligation,
1133
+ quotePriceId: typedKaminoEntry.quotePriceId,
1134
+ reservePriceMappings: typedKaminoEntry.reservePriceMappings ?? [],
961
1135
  }
1136
+ }
962
1137
 
963
- const orderbookKey = orderbook.selfAddress.toBase58()
964
- if (state.trackedOrderbooks.has(orderbookKey)) {
965
- return
1138
+ function getTrackedKaminoFarmFromPosition(position: unknown): {
1139
+ farmState: PublicKey
1140
+ userState: PublicKey
1141
+ } | null {
1142
+ if (!position || typeof position !== "object" || !("kaminoFarm" in position)) {
1143
+ return null
966
1144
  }
967
1145
 
968
- const ptPrice = findSimplePrice(
969
- state.prices,
970
- orderbook.mintPt,
971
- state.strategyVault.state.underlyingMint,
972
- )
973
- if (!ptPrice) {
974
- throw new Error(
975
- `Missing Exponent price for orderbook setup (${orderbook.selfAddress.toBase58()})`,
976
- )
1146
+ const kaminoFarmContainer = (position as { kaminoFarm?: unknown }).kaminoFarm
1147
+ const kaminoFarmEntry = unwrapTupleLikeValue(kaminoFarmContainer)
1148
+ if (
1149
+ !kaminoFarmEntry
1150
+ || typeof kaminoFarmEntry !== "object"
1151
+ || !((kaminoFarmEntry as { farmState?: unknown }).farmState instanceof PublicKey)
1152
+ || !((kaminoFarmEntry as { userState?: unknown }).userState instanceof PublicKey)
1153
+ ) {
1154
+ return null
977
1155
  }
978
1156
 
979
- buckets.setupInstructions.push(
980
- state.strategyVault.ixWrapperManageVaultSettings({
981
- manager: setupContext.signer,
982
- actions: [
983
- exponentVaults.vaultSettingsAction("AddOrderbookEntry", [{
984
- orderbook: orderbook.selfAddress,
985
- // Legacy field retained for layout compatibility. Order ownership is
986
- // derived from live orderbook state during AUM calculation.
987
- userEscrowIdx: 0,
988
- mint: orderbook.vault.mintSy,
989
- offerIdxVec: [],
990
- priceIdPt: exponentVaults.priceId("Simple", { priceId: ptPrice.priceId }),
991
- baseMint: state.strategyVault.state.underlyingMint,
992
- }]),
993
- ],
994
- remainingAccounts: [
995
- { pubkey: orderbook.selfAddress, isSigner: false, isWritable: false },
996
- { pubkey: orderbook.state.vault, isSigner: false, isWritable: false },
997
- ],
998
- }),
999
- )
1157
+ return {
1158
+ farmState: (kaminoFarmEntry as { farmState: PublicKey }).farmState,
1159
+ userState: (kaminoFarmEntry as { userState: PublicKey }).userState,
1160
+ }
1161
+ }
1000
1162
 
1001
- state.trackedOrderbooks.add(orderbookKey)
1002
- state.nextStrategyPositionIndex += 1
1163
+ function kaminoFarmKey(farmState: PublicKey, userState: PublicKey): string {
1164
+ return `${farmState.toBase58()}:${userState.toBase58()}`
1003
1165
  }
1004
1166
 
1005
- async function ensureYieldPositionSetup(
1006
- coreVault: Vault,
1007
- buckets: InstructionBuckets,
1008
- setupContext: StrategySetupContext,
1009
- ) {
1010
- const state = await loadStrategySetupState(setupContext)
1011
- if (!state) {
1012
- return
1167
+ function getTrackedClmmPositionFromPosition(position: unknown): {
1168
+ lpPosition: PublicKey
1169
+ market: PublicKey
1170
+ } | null {
1171
+ if (!position || typeof position !== "object" || !("clmmPosition" in position)) {
1172
+ return null
1013
1173
  }
1014
1174
 
1015
- const coreVaultKey = coreVault.selfAddress.toBase58()
1016
- if (state.trackedYieldVaults.has(coreVaultKey)) {
1017
- return
1175
+ const clmmContainer = (position as { clmmPosition?: unknown }).clmmPosition
1176
+ const clmmEntry = unwrapTupleLikeValue(clmmContainer)
1177
+ if (
1178
+ !clmmEntry
1179
+ || typeof clmmEntry !== "object"
1180
+ || !((clmmEntry as { lpPosition?: unknown }).lpPosition instanceof PublicKey)
1181
+ || !((clmmEntry as { market?: unknown }).market instanceof PublicKey)
1182
+ ) {
1183
+ return null
1018
1184
  }
1019
1185
 
1020
- const ptPrice = findSimplePrice(
1021
- state.prices,
1022
- coreVault.mintPt,
1023
- state.strategyVault.state.underlyingMint,
1024
- )
1025
- if (!ptPrice) {
1026
- throw new Error(`Missing Exponent price for core vault setup (${coreVault.selfAddress.toBase58()})`)
1186
+ return {
1187
+ lpPosition: (clmmEntry as { lpPosition: PublicKey }).lpPosition,
1188
+ market: (clmmEntry as { market: PublicKey }).market,
1027
1189
  }
1190
+ }
1028
1191
 
1029
- const yieldPosition = coreVault.pda.yieldPosition({ owner: setupContext.owner, vault: coreVault.selfAddress })
1030
- if (!(await accountExists(state, setupContext.connection, yieldPosition))) {
1031
- const requiredLamports = await setupContext.connection.getMinimumBalanceForRentExemption(
1032
- YIELD_POSITION_BASE_SIZE + (coreVault.state.emissions.length * YIELD_POSITION_TRACKER_SIZE),
1033
- )
1034
- const ownerLamports = await setupContext.connection.getBalance(setupContext.owner)
1035
- if (ownerLamports < requiredLamports) {
1192
+ async function accountExists(
1193
+ state: StrategySetupState,
1194
+ connection: Connection,
1195
+ address: PublicKey,
1196
+ ): Promise<boolean> {
1197
+ const cacheKey = address.toBase58()
1198
+ const cached = state.existingAccounts.get(cacheKey)
1199
+ if (cached !== undefined) {
1200
+ return cached
1201
+ }
1202
+
1203
+ const exists = (await connection.getAccountInfo(address)) !== null
1204
+ state.existingAccounts.set(cacheKey, exists)
1205
+ return exists
1206
+ }
1207
+
1208
+ type MutableStrategyVault = {
1209
+ aumRemainingAccounts(): AccountMeta[]
1210
+ clmmTicksMap: Map<string, PublicKey>
1211
+ }
1212
+
1213
+ function mutableStrategyVault(value: StrategySetupState | StrategyVault): MutableStrategyVault {
1214
+ return ("strategyVault" in value ? value.strategyVault : value) as unknown as MutableStrategyVault
1215
+ }
1216
+
1217
+ function buildTrackedAumRemainingAccounts(
1218
+ state: StrategySetupState,
1219
+ extraAccounts: AccountMeta[] = [],
1220
+ ): AccountMeta[] {
1221
+ return uniqueRemainingAccounts([
1222
+ ...state.baseAumAccounts,
1223
+ ...state.plannedAumAccounts,
1224
+ ...extraAccounts,
1225
+ ])
1226
+ }
1227
+
1228
+ function recordPlannedOrderbookEntry(
1229
+ state: StrategySetupState,
1230
+ params: { orderbook: PublicKey; mint: PublicKey; priceIdPt: ClientPriceId; baseMint: PublicKey },
1231
+ ) {
1232
+ state.trackedOrderbooks.add(params.orderbook.toBase58())
1233
+ state.plannedAumAccounts.push({ pubkey: params.orderbook, isSigner: false, isWritable: false })
1234
+ ;(state.strategyVault.state.strategyPositions as unknown as Array<Record<string, unknown>>).push({
1235
+ orderbook: [{
1236
+ orderbook: params.orderbook,
1237
+ mint: params.mint,
1238
+ offerIdxVec: [],
1239
+ priceIdPt: params.priceIdPt,
1240
+ baseMint: params.baseMint,
1241
+ }],
1242
+ })
1243
+ state.nextStrategyPositionIndex += 1
1244
+ }
1245
+
1246
+ function recordPlannedYieldPosition(
1247
+ state: StrategySetupState,
1248
+ params: { yieldPosition: PublicKey; vault: PublicKey; priceIdPt: ClientPriceId },
1249
+ ) {
1250
+ state.trackedYieldVaults.add(params.vault.toBase58())
1251
+ state.existingAccounts.set(params.yieldPosition.toBase58(), true)
1252
+ ;(state.strategyVault.state.strategyPositions as unknown as Array<Record<string, unknown>>).push({
1253
+ yieldPosition: [{
1254
+ yieldPosition: params.yieldPosition,
1255
+ vault: params.vault,
1256
+ priceIdPt: params.priceIdPt,
1257
+ }],
1258
+ })
1259
+ state.nextStrategyPositionIndex += 1
1260
+ }
1261
+
1262
+ function recordPlannedTokenAccountEntry(
1263
+ state: StrategySetupState,
1264
+ params: { tokenMint: PublicKey; tokenAccount: PublicKey; priceId: ClientPriceId },
1265
+ ) {
1266
+ const tokenMintKey = params.tokenMint.toBase58()
1267
+ const tokenAccountKey = params.tokenAccount.toBase58()
1268
+ state.tokenPositionIndexByMint.set(tokenMintKey, state.nextStrategyPositionIndex)
1269
+ state.trackedTokenAccounts.add(tokenAccountKey)
1270
+ state.plannedAumAccounts.push({ pubkey: params.tokenAccount, isSigner: false, isWritable: false })
1271
+ ;(state.strategyVault.state.strategyPositions as unknown as Array<Record<string, unknown>>).push({
1272
+ tokenAccount: [{
1273
+ tokenMint: params.tokenMint,
1274
+ balances: [{
1275
+ tokenAccount: params.tokenAccount,
1276
+ mint: params.tokenMint,
1277
+ priceId: params.priceId,
1278
+ }],
1279
+ }],
1280
+ })
1281
+ state.nextStrategyPositionIndex += 1
1282
+ }
1283
+
1284
+ function recordPlannedTokenAccountBalance(
1285
+ state: StrategySetupState,
1286
+ params: { tokenMint: PublicKey; tokenAccount: PublicKey; priceId: ClientPriceId },
1287
+ ) {
1288
+ const tokenMintKey = params.tokenMint.toBase58()
1289
+ const tokenAccountKey = params.tokenAccount.toBase58()
1290
+ state.trackedTokenAccounts.add(tokenAccountKey)
1291
+ state.plannedAumAccounts.push({ pubkey: params.tokenAccount, isSigner: false, isWritable: false })
1292
+
1293
+ const positions = state.strategyVault.state.strategyPositions as unknown as Array<Record<string, unknown>>
1294
+ const target = positions.find((position) => {
1295
+ const tokenAccountEntry = Array.isArray(position.tokenAccount) ? position.tokenAccount[0] : undefined
1296
+ return tokenAccountEntry && tokenAccountEntry.tokenMint instanceof PublicKey && tokenAccountEntry.tokenMint.equals(params.tokenMint)
1297
+ })
1298
+ const tokenAccountEntry = target?.tokenAccount?.[0] as
1299
+ | { balances?: Array<{ tokenAccount: PublicKey; mint: PublicKey; priceId: ClientPriceId }> }
1300
+ | undefined
1301
+ tokenAccountEntry?.balances?.push({
1302
+ tokenAccount: params.tokenAccount,
1303
+ mint: params.tokenMint,
1304
+ priceId: params.priceId,
1305
+ })
1306
+ }
1307
+
1308
+ function recordPlannedKaminoObligation(
1309
+ state: StrategySetupState,
1310
+ entry: {
1311
+ obligation: PublicKey
1312
+ quotePriceId: ClientPriceId
1313
+ quoteInputMint: PublicKey
1314
+ reservePriceMappings: Array<{ reserve: PublicKey; reservePriceId: ClientPriceId }>
1315
+ remainingAccountsAmount: bigint
1316
+ minPriceStatusFlags: number
1317
+ },
1318
+ ) {
1319
+ state.existingAccounts.set(entry.obligation.toBase58(), true)
1320
+ state.trackedKaminoObligations.set(entry.obligation.toBase58(), {
1321
+ quotePriceId: entry.quotePriceId,
1322
+ quoteInputMint: entry.quoteInputMint,
1323
+ mappedReserves: new Set(entry.reservePriceMappings.map((mapping) => mapping.reserve.toBase58())),
1324
+ })
1325
+ state.plannedAumAccounts.push({ pubkey: entry.obligation, isSigner: false, isWritable: false })
1326
+ for (const mapping of entry.reservePriceMappings) {
1327
+ state.plannedAumAccounts.push({ pubkey: mapping.reserve, isSigner: false, isWritable: false })
1328
+ }
1329
+ ;(state.strategyVault.state.strategyPositions as unknown as Array<Record<string, unknown>>).push({
1330
+ obligation: [{
1331
+ kaminoObligation: [{
1332
+ obligation: entry.obligation,
1333
+ lendingProgramId: KAMINO_LENDING_PROGRAM_ID,
1334
+ quotePriceId: entry.quotePriceId,
1335
+ reservePriceMappings: entry.reservePriceMappings,
1336
+ remainingAccountsAmount: entry.remainingAccountsAmount,
1337
+ minPriceStatusFlags: entry.minPriceStatusFlags,
1338
+ }],
1339
+ }],
1340
+ })
1341
+ state.nextStrategyPositionIndex += 1
1342
+ }
1343
+
1344
+ function recordPlannedKaminoReserveMappings(
1345
+ state: StrategySetupState,
1346
+ params: {
1347
+ obligation: PublicKey
1348
+ quoteInputMint: PublicKey
1349
+ reservePriceMappings: Array<{ reserve: PublicKey; reservePriceId: ClientPriceId }>
1350
+ },
1351
+ ) {
1352
+ const tracked = state.trackedKaminoObligations.get(params.obligation.toBase58())
1353
+ if (!tracked) {
1354
+ return
1355
+ }
1356
+
1357
+ tracked.quoteInputMint = params.quoteInputMint
1358
+ for (const mapping of params.reservePriceMappings) {
1359
+ tracked.mappedReserves.add(mapping.reserve.toBase58())
1360
+ }
1361
+
1362
+ const positions = state.strategyVault.state.strategyPositions as unknown as Array<Record<string, unknown>>
1363
+ const target = positions.find((position) => {
1364
+ const entry = getTrackedKaminoObligationFromPosition(position)
1365
+ return entry?.obligation.equals(params.obligation)
1366
+ })
1367
+ const kaminoEntry = (
1368
+ Array.isArray(target?.obligation)
1369
+ ? target?.obligation?.[0]?.kaminoObligation?.[0]
1370
+ : undefined
1371
+ ) as { reservePriceMappings?: Array<{ reserve: PublicKey; reservePriceId: ClientPriceId }>; remainingAccountsAmount?: bigint } | undefined
1372
+ if (!kaminoEntry) {
1373
+ return
1374
+ }
1375
+
1376
+ for (const mapping of params.reservePriceMappings) {
1377
+ const existing = kaminoEntry.reservePriceMappings?.find((item) => item.reserve.equals(mapping.reserve))
1378
+ if (existing) {
1379
+ existing.reservePriceId = mapping.reservePriceId
1380
+ } else {
1381
+ kaminoEntry.reservePriceMappings ??= []
1382
+ kaminoEntry.reservePriceMappings.push(mapping)
1383
+ }
1384
+ state.plannedAumAccounts.push({ pubkey: mapping.reserve, isSigner: false, isWritable: false })
1385
+ }
1386
+ kaminoEntry.remainingAccountsAmount = BigInt(1 + (kaminoEntry.reservePriceMappings?.length ?? 0))
1387
+ }
1388
+
1389
+ function recordPlannedKaminoFarmPosition(
1390
+ state: StrategySetupState,
1391
+ params: {
1392
+ farmState: PublicKey
1393
+ userState: PublicKey
1394
+ globalConfig: PublicKey
1395
+ scopePrices: PublicKey | null
1396
+ },
1397
+ ) {
1398
+ const trackingKey = kaminoFarmKey(params.farmState, params.userState)
1399
+ if (state.trackedKaminoFarms.has(trackingKey)) {
1400
+ return
1401
+ }
1402
+
1403
+ state.trackedKaminoFarms.add(trackingKey)
1404
+ state.existingAccounts.set(params.userState.toBase58(), true)
1405
+ state.plannedAumAccounts.push({ pubkey: params.farmState, isSigner: false, isWritable: false })
1406
+ state.plannedAumAccounts.push({ pubkey: params.userState, isSigner: false, isWritable: false })
1407
+ state.plannedAumAccounts.push({ pubkey: params.globalConfig, isSigner: false, isWritable: false })
1408
+ if (params.scopePrices) {
1409
+ state.plannedAumAccounts.push({ pubkey: params.scopePrices, isSigner: false, isWritable: false })
1410
+ }
1411
+ ;(state.strategyVault.state.strategyPositions as unknown as Array<Record<string, unknown>>).push({
1412
+ kaminoFarm: [{
1413
+ farmState: params.farmState,
1414
+ userState: params.userState,
1415
+ }],
1416
+ })
1417
+ state.nextStrategyPositionIndex += 1
1418
+ }
1419
+
1420
+ function recordPlannedClmmPosition(
1421
+ state: StrategySetupState,
1422
+ params: {
1423
+ lpPosition: PublicKey
1424
+ market: PublicKey
1425
+ priceIdPt: ClientPriceId
1426
+ priceIdSy: ClientPriceId
1427
+ ticksKey: PublicKey
1428
+ },
1429
+ ) {
1430
+ state.existingAccounts.set(params.lpPosition.toBase58(), true)
1431
+ state.trackedClmmPositions.add(params.lpPosition.toBase58())
1432
+ state.plannedAumAccounts.push({ pubkey: params.lpPosition, isSigner: false, isWritable: false })
1433
+ mutableStrategyVault(state).clmmTicksMap.set(params.market.toBase58(), params.ticksKey)
1434
+ ;(state.strategyVault.state.strategyPositions as unknown as Array<Record<string, unknown>>).push({
1435
+ clmmPosition: [{
1436
+ lpPosition: params.lpPosition,
1437
+ market: params.market,
1438
+ priceIdPt: params.priceIdPt,
1439
+ priceIdSy: params.priceIdSy,
1440
+ }],
1441
+ })
1442
+ state.nextStrategyPositionIndex += 1
1443
+ }
1444
+
1445
+ async function maybeCreateOwnedAtaSetupInstruction({
1446
+ state,
1447
+ connection,
1448
+ payer,
1449
+ owner,
1450
+ mint,
1451
+ tokenProgram,
1452
+ tokenAccount,
1453
+ }: {
1454
+ state: StrategySetupState
1455
+ connection: Connection
1456
+ payer: PublicKey
1457
+ owner: PublicKey
1458
+ mint: PublicKey
1459
+ tokenProgram: PublicKey
1460
+ tokenAccount: PublicKey
1461
+ }): Promise<TransactionInstruction | null> {
1462
+ if (await accountExists(state, connection, tokenAccount)) {
1463
+ return null
1464
+ }
1465
+
1466
+ const expectedAta = getAssociatedTokenAddressSync(mint, owner, true, tokenProgram)
1467
+ if (!expectedAta.equals(tokenAccount)) {
1468
+ return null
1469
+ }
1470
+
1471
+ state.existingAccounts.set(tokenAccount.toBase58(), true)
1472
+ return createAssociatedTokenAccountIdempotentInstruction(payer, tokenAccount, owner, mint, tokenProgram)
1473
+ }
1474
+
1475
+ async function ensureOrderbookPositionSetup(
1476
+ orderbook: Orderbook,
1477
+ buckets: InstructionBuckets,
1478
+ setupContext: StrategySetupContext,
1479
+ ) {
1480
+ const state = await loadStrategySetupState(setupContext)
1481
+ if (!state) {
1482
+ return
1483
+ }
1484
+
1485
+ const orderbookKey = orderbook.selfAddress.toBase58()
1486
+ if (state.trackedOrderbooks.has(orderbookKey)) {
1487
+ return
1488
+ }
1489
+
1490
+ const priceIdPt = resolvePriceIdFromMintToUnderlyingOrThrow({
1491
+ prices: state.prices,
1492
+ sourceMint: orderbook.mintPt,
1493
+ targetMint: state.strategyVault.state.underlyingMint,
1494
+ label: `orderbook setup (${orderbook.selfAddress.toBase58()})`,
1495
+ })
1496
+ trackRequiredPriceIds(state.requiredPriceIds, priceIdPt)
1497
+
1498
+ buckets.setupInstructions.push(
1499
+ state.strategyVault.ixWrapperManageVaultSettings({
1500
+ manager: setupContext.signer,
1501
+ actions: [
1502
+ exponentVaults.vaultSettingsAction("AddOrderbookEntry", [{
1503
+ orderbook: orderbook.selfAddress,
1504
+ // Legacy field retained for layout compatibility. Order ownership is
1505
+ // derived from live orderbook state during AUM calculation.
1506
+ userEscrowIdx: 0,
1507
+ mint: orderbook.vault.mintSy,
1508
+ offerIdxVec: [],
1509
+ priceIdPt,
1510
+ baseMint: state.strategyVault.state.underlyingMint,
1511
+ }]),
1512
+ ],
1513
+ remainingAccounts: [
1514
+ { pubkey: orderbook.selfAddress, isSigner: false, isWritable: false },
1515
+ { pubkey: orderbook.state.vault, isSigner: false, isWritable: false },
1516
+ ],
1517
+ }),
1518
+ )
1519
+
1520
+ recordPlannedOrderbookEntry(state, {
1521
+ orderbook: orderbook.selfAddress,
1522
+ mint: orderbook.vault.mintSy,
1523
+ priceIdPt,
1524
+ baseMint: state.strategyVault.state.underlyingMint,
1525
+ })
1526
+ }
1527
+
1528
+ async function ensureYieldPositionSetup(
1529
+ coreVault: Vault,
1530
+ buckets: InstructionBuckets,
1531
+ setupContext: StrategySetupContext,
1532
+ ) {
1533
+ const state = await loadStrategySetupState(setupContext)
1534
+ if (!state) {
1535
+ return
1536
+ }
1537
+
1538
+ const coreVaultKey = coreVault.selfAddress.toBase58()
1539
+ if (state.trackedYieldVaults.has(coreVaultKey)) {
1540
+ return
1541
+ }
1542
+
1543
+ const priceIdPt = resolvePriceIdFromMintToUnderlyingOrThrow({
1544
+ prices: state.prices,
1545
+ sourceMint: coreVault.mintPt,
1546
+ targetMint: state.strategyVault.state.underlyingMint,
1547
+ label: `core vault setup (${coreVault.selfAddress.toBase58()})`,
1548
+ })
1549
+ trackRequiredPriceIds(state.requiredPriceIds, priceIdPt)
1550
+
1551
+ const yieldPosition = coreVault.pda.yieldPosition({ owner: setupContext.owner, vault: coreVault.selfAddress })
1552
+ if (!(await accountExists(state, setupContext.connection, yieldPosition))) {
1553
+ const requiredLamports = await setupContext.connection.getMinimumBalanceForRentExemption(
1554
+ YIELD_POSITION_BASE_SIZE + (coreVault.state.emissions.length * YIELD_POSITION_TRACKER_SIZE),
1555
+ )
1556
+ const ownerLamports = await setupContext.connection.getBalance(setupContext.owner)
1557
+ if (ownerLamports < requiredLamports) {
1036
1558
  buckets.setupInstructions.push(
1037
1559
  SystemProgram.transfer({
1038
1560
  fromPubkey: setupContext.signer,
@@ -1066,7 +1588,7 @@ async function ensureYieldPositionSetup(
1066
1588
  exponentVaults.vaultSettingsAction("AddYieldPositionEntry", {
1067
1589
  yieldPosition,
1068
1590
  vault: coreVault.selfAddress,
1069
- priceIdPt: exponentVaults.priceId("Simple", { priceId: ptPrice.priceId }),
1591
+ priceIdPt,
1070
1592
  }),
1071
1593
  ],
1072
1594
  remainingAccounts: [
@@ -1076,8 +1598,11 @@ async function ensureYieldPositionSetup(
1076
1598
  }),
1077
1599
  )
1078
1600
 
1079
- state.trackedYieldVaults.add(coreVaultKey)
1080
- state.nextStrategyPositionIndex += 1
1601
+ recordPlannedYieldPosition(state, {
1602
+ yieldPosition,
1603
+ vault: coreVault.selfAddress,
1604
+ priceIdPt,
1605
+ })
1081
1606
  }
1082
1607
 
1083
1608
  async function wrapVaultSignedSetupInstruction({
@@ -1120,12 +1645,17 @@ async function wrapVaultSignedSetupInstruction({
1120
1645
  setupContext.vaultAddress
1121
1646
  && (!resolvedLeadingAccounts || !resolvedPreHookAccounts || !resolvedPostHookAccounts)
1122
1647
  ) {
1123
- const hooks = await resolveHookAccounts(
1124
- setupContext.connection,
1125
- resolvedPolicyPda,
1126
- setupContext.vaultAddress,
1127
- setupContext.signer,
1128
- )
1648
+ // Cache hook resolution on the context to avoid redundant RPC calls
1649
+ // when wrapping multiple setup instructions in the same build.
1650
+ if (!setupContext.resolvedHooksPromise) {
1651
+ setupContext.resolvedHooksPromise = resolveHookAccounts(
1652
+ setupContext.connection,
1653
+ resolvedPolicyPda,
1654
+ setupContext.vaultAddress,
1655
+ setupContext.signer,
1656
+ )
1657
+ }
1658
+ const hooks = await setupContext.resolvedHooksPromise
1129
1659
  resolvedLeadingAccounts ??= hooks.leadingAccounts
1130
1660
  resolvedPreHookAccounts ??= hooks.preHookAccounts
1131
1661
  resolvedPostHookAccounts ??= hooks.postHookAccounts
@@ -1180,14 +1710,13 @@ async function ensureTrackedTokenAccountSetup({
1180
1710
  return
1181
1711
  }
1182
1712
 
1183
- const price = findSimplePrice(
1184
- state.prices,
1185
- tokenMint,
1186
- state.strategyVault.state.underlyingMint,
1187
- )
1188
- if (!price) {
1189
- throw new Error(`Missing Exponent price for token position setup (${tokenMint.toBase58()})`)
1190
- }
1713
+ const priceId = resolvePriceIdFromMintToUnderlyingOrThrow({
1714
+ prices: state.prices,
1715
+ sourceMint: tokenMint,
1716
+ targetMint: state.strategyVault.state.underlyingMint,
1717
+ label: `token position setup (${tokenMint.toBase58()})`,
1718
+ })
1719
+ trackRequiredPriceIds(state.requiredPriceIds, priceId)
1191
1720
 
1192
1721
  const maybeAtaIx = await maybeCreateOwnedAtaSetupInstruction({
1193
1722
  state,
@@ -1213,7 +1742,7 @@ async function ensureTrackedTokenAccountSetup({
1213
1742
  balances: [{
1214
1743
  tokenAccount,
1215
1744
  mint: tokenMint,
1216
- priceId: exponentVaults.priceId("Simple", { priceId: price.priceId }),
1745
+ priceId,
1217
1746
  }],
1218
1747
  }]),
1219
1748
  ],
@@ -1222,8 +1751,11 @@ async function ensureTrackedTokenAccountSetup({
1222
1751
  ],
1223
1752
  }),
1224
1753
  )
1225
- state.tokenPositionIndexByMint.set(tokenMintKey, state.nextStrategyPositionIndex)
1226
- state.nextStrategyPositionIndex += 1
1754
+ recordPlannedTokenAccountEntry(state, {
1755
+ tokenMint,
1756
+ tokenAccount,
1757
+ priceId,
1758
+ })
1227
1759
  } else {
1228
1760
  buckets.setupInstructions.push(
1229
1761
  state.strategyVault.ixWrapperManagerUpdatePosition({
@@ -1234,7 +1766,7 @@ async function ensureTrackedTokenAccountSetup({
1234
1766
  balance: {
1235
1767
  tokenAccount,
1236
1768
  mint: tokenMint,
1237
- priceId: exponentVaults.priceId("Simple", { priceId: price.priceId }),
1769
+ priceId,
1238
1770
  },
1239
1771
  },
1240
1772
  remainingAccounts: [
@@ -1242,6 +1774,11 @@ async function ensureTrackedTokenAccountSetup({
1242
1774
  ],
1243
1775
  }),
1244
1776
  )
1777
+ recordPlannedTokenAccountBalance(state, {
1778
+ tokenMint,
1779
+ tokenAccount,
1780
+ priceId,
1781
+ })
1245
1782
  }
1246
1783
 
1247
1784
  state.trackedTokenAccounts.add(tokenAccountKey)
@@ -1260,6 +1797,8 @@ async function buildVaultInstructions(
1260
1797
  preHookAccounts?: PublicKey[] | AccountMeta[],
1261
1798
  postHookAccounts?: PublicKey[] | AccountMeta[],
1262
1799
  squadsProgram: PublicKey = SQUADS_PROGRAM_ID,
1800
+ autoManagePositions: boolean = true,
1801
+ sharedSetupContext?: StrategySetupContext,
1263
1802
  ): Promise<InstructionBuckets> {
1264
1803
  const buckets: InstructionBuckets = {
1265
1804
  setupInstructions: [],
@@ -1269,7 +1808,7 @@ async function buildVaultInstructions(
1269
1808
  signers: [],
1270
1809
  addressLookupTableAddresses: [],
1271
1810
  }
1272
- const setupContext = createStrategySetupContext({
1811
+ const setupContext = sharedSetupContext ?? createStrategySetupContext({
1273
1812
  connection,
1274
1813
  env: LOCAL_ENV,
1275
1814
  owner,
@@ -1282,9 +1821,20 @@ async function buildVaultInstructions(
1282
1821
  leadingAccounts,
1283
1822
  preHookAccounts,
1284
1823
  postHookAccounts,
1824
+ autoManagePositions,
1285
1825
  })
1286
1826
 
1287
1827
  for (const ix of instructions) {
1828
+ if (isKaminoVaultInstruction(ix)) {
1829
+ await buildKaminoVaultInstruction(ix, buckets, setupContext)
1830
+ continue
1831
+ }
1832
+
1833
+ if (isKaminoFarmInstruction(ix)) {
1834
+ await buildKaminoFarmInstruction(ix, buckets, setupContext)
1835
+ continue
1836
+ }
1837
+
1288
1838
  if (isOrderbookInstruction(ix)) {
1289
1839
  await buildOrderbookInstruction(ix, owner, connection, buckets, setupContext)
1290
1840
  continue
@@ -1320,19 +1870,19 @@ async function buildVaultInstructions(
1320
1870
  await buildInitUserMetadata(owner, connection, buckets)
1321
1871
  break
1322
1872
  case VaultAction.INIT_OBLIGATION:
1323
- await buildInitObligation(ix, owner, connection, buckets)
1873
+ await buildInitObligation(ix, owner, connection, buckets, setupContext)
1324
1874
  break
1325
1875
  case VaultAction.DEPOSIT:
1326
- await buildDeposit(ix, owner, connection, signer, buckets)
1876
+ await buildDeposit(ix, owner, connection, signer, buckets, setupContext)
1327
1877
  break
1328
1878
  case VaultAction.WITHDRAW:
1329
- await buildWithdraw(ix, owner, connection, signer, buckets)
1879
+ await buildWithdraw(ix, owner, connection, signer, buckets, setupContext)
1330
1880
  break
1331
1881
  case VaultAction.BORROW:
1332
- await buildBorrow(ix, owner, connection, signer, buckets)
1882
+ await buildBorrow(ix, owner, connection, signer, buckets, setupContext)
1333
1883
  break
1334
1884
  case VaultAction.REPAY:
1335
- await buildRepay(ix, owner, connection, signer, buckets)
1885
+ await buildRepay(ix, owner, connection, signer, buckets, setupContext)
1336
1886
  break
1337
1887
  }
1338
1888
  }
@@ -1340,138 +1890,1139 @@ async function buildVaultInstructions(
1340
1890
  return buckets
1341
1891
  }
1342
1892
 
1343
- async function buildTitanInstruction(
1344
- ix: TitanSwapInstruction,
1345
- buckets: InstructionBuckets,
1346
- setupContext: StrategySetupContext,
1347
- ): Promise<void> {
1348
- const inputMint = ix.instruction.keys[TITAN_INPUT_MINT_ACCOUNT_INDEX]?.pubkey
1349
- const inputTokenAccount = ix.instruction.keys[TITAN_INPUT_TOKEN_ACCOUNT_INDEX]?.pubkey
1350
- const outputMint = ix.instruction.keys[TITAN_OUTPUT_MINT_ACCOUNT_INDEX]?.pubkey
1351
- const outputTokenAccount = ix.instruction.keys[TITAN_OUTPUT_TOKEN_ACCOUNT_INDEX]?.pubkey
1352
- const inputTokenProgram = ix.instruction.keys[TITAN_INPUT_TOKEN_PROGRAM_ACCOUNT_INDEX]?.pubkey
1353
- const outputTokenProgram = ix.instruction.keys[TITAN_OUTPUT_TOKEN_PROGRAM_ACCOUNT_INDEX]?.pubkey
1893
+ const KAMINO_VAULT_EVENT_AUTHORITY = emitEventAuthority(KAMINO_VAULT_PROGRAM_ID)
1894
+ const KAMINO_FARM_USER_STATE_SIZE = 920
1895
+ const KAMINO_STAKE_ALL_AMOUNT = new BN("18446744073709551615")
1354
1896
 
1355
- if (
1356
- !inputMint
1357
- || !inputTokenAccount
1358
- || !outputMint
1359
- || !outputTokenAccount
1360
- || !inputTokenProgram
1361
- || !outputTokenProgram
1362
- ) {
1363
- throw new Error("Titan SwapRouteV2 instruction is missing expected token accounts")
1364
- }
1897
+ type KaminoVaultIndex = Awaited<ReturnType<typeof fetchKaminoVaultIndex>>
1365
1898
 
1366
- await ensureTrackedTokenAccountSetup({
1367
- tokenMint: inputMint,
1368
- tokenAccount: inputTokenAccount,
1369
- tokenProgram: inputTokenProgram,
1370
- buckets,
1371
- setupContext,
1372
- })
1373
- await ensureTrackedTokenAccountSetup({
1374
- tokenMint: outputMint,
1375
- tokenAccount: outputTokenAccount,
1376
- tokenProgram: outputTokenProgram,
1377
- buckets,
1378
- setupContext,
1379
- })
1899
+ type KaminoVaultDecodedState = {
1900
+ tokenAvailable: BN
1901
+ sharesIssued: BN
1902
+ pendingFeesSf: BN
1903
+ allocations: Array<{
1904
+ reserve: PublicKey
1905
+ ctokenAllocation: BN
1906
+ }>
1907
+ }
1380
1908
 
1381
- buckets.syncInstructions.push(ix.instruction)
1909
+ type KaminoVaultResolvedReserve = KaminoVaultIndex["reserves"][number] & {
1910
+ account: Reserve
1911
+ investedLiquidityAmount: Decimal
1912
+ availableLiquidityToWithdraw: Decimal
1382
1913
  }
1383
1914
 
1384
- // Loopscale account indices per instruction (from the Loopscale IDL)
1385
- const LOOPSCALE_DEPOSIT_COLLATERAL_MINT_INDEX = 6
1386
- const LOOPSCALE_DEPOSIT_COLLATERAL_BORROWER_TA_INDEX = 4
1387
- const LOOPSCALE_DEPOSIT_COLLATERAL_TOKEN_PROGRAM_INDEX = 9
1388
- const LOOPSCALE_BORROW_PRINCIPAL_MINT_INDEX = 6
1389
- const LOOPSCALE_BORROW_PRINCIPAL_BORROWER_TA_INDEX = 7
1390
- const LOOPSCALE_BORROW_PRINCIPAL_TOKEN_PROGRAM_INDEX = 10
1391
- const LOOPSCALE_REPAY_PRINCIPAL_MINT_INDEX = 6
1392
- const LOOPSCALE_REPAY_PRINCIPAL_BORROWER_TA_INDEX = 7
1393
- const LOOPSCALE_REPAY_PRINCIPAL_TOKEN_PROGRAM_INDEX = 10
1394
- const LOOPSCALE_WITHDRAW_COLLATERAL_MINT_INDEX = 7
1395
- const LOOPSCALE_WITHDRAW_COLLATERAL_BORROWER_TA_INDEX = 4
1396
- const LOOPSCALE_WITHDRAW_COLLATERAL_TOKEN_PROGRAM_INDEX = 8
1397
- const LOOPSCALE_DEPOSIT_STRATEGY_MINT_INDEX = 4
1398
- const LOOPSCALE_DEPOSIT_STRATEGY_LENDER_TA_INDEX = 6
1399
- const LOOPSCALE_DEPOSIT_STRATEGY_TOKEN_PROGRAM_INDEX = 8
1400
- const LOOPSCALE_WITHDRAW_STRATEGY_MINT_INDEX = 4
1401
- const LOOPSCALE_WITHDRAW_STRATEGY_LENDER_TA_INDEX = 6
1402
- const LOOPSCALE_WITHDRAW_STRATEGY_TOKEN_PROGRAM_INDEX = 9
1915
+ type KaminoVaultContext = {
1916
+ index: Omit<KaminoVaultIndex, "reserves"> & {
1917
+ reserves: KaminoVaultResolvedReserve[]
1918
+ }
1919
+ tokenAta: PublicKey
1920
+ sharesAta: PublicKey
1921
+ sharesBalance: BN
1922
+ state: KaminoVaultDecodedState
1923
+ }
1403
1924
 
1404
- /** Build a single Loopscale instruction (loan or strategy). Extracts token accounts for tracking. */
1405
- async function buildLoopscaleInstruction(
1406
- ix: LoopscaleInstruction,
1925
+ type KaminoFarmContext = {
1926
+ farm: ReturnType<typeof decodeKaminoFarmState>
1927
+ farmState: PublicKey
1928
+ userState: PublicKey
1929
+ delegatee: PublicKey
1930
+ sourceAta: PublicKey
1931
+ scopePrices: PublicKey | null
1932
+ }
1933
+
1934
+ function toBn(value: BN | bigint | number): BN {
1935
+ if (BN.isBN(value)) {
1936
+ return value
1937
+ }
1938
+ return new BN(value.toString())
1939
+ }
1940
+
1941
+ function minBn(lhs: BN, rhs: BN): BN {
1942
+ return lhs.lte(rhs) ? lhs : rhs
1943
+ }
1944
+
1945
+ function encodeU64InstructionData(discriminator: Buffer, value: BN | bigint | number): Buffer {
1946
+ return Buffer.concat([discriminator, toBn(value).toArrayLike(Buffer, "le", 8)])
1947
+ }
1948
+
1949
+ function encodeU128InstructionData(discriminator: Buffer, value: BN | bigint | number): Buffer {
1950
+ return Buffer.concat([discriminator, toBn(value).toArrayLike(Buffer, "le", 16)])
1951
+ }
1952
+
1953
+ function getKaminoFarmUserStateAddress(delegatee: PublicKey, farmState: PublicKey): PublicKey {
1954
+ return getKaminoFarmsObligationFarm(delegatee, farmState, KAMINO_FARMS_PROGRAM_ID)
1955
+ }
1956
+
1957
+ async function accountExistsMaybeTracked(
1958
+ setupContext: StrategySetupContext | undefined,
1959
+ address: PublicKey,
1960
+ ): Promise<boolean> {
1961
+ if (!setupContext) {
1962
+ return false
1963
+ }
1964
+
1965
+ const state = await loadStrategySetupState(setupContext)
1966
+ if (state) {
1967
+ return accountExists(state, setupContext.connection, address)
1968
+ }
1969
+
1970
+ return (await setupContext.connection.getAccountInfo(address)) !== null
1971
+ }
1972
+
1973
+ function matchesKaminoVaultInterfaceAccounts(
1974
+ entry: ExponentPrice,
1975
+ interfaceAccounts: PublicKey[],
1976
+ ): boolean {
1977
+ return (
1978
+ entry.interfaceAccounts.length === interfaceAccounts.length
1979
+ && entry.interfaceAccounts.every((account, index) => account.equals(interfaceAccounts[index]!))
1980
+ )
1981
+ }
1982
+
1983
+ // ExponentPrices stores explicit wire discriminators, which can drift from the
1984
+ // generated TypeScript enum ordinals. Accept both so Kamino vault share
1985
+ // tracking works across current program/IDL combinations.
1986
+ function isKaminoVaultPriceType(priceType: number): boolean {
1987
+ return priceType === KAMINO_VAULT_PRICE_TYPE_WIRE || priceType === exponentVaults.PriceType.KaminoVault
1988
+ }
1989
+
1990
+ function resolveKaminoVaultPriceEntry(params: {
1991
+ prices: ExponentPrices
1992
+ sharesMint: PublicKey
1993
+ depositTokenMint: PublicKey
1994
+ interfaceAccounts: PublicKey[]
1995
+ }): ExponentPrice {
1996
+ const candidates = params.prices.prices.filter((entry): entry is ExponentPrice => entry !== null).filter((entry) =>
1997
+ isKaminoVaultPriceType(entry.priceType)
1998
+ && entry.priceMint.equals(params.sharesMint)
1999
+ && entry.underlyingMint.equals(params.depositTokenMint)
2000
+ && matchesKaminoVaultInterfaceAccounts(entry, params.interfaceAccounts),
2001
+ )
2002
+
2003
+ if (candidates.length === 0) {
2004
+ const interfaceAccountsLabel = params.interfaceAccounts.map((account) => account.toBase58()).join(", ")
2005
+ throw new Error(
2006
+ `Missing Exponent KaminoVault price for shares mint ${params.sharesMint.toBase58()} and deposit mint ${params.depositTokenMint.toBase58()} (interface accounts: ${interfaceAccountsLabel}). Register a PriceType.KaminoVault price for this vault before using auto-managed Kamino vault share tracking.`,
2007
+ )
2008
+ }
2009
+
2010
+ if (candidates.length > 1) {
2011
+ throw new Error(
2012
+ `Multiple Exponent KaminoVault prices matched shares mint ${params.sharesMint.toBase58()} and deposit mint ${params.depositTokenMint.toBase58()}`,
2013
+ )
2014
+ }
2015
+
2016
+ return candidates[0]!
2017
+ }
2018
+
2019
+ function resolveKaminoVaultTrackedPriceId(params: {
2020
+ state: StrategySetupState
2021
+ depositTokenMint: PublicKey
2022
+ sharePriceId: bigint
2023
+ label: string
2024
+ }): ClientPriceId {
2025
+ if (params.depositTokenMint.equals(params.state.strategyVault.state.underlyingMint)) {
2026
+ return { __kind: "Simple", priceId: params.sharePriceId }
2027
+ }
2028
+
2029
+ const reservePriceId = resolvePriceIdFromMintToUnderlyingOrThrow({
2030
+ prices: params.state.prices,
2031
+ sourceMint: params.depositTokenMint,
2032
+ targetMint: params.state.strategyVault.state.underlyingMint,
2033
+ label: params.label,
2034
+ })
2035
+ const reservePriceIds = extractPriceIds(reservePriceId).map((id) => BigInt(id))
2036
+ return { __kind: "Multiply", priceIds: [...reservePriceIds, params.sharePriceId] }
2037
+ }
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
+
2184
+ async function resolveKaminoVaultContext(
2185
+ vaultAddress: PublicKey,
2186
+ setupContext: StrategySetupContext,
2187
+ ): Promise<KaminoVaultContext> {
2188
+ const rawIndex = await fetchKaminoVaultIndex({
2189
+ connection: setupContext.connection,
2190
+ kaminoVaultAccount: vaultAddress,
2191
+ })
2192
+ const reserveAddresses = rawIndex.reserves.map((reserve) => reserve.reserveAddress)
2193
+ const [vaultInfo, tokenMintInfo, sharesMintInfo, reserveInfos] = await Promise.all([
2194
+ setupContext.connection.getAccountInfo(vaultAddress),
2195
+ setupContext.connection.getAccountInfo(rawIndex.tokenMint),
2196
+ setupContext.connection.getAccountInfo(rawIndex.sharesMint),
2197
+ setupContext.connection.getMultipleAccountsInfo(reserveAddresses),
2198
+ ])
2199
+ if (!vaultInfo?.data) {
2200
+ throw new Error(`Kamino vault account not found: ${vaultAddress.toBase58()}`)
2201
+ }
2202
+
2203
+ const decodedVaultState = decodeKaminoVaultState(Buffer.from(vaultInfo.data))
2204
+ const decodedReserves = reserveInfos.map((reserveInfo, index) => {
2205
+ if (!reserveInfo?.data) {
2206
+ throw new Error(`Missing Kamino reserve account ${reserveAddresses[index]!.toBase58()}`)
2207
+ }
2208
+ return Reserve.decode(reserveInfo.data)
2209
+ })
2210
+ const collateralMintInfos = await setupContext.connection.getMultipleAccountsInfo(
2211
+ decodedReserves.map((reserve) => reserve.collateral.mintPubkey),
2212
+ )
2213
+ const normalizedReserves = reserveAddresses.map((reserveAddress, index) => {
2214
+ const reserveAccount = decodedReserves[index]!
2215
+ const allocation = decodedVaultState.allocations[index]
2216
+ const rawReserve = rawIndex.reserves[index] as typeof rawIndex.reserves[number] & {
2217
+ reserve?: PublicKey
2218
+ marketAddress?: PublicKey
2219
+ ctokenVault?: PublicKey
2220
+ lendingMarketAuthority?: PublicKey
2221
+ pythOracle?: PublicKey
2222
+ switchboardPriceOracle?: PublicKey
2223
+ switchboardTwapOracle?: PublicKey
2224
+ scopePrices?: PublicKey
2225
+ reserveLiquiditySupply?: PublicKey
2226
+ reserveCollateralMint?: PublicKey
2227
+ reserveCollateralTokenProgram?: PublicKey
2228
+ }
2229
+ const allocationOffset =
2230
+ KAMINO_VAULT_ACCOUNT_DISCRIMINATOR_LEN
2231
+ + KAMINO_VAULT_ALLOCATION_STRATEGY_OFFSET
2232
+ + (index * KAMINO_VAULT_ALLOCATION_SIZE)
2233
+ const ctokenVaultOffset = allocationOffset + KAMINO_VAULT_ALLOCATION_CTOKEN_VAULT_OFFSET
2234
+ const [lendingMarketAuthority] = PublicKey.findProgramAddressSync(
2235
+ [Buffer.from("lma"), reserveAccount.lendingMarket.toBuffer()],
2236
+ KAMINO_LENDING_PROGRAM_ID,
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
+ )
2245
+
2246
+ return {
2247
+ reserveAddress,
2248
+ marketAddress: rawReserve.marketAddress ?? rawReserve.reserve ?? reserveAccount.lendingMarket,
2249
+ ctokenVault:
2250
+ rawReserve.ctokenVault
2251
+ ?? new PublicKey(vaultInfo.data.subarray(ctokenVaultOffset, ctokenVaultOffset + 32)),
2252
+ lendingMarketAuthority: rawReserve.lendingMarketAuthority ?? lendingMarketAuthority,
2253
+ pythOracle: rawReserve.pythOracle ?? reserveAccount.config.tokenInfo.pythConfiguration.price,
2254
+ switchboardPriceOracle:
2255
+ rawReserve.switchboardPriceOracle
2256
+ ?? reserveAccount.config.tokenInfo.switchboardConfiguration.priceAggregator,
2257
+ switchboardTwapOracle:
2258
+ rawReserve.switchboardTwapOracle
2259
+ ?? reserveAccount.config.tokenInfo.switchboardConfiguration.twapAggregator,
2260
+ scopePrices: rawReserve.scopePrices ?? reserveAccount.config.tokenInfo.scopeConfiguration.priceFeed,
2261
+ reserveLiquiditySupply: rawReserve.reserveLiquiditySupply ?? reserveAccount.liquidity.supplyVault,
2262
+ reserveCollateralMint: rawReserve.reserveCollateralMint ?? reserveAccount.collateral.mintPubkey,
2263
+ reserveCollateralTokenProgram:
2264
+ rawReserve.reserveCollateralTokenProgram ?? collateralMintInfos[index]?.owner ?? PublicKey.default,
2265
+ account: reserveAccount,
2266
+ investedLiquidityAmount,
2267
+ availableLiquidityToWithdraw,
2268
+ }
2269
+ })
2270
+ const index = {
2271
+ ...rawIndex,
2272
+ reserves: normalizedReserves,
2273
+ tokenProgram: (rawIndex as typeof rawIndex & { tokenProgram?: PublicKey }).tokenProgram ?? tokenMintInfo?.owner ?? TOKEN_PROGRAM_ID,
2274
+ sharesTokenProgram:
2275
+ (rawIndex as typeof rawIndex & { sharesTokenProgram?: PublicKey }).sharesTokenProgram
2276
+ ?? sharesMintInfo?.owner
2277
+ ?? TOKEN_PROGRAM_ID,
2278
+ vaultLookupTable:
2279
+ (rawIndex as typeof rawIndex & { vaultLookupTable?: PublicKey }).vaultLookupTable ?? PublicKey.default,
2280
+ }
2281
+ const tokenAta = getAssociatedTokenAddressSync(index.tokenMint, setupContext.owner, true, index.tokenProgram)
2282
+ const sharesAta = getAssociatedTokenAddressSync(index.sharesMint, setupContext.owner, true, index.sharesTokenProgram)
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 }
2288
+ }
2289
+
2290
+ async function queueKaminoVaultSharesTracking(params: {
2291
+ setupContext: StrategySetupContext
2292
+ buckets: InstructionBuckets
2293
+ kaminoVaultAddress: PublicKey
2294
+ vaultContext: KaminoVaultContext
2295
+ }) {
2296
+ const state = await loadStrategySetupState(params.setupContext)
2297
+ if (!state) {
2298
+ return
2299
+ }
2300
+
2301
+ const sharesMintKey = params.vaultContext.index.sharesMint.toBase58()
2302
+ const sharesAtaKey = params.vaultContext.sharesAta.toBase58()
2303
+
2304
+ if (state.trackedTokenAccounts.has(sharesAtaKey)) {
2305
+ return
2306
+ }
2307
+
2308
+ const tokenEntryAccount = state.tokenEntryAccountByMint.get(sharesMintKey)
2309
+ if (tokenEntryAccount) {
2310
+ throw new Error(
2311
+ `Kamino Vault shares mint ${sharesMintKey} is already configured as a token entry on ${tokenEntryAccount}`,
2312
+ )
2313
+ }
2314
+
2315
+ const interfaceAccounts = [
2316
+ params.kaminoVaultAddress,
2317
+ ...params.vaultContext.index.reserves.map((reserve) => reserve.reserveAddress),
2318
+ ]
2319
+ const priceEntry = resolveKaminoVaultPriceEntry({
2320
+ prices: state.prices,
2321
+ sharesMint: params.vaultContext.index.sharesMint,
2322
+ depositTokenMint: params.vaultContext.index.tokenMint,
2323
+ interfaceAccounts,
2324
+ })
2325
+ const resolvedPriceId = resolveKaminoVaultTrackedPriceId({
2326
+ state,
2327
+ depositTokenMint: params.vaultContext.index.tokenMint,
2328
+ sharePriceId: priceEntry.priceId,
2329
+ label: `Kamino Vault shares tracking (${params.kaminoVaultAddress.toBase58()})`,
2330
+ })
2331
+ const remainingAccounts = uniqueRemainingAccounts([
2332
+ { pubkey: params.vaultContext.sharesAta, isSigner: false, isWritable: false },
2333
+ { pubkey: priceEntry.priceInterfaceAccounts, isSigner: false, isWritable: false },
2334
+ ...priceEntry.interfaceAccounts.map((account) => ({
2335
+ pubkey: account,
2336
+ isSigner: false,
2337
+ isWritable: false,
2338
+ })),
2339
+ ...buildTrackedAumRemainingAccounts(state),
2340
+ ])
2341
+
2342
+ // The hook validates Kamino vault deposits against the currently tracked
2343
+ // shares ATA, so this registration must happen before the Squads sync step.
2344
+ params.buckets.preInstructions.push(
2345
+ state.strategyVault.ixWrapperManagerUpdatePosition({
2346
+ manager: params.setupContext.signer,
2347
+ update: exponentVaults.positionUpdate("TrackKaminoVaultShares", {
2348
+ sharesMint: params.vaultContext.index.sharesMint,
2349
+ depositTokenMint: params.vaultContext.index.tokenMint,
2350
+ sharesTokenAccount: params.vaultContext.sharesAta,
2351
+ priceInterfaceAccounts: priceEntry.priceInterfaceAccounts,
2352
+ }),
2353
+ remainingAccounts,
2354
+ }),
2355
+ )
2356
+
2357
+ trackRequiredPriceIds(state.requiredPriceIds, resolvedPriceId)
2358
+ if (state.tokenPositionIndexByMint.get(sharesMintKey) === undefined) {
2359
+ recordPlannedTokenAccountEntry(state, {
2360
+ tokenMint: params.vaultContext.index.sharesMint,
2361
+ tokenAccount: params.vaultContext.sharesAta,
2362
+ priceId: resolvedPriceId,
2363
+ })
2364
+ } else {
2365
+ recordPlannedTokenAccountBalance(state, {
2366
+ tokenMint: params.vaultContext.index.sharesMint,
2367
+ tokenAccount: params.vaultContext.sharesAta,
2368
+ priceId: resolvedPriceId,
2369
+ })
2370
+ }
2371
+ }
2372
+
2373
+ async function resolveKaminoVaultValidationAccounts(params: {
2374
+ setupContext: StrategySetupContext
2375
+ kaminoVaultAddress: PublicKey
2376
+ vaultContext: KaminoVaultContext
2377
+ }): Promise<AccountMeta[]> {
2378
+ const state = await loadStrategySetupState(params.setupContext)
2379
+ if (!state) {
2380
+ return []
2381
+ }
2382
+
2383
+ const priceEntry = resolveKaminoVaultPriceEntry({
2384
+ prices: state.prices,
2385
+ sharesMint: params.vaultContext.index.sharesMint,
2386
+ depositTokenMint: params.vaultContext.index.tokenMint,
2387
+ interfaceAccounts: [
2388
+ params.kaminoVaultAddress,
2389
+ ...params.vaultContext.index.reserves.map((reserve) => reserve.reserveAddress),
2390
+ ],
2391
+ })
2392
+
2393
+ return [
2394
+ { pubkey: priceEntry.priceInterfaceAccounts, isSigner: false, isWritable: false },
2395
+ ]
2396
+ }
2397
+
2398
+ function buildKaminoVaultDepositInstruction(params: {
2399
+ owner: PublicKey
2400
+ kaminoVaultAddress: PublicKey
2401
+ vaultContext: KaminoVaultContext
2402
+ amount: BN
2403
+ validationAccounts?: AccountMeta[]
2404
+ }): TransactionInstruction {
2405
+ const reserveAccounts = params.vaultContext.index.reserves.flatMap((reserve) => [
2406
+ { pubkey: reserve.reserveAddress, isSigner: false, isWritable: true },
2407
+ { pubkey: reserve.ctokenVault, isSigner: false, isWritable: true },
2408
+ { pubkey: reserve.marketAddress, isSigner: false, isWritable: false },
2409
+ { pubkey: reserve.lendingMarketAuthority, isSigner: false, isWritable: false },
2410
+ { pubkey: reserve.reserveLiquiditySupply, isSigner: false, isWritable: true },
2411
+ { pubkey: reserve.reserveCollateralMint, isSigner: false, isWritable: true },
2412
+ { pubkey: reserve.reserveCollateralTokenProgram, isSigner: false, isWritable: false },
2413
+ { pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, isSigner: false, isWritable: false },
2414
+ { pubkey: KAMINO_VAULT_EVENT_AUTHORITY, isSigner: false, isWritable: false },
2415
+ { pubkey: KAMINO_VAULT_PROGRAM_ID, isSigner: false, isWritable: false },
2416
+ ])
2417
+
2418
+ return new TransactionInstruction({
2419
+ programId: KAMINO_VAULT_PROGRAM_ID,
2420
+ keys: [
2421
+ { pubkey: params.owner, isSigner: true, isWritable: true },
2422
+ { pubkey: params.kaminoVaultAddress, isSigner: false, isWritable: true },
2423
+ { pubkey: params.vaultContext.index.tokenVault, isSigner: false, isWritable: true },
2424
+ { pubkey: params.vaultContext.index.tokenMint, isSigner: false, isWritable: false },
2425
+ { pubkey: params.vaultContext.index.baseVaultAuthority, isSigner: false, isWritable: false },
2426
+ { pubkey: params.vaultContext.index.sharesMint, isSigner: false, isWritable: true },
2427
+ { pubkey: params.vaultContext.tokenAta, isSigner: false, isWritable: true },
2428
+ { pubkey: params.vaultContext.sharesAta, isSigner: false, isWritable: true },
2429
+ { pubkey: KAMINO_LENDING_PROGRAM_ID, isSigner: false, isWritable: false },
2430
+ { pubkey: params.vaultContext.index.tokenProgram, isSigner: false, isWritable: false },
2431
+ { pubkey: params.vaultContext.index.sharesTokenProgram, isSigner: false, isWritable: false },
2432
+ { pubkey: KAMINO_VAULT_EVENT_AUTHORITY, isSigner: false, isWritable: false },
2433
+ { pubkey: KAMINO_VAULT_PROGRAM_ID, isSigner: false, isWritable: false },
2434
+ ...reserveAccounts,
2435
+ ...params.vaultContext.index.reserves.map((reserve) => ({
2436
+ pubkey: reserve.reserveAddress,
2437
+ isSigner: false,
2438
+ isWritable: true,
2439
+ })),
2440
+ ...(params.validationAccounts ?? []),
2441
+ ],
2442
+ data: encodeU64InstructionData(KAMINO_VAULT_DISCRIMINATORS.deposit, params.amount),
2443
+ })
2444
+ }
2445
+
2446
+ function resolveExplicitKaminoVaultWithdrawReserve(
2447
+ ix: KaminoVaultWithdrawInstruction,
2448
+ vaultContext: KaminoVaultContext,
2449
+ ): KaminoVaultContext["index"]["reserves"][number] {
2450
+ if (!ix.reserve) {
2451
+ throw new Error("reserve is required when resolving an explicit Kamino Vault withdraw reserve")
2452
+ }
2453
+
2454
+ const reserve = vaultContext.index.reserves.find((entry) => entry.reserveAddress.equals(ix.reserve))
2455
+ if (!reserve) {
2456
+ throw new Error(
2457
+ `Kamino Vault ${ix.vault.toBase58()} does not use reserve ${ix.reserve.toBase58()}`,
2458
+ )
2459
+ }
2460
+
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
+ })
2506
+ }
2507
+
2508
+ function buildKaminoVaultWithdrawInstruction(params: {
2509
+ owner: PublicKey
2510
+ ix: KaminoVaultWithdrawInstruction
2511
+ reserve: KaminoVaultContext["index"]["reserves"][number]
2512
+ sharesAmount: BN
2513
+ vaultContext: KaminoVaultContext
2514
+ validationAccounts?: AccountMeta[]
2515
+ }): TransactionInstruction {
2516
+ const [globalConfig] = PublicKey.findProgramAddressSync(
2517
+ [KAMINO_VAULT_GLOBAL_CONFIG_SEED],
2518
+ KAMINO_VAULT_PROGRAM_ID,
2519
+ )
2520
+ return new TransactionInstruction({
2521
+ programId: KAMINO_VAULT_PROGRAM_ID,
2522
+ keys: [
2523
+ { pubkey: params.owner, isSigner: true, isWritable: true },
2524
+ { pubkey: params.ix.vault, isSigner: false, isWritable: true },
2525
+ { pubkey: globalConfig, isSigner: false, isWritable: false },
2526
+ { pubkey: params.vaultContext.index.tokenVault, isSigner: false, isWritable: true },
2527
+ { pubkey: params.vaultContext.index.baseVaultAuthority, isSigner: false, isWritable: false },
2528
+ { pubkey: params.vaultContext.tokenAta, isSigner: false, isWritable: true },
2529
+ { pubkey: params.vaultContext.index.tokenMint, isSigner: false, isWritable: true },
2530
+ { pubkey: params.vaultContext.sharesAta, isSigner: false, isWritable: true },
2531
+ { pubkey: params.vaultContext.index.sharesMint, isSigner: false, isWritable: true },
2532
+ { pubkey: params.vaultContext.index.tokenProgram, isSigner: false, isWritable: false },
2533
+ { pubkey: params.vaultContext.index.sharesTokenProgram, isSigner: false, isWritable: false },
2534
+ { pubkey: KAMINO_LENDING_PROGRAM_ID, isSigner: false, isWritable: false },
2535
+ { pubkey: KAMINO_VAULT_EVENT_AUTHORITY, isSigner: false, isWritable: false },
2536
+ { pubkey: KAMINO_VAULT_PROGRAM_ID, isSigner: false, isWritable: false },
2537
+ { pubkey: params.ix.vault, isSigner: false, isWritable: true },
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 },
2545
+ { pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, isSigner: false, isWritable: false },
2546
+ { pubkey: KAMINO_VAULT_EVENT_AUTHORITY, isSigner: false, isWritable: false },
2547
+ { pubkey: KAMINO_VAULT_PROGRAM_ID, isSigner: false, isWritable: false },
2548
+ ...params.vaultContext.index.reserves.map((entry) => ({
2549
+ pubkey: entry.reserveAddress,
2550
+ isSigner: false,
2551
+ isWritable: true,
2552
+ })),
2553
+ ...(params.validationAccounts ?? []),
2554
+ ],
2555
+ data: encodeU64InstructionData(KAMINO_VAULT_DISCRIMINATORS.withdraw, params.sharesAmount),
2556
+ })
2557
+ }
2558
+
2559
+ async function buildKaminoVaultInstruction(
2560
+ ix: KaminoVaultInstruction,
2561
+ buckets: InstructionBuckets,
2562
+ setupContext: StrategySetupContext,
2563
+ ): Promise<void> {
2564
+ const vaultContext = await resolveKaminoVaultContext(ix.vault, setupContext)
2565
+ const validationAccounts = await resolveKaminoVaultValidationAccounts({
2566
+ setupContext,
2567
+ kaminoVaultAddress: ix.vault,
2568
+ vaultContext,
2569
+ })
2570
+
2571
+ buckets.setupInstructions.push(
2572
+ createAssociatedTokenAccountIdempotentInstruction(
2573
+ setupContext.signer,
2574
+ vaultContext.sharesAta,
2575
+ setupContext.owner,
2576
+ vaultContext.index.sharesMint,
2577
+ vaultContext.index.sharesTokenProgram,
2578
+ ),
2579
+ )
2580
+
2581
+ if (setupContext.autoManagePositions) {
2582
+ await ensureTrackedTokenAccountSetup({
2583
+ tokenMint: vaultContext.index.tokenMint,
2584
+ tokenAccount: vaultContext.tokenAta,
2585
+ tokenProgram: vaultContext.index.tokenProgram,
2586
+ buckets,
2587
+ setupContext,
2588
+ })
2589
+ }
2590
+
2591
+ const vaultLookupTable = (vaultContext.index as typeof vaultContext.index & {
2592
+ vaultLookupTable?: PublicKey
2593
+ }).vaultLookupTable ?? PublicKey.default
2594
+
2595
+ if (!vaultLookupTable.equals(PublicKey.default)) {
2596
+ buckets.addressLookupTableAddresses.push(vaultLookupTable)
2597
+ }
2598
+
2599
+ if (ix.action === KaminoVaultAction.DEPOSIT) {
2600
+ buckets.syncInstructions.push(buildKaminoVaultDepositInstruction({
2601
+ owner: setupContext.owner,
2602
+ kaminoVaultAddress: ix.vault,
2603
+ vaultContext,
2604
+ amount: ix.amount,
2605
+ validationAccounts,
2606
+ }))
2607
+
2608
+ if (isAutoManagePositionsEnabled(setupContext)) {
2609
+ await queueKaminoVaultSharesTracking({
2610
+ setupContext,
2611
+ buckets,
2612
+ kaminoVaultAddress: ix.vault,
2613
+ vaultContext,
2614
+ })
2615
+ }
2616
+ return
2617
+ }
2618
+
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
+ }
2630
+ }
2631
+
2632
+ function getOptionalReadonlyAccountMeta(account: PublicKey | null, placeholderProgram: PublicKey): AccountMeta {
2633
+ return {
2634
+ pubkey: account ?? placeholderProgram,
2635
+ isSigner: false,
2636
+ isWritable: false,
2637
+ }
2638
+ }
2639
+
2640
+ async function resolveKaminoFarmContext(
2641
+ ix: KaminoFarmInstruction,
2642
+ setupContext: StrategySetupContext,
2643
+ ): Promise<KaminoFarmContext> {
2644
+ const farmInfo = await setupContext.connection.getAccountInfo(ix.farmState)
2645
+ if (!farmInfo?.data) {
2646
+ throw new Error(`Kamino farm not found: ${ix.farmState.toBase58()}`)
2647
+ }
2648
+
2649
+ const farm = decodeKaminoFarmState(Buffer.from(farmInfo.data))
2650
+ const delegatee = ix.delegatee ?? setupContext.owner
2651
+ const userState = getKaminoFarmUserStateAddress(delegatee, ix.farmState)
2652
+ const sourceAta = getAssociatedTokenAddressSync(
2653
+ farm.underlyingMint,
2654
+ setupContext.owner,
2655
+ true,
2656
+ farm.tokenProgram,
2657
+ )
2658
+
2659
+ return {
2660
+ farm,
2661
+ farmState: ix.farmState,
2662
+ userState,
2663
+ delegatee,
2664
+ sourceAta,
2665
+ scopePrices: getKaminoFarmScopePricesAddress(farm),
2666
+ }
2667
+ }
2668
+
2669
+ function buildKaminoFarmInitializeUserRawInstruction(params: {
2670
+ owner: PublicKey
2671
+ delegatee: PublicKey
2672
+ userState: PublicKey
2673
+ farmState: PublicKey
2674
+ }): TransactionInstruction {
2675
+ return new TransactionInstruction({
2676
+ programId: KAMINO_FARMS_PROGRAM_ID,
2677
+ keys: [
2678
+ { pubkey: params.owner, isSigner: true, isWritable: true },
2679
+ { pubkey: params.owner, isSigner: true, isWritable: true },
2680
+ { pubkey: params.owner, isSigner: false, isWritable: false },
2681
+ { pubkey: params.delegatee, isSigner: false, isWritable: false },
2682
+ { pubkey: params.userState, isSigner: false, isWritable: true },
2683
+ { pubkey: params.farmState, isSigner: false, isWritable: true },
2684
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
2685
+ { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
2686
+ ],
2687
+ data: Buffer.from(KAMINO_FARM_DISCRIMINATORS.initializeUser),
2688
+ })
2689
+ }
2690
+
2691
+ async function ensureKaminoFarmUserSetup(params: {
2692
+ farmContext: KaminoFarmContext
2693
+ buckets: InstructionBuckets
2694
+ setupContext: StrategySetupContext
2695
+ }) {
2696
+ if (await accountExistsMaybeTracked(params.setupContext, params.farmContext.userState)) {
2697
+ return
2698
+ }
2699
+
2700
+ const requiredLamports = await params.setupContext.connection.getMinimumBalanceForRentExemption(
2701
+ KAMINO_FARM_USER_STATE_SIZE,
2702
+ )
2703
+ const ownerLamports = await params.setupContext.connection.getBalance(params.setupContext.owner)
2704
+ if (ownerLamports < requiredLamports) {
2705
+ params.buckets.setupInstructions.push(
2706
+ SystemProgram.transfer({
2707
+ fromPubkey: params.setupContext.signer,
2708
+ toPubkey: params.setupContext.owner,
2709
+ lamports: requiredLamports - ownerLamports,
2710
+ }),
2711
+ )
2712
+ }
2713
+
2714
+ params.buckets.syncInstructions.push(
2715
+ buildKaminoFarmInitializeUserRawInstruction({
2716
+ owner: params.setupContext.owner,
2717
+ delegatee: params.farmContext.delegatee,
2718
+ userState: params.farmContext.userState,
2719
+ farmState: params.farmContext.farmState,
2720
+ }),
2721
+ )
2722
+
2723
+ const state = await loadStrategySetupState(params.setupContext)
2724
+ state?.existingAccounts.set(params.farmContext.userState.toBase58(), true)
2725
+ }
2726
+
2727
+ function buildKaminoFarmStakeInstructionRaw(params: {
2728
+ owner: PublicKey
2729
+ farmContext: KaminoFarmContext
2730
+ amount: BN | "ALL"
2731
+ }): TransactionInstruction {
2732
+ const amount = params.amount === "ALL" ? KAMINO_STAKE_ALL_AMOUNT : params.amount
2733
+ return new TransactionInstruction({
2734
+ programId: KAMINO_FARMS_PROGRAM_ID,
2735
+ keys: [
2736
+ { pubkey: params.owner, isSigner: true, isWritable: true },
2737
+ { pubkey: params.farmContext.userState, isSigner: false, isWritable: true },
2738
+ { pubkey: params.farmContext.farmState, isSigner: false, isWritable: true },
2739
+ { pubkey: params.farmContext.farm.farmVault, isSigner: false, isWritable: true },
2740
+ { pubkey: params.farmContext.sourceAta, isSigner: false, isWritable: true },
2741
+ { pubkey: params.farmContext.farm.underlyingMint, isSigner: false, isWritable: false },
2742
+ getOptionalReadonlyAccountMeta(params.farmContext.scopePrices, KAMINO_FARMS_PROGRAM_ID),
2743
+ { pubkey: params.farmContext.farm.tokenProgram, isSigner: false, isWritable: false },
2744
+ ],
2745
+ data: encodeU64InstructionData(KAMINO_FARM_DISCRIMINATORS.stake, amount),
2746
+ })
2747
+ }
2748
+
2749
+ function buildKaminoFarmUnstakeInstructionRaw(params: {
2750
+ owner: PublicKey
2751
+ farmContext: KaminoFarmContext
2752
+ stakeSharesScaled: BN
2753
+ }): TransactionInstruction {
2754
+ return new TransactionInstruction({
2755
+ programId: KAMINO_FARMS_PROGRAM_ID,
2756
+ keys: [
2757
+ { pubkey: params.owner, isSigner: true, isWritable: true },
2758
+ { pubkey: params.farmContext.userState, isSigner: false, isWritable: true },
2759
+ { pubkey: params.farmContext.farmState, isSigner: false, isWritable: true },
2760
+ getOptionalReadonlyAccountMeta(params.farmContext.scopePrices, KAMINO_FARMS_PROGRAM_ID),
2761
+ ],
2762
+ data: encodeU128InstructionData(KAMINO_FARM_DISCRIMINATORS.unstake, params.stakeSharesScaled),
2763
+ })
2764
+ }
2765
+
2766
+ function buildKaminoFarmWithdrawUnstakedDepositsRawInstruction(params: {
2767
+ owner: PublicKey
2768
+ farmContext: KaminoFarmContext
2769
+ }): TransactionInstruction {
2770
+ return new TransactionInstruction({
2771
+ programId: KAMINO_FARMS_PROGRAM_ID,
2772
+ keys: [
2773
+ { pubkey: params.owner, isSigner: true, isWritable: true },
2774
+ { pubkey: params.farmContext.userState, isSigner: false, isWritable: true },
2775
+ { pubkey: params.farmContext.farmState, isSigner: false, isWritable: true },
2776
+ { pubkey: params.farmContext.sourceAta, isSigner: false, isWritable: true },
2777
+ { pubkey: params.farmContext.farm.farmVault, isSigner: false, isWritable: true },
2778
+ { pubkey: params.farmContext.farm.farmVaultsAuthority, isSigner: false, isWritable: false },
2779
+ { pubkey: params.farmContext.farm.tokenProgram, isSigner: false, isWritable: false },
2780
+ ],
2781
+ data: Buffer.from(KAMINO_FARM_DISCRIMINATORS.withdrawUnstakedDeposits),
2782
+ })
2783
+ }
2784
+
2785
+ function buildKaminoFarmHarvestRewardRawInstruction(params: {
2786
+ owner: PublicKey
2787
+ farmContext: KaminoFarmContext
2788
+ rewardIndex: number
2789
+ rewardAta: PublicKey
2790
+ }): TransactionInstruction {
2791
+ const rewardInfo = params.farmContext.farm.rewardInfos[params.rewardIndex]
2792
+ if (!rewardInfo) {
2793
+ throw new Error(
2794
+ `Reward index ${params.rewardIndex} is out of range for Kamino farm ${params.farmContext.farmState.toBase58()}`,
2795
+ )
2796
+ }
2797
+
2798
+ return new TransactionInstruction({
2799
+ programId: KAMINO_FARMS_PROGRAM_ID,
2800
+ keys: [
2801
+ { pubkey: params.owner, isSigner: true, isWritable: true },
2802
+ { pubkey: params.farmContext.userState, isSigner: false, isWritable: true },
2803
+ { pubkey: params.farmContext.farmState, isSigner: false, isWritable: true },
2804
+ { pubkey: params.farmContext.farm.globalConfig, isSigner: false, isWritable: false },
2805
+ { pubkey: rewardInfo.rewardMint, isSigner: false, isWritable: false },
2806
+ { pubkey: params.rewardAta, isSigner: false, isWritable: true },
2807
+ { pubkey: rewardInfo.rewardsVault, isSigner: false, isWritable: true },
2808
+ {
2809
+ pubkey: getKaminoFarmsRewardsTreasuryVault(
2810
+ rewardInfo.rewardMint,
2811
+ params.farmContext.farm.globalConfig,
2812
+ KAMINO_FARMS_PROGRAM_ID,
2813
+ ),
2814
+ isSigner: false,
2815
+ isWritable: true,
2816
+ },
2817
+ { pubkey: params.farmContext.farm.farmVaultsAuthority, isSigner: false, isWritable: false },
2818
+ getOptionalReadonlyAccountMeta(params.farmContext.scopePrices, KAMINO_FARMS_PROGRAM_ID),
2819
+ { pubkey: rewardInfo.tokenProgram, isSigner: false, isWritable: false },
2820
+ ],
2821
+ data: encodeU64InstructionData(KAMINO_FARM_DISCRIMINATORS.harvestReward, params.rewardIndex),
2822
+ })
2823
+ }
2824
+
2825
+ async function buildKaminoFarmInstruction(
2826
+ ix: KaminoFarmInstruction,
1407
2827
  buckets: InstructionBuckets,
1408
2828
  setupContext: StrategySetupContext,
1409
2829
  ): Promise<void> {
2830
+ const farmContext = await resolveKaminoFarmContext(ix, setupContext)
2831
+
1410
2832
  switch (ix.action) {
1411
- case LoopscaleAction.DEPOSIT_COLLATERAL: {
1412
- const tokenMint = ix.instruction.keys[LOOPSCALE_DEPOSIT_COLLATERAL_MINT_INDEX]?.pubkey
1413
- const tokenAccount = ix.instruction.keys[LOOPSCALE_DEPOSIT_COLLATERAL_BORROWER_TA_INDEX]?.pubkey
1414
- const tokenProgram = ix.instruction.keys[LOOPSCALE_DEPOSIT_COLLATERAL_TOKEN_PROGRAM_INDEX]?.pubkey
1415
- if (!tokenMint || !tokenAccount || !tokenProgram) {
1416
- throw new Error("Loopscale deposit_collateral instruction is missing expected token accounts")
2833
+ case KaminoFarmAction.INITIALIZE_USER: {
2834
+ if (farmContext.farm.isDelegated && !farmContext.delegatee.equals(setupContext.owner)) {
2835
+ throw new Error(`Delegated Kamino farm initialization is not supported for ${ix.farmState.toBase58()}`)
1417
2836
  }
1418
- await ensureTrackedTokenAccountSetup({ tokenMint, tokenAccount, tokenProgram, buckets, setupContext })
1419
- break
2837
+
2838
+ await ensureKaminoFarmUserSetup({ farmContext, buckets, setupContext })
2839
+ return
1420
2840
  }
1421
- case LoopscaleAction.BORROW_PRINCIPAL: {
1422
- const tokenMint = ix.instruction.keys[LOOPSCALE_BORROW_PRINCIPAL_MINT_INDEX]?.pubkey
1423
- const tokenAccount = ix.instruction.keys[LOOPSCALE_BORROW_PRINCIPAL_BORROWER_TA_INDEX]?.pubkey
1424
- const tokenProgram = ix.instruction.keys[LOOPSCALE_BORROW_PRINCIPAL_TOKEN_PROGRAM_INDEX]?.pubkey
1425
- if (!tokenMint || !tokenAccount || !tokenProgram) {
1426
- throw new Error("Loopscale borrow_principal instruction is missing expected token accounts")
2841
+ case KaminoFarmAction.STAKE: {
2842
+ if (farmContext.farm.isDelegated) {
2843
+ throw new Error(`Kamino farm ${ix.farmState.toBase58()} is delegated and cannot be staked directly`)
1427
2844
  }
1428
- await ensureTrackedTokenAccountSetup({ tokenMint, tokenAccount, tokenProgram, buckets, setupContext })
1429
- break
1430
- }
1431
- case LoopscaleAction.REPAY_PRINCIPAL: {
1432
- const tokenMint = ix.instruction.keys[LOOPSCALE_REPAY_PRINCIPAL_MINT_INDEX]?.pubkey
1433
- const tokenAccount = ix.instruction.keys[LOOPSCALE_REPAY_PRINCIPAL_BORROWER_TA_INDEX]?.pubkey
1434
- const tokenProgram = ix.instruction.keys[LOOPSCALE_REPAY_PRINCIPAL_TOKEN_PROGRAM_INDEX]?.pubkey
1435
- if (!tokenMint || !tokenAccount || !tokenProgram) {
1436
- throw new Error("Loopscale repay_principal instruction is missing expected token accounts")
2845
+ if (isAutoManagePositionsEnabled(setupContext)) {
2846
+ await ensureKaminoFarmUserSetup({ farmContext, buckets, setupContext })
2847
+ await ensureTrackedTokenAccountSetup({
2848
+ tokenMint: farmContext.farm.underlyingMint,
2849
+ tokenAccount: farmContext.sourceAta,
2850
+ tokenProgram: farmContext.farm.tokenProgram,
2851
+ buckets,
2852
+ setupContext,
2853
+ })
1437
2854
  }
1438
- await ensureTrackedTokenAccountSetup({ tokenMint, tokenAccount, tokenProgram, buckets, setupContext })
1439
- break
2855
+
2856
+ buckets.syncInstructions.push(
2857
+ buildKaminoFarmStakeInstructionRaw({
2858
+ owner: setupContext.owner,
2859
+ farmContext,
2860
+ amount: ix.amount,
2861
+ }),
2862
+ )
2863
+
2864
+ if (isAutoManagePositionsEnabled(setupContext)) {
2865
+ const state = await loadStrategySetupState(setupContext)
2866
+ if (state) {
2867
+ recordPlannedKaminoFarmPosition(state, {
2868
+ farmState: ix.farmState,
2869
+ userState: farmContext.userState,
2870
+ globalConfig: farmContext.farm.globalConfig,
2871
+ scopePrices: farmContext.scopePrices,
2872
+ })
2873
+ }
2874
+ }
2875
+ return
1440
2876
  }
1441
- case LoopscaleAction.WITHDRAW_COLLATERAL: {
1442
- const tokenMint = ix.instruction.keys[LOOPSCALE_WITHDRAW_COLLATERAL_MINT_INDEX]?.pubkey
1443
- const tokenAccount = ix.instruction.keys[LOOPSCALE_WITHDRAW_COLLATERAL_BORROWER_TA_INDEX]?.pubkey
1444
- const tokenProgram = ix.instruction.keys[LOOPSCALE_WITHDRAW_COLLATERAL_TOKEN_PROGRAM_INDEX]?.pubkey
1445
- if (!tokenMint || !tokenAccount || !tokenProgram) {
1446
- throw new Error("Loopscale withdraw_collateral instruction is missing expected token accounts")
2877
+ case KaminoFarmAction.UNSTAKE: {
2878
+ if (farmContext.farm.isDelegated) {
2879
+ throw new Error(`Kamino farm ${ix.farmState.toBase58()} is delegated and cannot be unstaked directly`)
1447
2880
  }
1448
- await ensureTrackedTokenAccountSetup({ tokenMint, tokenAccount, tokenProgram, buckets, setupContext })
1449
- break
2881
+
2882
+ buckets.syncInstructions.push(
2883
+ buildKaminoFarmUnstakeInstructionRaw({
2884
+ owner: setupContext.owner,
2885
+ farmContext,
2886
+ stakeSharesScaled: ix.stakeSharesScaled,
2887
+ }),
2888
+ )
2889
+ return
1450
2890
  }
1451
- // create_loan, close_loan, and update_weight_matrix have no token accounts to track —
1452
- // the on-chain hook handles TrackLoopscaleLoan/UntrackLoopscaleLoan mutations.
1453
- // update_weight_matrix only has 3 accounts (bs_auth, borrower, loan).
1454
- case LoopscaleAction.DEPOSIT_STRATEGY: {
1455
- const tokenMint = ix.instruction.keys[LOOPSCALE_DEPOSIT_STRATEGY_MINT_INDEX]?.pubkey
1456
- const tokenAccount = ix.instruction.keys[LOOPSCALE_DEPOSIT_STRATEGY_LENDER_TA_INDEX]?.pubkey
1457
- const tokenProgram = ix.instruction.keys[LOOPSCALE_DEPOSIT_STRATEGY_TOKEN_PROGRAM_INDEX]?.pubkey
1458
- if (!tokenMint || !tokenAccount || !tokenProgram) {
1459
- throw new Error("Loopscale deposit_strategy instruction is missing expected token accounts")
2891
+ case KaminoFarmAction.WITHDRAW_UNSTAKED_DEPOSITS: {
2892
+ if (farmContext.farm.isDelegated) {
2893
+ throw new Error(
2894
+ `Kamino farm ${ix.farmState.toBase58()} is delegated and cannot withdraw unstaked deposits directly`,
2895
+ )
1460
2896
  }
1461
- await ensureTrackedTokenAccountSetup({ tokenMint, tokenAccount, tokenProgram, buckets, setupContext })
1462
- break
2897
+
2898
+ buckets.syncInstructions.push(
2899
+ buildKaminoFarmWithdrawUnstakedDepositsRawInstruction({
2900
+ owner: setupContext.owner,
2901
+ farmContext,
2902
+ }),
2903
+ )
2904
+ return
1463
2905
  }
1464
- case LoopscaleAction.WITHDRAW_STRATEGY: {
1465
- const tokenMint = ix.instruction.keys[LOOPSCALE_WITHDRAW_STRATEGY_MINT_INDEX]?.pubkey
1466
- const tokenAccount = ix.instruction.keys[LOOPSCALE_WITHDRAW_STRATEGY_LENDER_TA_INDEX]?.pubkey
1467
- const tokenProgram = ix.instruction.keys[LOOPSCALE_WITHDRAW_STRATEGY_TOKEN_PROGRAM_INDEX]?.pubkey
1468
- if (!tokenMint || !tokenAccount || !tokenProgram) {
1469
- throw new Error("Loopscale withdraw_strategy instruction is missing expected token accounts")
2906
+ case KaminoFarmAction.HARVEST_REWARD: {
2907
+ const rewardInfo = farmContext.farm.rewardInfos[ix.rewardIndex]
2908
+ if (!rewardInfo) {
2909
+ throw new Error(`Reward index ${ix.rewardIndex} is out of range for Kamino farm ${ix.farmState.toBase58()}`)
1470
2910
  }
1471
- await ensureTrackedTokenAccountSetup({ tokenMint, tokenAccount, tokenProgram, buckets, setupContext })
1472
- break
2911
+
2912
+ const rewardAta = getAssociatedTokenAddressSync(
2913
+ rewardInfo.rewardMint,
2914
+ setupContext.owner,
2915
+ true,
2916
+ rewardInfo.tokenProgram,
2917
+ )
2918
+ if (isAutoManagePositionsEnabled(setupContext)) {
2919
+ await ensureTrackedTokenAccountSetup({
2920
+ tokenMint: rewardInfo.rewardMint,
2921
+ tokenAccount: rewardAta,
2922
+ tokenProgram: rewardInfo.tokenProgram,
2923
+ buckets,
2924
+ setupContext,
2925
+ })
2926
+ }
2927
+
2928
+ buckets.setupInstructions.push(
2929
+ createAssociatedTokenAccountIdempotentInstruction(
2930
+ setupContext.signer,
2931
+ rewardAta,
2932
+ setupContext.owner,
2933
+ rewardInfo.rewardMint,
2934
+ rewardInfo.tokenProgram,
2935
+ ),
2936
+ )
2937
+ buckets.syncInstructions.push(
2938
+ buildKaminoFarmHarvestRewardRawInstruction({
2939
+ owner: setupContext.owner,
2940
+ farmContext,
2941
+ rewardIndex: ix.rewardIndex,
2942
+ rewardAta,
2943
+ }),
2944
+ )
2945
+ return
2946
+ }
2947
+ }
2948
+ }
2949
+
2950
+
2951
+ async function buildTitanInstruction(
2952
+ ix: TitanSwapInstruction,
2953
+ buckets: InstructionBuckets,
2954
+ setupContext: StrategySetupContext,
2955
+ ): Promise<void> {
2956
+ const inputMint = ix.instruction.keys[TITAN_INPUT_MINT_ACCOUNT_INDEX]?.pubkey
2957
+ const inputTokenAccount = ix.instruction.keys[TITAN_INPUT_TOKEN_ACCOUNT_INDEX]?.pubkey
2958
+ const outputMint = ix.instruction.keys[TITAN_OUTPUT_MINT_ACCOUNT_INDEX]?.pubkey
2959
+ const outputTokenAccount = ix.instruction.keys[TITAN_OUTPUT_TOKEN_ACCOUNT_INDEX]?.pubkey
2960
+ const inputTokenProgram = ix.instruction.keys[TITAN_INPUT_TOKEN_PROGRAM_ACCOUNT_INDEX]?.pubkey
2961
+ const outputTokenProgram = ix.instruction.keys[TITAN_OUTPUT_TOKEN_PROGRAM_ACCOUNT_INDEX]?.pubkey
2962
+
2963
+ if (
2964
+ !inputMint
2965
+ || !inputTokenAccount
2966
+ || !outputMint
2967
+ || !outputTokenAccount
2968
+ || !inputTokenProgram
2969
+ || !outputTokenProgram
2970
+ ) {
2971
+ throw new Error("Titan SwapRouteV2 instruction is missing expected token accounts")
2972
+ }
2973
+
2974
+ await ensureTrackedTokenAccountSetup({
2975
+ tokenMint: inputMint,
2976
+ tokenAccount: inputTokenAccount,
2977
+ tokenProgram: inputTokenProgram,
2978
+ buckets,
2979
+ setupContext,
2980
+ })
2981
+ await ensureTrackedTokenAccountSetup({
2982
+ tokenMint: outputMint,
2983
+ tokenAccount: outputTokenAccount,
2984
+ tokenProgram: outputTokenProgram,
2985
+ buckets,
2986
+ setupContext,
2987
+ })
2988
+
2989
+ if (ix.addressLookupTableAddresses?.length) {
2990
+ buckets.addressLookupTableAddresses.push(...ix.addressLookupTableAddresses)
2991
+ }
2992
+ buckets.syncInstructions.push(ix.instruction)
2993
+ }
2994
+
2995
+ /**
2996
+ * Loopscale account index mapping per instruction (from the Loopscale IDL).
2997
+ * Actions not listed here (create_loan, close_loan, update_weight_matrix,
2998
+ * create_strategy, close_strategy, lock_loan, unlock_loan, refinance_ledger,
2999
+ * update_strategy) have no token accounts to track — the on-chain hook handles
3000
+ * TrackLoopscaleLoan/UntrackLoopscaleLoan mutations.
3001
+ */
3002
+ const LOOPSCALE_TOKEN_INDICES: Partial<Record<LoopscaleAction, { mint: number; account: number; program: number }>> = {
3003
+ [LoopscaleAction.DEPOSIT_COLLATERAL]: { mint: 6, account: 4, program: 9 },
3004
+ [LoopscaleAction.BORROW_PRINCIPAL]: { mint: 6, account: 7, program: 10 },
3005
+ [LoopscaleAction.REPAY_PRINCIPAL]: { mint: 6, account: 7, program: 10 },
3006
+ [LoopscaleAction.WITHDRAW_COLLATERAL]: { mint: 7, account: 4, program: 8 },
3007
+ [LoopscaleAction.DEPOSIT_STRATEGY]: { mint: 4, account: 6, program: 8 },
3008
+ [LoopscaleAction.WITHDRAW_STRATEGY]: { mint: 4, account: 6, program: 9 },
3009
+ }
3010
+
3011
+ /** Build a single Loopscale instruction (loan or strategy). Extracts token accounts for tracking. */
3012
+ async function buildLoopscaleInstruction(
3013
+ ix: LoopscaleInstruction,
3014
+ buckets: InstructionBuckets,
3015
+ setupContext: StrategySetupContext,
3016
+ ): Promise<void> {
3017
+ const indices = LOOPSCALE_TOKEN_INDICES[ix.action]
3018
+ if (indices) {
3019
+ const tokenMint = ix.instruction.keys[indices.mint]?.pubkey
3020
+ const tokenAccount = ix.instruction.keys[indices.account]?.pubkey
3021
+ const tokenProgram = ix.instruction.keys[indices.program]?.pubkey
3022
+ if (!tokenMint || !tokenAccount || !tokenProgram) {
3023
+ throw new Error(`Loopscale ${ix.action} instruction is missing expected token accounts`)
1473
3024
  }
1474
- // create_strategy and close_strategy have no token accounts to track
3025
+ await ensureTrackedTokenAccountSetup({ tokenMint, tokenAccount, tokenProgram, buckets, setupContext })
1475
3026
  }
1476
3027
 
1477
3028
  buckets.syncInstructions.push(ix.instruction)
@@ -1513,6 +3064,45 @@ async function buildOrderbookInstruction(
1513
3064
  // Action Builders (one per VaultAction)
1514
3065
  // ============================================================================
1515
3066
 
3067
+ function createKaminoInitUserMetadataInstruction(owner: PublicKey): TransactionInstruction {
3068
+ const userMetadata = getKaminoUserMetadata(owner, KAMINO_LENDING_PROGRAM_ID)
3069
+ return initUserMetadata(
3070
+ { userLookupTable: PublicKey.default },
3071
+ {
3072
+ owner,
3073
+ feePayer: owner,
3074
+ userMetadata,
3075
+ referrerUserMetadata: KAMINO_LENDING_PROGRAM_ID,
3076
+ rent: SYSVAR_RENT_PUBKEY,
3077
+ systemProgram: SystemProgram.programId,
3078
+ },
3079
+ )
3080
+ }
3081
+
3082
+ function createKaminoInitObligationInstruction(params: {
3083
+ market: KaminoMarket
3084
+ owner: PublicKey
3085
+ }): TransactionInstruction {
3086
+ const lendingMarket = KAMINO_MARKETS[params.market]
3087
+ const obligation = getKaminoLendObligation(params.owner, lendingMarket, KAMINO_LENDING_PROGRAM_ID)
3088
+ const userMetadata = getKaminoUserMetadata(params.owner, KAMINO_LENDING_PROGRAM_ID)
3089
+
3090
+ return initObligation(
3091
+ { args: { tag: 0, id: 0 } },
3092
+ {
3093
+ obligationOwner: params.owner,
3094
+ feePayer: params.owner,
3095
+ obligation,
3096
+ lendingMarket,
3097
+ seed1Account: SystemProgram.programId,
3098
+ seed2Account: SystemProgram.programId,
3099
+ ownerUserMetadata: userMetadata,
3100
+ rent: SYSVAR_RENT_PUBKEY,
3101
+ systemProgram: SystemProgram.programId,
3102
+ },
3103
+ )
3104
+ }
3105
+
1516
3106
  async function buildInitUserMetadata(
1517
3107
  owner: PublicKey,
1518
3108
  connection: Connection,
@@ -1522,49 +3112,317 @@ async function buildInitUserMetadata(
1522
3112
  const userMetadataAccount = await connection.getAccountInfo(userMetadata)
1523
3113
  if (userMetadataAccount) return
1524
3114
 
1525
- syncInstructions.push(
1526
- initUserMetadata(
1527
- { userLookupTable: PublicKey.default },
1528
- {
1529
- owner,
1530
- feePayer: owner,
1531
- userMetadata,
1532
- referrerUserMetadata: KAMINO_LENDING_PROGRAM_ID,
1533
- rent: SYSVAR_RENT_PUBKEY,
1534
- systemProgram: SystemProgram.programId,
1535
- },
1536
- ),
1537
- )
3115
+ syncInstructions.push(createKaminoInitUserMetadataInstruction(owner))
1538
3116
  }
1539
3117
 
1540
3118
  async function buildInitObligation(
1541
3119
  ix: MarketInstruction,
1542
3120
  owner: PublicKey,
1543
3121
  connection: Connection,
1544
- { syncInstructions }: InstructionBuckets,
3122
+ { syncInstructions, postInstructions }: InstructionBuckets,
3123
+ setupContext?: StrategySetupContext,
1545
3124
  ) {
1546
3125
  const lendingMarket = KAMINO_MARKETS[ix.market]
1547
3126
  const obligation = getKaminoLendObligation(owner, lendingMarket, KAMINO_LENDING_PROGRAM_ID)
1548
- const userMetadata = getKaminoUserMetadata(owner, KAMINO_LENDING_PROGRAM_ID)
1549
3127
  const obligationAccount = await connection.getAccountInfo(obligation)
1550
3128
  if (obligationAccount) return
1551
3129
 
1552
- syncInstructions.push(
1553
- initObligation(
1554
- { args: { tag: 0, id: 0 } },
1555
- {
1556
- obligationOwner: owner,
1557
- feePayer: owner,
1558
- obligation,
3130
+ syncInstructions.push(createKaminoInitObligationInstruction({ market: ix.market, owner }))
3131
+ if (setupContext && isAutoManagePositionsEnabled(setupContext)) {
3132
+ await queueKaminoObligationTrackingAfterInit({
3133
+ market: ix.market,
3134
+ obligation,
3135
+ postInstructions,
3136
+ setupContext,
3137
+ })
3138
+ }
3139
+ }
3140
+
3141
+ async function queueKaminoObligationTrackingAfterInit(params: {
3142
+ market: KaminoMarket
3143
+ obligation: PublicKey
3144
+ postInstructions: TransactionInstruction[]
3145
+ setupContext: StrategySetupContext
3146
+ }) {
3147
+ const state = await loadStrategySetupState(params.setupContext)
3148
+ if (!state || state.trackedKaminoObligations.has(params.obligation.toBase58())) {
3149
+ return
3150
+ }
3151
+
3152
+ const quotePath = resolveBestKaminoQuotePath({
3153
+ prices: state.prices,
3154
+ vaultUnderlyingMint: state.strategyVault.state.underlyingMint,
3155
+ })
3156
+ trackRequiredPriceIds(state.requiredPriceIds, quotePath.quotePriceId)
3157
+ const remainingAccountsAmount = 1n
3158
+ const obligationEntry: exponentVaults.KaminoObligationEntry = {
3159
+ obligation: params.obligation,
3160
+ lendingProgramId: KAMINO_LENDING_PROGRAM_ID,
3161
+ quotePriceId: quotePath.quotePriceId,
3162
+ reservePriceMappings: [],
3163
+ reserveFarmMappings: [],
3164
+ minPriceStatusFlags: 0,
3165
+ }
3166
+ params.postInstructions.push(
3167
+ state.strategyVault.ixWrapperManagerUpdatePosition({
3168
+ manager: params.setupContext.signer,
3169
+ update: exponentVaults.positionUpdate("AddKaminoObligationEntry", [obligationEntry] as [exponentVaults.KaminoObligationEntry]),
3170
+ remainingAccounts: buildTrackedAumRemainingAccounts(state, [
3171
+ { pubkey: params.obligation, isSigner: false, isWritable: false },
3172
+ ]),
3173
+ }),
3174
+ )
3175
+
3176
+ recordPlannedKaminoObligation(state, {
3177
+ obligation: params.obligation,
3178
+ quotePriceId: quotePath.quotePriceId,
3179
+ quoteInputMint: quotePath.quoteInputMint,
3180
+ reservePriceMappings: [],
3181
+ remainingAccountsAmount,
3182
+ minPriceStatusFlags: 0,
3183
+ })
3184
+ }
3185
+
3186
+ async function ensureKaminoObligationSetup(params: {
3187
+ ix: ReserveInstruction
3188
+ reserveContext: ReserveContext
3189
+ buckets: InstructionBuckets
3190
+ setupContext?: StrategySetupContext
3191
+ }) {
3192
+ const { ix, reserveContext, buckets, setupContext } = params
3193
+ if (!setupContext || !isAutoManagePositionsEnabled(setupContext)) {
3194
+ return
3195
+ }
3196
+
3197
+ const state = await loadStrategySetupState(setupContext)
3198
+ if (!state) {
3199
+ return
3200
+ }
3201
+
3202
+ // Ensure user metadata exists
3203
+ const userMetadata = getKaminoUserMetadata(setupContext.owner, KAMINO_LENDING_PROGRAM_ID)
3204
+ if (!(await accountExists(state, setupContext.connection, userMetadata))) {
3205
+ buckets.setupInstructions.push(
3206
+ await wrapVaultSignedSetupInstruction({
3207
+ instruction: createKaminoInitUserMetadataInstruction(setupContext.owner),
3208
+ setupContext,
3209
+ }),
3210
+ )
3211
+ state.existingAccounts.set(userMetadata.toBase58(), true)
3212
+ }
3213
+
3214
+ // Ensure obligation exists and is tracked
3215
+ const obligationKey = reserveContext.obligation.toBase58()
3216
+ const obligationExistsOnChain = await accountExists(state, setupContext.connection, reserveContext.obligation)
3217
+ const trackedObligation = state.trackedKaminoObligations.get(obligationKey)
3218
+
3219
+ if (!trackedObligation) {
3220
+ if (obligationExistsOnChain) {
3221
+ throw new Error(
3222
+ `Kamino obligation ${obligationKey} already exists on-chain but is not tracked on vault ${state.strategyVault.selfAddress.toBase58()}. Automatic repair is disabled; manually add the Kamino obligation entry.`,
3223
+ )
3224
+ }
3225
+
3226
+ buckets.setupInstructions.push(
3227
+ await wrapVaultSignedSetupInstruction({
3228
+ instruction: createKaminoInitObligationInstruction({ market: ix.market, owner: setupContext.owner }),
3229
+ setupContext,
3230
+ }),
3231
+ )
3232
+ state.existingAccounts.set(obligationKey, true)
3233
+
3234
+ const quotePath = resolveBestKaminoQuotePath({
3235
+ prices: state.prices,
3236
+ vaultUnderlyingMint: state.strategyVault.state.underlyingMint,
3237
+ })
3238
+ const reservePriceMapping = {
3239
+ reserve: reserveContext.reservePubkey,
3240
+ reservePriceId: resolveKaminoReservePriceIdOrThrow({
3241
+ prices: state.prices,
3242
+ reserveMint: reserveContext.reserveAccount.liquidity.mintPubkey,
3243
+ quoteInputMint: quotePath.quoteInputMint,
3244
+ }),
3245
+ }
3246
+ trackRequiredPriceIds(state.requiredPriceIds, quotePath.quotePriceId)
3247
+ trackRequiredPriceIds(state.requiredPriceIds, reservePriceMapping.reservePriceId)
3248
+ const remainingAccountsAmount = BigInt(1 + 1)
3249
+ const obligationEntry: exponentVaults.KaminoObligationEntry = {
3250
+ obligation: reserveContext.obligation,
3251
+ lendingProgramId: KAMINO_LENDING_PROGRAM_ID,
3252
+ quotePriceId: quotePath.quotePriceId,
3253
+ reservePriceMappings: [reservePriceMapping],
3254
+ reserveFarmMappings: [],
3255
+ minPriceStatusFlags: 0,
3256
+ }
3257
+
3258
+ buckets.setupInstructions.push(
3259
+ state.strategyVault.ixWrapperManagerUpdatePosition({
3260
+ manager: setupContext.signer,
3261
+ update: exponentVaults.positionUpdate("AddKaminoObligationEntry", [obligationEntry] as [exponentVaults.KaminoObligationEntry]),
3262
+ remainingAccounts: buildTrackedAumRemainingAccounts(state, [
3263
+ { pubkey: reserveContext.obligation, isSigner: false, isWritable: false },
3264
+ { pubkey: reserveContext.reservePubkey, isSigner: false, isWritable: false },
3265
+ ]),
3266
+ }),
3267
+ )
3268
+
3269
+ recordPlannedKaminoObligation(state, {
3270
+ obligation: reserveContext.obligation,
3271
+ quotePriceId: quotePath.quotePriceId,
3272
+ quoteInputMint: quotePath.quoteInputMint,
3273
+ reservePriceMappings: [reservePriceMapping],
3274
+ remainingAccountsAmount,
3275
+ minPriceStatusFlags: 0,
3276
+ })
3277
+ return
3278
+ }
3279
+
3280
+ // Obligation exists and is tracked — ensure the reserve is mapped
3281
+ await ensureKaminoReserveMapped({
3282
+ state,
3283
+ reserveContext,
3284
+ setupContext,
3285
+ buckets,
3286
+ trackedObligation,
3287
+ })
3288
+ }
3289
+
3290
+ /** Ensure a Kamino reserve is mapped on an already-tracked obligation. */
3291
+ async function ensureKaminoReserveMapped(params: {
3292
+ state: StrategySetupState
3293
+ reserveContext: ReserveContext
3294
+ setupContext: StrategySetupContext
3295
+ buckets: InstructionBuckets
3296
+ trackedObligation: TrackedKaminoObligationState
3297
+ }) {
3298
+ const { state, reserveContext, setupContext, buckets, trackedObligation } = params
3299
+
3300
+ if (trackedObligation.mappedReserves.has(reserveContext.reservePubkey.toBase58())) {
3301
+ return
3302
+ }
3303
+
3304
+ const reservePriceMapping = {
3305
+ reserve: reserveContext.reservePubkey,
3306
+ reservePriceId: resolveKaminoReservePriceIdOrThrow({
3307
+ prices: state.prices,
3308
+ reserveMint: reserveContext.reserveAccount.liquidity.mintPubkey,
3309
+ quoteInputMint: trackedObligation.quoteInputMint,
3310
+ }),
3311
+ }
3312
+ trackRequiredPriceIds(state.requiredPriceIds, reservePriceMapping.reservePriceId)
3313
+
3314
+ buckets.setupInstructions.push(...await buildKaminoPositionFreshnessInstructions({
3315
+ reservePubkey: reserveContext.reservePubkey,
3316
+ reserveAccount: reserveContext.reserveAccount,
3317
+ lendingMarket: reserveContext.lendingMarket,
3318
+ obligation: reserveContext.obligation,
3319
+ connection: setupContext.connection,
3320
+ }))
3321
+
3322
+ buckets.setupInstructions.push(
3323
+ state.strategyVault.ixWrapperManagerUpdatePosition({
3324
+ manager: setupContext.signer,
3325
+ update: exponentVaults.positionUpdate("UpsertKaminoObligationReservePriceMappings", {
3326
+ obligation: reserveContext.obligation,
3327
+ reservePriceMappings: [reservePriceMapping],
3328
+ reserveFarmMappings: [],
3329
+ }),
3330
+ remainingAccounts: buildTrackedAumRemainingAccounts(state, [
3331
+ { pubkey: reserveContext.reservePubkey, isSigner: false, isWritable: false },
3332
+ ]),
3333
+ }),
3334
+ )
3335
+
3336
+ recordPlannedKaminoReserveMappings(state, {
3337
+ obligation: reserveContext.obligation,
3338
+ quoteInputMint: trackedObligation.quoteInputMint,
3339
+ reservePriceMappings: [reservePriceMapping],
3340
+ })
3341
+ }
3342
+
3343
+ async function buildKaminoPositionFreshnessInstructions({
3344
+ reservePubkey,
3345
+ reserveAccount,
3346
+ lendingMarket,
3347
+ obligation,
3348
+ connection,
3349
+ }: {
3350
+ reservePubkey: PublicKey
3351
+ reserveAccount: Reserve
3352
+ lendingMarket: PublicKey
3353
+ obligation: PublicKey
3354
+ connection: Connection
3355
+ }): Promise<TransactionInstruction[]> {
3356
+ const instructions: TransactionInstruction[] = []
3357
+ const defaultKey = PublicKey.default
3358
+ const oracleOrSentinel = (key: PublicKey) =>
3359
+ key.equals(defaultKey) ? KAMINO_LENDING_PROGRAM_ID : key
3360
+
3361
+ const obligationState = await Obligation.fetch(connection, obligation)
3362
+ const otherReservePubkeys: PublicKey[] = []
3363
+ if (obligationState) {
3364
+ for (const deposit of obligationState.deposits) {
3365
+ if (!deposit.depositReserve.equals(defaultKey) && !deposit.depositReserve.equals(reservePubkey)) {
3366
+ otherReservePubkeys.push(deposit.depositReserve)
3367
+ }
3368
+ }
3369
+ for (const borrow of obligationState.borrows) {
3370
+ if (!borrow.borrowReserve.equals(defaultKey) && !borrow.borrowReserve.equals(reservePubkey)) {
3371
+ otherReservePubkeys.push(borrow.borrowReserve)
3372
+ }
3373
+ }
3374
+ }
3375
+
3376
+ const otherReserves: Array<{ pubkey: PublicKey; account: Reserve }> = []
3377
+ for (const otherReservePubkey of otherReservePubkeys) {
3378
+ const otherReserveAccount = await Reserve.fetch(connection, otherReservePubkey)
3379
+ if (!otherReserveAccount) {
3380
+ continue
3381
+ }
3382
+ otherReserves.push({ pubkey: otherReservePubkey, account: otherReserveAccount })
3383
+ }
3384
+
3385
+ for (const { pubkey, account } of otherReserves) {
3386
+ const tokenInfo = account.config.tokenInfo
3387
+ instructions.push(
3388
+ refreshReserve({
3389
+ reserve: pubkey,
1559
3390
  lendingMarket,
1560
- seed1Account: SystemProgram.programId,
1561
- seed2Account: SystemProgram.programId,
1562
- ownerUserMetadata: userMetadata,
1563
- rent: SYSVAR_RENT_PUBKEY,
1564
- systemProgram: SystemProgram.programId,
1565
- },
1566
- ),
3391
+ pythOracle: oracleOrSentinel(tokenInfo.pythConfiguration.price),
3392
+ switchboardPriceOracle: oracleOrSentinel(tokenInfo.switchboardConfiguration.priceAggregator),
3393
+ switchboardTwapOracle: oracleOrSentinel(tokenInfo.switchboardConfiguration.twapAggregator),
3394
+ scopePrices: oracleOrSentinel(tokenInfo.scopeConfiguration.priceFeed),
3395
+ }),
3396
+ )
3397
+ }
3398
+
3399
+ const tokenInfo = reserveAccount.config.tokenInfo
3400
+ instructions.push(
3401
+ refreshReserve({
3402
+ reserve: reservePubkey,
3403
+ lendingMarket,
3404
+ pythOracle: oracleOrSentinel(tokenInfo.pythConfiguration.price),
3405
+ switchboardPriceOracle: oracleOrSentinel(tokenInfo.switchboardConfiguration.priceAggregator),
3406
+ switchboardTwapOracle: oracleOrSentinel(tokenInfo.switchboardConfiguration.twapAggregator),
3407
+ scopePrices: oracleOrSentinel(tokenInfo.scopeConfiguration.priceFeed),
3408
+ }),
1567
3409
  )
3410
+
3411
+ const refreshObligationIx = refreshObligation({ lendingMarket, obligation })
3412
+ if (obligationState) {
3413
+ const depositReserves = obligationState.deposits
3414
+ .map((deposit) => deposit.depositReserve)
3415
+ .filter((reserve) => !reserve.equals(defaultKey))
3416
+ const borrowReserves = obligationState.borrows
3417
+ .map((borrow) => borrow.borrowReserve)
3418
+ .filter((reserve) => !reserve.equals(defaultKey))
3419
+ for (const reserve of [...depositReserves, ...borrowReserves]) {
3420
+ refreshObligationIx.keys.push({ pubkey: reserve, isSigner: false, isWritable: false })
3421
+ }
3422
+ }
3423
+ instructions.push(refreshObligationIx)
3424
+
3425
+ return instructions
1568
3426
  }
1569
3427
 
1570
3428
  async function buildDeposit(
@@ -1573,8 +3431,10 @@ async function buildDeposit(
1573
3431
  connection: Connection,
1574
3432
  signer: PublicKey,
1575
3433
  { setupInstructions, syncInstructions, preInstructions, postInstructions }: InstructionBuckets,
3434
+ setupContext?: StrategySetupContext,
1576
3435
  ) {
1577
3436
  const ctx = await resolveReserveContext(ix, owner, connection)
3437
+ await ensureKaminoObligationSetup({ ix, reserveContext: ctx, buckets: { setupInstructions, syncInstructions, preInstructions, postInstructions, signers: [], addressLookupTableAddresses: [] }, setupContext })
1578
3438
  const refreshes = await buildRefreshInstructions({
1579
3439
  ...ctx, owner, farmMode: FARM_COLLATERAL, signer, connection, needsScopeRefresh: false,
1580
3440
  })
@@ -1628,8 +3488,10 @@ async function buildWithdraw(
1628
3488
  connection: Connection,
1629
3489
  signer: PublicKey,
1630
3490
  { setupInstructions, syncInstructions, preInstructions, postInstructions }: InstructionBuckets,
3491
+ setupContext?: StrategySetupContext,
1631
3492
  ) {
1632
3493
  const ctx = await resolveReserveContext(ix, owner, connection)
3494
+ await ensureKaminoObligationSetup({ ix, reserveContext: ctx, buckets: { setupInstructions, syncInstructions, preInstructions, postInstructions, signers: [], addressLookupTableAddresses: [] }, setupContext })
1633
3495
  const refreshes = await buildRefreshInstructions({
1634
3496
  ...ctx, owner, farmMode: FARM_COLLATERAL, signer, connection, needsScopeRefresh: true,
1635
3497
  })
@@ -1690,8 +3552,10 @@ async function buildBorrow(
1690
3552
  connection: Connection,
1691
3553
  signer: PublicKey,
1692
3554
  { setupInstructions, syncInstructions, preInstructions, postInstructions }: InstructionBuckets,
3555
+ setupContext?: StrategySetupContext,
1693
3556
  ) {
1694
3557
  const ctx = await resolveReserveContext(ix, owner, connection)
3558
+ await ensureKaminoObligationSetup({ ix, reserveContext: ctx, buckets: { setupInstructions, syncInstructions, preInstructions, postInstructions, signers: [], addressLookupTableAddresses: [] }, setupContext })
1695
3559
  const refreshes = await buildRefreshInstructions({
1696
3560
  ...ctx, owner, farmMode: FARM_DEBT, signer, connection, needsScopeRefresh: true,
1697
3561
  })
@@ -1743,8 +3607,10 @@ async function buildRepay(
1743
3607
  connection: Connection,
1744
3608
  signer: PublicKey,
1745
3609
  { setupInstructions, syncInstructions, preInstructions, postInstructions }: InstructionBuckets,
3610
+ setupContext?: StrategySetupContext,
1746
3611
  ) {
1747
3612
  const ctx = await resolveReserveContext(ix, owner, connection)
3613
+ await ensureKaminoObligationSetup({ ix, reserveContext: ctx, buckets: { setupInstructions, syncInstructions, preInstructions, postInstructions, signers: [], addressLookupTableAddresses: [] }, setupContext })
1748
3614
  const refreshes = await buildRefreshInstructions({
1749
3615
  ...ctx, owner, farmMode: FARM_DEBT, signer, connection, needsScopeRefresh: false,
1750
3616
  })
@@ -2023,17 +3889,13 @@ export const orderbookAction = {
2023
3889
  /**
2024
3890
  * Post a limit order on the orderbook.
2025
3891
  * @param params - Order parameters
2026
- * @param params.offerIdx - Required offer index for position tracking. Must be unique per trader.
2027
3892
  */
2028
3893
  postOffer(params: {
2029
3894
  orderbook: PublicKey
2030
3895
  direction: OrderbookTradeDirection
2031
3896
  priceApy: number
2032
3897
  amount: bigint
2033
- /** Required offer index for position tracking. Must be unique per trader. */
2034
- offerIdx: number
2035
3898
  offerOption?: OrderbookOfferOption
2036
- virtualOffer?: boolean
2037
3899
  expirySeconds?: number
2038
3900
  mode?: OrderbookInstructionMode
2039
3901
  }): OrderbookPostOfferInstruction {
@@ -2044,9 +3906,7 @@ export const orderbookAction = {
2044
3906
  direction: params.direction,
2045
3907
  priceApy: params.priceApy,
2046
3908
  amount: params.amount,
2047
- offerIdx: params.offerIdx,
2048
3909
  offerOption: params.offerOption,
2049
- virtualOffer: params.virtualOffer,
2050
3910
  expirySeconds: params.expirySeconds,
2051
3911
  }
2052
3912
  },
@@ -2237,7 +4097,8 @@ export const coreAction = {
2237
4097
 
2238
4098
  /**
2239
4099
  * Initialize yield position for the Squads vault (owner).
2240
- * Required before buying YT or depositing YT.
4100
+ * Required before buying YT or depositing YT. When `autoManagePositions`
4101
+ * is enabled, the SDK also tracks the new yield position automatically.
2241
4102
  */
2242
4103
  initializeYieldPosition(params: { vault: PublicKey }): CoreInitializeYieldPositionInstruction {
2243
4104
  return {
@@ -2311,7 +4172,7 @@ export async function createOrderbookSyncTransaction({
2311
4172
  owner: PublicKey
2312
4173
  connection: Connection
2313
4174
  policyPda?: PublicKey
2314
- vaultPda: PublicKey
4175
+ vaultPda?: PublicKey
2315
4176
  signer: PublicKey
2316
4177
  accountIndex?: number
2317
4178
  constraintIndices?: number[]
@@ -2323,6 +4184,7 @@ export async function createOrderbookSyncTransaction({
2323
4184
  squadsProgram?: PublicKey
2324
4185
  env?: Environment
2325
4186
  }): Promise<VaultSyncTransactionResult> {
4187
+ vaultPda ??= owner
2326
4188
  const { setupInstructions, syncInstructions, preInstructions, postInstructions, signers, addressLookupTableAddresses } = await buildOrderbookInstructions(
2327
4189
  instructions,
2328
4190
  owner,
@@ -2445,6 +4307,7 @@ async function buildPostOffer(
2445
4307
  ) {
2446
4308
  const offerType = directionToOfferType(ix.direction)
2447
4309
  const option = ix.offerOption ?? "FillOrKill"
4310
+ const offerIdx = orderbook.getNextOfferIndex()
2448
4311
 
2449
4312
  if (setupContext) {
2450
4313
  await ensureOrderbookPositionSetup(orderbook, buckets, setupContext)
@@ -2480,10 +4343,10 @@ async function buildPostOffer(
2480
4343
  amount: ix.amount,
2481
4344
  offerType,
2482
4345
  offerOption: offerOptions(option, [false]),
2483
- virtualOffer: ix.virtualOffer ?? false,
4346
+ virtualOffer: true,
2484
4347
  expirySeconds: ix.expirySeconds ?? 3600,
2485
4348
  mintSy: orderbook.vault.mintSy,
2486
- offerIdx: ix.offerIdx,
4349
+ offerIdx,
2487
4350
  }),
2488
4351
  )
2489
4352
  return
@@ -2496,10 +4359,10 @@ async function buildPostOffer(
2496
4359
  amount: ix.amount,
2497
4360
  offerType,
2498
4361
  offerOption: offerOptions(option, [false]),
2499
- virtualOffer: ix.virtualOffer ?? false,
4362
+ virtualOffer: true,
2500
4363
  expirySeconds: ix.expirySeconds ?? 3600,
2501
4364
  mintSy: orderbook.vault.mintSy,
2502
- offerIdx: ix.offerIdx,
4365
+ offerIdx,
2503
4366
  })
2504
4367
 
2505
4368
  buckets.preInstructions.push(...setupIxs)
@@ -2616,6 +4479,52 @@ async function buildWithdrawFunds(
2616
4479
  // Core Instruction Builders (Strip/Merge)
2617
4480
  // ============================================================================
2618
4481
 
4482
+ async function queueYieldPositionTrackingAfterInit(params: {
4483
+ vault: Vault
4484
+ setupContext: StrategySetupContext
4485
+ postInstructions: TransactionInstruction[]
4486
+ }) {
4487
+ const state = await loadStrategySetupState(params.setupContext)
4488
+ if (!state || state.trackedYieldVaults.has(params.vault.selfAddress.toBase58())) {
4489
+ return
4490
+ }
4491
+
4492
+ const yieldPosition = params.vault.pda.yieldPosition({
4493
+ owner: params.setupContext.owner,
4494
+ vault: params.vault.selfAddress,
4495
+ })
4496
+ const priceIdPt = resolvePriceIdFromMintToUnderlyingOrThrow({
4497
+ prices: state.prices,
4498
+ sourceMint: params.vault.mintPt,
4499
+ targetMint: state.strategyVault.state.underlyingMint,
4500
+ label: `yield position setup (${params.vault.selfAddress.toBase58()})`,
4501
+ })
4502
+ trackRequiredPriceIds(state.requiredPriceIds, priceIdPt)
4503
+
4504
+ params.postInstructions.push(
4505
+ state.strategyVault.ixWrapperManageVaultSettings({
4506
+ manager: params.setupContext.signer,
4507
+ actions: [
4508
+ exponentVaults.vaultSettingsAction("AddYieldPositionEntry", {
4509
+ yieldPosition,
4510
+ vault: params.vault.selfAddress,
4511
+ priceIdPt,
4512
+ }),
4513
+ ],
4514
+ remainingAccounts: [
4515
+ { pubkey: params.vault.selfAddress, isSigner: false, isWritable: false },
4516
+ { pubkey: yieldPosition, isSigner: false, isWritable: false },
4517
+ ],
4518
+ }),
4519
+ )
4520
+
4521
+ recordPlannedYieldPosition(state, {
4522
+ yieldPosition,
4523
+ vault: params.vault.selfAddress,
4524
+ priceIdPt,
4525
+ })
4526
+ }
4527
+
2619
4528
  /** Build a single core instruction (strip/merge) */
2620
4529
  async function buildCoreInstruction(
2621
4530
  ix: CoreInstruction,
@@ -2667,7 +4576,7 @@ async function buildCoreInstruction(
2667
4576
  await buildDepositYt(ix, vault, owner, buckets)
2668
4577
  break
2669
4578
  case CoreAction.INITIALIZE_YIELD_POSITION:
2670
- await buildInitializeYieldPosition(ix, vault, owner, buckets)
4579
+ await buildInitializeYieldPosition(ix, vault, owner, buckets, setupContext)
2671
4580
  break
2672
4581
  }
2673
4582
  }
@@ -2676,10 +4585,18 @@ async function buildInitializeYieldPosition(
2676
4585
  ix: CoreInitializeYieldPositionInstruction,
2677
4586
  vault: Vault,
2678
4587
  owner: PublicKey,
2679
- { syncInstructions }: InstructionBuckets,
4588
+ { syncInstructions, postInstructions }: InstructionBuckets,
4589
+ setupContext?: StrategySetupContext,
2680
4590
  ) {
2681
4591
  const initIx = vault.ixInitializeYieldPosition({ owner })
2682
4592
  syncInstructions.push(initIx)
4593
+ if (setupContext && isAutoManagePositionsEnabled(setupContext)) {
4594
+ await queueYieldPositionTrackingAfterInit({
4595
+ vault,
4596
+ setupContext,
4597
+ postInstructions,
4598
+ })
4599
+ }
2683
4600
  }
2684
4601
 
2685
4602
  async function buildStrip(
@@ -2859,9 +4776,17 @@ export const titanAction = {
2859
4776
  /**
2860
4777
  * Wrap a Titan SwapRouteV2 instruction for vault execution.
2861
4778
  * @param params.instruction - Pre-built TransactionInstruction from Titan's router API
4779
+ * @param params.addressLookupTableAddresses - Optional ALT addresses returned by Titan for this route
2862
4780
  */
2863
- swap(params: { instruction: TransactionInstruction }): TitanSwapInstruction {
2864
- return { action: TitanAction.SWAP, instruction: params.instruction }
4781
+ swap(params: {
4782
+ instruction: TransactionInstruction
4783
+ addressLookupTableAddresses?: PublicKey[]
4784
+ }): TitanSwapInstruction {
4785
+ return {
4786
+ action: TitanAction.SWAP,
4787
+ instruction: params.instruction,
4788
+ addressLookupTableAddresses: params.addressLookupTableAddresses,
4789
+ }
2865
4790
  },
2866
4791
  }
2867
4792
 
@@ -2870,7 +4795,7 @@ export const titanAction = {
2870
4795
  // ============================================================================
2871
4796
 
2872
4797
  /**
2873
- * Builder for Loopscale vault action descriptors.
4798
+ * Builder for Loopscale action descriptors used in Exponent sync transactions.
2874
4799
  *
2875
4800
  * Loans (BORROWER side): create/close loan, deposit/withdraw collateral, borrow/repay principal.
2876
4801
  * Strategies (LENDER side): create/close strategy, deposit/withdraw into strategy.
@@ -2990,7 +4915,9 @@ export const clmmAction = {
2990
4915
  /**
2991
4916
  * Create a new LP position on the CLMM with a specified tick range.
2992
4917
  * The LP position keypair is generated internally — retrieve it from
2993
- * `result.signers[0]` after calling `createVaultSyncTransaction`.
4918
+ * `result.signers[0]` after calling `createVaultSyncTransaction`. When
4919
+ * `autoManagePositions` is enabled, the SDK also tracks the new LP
4920
+ * position automatically after the deposit sync instruction succeeds.
2994
4921
  *
2995
4922
  * @param params.market - CLMM MarketThree account address
2996
4923
  * @param params.ptInIntent - Maximum PT to deposit
@@ -3158,6 +5085,86 @@ export { SwapDirection }
3158
5085
  /** Cache for loaded MarketThree instances to avoid redundant fetches. */
3159
5086
  const marketThreeCache = new Map<string, MarketThree>()
3160
5087
 
5088
+ async function ensureTrackedClmmPosition(params: {
5089
+ lpPosition: PublicKey
5090
+ setupContext?: StrategySetupContext
5091
+ }) {
5092
+ if (!params.setupContext || !isAutoManagePositionsEnabled(params.setupContext)) {
5093
+ return
5094
+ }
5095
+
5096
+ const state = await loadStrategySetupState(params.setupContext)
5097
+ if (!state || state.trackedClmmPositions.has(params.lpPosition.toBase58())) {
5098
+ return
5099
+ }
5100
+
5101
+ const existsOnChain = await accountExists(state, params.setupContext.connection, params.lpPosition)
5102
+ if (existsOnChain) {
5103
+ throw new Error(
5104
+ `CLMM lp position ${params.lpPosition.toBase58()} already exists on-chain but is not tracked on vault ${state.strategyVault.selfAddress.toBase58()}. Automatic repair is disabled; manually add the CLMM position entry.`,
5105
+ )
5106
+ }
5107
+
5108
+ throw new Error(
5109
+ `CLMM lp position ${params.lpPosition.toBase58()} is not tracked on vault ${state.strategyVault.selfAddress.toBase58()}. Create it with clmmAction.depositLiquidity() through this SDK flow or manually add the position entry.`,
5110
+ )
5111
+ }
5112
+
5113
+ async function queueClmmPositionTrackingAfterDeposit(params: {
5114
+ market: MarketThree
5115
+ lpPosition: PublicKey
5116
+ postInstructions: TransactionInstruction[]
5117
+ setupContext?: StrategySetupContext
5118
+ }) {
5119
+ if (!params.setupContext || !isAutoManagePositionsEnabled(params.setupContext)) {
5120
+ return
5121
+ }
5122
+
5123
+ const state = await loadStrategySetupState(params.setupContext)
5124
+ if (!state || state.trackedClmmPositions.has(params.lpPosition.toBase58())) {
5125
+ return
5126
+ }
5127
+
5128
+ const priceIdPt = resolvePriceIdFromMintToUnderlyingOrThrow({
5129
+ prices: state.prices,
5130
+ sourceMint: params.market.mintPt,
5131
+ targetMint: state.strategyVault.state.underlyingMint,
5132
+ label: `CLMM PT position setup (${params.market.selfAddress.toBase58()})`,
5133
+ })
5134
+ trackRequiredPriceIds(state.requiredPriceIds, priceIdPt)
5135
+ const priceIdSy = resolvePriceIdFromMintToUnderlyingOrThrow({
5136
+ prices: state.prices,
5137
+ sourceMint: params.market.mintSy,
5138
+ targetMint: state.strategyVault.state.underlyingMint,
5139
+ label: `CLMM SY position setup (${params.market.selfAddress.toBase58()})`,
5140
+ })
5141
+ trackRequiredPriceIds(state.requiredPriceIds, priceIdSy)
5142
+
5143
+ params.postInstructions.push(
5144
+ state.strategyVault.ixWrapperManageVaultSettings({
5145
+ manager: params.setupContext.signer,
5146
+ actions: [exponentVaults.vaultSettingsAction("AddClmmPositionEntry", [{
5147
+ lpPosition: params.lpPosition,
5148
+ market: params.market.selfAddress,
5149
+ priceIdPt,
5150
+ priceIdSy,
5151
+ }])],
5152
+ remainingAccounts: [
5153
+ { pubkey: params.lpPosition, isSigner: false, isWritable: false },
5154
+ { pubkey: params.market.selfAddress, isSigner: false, isWritable: false },
5155
+ ],
5156
+ }),
5157
+ )
5158
+
5159
+ recordPlannedClmmPosition(state, {
5160
+ lpPosition: params.lpPosition,
5161
+ market: params.market.selfAddress,
5162
+ priceIdPt,
5163
+ priceIdSy,
5164
+ ticksKey: params.market.ticksKey,
5165
+ })
5166
+ }
5167
+
3161
5168
  /**
3162
5169
  * Resolve a high-level CLMM action descriptor into raw Solana instructions.
3163
5170
  * Loads the MarketThree from cache, derives token accounts from the vault
@@ -3201,12 +5208,14 @@ async function buildClmmInstruction(
3201
5208
 
3202
5209
  switch (ix.action) {
3203
5210
  case ClmmAction.DEPOSIT_LIQUIDITY:
3204
- buildClmmDepositLiquidity(ix, market, owner, buckets)
5211
+ await buildClmmDepositLiquidity(ix, market, owner, buckets, setupContext)
3205
5212
  break
3206
5213
  case ClmmAction.ADD_LIQUIDITY:
5214
+ await ensureTrackedClmmPosition({ lpPosition: ix.lpPosition, setupContext })
3207
5215
  buildClmmAddLiquidity(ix, market, owner, buckets)
3208
5216
  break
3209
5217
  case ClmmAction.WITHDRAW_LIQUIDITY:
5218
+ await ensureTrackedClmmPosition({ lpPosition: ix.lpPosition, setupContext })
3210
5219
  buildClmmWithdrawLiquidity(ix, market, owner, buckets)
3211
5220
  break
3212
5221
  case ClmmAction.TRADE_PT:
@@ -3225,6 +5234,7 @@ async function buildClmmInstruction(
3225
5234
  await buildClmmSellYt(ix, market, owner, buckets, setupContext)
3226
5235
  break
3227
5236
  case ClmmAction.CLAIM_FARM_EMISSION:
5237
+ await ensureTrackedClmmPosition({ lpPosition: ix.lpPosition, setupContext })
3228
5238
  buildClmmClaimFarmEmission(ix, market, owner, buckets)
3229
5239
  break
3230
5240
  }
@@ -3234,12 +5244,13 @@ async function buildClmmInstruction(
3234
5244
  * Create a new LP position. Generates the keypair internally and adds it
3235
5245
  * to `buckets.signers` so consumers can include it in the transaction.
3236
5246
  */
3237
- function buildClmmDepositLiquidity(
5247
+ async function buildClmmDepositLiquidity(
3238
5248
  ix: ClmmDepositLiquidityInstruction,
3239
5249
  market: MarketThree,
3240
5250
  owner: PublicKey,
3241
5251
  buckets: InstructionBuckets,
3242
- ): void {
5252
+ setupContext?: StrategySetupContext,
5253
+ ): Promise<void> {
3243
5254
  const ptSrc = getAssociatedTokenAddressSync(market.mintPt, owner, true, TOKEN_PROGRAM_ID)
3244
5255
  const sySrc = getAssociatedTokenAddressSync(market.mintSy, owner, true, TOKEN_PROGRAM_ID)
3245
5256
 
@@ -3255,6 +5266,12 @@ function buildClmmDepositLiquidity(
3255
5266
 
3256
5267
  buckets.syncInstructions.push(depositIx)
3257
5268
  buckets.signers.push(lpPositionKeypair)
5269
+ await queueClmmPositionTrackingAfterDeposit({
5270
+ market,
5271
+ lpPosition: lpPositionKeypair.publicKey,
5272
+ postInstructions: buckets.postInstructions,
5273
+ setupContext,
5274
+ })
3258
5275
  }
3259
5276
 
3260
5277
  /** Add liquidity to an existing LP position. */
@@ -3396,6 +5413,7 @@ async function buildClmmBuyYt(
3396
5413
 
3397
5414
  const { ixs, setupIxs } = market.ixBuyYt({
3398
5415
  trader: owner,
5416
+ payer: setupContext?.signer,
3399
5417
  ytOut: ix.ytOut,
3400
5418
  maxSyIn: ix.maxSyIn,
3401
5419
  lnImpliedApyLimit: ix.lnImpliedApyLimit,
@@ -3426,6 +5444,7 @@ async function buildClmmSellYt(
3426
5444
 
3427
5445
  const { ixs, setupIxs } = market.ixSellYt({
3428
5446
  trader: owner,
5447
+ payer: setupContext?.signer,
3429
5448
  ytIn: ix.ytIn,
3430
5449
  minSyOut: ix.minSyOut,
3431
5450
  lnImpliedApyLimit: ix.lnImpliedApyLimit,