@exponent-labs/exponent-sdk 0.9.0 → 0.9.1

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 (153) 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 +24 -7
  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/obligationType.d.ts +1 -1
  14. package/build/client/vaults/types/positionUpdate.d.ts +9 -0
  15. package/build/client/vaults/types/positionUpdate.js +23 -0
  16. package/build/client/vaults/types/positionUpdate.js.map +1 -1
  17. package/build/client/vaults/types/proposalAction.d.ts +54 -54
  18. package/build/client/vaults/types/proposalAction.js +0 -3
  19. package/build/client/vaults/types/proposalAction.js.map +1 -1
  20. package/build/client/vaults/types/reserveFarmMapping.d.ts +19 -0
  21. package/build/client/vaults/types/reserveFarmMapping.js +18 -0
  22. package/build/client/vaults/types/reserveFarmMapping.js.map +1 -0
  23. package/build/client/vaults/types/strategyPosition.d.ts +6 -1
  24. package/build/client/vaults/types/strategyPosition.js +5 -0
  25. package/build/client/vaults/types/strategyPosition.js.map +1 -1
  26. package/build/exponentVaults/aumCalculator.d.ts +25 -4
  27. package/build/exponentVaults/aumCalculator.js +236 -15
  28. package/build/exponentVaults/aumCalculator.js.map +1 -1
  29. package/build/exponentVaults/fetcher.d.ts +52 -0
  30. package/build/exponentVaults/fetcher.js +199 -0
  31. package/build/exponentVaults/fetcher.js.map +1 -0
  32. package/build/exponentVaults/index.d.ts +10 -9
  33. package/build/exponentVaults/index.js +25 -8
  34. package/build/exponentVaults/index.js.map +1 -1
  35. package/build/exponentVaults/kamino-farms.d.ts +144 -0
  36. package/build/exponentVaults/kamino-farms.js +396 -0
  37. package/build/exponentVaults/kamino-farms.js.map +1 -0
  38. package/build/exponentVaults/loopscale/client.d.ts +240 -0
  39. package/build/exponentVaults/loopscale/client.js +590 -0
  40. package/build/exponentVaults/loopscale/client.js.map +1 -0
  41. package/build/exponentVaults/loopscale/client.test.d.ts +1 -0
  42. package/build/exponentVaults/loopscale/client.test.js +183 -0
  43. package/build/exponentVaults/loopscale/client.test.js.map +1 -0
  44. package/build/exponentVaults/loopscale/helpers.d.ts +29 -0
  45. package/build/exponentVaults/loopscale/helpers.js +119 -0
  46. package/build/exponentVaults/loopscale/helpers.js.map +1 -0
  47. package/build/exponentVaults/loopscale/index.d.ts +3 -0
  48. package/build/exponentVaults/loopscale/index.js +12 -0
  49. package/build/exponentVaults/loopscale/index.js.map +1 -0
  50. package/build/exponentVaults/loopscale/prepared-transactions.d.ts +13 -0
  51. package/build/exponentVaults/loopscale/prepared-transactions.js +271 -0
  52. package/build/exponentVaults/loopscale/prepared-transactions.js.map +1 -0
  53. package/build/exponentVaults/loopscale/prepared-transactions.test.d.ts +1 -0
  54. package/build/exponentVaults/loopscale/prepared-transactions.test.js +400 -0
  55. package/build/exponentVaults/loopscale/prepared-transactions.test.js.map +1 -0
  56. package/build/exponentVaults/loopscale/prepared-types.d.ts +62 -0
  57. package/build/exponentVaults/loopscale/prepared-types.js +3 -0
  58. package/build/exponentVaults/loopscale/prepared-types.js.map +1 -0
  59. package/build/exponentVaults/loopscale/response-plan.d.ts +69 -0
  60. package/build/exponentVaults/loopscale/response-plan.js +141 -0
  61. package/build/exponentVaults/loopscale/response-plan.js.map +1 -0
  62. package/build/exponentVaults/loopscale/response-plan.test.d.ts +1 -0
  63. package/build/exponentVaults/loopscale/response-plan.test.js +139 -0
  64. package/build/exponentVaults/loopscale/response-plan.test.js.map +1 -0
  65. package/build/exponentVaults/loopscale/send-plan.d.ts +75 -0
  66. package/build/exponentVaults/loopscale/send-plan.js +235 -0
  67. package/build/exponentVaults/loopscale/send-plan.js.map +1 -0
  68. package/build/exponentVaults/loopscale/types.d.ts +443 -0
  69. package/build/exponentVaults/loopscale/types.js +3 -0
  70. package/build/exponentVaults/loopscale/types.js.map +1 -0
  71. package/build/exponentVaults/loopscale-client.d.ts +113 -524
  72. package/build/exponentVaults/loopscale-client.js +296 -539
  73. package/build/exponentVaults/loopscale-client.js.map +1 -1
  74. package/build/exponentVaults/loopscale-client.test.d.ts +1 -0
  75. package/build/exponentVaults/loopscale-client.test.js +162 -0
  76. package/build/exponentVaults/loopscale-client.test.js.map +1 -0
  77. package/build/exponentVaults/loopscale-client.types.d.ts +425 -0
  78. package/build/exponentVaults/loopscale-client.types.js +3 -0
  79. package/build/exponentVaults/loopscale-client.types.js.map +1 -0
  80. package/build/exponentVaults/loopscale-execution.d.ts +125 -0
  81. package/build/exponentVaults/loopscale-execution.js +341 -0
  82. package/build/exponentVaults/loopscale-execution.js.map +1 -0
  83. package/build/exponentVaults/loopscale-execution.test.d.ts +1 -0
  84. package/build/exponentVaults/loopscale-execution.test.js +139 -0
  85. package/build/exponentVaults/loopscale-execution.test.js.map +1 -0
  86. package/build/exponentVaults/loopscale-vault.d.ts +115 -0
  87. package/build/exponentVaults/loopscale-vault.js +275 -0
  88. package/build/exponentVaults/loopscale-vault.js.map +1 -0
  89. package/build/exponentVaults/loopscale-vault.test.d.ts +1 -0
  90. package/build/exponentVaults/loopscale-vault.test.js +102 -0
  91. package/build/exponentVaults/loopscale-vault.test.js.map +1 -0
  92. package/build/exponentVaults/policyBuilders.d.ts +62 -0
  93. package/build/exponentVaults/policyBuilders.js +119 -2
  94. package/build/exponentVaults/policyBuilders.js.map +1 -1
  95. package/build/exponentVaults/pricePathResolver.d.ts +45 -0
  96. package/build/exponentVaults/pricePathResolver.js +198 -0
  97. package/build/exponentVaults/pricePathResolver.js.map +1 -0
  98. package/build/exponentVaults/pricePathResolver.test.d.ts +1 -0
  99. package/build/exponentVaults/pricePathResolver.test.js +369 -0
  100. package/build/exponentVaults/pricePathResolver.test.js.map +1 -0
  101. package/build/exponentVaults/syncTransaction.js +4 -1
  102. package/build/exponentVaults/syncTransaction.js.map +1 -1
  103. package/build/exponentVaults/titan-quote.js +170 -36
  104. package/build/exponentVaults/titan-quote.js.map +1 -1
  105. package/build/exponentVaults/vault-instruction-types.d.ts +363 -0
  106. package/build/exponentVaults/vault-instruction-types.js +128 -0
  107. package/build/exponentVaults/vault-instruction-types.js.map +1 -0
  108. package/build/exponentVaults/vault-interaction.d.ts +156 -313
  109. package/build/exponentVaults/vault-interaction.js +1581 -353
  110. package/build/exponentVaults/vault-interaction.js.map +1 -1
  111. package/build/exponentVaults/vault.d.ts +51 -2
  112. package/build/exponentVaults/vault.js +324 -48
  113. package/build/exponentVaults/vault.js.map +1 -1
  114. package/build/exponentVaults/vaultTransactionBuilder.d.ts +100 -134
  115. package/build/exponentVaults/vaultTransactionBuilder.js +359 -266
  116. package/build/exponentVaults/vaultTransactionBuilder.js.map +1 -1
  117. package/build/exponentVaults/vaultTransactionBuilder.test.d.ts +1 -0
  118. package/build/exponentVaults/vaultTransactionBuilder.test.js +214 -0
  119. package/build/exponentVaults/vaultTransactionBuilder.test.js.map +1 -0
  120. package/build/marketThree.d.ts +6 -2
  121. package/build/marketThree.js +10 -8
  122. package/build/marketThree.js.map +1 -1
  123. package/package.json +32 -32
  124. package/src/client/vaults/index.ts +2 -0
  125. package/src/client/vaults/types/index.ts +2 -0
  126. package/src/client/vaults/types/kaminoFarmEntry.ts +32 -0
  127. package/src/client/vaults/types/kaminoObligationEntry.ts +6 -3
  128. package/src/client/vaults/types/positionUpdate.ts +62 -0
  129. package/src/client/vaults/types/proposalAction.ts +0 -3
  130. package/src/client/vaults/types/reserveFarmMapping.ts +35 -0
  131. package/src/client/vaults/types/strategyPosition.ts +18 -1
  132. package/src/exponentVaults/aumCalculator.ts +353 -16
  133. package/src/exponentVaults/fetcher.ts +257 -0
  134. package/src/exponentVaults/index.ts +64 -40
  135. package/src/exponentVaults/kamino-farms.ts +538 -0
  136. package/src/exponentVaults/loopscale/client.ts +808 -0
  137. package/src/exponentVaults/loopscale/helpers.ts +172 -0
  138. package/src/exponentVaults/loopscale/index.ts +57 -0
  139. package/src/exponentVaults/loopscale/prepared-transactions.ts +435 -0
  140. package/src/exponentVaults/loopscale/prepared-types.ts +73 -0
  141. package/src/exponentVaults/loopscale/types.ts +466 -0
  142. package/src/exponentVaults/policyBuilders.ts +170 -0
  143. package/src/exponentVaults/pricePathResolver.test.ts +466 -0
  144. package/src/exponentVaults/pricePathResolver.ts +273 -0
  145. package/src/exponentVaults/syncTransaction.ts +6 -1
  146. package/src/exponentVaults/titan-quote.ts +231 -45
  147. package/src/exponentVaults/vault-instruction-types.ts +493 -0
  148. package/src/exponentVaults/vault-interaction.ts +2227 -636
  149. package/src/exponentVaults/vault.ts +474 -63
  150. package/src/exponentVaults/vaultTransactionBuilder.test.ts +256 -0
  151. package/src/exponentVaults/vaultTransactionBuilder.ts +555 -413
  152. package/src/marketThree.ts +14 -6
  153. package/src/exponentVaults/loopscale-client.ts +0 -1373
@@ -1,22 +1,42 @@
1
1
  import { Reserve } from "@exponent-labs/kamino-reserve-deserializer"
2
+ import { fetchKaminoVaultIndex } from "@exponent-labs/exponent-fetcher"
2
3
  import { KAMINO_MARKETS, KAMINO_RESERVES, KaminoMarket } from "./kamino-markets"
3
4
  import Decimal from "decimal.js"
4
- import { getKaminoLendObligation, getKaminoUserMetadata, getKaminoFarmsObligationFarm } from "./../../../kamino-lend-standard/src/constants"
5
- import { KAMINO_LENDING_PROGRAM_ID } from "./policyBuilders"
5
+ import {
6
+ getKaminoFarmsObligationFarm,
7
+ getKaminoFarmsRewardsTreasuryVault,
8
+ getKaminoLendObligation,
9
+ getKaminoUserMetadata,
10
+ } from "./../../../kamino-lend-standard/src/constants"
11
+ import {
12
+ KAMINO_FARM_DISCRIMINATORS,
13
+ KAMINO_LENDING_PROGRAM_ID,
14
+ KAMINO_VAULT_DISCRIMINATORS,
15
+ KAMINO_VAULT_PROGRAM_ID,
16
+ } from "./policyBuilders"
6
17
  import { Obligation } from "@exponent-labs/klend-idl/accounts"
7
- import { bigintU256ToString } from "@exponent-labs/precise-number"
8
18
  import { Orderbook } from "../orderbook/orderbook"
9
19
  import { MarketThree } from "../marketThree"
10
- import { uniqueRemainingAccounts } from "../utils"
20
+ import { emitEventAuthority, uniqueRemainingAccounts } from "../utils"
11
21
  import { SwapDirection } from "../client/clmm"
12
22
  import * as exponentClmm from "../client/clmm"
13
23
  import * as exponentVaults from "../client/vaults"
14
24
  import { OfferType, offerOptions, amount as createAmount } from "../client/orderbook"
15
25
  import { LOCAL_ENV, Environment } from "../environment"
16
26
  import { Vault } from "../vault"
27
+ import { collectTrackedStrategyVaultPriceIds } from "./vault"
17
28
  import { YtPosition } from "../ytPosition"
18
29
  import { ExponentVault as StrategyVault } from "./vault"
19
30
  import type { ExponentPrice, ExponentPrices } from "@exponent-labs/exponent-vaults-fetcher"
31
+ import { decodeKaminoFarmState, getKaminoFarmScopePricesAddress } from "./kamino-farms"
32
+ import type { PriceId as ClientPriceId } from "../client/vaults/types/priceId"
33
+ import {
34
+ extractPriceIds,
35
+ getPriceInputMintFromPriceId,
36
+ resolveBestKaminoQuotePath,
37
+ resolveKaminoReservePriceIdOrThrow,
38
+ resolvePriceIdFromMintToUnderlyingOrThrow,
39
+ } from "./pricePathResolver"
20
40
 
21
41
  import {
22
42
  depositReserveLiquidityAndObligationCollateralV2,
@@ -47,399 +67,130 @@ import { AccountMeta, Connection, PublicKey, SystemProgram, SYSVAR_INSTRUCTIONS_
47
67
  import BN from "bn.js"
48
68
 
49
69
  const KAMINO_FARMS_PROGRAM_ID = new PublicKey("FarmsPZpWu9i7Kky8tPN37rs2TpmMrAZrC7S7vJa91Hr")
70
+ const KAMINO_VAULT_PRICE_TYPE_WIRE = 14
71
+ const KAMINO_VAULT_ACCOUNT_DISCRIMINATOR_LEN = 8
72
+ const KAMINO_VAULT_ALLOCATION_STRATEGY_OFFSET = 304
73
+ const KAMINO_VAULT_ALLOCATION_SIZE = 2160
74
+ const KAMINO_VAULT_ALLOCATION_CTOKEN_VAULT_OFFSET = 32
75
+ const KAMINO_VAULT_GLOBAL_CONFIG_SEED = Buffer.from("global_config")
50
76
 
51
77
  // ============================================================================
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
78
+ // Vault Instruction Types (re-exported from vault-instruction-types.ts)
287
79
  // ============================================================================
288
80
 
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
- }
81
+ export {
82
+ VaultAction,
83
+ KaminoVaultAction,
84
+ KaminoFarmAction,
85
+ OrderbookTradeDirection,
86
+ OrderbookAction,
87
+ CoreAction,
88
+ SyAction,
89
+ TitanAction,
90
+ LoopscaleAction,
91
+ ClmmAction,
92
+ } from "./vault-instruction-types"
93
+
94
+ export type {
95
+ MarketInstruction,
96
+ ReserveInstruction,
97
+ KaminoVaultInstruction,
98
+ KaminoVaultDepositInstruction,
99
+ KaminoVaultWithdrawInstruction,
100
+ KaminoFarmInstruction,
101
+ KaminoFarmInitializeUserInstruction,
102
+ KaminoFarmStakeInstruction,
103
+ KaminoFarmUnstakeInstruction,
104
+ KaminoFarmWithdrawUnstakedDepositsInstruction,
105
+ KaminoFarmHarvestRewardInstruction,
106
+ OrderbookOfferOption,
107
+ OrderbookInstructionMode,
108
+ OrderbookPostOfferInstruction,
109
+ OrderbookMarketOfferInstruction,
110
+ OrderbookRemoveOfferInstruction,
111
+ OrderbookWithdrawFundsInstruction,
112
+ OrderbookInstruction,
113
+ CoreStripInstruction,
114
+ CoreMergeInstruction,
115
+ CoreWithdrawYtInstruction,
116
+ CoreDepositYtInstruction,
117
+ CoreInitializeYieldPositionInstruction,
118
+ CoreInstruction,
119
+ SyMintInstruction,
120
+ SyRedeemInstruction,
121
+ SyInstruction,
122
+ TitanSwapInstruction,
123
+ LoopscaleInstruction,
124
+ ClmmDepositLiquidityInstruction,
125
+ ClmmAddLiquidityInstruction,
126
+ ClmmWithdrawLiquidityInstruction,
127
+ ClmmTradePtInstruction,
128
+ ClmmBuyPtInstruction,
129
+ ClmmSellPtInstruction,
130
+ ClmmBuyYtInstruction,
131
+ ClmmSellYtInstruction,
132
+ ClmmClaimFarmEmissionInstruction,
133
+ ClmmInstruction,
134
+ VaultInstruction,
135
+ KaminoReserves,
136
+ } from "./vault-instruction-types"
137
+
138
+ import type {
139
+ MarketInstruction,
140
+ ReserveInstruction,
141
+ KaminoVaultInstruction,
142
+ KaminoVaultDepositInstruction,
143
+ KaminoVaultWithdrawInstruction,
144
+ KaminoFarmInstruction,
145
+ KaminoFarmInitializeUserInstruction,
146
+ KaminoFarmStakeInstruction,
147
+ KaminoFarmUnstakeInstruction,
148
+ KaminoFarmWithdrawUnstakedDepositsInstruction,
149
+ KaminoFarmHarvestRewardInstruction,
150
+ OrderbookOfferOption,
151
+ OrderbookInstructionMode,
152
+ OrderbookInstruction,
153
+ OrderbookPostOfferInstruction,
154
+ OrderbookMarketOfferInstruction,
155
+ OrderbookRemoveOfferInstruction,
156
+ OrderbookWithdrawFundsInstruction,
157
+ CoreInstruction,
158
+ CoreStripInstruction,
159
+ CoreMergeInstruction,
160
+ CoreWithdrawYtInstruction,
161
+ CoreDepositYtInstruction,
162
+ CoreInitializeYieldPositionInstruction,
163
+ SyInstruction,
164
+ TitanSwapInstruction,
165
+ LoopscaleInstruction,
166
+ ClmmInstruction,
167
+ ClmmDepositLiquidityInstruction,
168
+ ClmmAddLiquidityInstruction,
169
+ ClmmWithdrawLiquidityInstruction,
170
+ ClmmTradePtInstruction,
171
+ ClmmBuyPtInstruction,
172
+ ClmmSellPtInstruction,
173
+ ClmmBuyYtInstruction,
174
+ ClmmSellYtInstruction,
175
+ ClmmClaimFarmEmissionInstruction,
176
+ VaultInstruction,
177
+ KaminoReserves,
178
+ SyMintInstruction,
179
+ SyRedeemInstruction,
180
+ } from "./vault-instruction-types"
341
181
 
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
182
+ import {
183
+ VaultAction,
184
+ KaminoVaultAction,
185
+ KaminoFarmAction,
186
+ OrderbookAction,
187
+ CoreAction,
188
+ SyAction,
189
+ TitanAction,
190
+ LoopscaleAction,
191
+ ClmmAction,
192
+ OrderbookTradeDirection,
193
+ } from "./vault-instruction-types"
443
194
 
444
195
  // ============================================================================
445
196
  // Kamino Action Builders
@@ -483,7 +234,8 @@ export const kaminoAction = {
483
234
 
484
235
  /**
485
236
  * Initialize a Kamino obligation for a market.
486
- * No-ops if the account already exists on-chain.
237
+ * When `autoManagePositions` is enabled, the SDK also registers the new
238
+ * obligation as a tracked strategy position after the init succeeds.
487
239
  * @param market - The Kamino lending market
488
240
  */
489
241
  initObligation(market: KaminoMarket): MarketInstruction {
@@ -532,6 +284,92 @@ export const kaminoAction = {
532
284
  },
533
285
  }
534
286
 
287
+ /**
288
+ * Builder for direct Kamino Vault action descriptors.
289
+ *
290
+ * These actions move vault-owned tokens into a Kamino Vault and, when
291
+ * `autoManagePositions` is enabled, automatically track the resulting share
292
+ * token account as a managed strategy position.
293
+ */
294
+ export const kaminoVaultAction = {
295
+ /**
296
+ * Deposit the vault-owned token account into a Kamino Vault.
297
+ * @param params.vault - Kamino Vault address
298
+ * @param params.amount - Amount of deposit tokens to move into the vault
299
+ */
300
+ deposit(params: {
301
+ vault: PublicKey
302
+ amount: BN
303
+ }): KaminoVaultDepositInstruction {
304
+ return { action: KaminoVaultAction.DEPOSIT, ...params }
305
+ },
306
+
307
+ /**
308
+ * Withdraw Kamino Vault shares back into the vault-owned token account.
309
+ * When the Kamino Vault currently allocates across multiple reserves,
310
+ * specify the reserve to disinvest from.
311
+ */
312
+ withdraw(params: {
313
+ vault: PublicKey
314
+ sharesAmount: BN
315
+ reserve?: PublicKey
316
+ }): KaminoVaultWithdrawInstruction {
317
+ return { action: KaminoVaultAction.WITHDRAW, ...params }
318
+ },
319
+ }
320
+
321
+ /**
322
+ * Builder for direct Kamino Farm action descriptors.
323
+ *
324
+ * These actions operate on a farm `user_state` derived from the managed vault
325
+ * owner by default. Pass `delegatee` when targeting a delegated farm user
326
+ * state, such as a Kamino obligation-owned farm entry.
327
+ */
328
+ export const kaminoFarmAction = {
329
+ /** Initialize the farm `user_state` PDA. */
330
+ initializeUser(params: {
331
+ farmState: PublicKey
332
+ delegatee?: PublicKey
333
+ }): KaminoFarmInitializeUserInstruction {
334
+ return { action: KaminoFarmAction.INITIALIZE_USER, ...params }
335
+ },
336
+
337
+ /** Stake the managed vault's token ATA into the farm. */
338
+ stake(params: {
339
+ farmState: PublicKey
340
+ amount: BN | "ALL"
341
+ delegatee?: PublicKey
342
+ }): KaminoFarmStakeInstruction {
343
+ return { action: KaminoFarmAction.STAKE, ...params }
344
+ },
345
+
346
+ /** Unstake a scaled share amount from the farm. */
347
+ unstake(params: {
348
+ farmState: PublicKey
349
+ stakeSharesScaled: BN
350
+ delegatee?: PublicKey
351
+ }): KaminoFarmUnstakeInstruction {
352
+ return { action: KaminoFarmAction.UNSTAKE, ...params }
353
+ },
354
+
355
+ /** Withdraw matured unstaked deposits back into the managed vault ATA. */
356
+ withdrawUnstakedDeposits(params: {
357
+ farmState: PublicKey
358
+ delegatee?: PublicKey
359
+ }): KaminoFarmWithdrawUnstakedDepositsInstruction {
360
+ return { action: KaminoFarmAction.WITHDRAW_UNSTAKED_DEPOSITS, ...params }
361
+ },
362
+
363
+ /** Harvest a specific reward index into a managed vault reward ATA. */
364
+ harvestReward(params: {
365
+ farmState: PublicKey
366
+ rewardIndex: number
367
+ delegatee?: PublicKey
368
+ }): KaminoFarmHarvestRewardInstruction {
369
+ return { action: KaminoFarmAction.HARVEST_REWARD, ...params }
370
+ },
371
+ }
372
+
535
373
  // ============================================================================
536
374
  // Sync Transaction Builder
537
375
  // ============================================================================
@@ -610,12 +448,14 @@ export async function createVaultSyncTransaction({
610
448
  preHookAccounts,
611
449
  postHookAccounts,
612
450
  squadsProgram = SQUADS_PROGRAM_ID,
451
+ autoManagePositions = false,
452
+ setupContext,
613
453
  }: {
614
454
  instructions: VaultInstruction[]
615
455
  owner: PublicKey
616
456
  connection: Connection
617
457
  policyPda?: PublicKey
618
- vaultPda: PublicKey
458
+ vaultPda?: PublicKey
619
459
  signer: PublicKey
620
460
  accountIndex?: number
621
461
  constraintIndices?: number[]
@@ -625,7 +465,27 @@ export async function createVaultSyncTransaction({
625
465
  preHookAccounts?: PublicKey[] | AccountMeta[]
626
466
  postHookAccounts?: PublicKey[] | AccountMeta[]
627
467
  squadsProgram?: PublicKey
468
+ /** Automatically manage new Kamino/CLMM/yield position tracking for manager flows. Defaults to `false` for this low-level helper. */
469
+ autoManagePositions?: boolean
470
+ /** Optional shared setup context — when provided, setup state (tracked accounts, positions) is shared across calls, preventing duplicate setup instructions. */
471
+ setupContext?: StrategySetupContext
628
472
  }): Promise<VaultSyncTransactionResult> {
473
+ vaultPda ??= owner
474
+ const resolvedSetupContext = setupContext ?? createStrategySetupContext({
475
+ connection,
476
+ env: LOCAL_ENV,
477
+ owner,
478
+ signer,
479
+ vaultAddress,
480
+ policyPda,
481
+ vaultPda,
482
+ accountIndex,
483
+ squadsProgram,
484
+ leadingAccounts,
485
+ preHookAccounts,
486
+ postHookAccounts,
487
+ autoManagePositions,
488
+ })
629
489
  const { setupInstructions, syncInstructions, preInstructions, postInstructions, signers, addressLookupTableAddresses } = await buildVaultInstructions(
630
490
  instructions,
631
491
  owner,
@@ -639,7 +499,12 @@ export async function createVaultSyncTransaction({
639
499
  preHookAccounts,
640
500
  postHookAccounts,
641
501
  squadsProgram,
502
+ autoManagePositions,
503
+ resolvedSetupContext,
642
504
  )
505
+ const setupStatePriceRefreshInstructions = setupContext
506
+ ? []
507
+ : await buildSetupStatePriceRefreshInstructions(resolvedSetupContext)
643
508
 
644
509
  let resolvedPolicyPda = policyPda
645
510
  let resolvedConstraintIndices = constraintIndices
@@ -683,7 +548,14 @@ export async function createVaultSyncTransaction({
683
548
  postHookAccounts: resolvedPostHookAccounts,
684
549
  })
685
550
 
686
- return { setupInstructions, preInstructions, instruction, postInstructions, signers, addressLookupTableAddresses }
551
+ return {
552
+ setupInstructions,
553
+ preInstructions: [...setupStatePriceRefreshInstructions, ...preInstructions],
554
+ instruction,
555
+ postInstructions,
556
+ signers,
557
+ addressLookupTableAddresses,
558
+ }
687
559
  }
688
560
 
689
561
  // ============================================================================
@@ -704,6 +576,14 @@ interface InstructionBuckets {
704
576
  addressLookupTableAddresses: PublicKey[]
705
577
  }
706
578
 
579
+ function isKaminoVaultInstruction(ix: VaultInstruction): ix is KaminoVaultInstruction {
580
+ return Object.values(KaminoVaultAction).includes(ix.action as KaminoVaultAction)
581
+ }
582
+
583
+ function isKaminoFarmInstruction(ix: VaultInstruction): ix is KaminoFarmInstruction {
584
+ return Object.values(KaminoFarmAction).includes(ix.action as KaminoFarmAction)
585
+ }
586
+
707
587
  function isOrderbookInstruction(ix: VaultInstruction): ix is OrderbookInstruction {
708
588
  return Object.values(OrderbookAction).includes(ix.action as OrderbookAction)
709
589
  }
@@ -740,24 +620,37 @@ const TITAN_OUTPUT_TOKEN_PROGRAM_ACCOUNT_INDEX = 7
740
620
  const YIELD_POSITION_BASE_SIZE = 124
741
621
  const YIELD_POSITION_TRACKER_SIZE = 40
742
622
 
743
- type SimplePriceLookup = {
744
- priceId: bigint
745
- price: Decimal
623
+ type TrackedKaminoObligationState = {
624
+ quotePriceId: ClientPriceId
625
+ quoteInputMint: PublicKey
626
+ mappedReserves: Set<string>
746
627
  }
747
628
 
748
629
  type StrategySetupState = {
749
630
  strategyVault: StrategyVault
750
631
  prices: ExponentPrices
632
+ requiredPriceIds: Set<number>
751
633
  nextStrategyPositionIndex: number
752
634
  trackedOrderbooks: Set<string>
753
635
  trackedYieldVaults: Set<string>
636
+ trackedKaminoObligations: Map<string, TrackedKaminoObligationState>
637
+ trackedKaminoFarms: Set<string>
638
+ trackedClmmPositions: Set<string>
754
639
  tokenEntryAccountByMint: Map<string, string>
755
640
  tokenPositionIndexByMint: Map<string, number>
756
641
  trackedTokenAccounts: Set<string>
642
+ baseAumAccounts: AccountMeta[]
643
+ plannedAumAccounts: AccountMeta[]
757
644
  existingAccounts: Map<string, boolean>
758
645
  }
759
646
 
760
- type StrategySetupContext = {
647
+ type ResolvedHookAccounts = {
648
+ leadingAccounts: PublicKey[] | AccountMeta[]
649
+ preHookAccounts: PublicKey[] | AccountMeta[]
650
+ postHookAccounts: PublicKey[] | AccountMeta[]
651
+ }
652
+
653
+ export type StrategySetupContext = {
761
654
  connection: Connection
762
655
  env: Environment
763
656
  owner: PublicKey
@@ -770,10 +663,14 @@ type StrategySetupContext = {
770
663
  leadingAccounts?: PublicKey[] | AccountMeta[]
771
664
  preHookAccounts?: PublicKey[] | AccountMeta[]
772
665
  postHookAccounts?: PublicKey[] | AccountMeta[]
666
+ autoManagePositions: boolean
667
+ pricesAccount?: ExponentPrices
773
668
  statePromise?: Promise<StrategySetupState | null>
669
+ /** Cached hook resolution promise — avoids redundant RPC calls when wrapping multiple setup instructions. */
670
+ resolvedHooksPromise?: Promise<ResolvedHookAccounts>
774
671
  }
775
672
 
776
- function createStrategySetupContext({
673
+ export function createStrategySetupContext({
777
674
  connection,
778
675
  env,
779
676
  owner,
@@ -786,6 +683,8 @@ function createStrategySetupContext({
786
683
  leadingAccounts,
787
684
  preHookAccounts,
788
685
  postHookAccounts,
686
+ autoManagePositions = true,
687
+ pricesAccount,
789
688
  }: {
790
689
  connection: Connection
791
690
  env: Environment
@@ -799,6 +698,8 @@ function createStrategySetupContext({
799
698
  leadingAccounts?: PublicKey[] | AccountMeta[]
800
699
  preHookAccounts?: PublicKey[] | AccountMeta[]
801
700
  postHookAccounts?: PublicKey[] | AccountMeta[]
701
+ autoManagePositions?: boolean
702
+ pricesAccount?: ExponentPrices
802
703
  }): StrategySetupContext {
803
704
  return {
804
705
  connection,
@@ -813,6 +714,17 @@ function createStrategySetupContext({
813
714
  leadingAccounts,
814
715
  preHookAccounts,
815
716
  postHookAccounts,
717
+ autoManagePositions,
718
+ pricesAccount,
719
+ }
720
+ }
721
+
722
+ function trackRequiredPriceIds(requiredPriceIds: Set<number>, priceIdValue: unknown) {
723
+ for (const id of extractPriceIds(priceIdValue)) {
724
+ const numericId = Number(id)
725
+ if (numericId !== 0) {
726
+ requiredPriceIds.add(numericId)
727
+ }
816
728
  }
817
729
  }
818
730
 
@@ -828,10 +740,13 @@ async function loadStrategySetupState(context: StrategySetupContext): Promise<St
828
740
  connection: context.connection,
829
741
  address: context.vaultAddress!,
830
742
  })
831
- const prices = await strategyVault.fetcher.fetchExponentPrices()
743
+ const prices = context.pricesAccount ?? await strategyVault.fetcher.fetchExponentPrices()
832
744
 
833
745
  const trackedOrderbooks = new Set<string>()
834
746
  const trackedYieldVaults = new Set<string>()
747
+ const trackedKaminoObligations = new Map<string, TrackedKaminoObligationState>()
748
+ const trackedKaminoFarms = new Set<string>()
749
+ const trackedClmmPositions = new Set<string>()
835
750
  const tokenEntryAccountByMint = new Map<string, string>()
836
751
  const tokenPositionIndexByMint = new Map<string, number>()
837
752
  const trackedTokenAccounts = new Set<string>()
@@ -850,6 +765,27 @@ async function loadStrategySetupState(context: StrategySetupContext): Promise<St
850
765
  trackedYieldVaults.add(position.yieldPosition[0].vault.toBase58())
851
766
  continue
852
767
  }
768
+ const kaminoEntry = getTrackedKaminoObligationFromPosition(position)
769
+ if (kaminoEntry) {
770
+ trackedKaminoObligations.set(kaminoEntry.obligation.toBase58(), {
771
+ quotePriceId: kaminoEntry.quotePriceId,
772
+ quoteInputMint: getPriceInputMintFromPriceId(prices, kaminoEntry.quotePriceId),
773
+ mappedReserves: new Set(
774
+ (kaminoEntry.reservePriceMappings ?? []).map((mapping) => mapping.reserve.toBase58()),
775
+ ),
776
+ })
777
+ continue
778
+ }
779
+ const kaminoFarmEntry = getTrackedKaminoFarmFromPosition(position)
780
+ if (kaminoFarmEntry) {
781
+ trackedKaminoFarms.add(kaminoFarmKey(kaminoFarmEntry.farmState, kaminoFarmEntry.userState))
782
+ continue
783
+ }
784
+ const clmmEntry = getTrackedClmmPositionFromPosition(position)
785
+ if (clmmEntry) {
786
+ trackedClmmPositions.add(clmmEntry.lpPosition.toBase58())
787
+ continue
788
+ }
853
789
  if ("tokenAccount" in position && position.tokenAccount?.[0]) {
854
790
  const entry = position.tokenAccount[0]
855
791
  tokenPositionIndexByMint.set(entry.tokenMint.toBase58(), index)
@@ -862,12 +798,21 @@ async function loadStrategySetupState(context: StrategySetupContext): Promise<St
862
798
  return {
863
799
  strategyVault,
864
800
  prices,
801
+ requiredPriceIds: collectTrackedStrategyVaultPriceIds({
802
+ tokenEntries: strategyVault.state.tokenEntries,
803
+ strategyPositions: strategyVault.state.strategyPositions as Array<Record<string, unknown>>,
804
+ }),
865
805
  nextStrategyPositionIndex: strategyVault.state.strategyPositions.length,
866
806
  trackedOrderbooks,
867
807
  trackedYieldVaults,
808
+ trackedKaminoObligations,
809
+ trackedKaminoFarms,
810
+ trackedClmmPositions,
868
811
  tokenEntryAccountByMint,
869
812
  tokenPositionIndexByMint,
870
813
  trackedTokenAccounts,
814
+ baseAumAccounts: mutableStrategyVault(strategyVault).aumRemainingAccounts(),
815
+ plannedAumAccounts: [],
871
816
  existingAccounts: new Map<string, boolean>(),
872
817
  }
873
818
  })()
@@ -876,32 +821,176 @@ async function loadStrategySetupState(context: StrategySetupContext): Promise<St
876
821
  return context.statePromise
877
822
  }
878
823
 
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)) {
824
+ export async function buildSetupStatePriceRefreshInstructions(
825
+ setupContext: StrategySetupContext,
826
+ ): Promise<TransactionInstruction[]> {
827
+ const state = await loadStrategySetupState(setupContext)
828
+ if (!state) {
829
+ return []
830
+ }
831
+
832
+ const refreshInstructions: TransactionInstruction[] = []
833
+ const reserveAccounts = new Map<string, { account: Reserve }>()
834
+ for (const priceId of state.requiredPriceIds) {
835
+ const priceEntry = state.prices.prices[priceId]
836
+ if (!priceEntry || !isKaminoVaultPriceType(priceEntry.priceType)) {
887
837
  continue
888
838
  }
889
839
 
890
- return {
891
- priceId: entry.priceId,
892
- price: exponentPriceToDecimal(entry),
840
+ for (const reserveAddress of priceEntry.interfaceAccounts.slice(1)) {
841
+ const reserveKey = reserveAddress.toBase58()
842
+ if (reserveAccounts.has(reserveKey)) {
843
+ continue
844
+ }
845
+
846
+ const reserveAccount = await Reserve.fetch(setupContext.connection, reserveAddress)
847
+ if (!reserveAccount) {
848
+ throw new Error(`Missing Kamino reserve account ${reserveKey} required to refresh Kamino vault prices`)
849
+ }
850
+
851
+ reserveAccounts.set(reserveKey, { account: reserveAccount })
852
+ }
853
+ }
854
+
855
+ if (reserveAccounts.size > 0) {
856
+ const reserves = [...reserveAccounts.values()]
857
+ const defaultKey = PublicKey.default
858
+ const oracleOrSentinel = (key: PublicKey) =>
859
+ key.equals(defaultKey) ? KAMINO_LENDING_PROGRAM_ID : key
860
+
861
+ refreshInstructions.push(...await buildScopeRefreshInstructions(setupContext.connection, reserves))
862
+
863
+ for (const [reserveKey, { account }] of reserveAccounts.entries()) {
864
+ const tokenInfo = account.config.tokenInfo
865
+ refreshInstructions.push(
866
+ refreshReserve({
867
+ reserve: new PublicKey(reserveKey),
868
+ lendingMarket: account.lendingMarket,
869
+ pythOracle: oracleOrSentinel(tokenInfo.pythConfiguration.price),
870
+ switchboardPriceOracle: oracleOrSentinel(tokenInfo.switchboardConfiguration.priceAggregator),
871
+ switchboardTwapOracle: oracleOrSentinel(tokenInfo.switchboardConfiguration.twapAggregator),
872
+ scopePrices: oracleOrSentinel(tokenInfo.scopeConfiguration.priceFeed),
873
+ }),
874
+ )
893
875
  }
894
876
  }
895
877
 
896
- return null
878
+ const updatePriceInstructions = await state.strategyVault.ixsUpdateStrategyVaultPrices(state.prices, {
879
+ manager: setupContext.signer,
880
+ priceIds: [...state.requiredPriceIds].sort((a, b) => a - b),
881
+ })
882
+
883
+ return [...refreshInstructions, ...updatePriceInstructions]
884
+ }
885
+
886
+ function isAutoManagePositionsEnabled(setupContext?: StrategySetupContext): boolean {
887
+ return setupContext?.autoManagePositions ?? true
888
+ }
889
+
890
+ function unwrapTupleLikeValue(value: unknown): unknown {
891
+ if (Array.isArray(value)) {
892
+ return value[0]
893
+ }
894
+ if (value && typeof value === "object" && "0" in value) {
895
+ return (value as { 0?: unknown })[0]
896
+ }
897
+ return value ?? undefined
898
+ }
899
+
900
+ function getTrackedKaminoObligationFromPosition(position: unknown): {
901
+ obligation: PublicKey
902
+ quotePriceId: ClientPriceId
903
+ reservePriceMappings: Array<{ reserve: PublicKey; reservePriceId: ClientPriceId }>
904
+ } | null {
905
+ if (!position || typeof position !== "object" || !("obligation" in position)) {
906
+ return null
907
+ }
908
+
909
+ const obligationContainer = (position as { obligation?: unknown }).obligation
910
+ const obligationValue = unwrapTupleLikeValue(obligationContainer)
911
+ const kaminoContainer = (
912
+ obligationValue
913
+ && typeof obligationValue === "object"
914
+ && "kaminoObligation" in (obligationValue as Record<string, unknown>)
915
+ )
916
+ ? (obligationValue as { kaminoObligation?: unknown }).kaminoObligation
917
+ : obligationValue
918
+ const kaminoEntry = unwrapTupleLikeValue(kaminoContainer)
919
+
920
+ if (
921
+ !kaminoEntry
922
+ || typeof kaminoEntry !== "object"
923
+ || !("obligation" in kaminoEntry)
924
+ || !((kaminoEntry as { obligation?: unknown }).obligation instanceof PublicKey)
925
+ ) {
926
+ return null
927
+ }
928
+
929
+ const typedKaminoEntry = kaminoEntry as {
930
+ obligation: PublicKey
931
+ quotePriceId: ClientPriceId
932
+ reservePriceMappings?: Array<{ reserve: PublicKey; reservePriceId: ClientPriceId }>
933
+ }
934
+
935
+ return {
936
+ obligation: typedKaminoEntry.obligation,
937
+ quotePriceId: typedKaminoEntry.quotePriceId,
938
+ reservePriceMappings: typedKaminoEntry.reservePriceMappings ?? [],
939
+ }
940
+ }
941
+
942
+ function getTrackedKaminoFarmFromPosition(position: unknown): {
943
+ farmState: PublicKey
944
+ userState: PublicKey
945
+ } | null {
946
+ if (!position || typeof position !== "object" || !("kaminoFarm" in position)) {
947
+ return null
948
+ }
949
+
950
+ const kaminoFarmContainer = (position as { kaminoFarm?: unknown }).kaminoFarm
951
+ const kaminoFarmEntry = unwrapTupleLikeValue(kaminoFarmContainer)
952
+ if (
953
+ !kaminoFarmEntry
954
+ || typeof kaminoFarmEntry !== "object"
955
+ || !((kaminoFarmEntry as { farmState?: unknown }).farmState instanceof PublicKey)
956
+ || !((kaminoFarmEntry as { userState?: unknown }).userState instanceof PublicKey)
957
+ ) {
958
+ return null
959
+ }
960
+
961
+ return {
962
+ farmState: (kaminoFarmEntry as { farmState: PublicKey }).farmState,
963
+ userState: (kaminoFarmEntry as { userState: PublicKey }).userState,
964
+ }
965
+ }
966
+
967
+ function kaminoFarmKey(farmState: PublicKey, userState: PublicKey): string {
968
+ return `${farmState.toBase58()}:${userState.toBase58()}`
897
969
  }
898
970
 
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()}`)
971
+ function getTrackedClmmPositionFromPosition(position: unknown): {
972
+ lpPosition: PublicKey
973
+ market: PublicKey
974
+ } | null {
975
+ if (!position || typeof position !== "object" || !("clmmPosition" in position)) {
976
+ return null
977
+ }
978
+
979
+ const clmmContainer = (position as { clmmPosition?: unknown }).clmmPosition
980
+ const clmmEntry = unwrapTupleLikeValue(clmmContainer)
981
+ if (
982
+ !clmmEntry
983
+ || typeof clmmEntry !== "object"
984
+ || !((clmmEntry as { lpPosition?: unknown }).lpPosition instanceof PublicKey)
985
+ || !((clmmEntry as { market?: unknown }).market instanceof PublicKey)
986
+ ) {
987
+ return null
988
+ }
989
+
990
+ return {
991
+ lpPosition: (clmmEntry as { lpPosition: PublicKey }).lpPosition,
992
+ market: (clmmEntry as { market: PublicKey }).market,
903
993
  }
904
- return new Decimal(bigintU256ToString(raw.map((value) => BigInt(value.toString()))))
905
994
  }
906
995
 
907
996
  async function accountExists(
@@ -920,42 +1009,279 @@ async function accountExists(
920
1009
  return exists
921
1010
  }
922
1011
 
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)) {
941
- return null
942
- }
1012
+ type MutableStrategyVault = {
1013
+ aumRemainingAccounts(): AccountMeta[]
1014
+ clmmTicksMap: Map<string, PublicKey>
1015
+ }
943
1016
 
944
- const expectedAta = getAssociatedTokenAddressSync(mint, owner, true, tokenProgram)
945
- if (!expectedAta.equals(tokenAccount)) {
946
- return null
947
- }
1017
+ function mutableStrategyVault(value: StrategySetupState | StrategyVault): MutableStrategyVault {
1018
+ return ("strategyVault" in value ? value.strategyVault : value) as unknown as MutableStrategyVault
1019
+ }
948
1020
 
949
- state.existingAccounts.set(tokenAccount.toBase58(), true)
950
- return createAssociatedTokenAccountIdempotentInstruction(payer, tokenAccount, owner, mint, tokenProgram)
1021
+ function buildTrackedAumRemainingAccounts(
1022
+ state: StrategySetupState,
1023
+ extraAccounts: AccountMeta[] = [],
1024
+ ): AccountMeta[] {
1025
+ return uniqueRemainingAccounts([
1026
+ ...state.baseAumAccounts,
1027
+ ...state.plannedAumAccounts,
1028
+ ...extraAccounts,
1029
+ ])
951
1030
  }
952
1031
 
953
- async function ensureOrderbookPositionSetup(
954
- orderbook: Orderbook,
955
- buckets: InstructionBuckets,
956
- setupContext: StrategySetupContext,
1032
+ function recordPlannedOrderbookEntry(
1033
+ state: StrategySetupState,
1034
+ params: { orderbook: PublicKey; mint: PublicKey; priceIdPt: ClientPriceId; baseMint: PublicKey },
957
1035
  ) {
958
- const state = await loadStrategySetupState(setupContext)
1036
+ state.trackedOrderbooks.add(params.orderbook.toBase58())
1037
+ state.plannedAumAccounts.push({ pubkey: params.orderbook, isSigner: false, isWritable: false })
1038
+ ;(state.strategyVault.state.strategyPositions as unknown as Array<Record<string, unknown>>).push({
1039
+ orderbook: [{
1040
+ orderbook: params.orderbook,
1041
+ mint: params.mint,
1042
+ offerIdxVec: [],
1043
+ priceIdPt: params.priceIdPt,
1044
+ baseMint: params.baseMint,
1045
+ }],
1046
+ })
1047
+ state.nextStrategyPositionIndex += 1
1048
+ }
1049
+
1050
+ function recordPlannedYieldPosition(
1051
+ state: StrategySetupState,
1052
+ params: { yieldPosition: PublicKey; vault: PublicKey; priceIdPt: ClientPriceId },
1053
+ ) {
1054
+ state.trackedYieldVaults.add(params.vault.toBase58())
1055
+ state.existingAccounts.set(params.yieldPosition.toBase58(), true)
1056
+ ;(state.strategyVault.state.strategyPositions as unknown as Array<Record<string, unknown>>).push({
1057
+ yieldPosition: [{
1058
+ yieldPosition: params.yieldPosition,
1059
+ vault: params.vault,
1060
+ priceIdPt: params.priceIdPt,
1061
+ }],
1062
+ })
1063
+ state.nextStrategyPositionIndex += 1
1064
+ }
1065
+
1066
+ function recordPlannedTokenAccountEntry(
1067
+ state: StrategySetupState,
1068
+ params: { tokenMint: PublicKey; tokenAccount: PublicKey; priceId: ClientPriceId },
1069
+ ) {
1070
+ const tokenMintKey = params.tokenMint.toBase58()
1071
+ const tokenAccountKey = params.tokenAccount.toBase58()
1072
+ state.tokenPositionIndexByMint.set(tokenMintKey, state.nextStrategyPositionIndex)
1073
+ state.trackedTokenAccounts.add(tokenAccountKey)
1074
+ state.plannedAumAccounts.push({ pubkey: params.tokenAccount, isSigner: false, isWritable: false })
1075
+ ;(state.strategyVault.state.strategyPositions as unknown as Array<Record<string, unknown>>).push({
1076
+ tokenAccount: [{
1077
+ tokenMint: params.tokenMint,
1078
+ balances: [{
1079
+ tokenAccount: params.tokenAccount,
1080
+ mint: params.tokenMint,
1081
+ priceId: params.priceId,
1082
+ }],
1083
+ }],
1084
+ })
1085
+ state.nextStrategyPositionIndex += 1
1086
+ }
1087
+
1088
+ function recordPlannedTokenAccountBalance(
1089
+ state: StrategySetupState,
1090
+ params: { tokenMint: PublicKey; tokenAccount: PublicKey; priceId: ClientPriceId },
1091
+ ) {
1092
+ const tokenMintKey = params.tokenMint.toBase58()
1093
+ const tokenAccountKey = params.tokenAccount.toBase58()
1094
+ state.trackedTokenAccounts.add(tokenAccountKey)
1095
+ state.plannedAumAccounts.push({ pubkey: params.tokenAccount, isSigner: false, isWritable: false })
1096
+
1097
+ const positions = state.strategyVault.state.strategyPositions as unknown as Array<Record<string, unknown>>
1098
+ const target = positions.find((position) => {
1099
+ const tokenAccountEntry = Array.isArray(position.tokenAccount) ? position.tokenAccount[0] : undefined
1100
+ return tokenAccountEntry && tokenAccountEntry.tokenMint instanceof PublicKey && tokenAccountEntry.tokenMint.equals(params.tokenMint)
1101
+ })
1102
+ const tokenAccountEntry = target?.tokenAccount?.[0] as
1103
+ | { balances?: Array<{ tokenAccount: PublicKey; mint: PublicKey; priceId: ClientPriceId }> }
1104
+ | undefined
1105
+ tokenAccountEntry?.balances?.push({
1106
+ tokenAccount: params.tokenAccount,
1107
+ mint: params.tokenMint,
1108
+ priceId: params.priceId,
1109
+ })
1110
+ }
1111
+
1112
+ function recordPlannedKaminoObligation(
1113
+ state: StrategySetupState,
1114
+ entry: {
1115
+ obligation: PublicKey
1116
+ quotePriceId: ClientPriceId
1117
+ quoteInputMint: PublicKey
1118
+ reservePriceMappings: Array<{ reserve: PublicKey; reservePriceId: ClientPriceId }>
1119
+ remainingAccountsAmount: bigint
1120
+ minPriceStatusFlags: number
1121
+ },
1122
+ ) {
1123
+ state.existingAccounts.set(entry.obligation.toBase58(), true)
1124
+ state.trackedKaminoObligations.set(entry.obligation.toBase58(), {
1125
+ quotePriceId: entry.quotePriceId,
1126
+ quoteInputMint: entry.quoteInputMint,
1127
+ mappedReserves: new Set(entry.reservePriceMappings.map((mapping) => mapping.reserve.toBase58())),
1128
+ })
1129
+ state.plannedAumAccounts.push({ pubkey: entry.obligation, isSigner: false, isWritable: false })
1130
+ for (const mapping of entry.reservePriceMappings) {
1131
+ state.plannedAumAccounts.push({ pubkey: mapping.reserve, isSigner: false, isWritable: false })
1132
+ }
1133
+ ;(state.strategyVault.state.strategyPositions as unknown as Array<Record<string, unknown>>).push({
1134
+ obligation: [{
1135
+ kaminoObligation: [{
1136
+ obligation: entry.obligation,
1137
+ lendingProgramId: KAMINO_LENDING_PROGRAM_ID,
1138
+ quotePriceId: entry.quotePriceId,
1139
+ reservePriceMappings: entry.reservePriceMappings,
1140
+ remainingAccountsAmount: entry.remainingAccountsAmount,
1141
+ minPriceStatusFlags: entry.minPriceStatusFlags,
1142
+ }],
1143
+ }],
1144
+ })
1145
+ state.nextStrategyPositionIndex += 1
1146
+ }
1147
+
1148
+ function recordPlannedKaminoReserveMappings(
1149
+ state: StrategySetupState,
1150
+ params: {
1151
+ obligation: PublicKey
1152
+ quoteInputMint: PublicKey
1153
+ reservePriceMappings: Array<{ reserve: PublicKey; reservePriceId: ClientPriceId }>
1154
+ },
1155
+ ) {
1156
+ const tracked = state.trackedKaminoObligations.get(params.obligation.toBase58())
1157
+ if (!tracked) {
1158
+ return
1159
+ }
1160
+
1161
+ tracked.quoteInputMint = params.quoteInputMint
1162
+ for (const mapping of params.reservePriceMappings) {
1163
+ tracked.mappedReserves.add(mapping.reserve.toBase58())
1164
+ }
1165
+
1166
+ const positions = state.strategyVault.state.strategyPositions as unknown as Array<Record<string, unknown>>
1167
+ const target = positions.find((position) => {
1168
+ const entry = getTrackedKaminoObligationFromPosition(position)
1169
+ return entry?.obligation.equals(params.obligation)
1170
+ })
1171
+ const kaminoEntry = (
1172
+ Array.isArray(target?.obligation)
1173
+ ? target?.obligation?.[0]?.kaminoObligation?.[0]
1174
+ : undefined
1175
+ ) as { reservePriceMappings?: Array<{ reserve: PublicKey; reservePriceId: ClientPriceId }>; remainingAccountsAmount?: bigint } | undefined
1176
+ if (!kaminoEntry) {
1177
+ return
1178
+ }
1179
+
1180
+ for (const mapping of params.reservePriceMappings) {
1181
+ const existing = kaminoEntry.reservePriceMappings?.find((item) => item.reserve.equals(mapping.reserve))
1182
+ if (existing) {
1183
+ existing.reservePriceId = mapping.reservePriceId
1184
+ } else {
1185
+ kaminoEntry.reservePriceMappings ??= []
1186
+ kaminoEntry.reservePriceMappings.push(mapping)
1187
+ }
1188
+ state.plannedAumAccounts.push({ pubkey: mapping.reserve, isSigner: false, isWritable: false })
1189
+ }
1190
+ kaminoEntry.remainingAccountsAmount = BigInt(1 + (kaminoEntry.reservePriceMappings?.length ?? 0))
1191
+ }
1192
+
1193
+ function recordPlannedKaminoFarmPosition(
1194
+ state: StrategySetupState,
1195
+ params: {
1196
+ farmState: PublicKey
1197
+ userState: PublicKey
1198
+ globalConfig: PublicKey
1199
+ scopePrices: PublicKey | null
1200
+ },
1201
+ ) {
1202
+ const trackingKey = kaminoFarmKey(params.farmState, params.userState)
1203
+ if (state.trackedKaminoFarms.has(trackingKey)) {
1204
+ return
1205
+ }
1206
+
1207
+ state.trackedKaminoFarms.add(trackingKey)
1208
+ state.existingAccounts.set(params.userState.toBase58(), true)
1209
+ state.plannedAumAccounts.push({ pubkey: params.farmState, isSigner: false, isWritable: false })
1210
+ state.plannedAumAccounts.push({ pubkey: params.userState, isSigner: false, isWritable: false })
1211
+ state.plannedAumAccounts.push({ pubkey: params.globalConfig, isSigner: false, isWritable: false })
1212
+ if (params.scopePrices) {
1213
+ state.plannedAumAccounts.push({ pubkey: params.scopePrices, isSigner: false, isWritable: false })
1214
+ }
1215
+ ;(state.strategyVault.state.strategyPositions as unknown as Array<Record<string, unknown>>).push({
1216
+ kaminoFarm: [{
1217
+ farmState: params.farmState,
1218
+ userState: params.userState,
1219
+ }],
1220
+ })
1221
+ state.nextStrategyPositionIndex += 1
1222
+ }
1223
+
1224
+ function recordPlannedClmmPosition(
1225
+ state: StrategySetupState,
1226
+ params: {
1227
+ lpPosition: PublicKey
1228
+ market: PublicKey
1229
+ priceIdPt: ClientPriceId
1230
+ priceIdSy: ClientPriceId
1231
+ ticksKey: PublicKey
1232
+ },
1233
+ ) {
1234
+ state.existingAccounts.set(params.lpPosition.toBase58(), true)
1235
+ state.trackedClmmPositions.add(params.lpPosition.toBase58())
1236
+ state.plannedAumAccounts.push({ pubkey: params.lpPosition, isSigner: false, isWritable: false })
1237
+ mutableStrategyVault(state).clmmTicksMap.set(params.market.toBase58(), params.ticksKey)
1238
+ ;(state.strategyVault.state.strategyPositions as unknown as Array<Record<string, unknown>>).push({
1239
+ clmmPosition: [{
1240
+ lpPosition: params.lpPosition,
1241
+ market: params.market,
1242
+ priceIdPt: params.priceIdPt,
1243
+ priceIdSy: params.priceIdSy,
1244
+ }],
1245
+ })
1246
+ state.nextStrategyPositionIndex += 1
1247
+ }
1248
+
1249
+ async function maybeCreateOwnedAtaSetupInstruction({
1250
+ state,
1251
+ connection,
1252
+ payer,
1253
+ owner,
1254
+ mint,
1255
+ tokenProgram,
1256
+ tokenAccount,
1257
+ }: {
1258
+ state: StrategySetupState
1259
+ connection: Connection
1260
+ payer: PublicKey
1261
+ owner: PublicKey
1262
+ mint: PublicKey
1263
+ tokenProgram: PublicKey
1264
+ tokenAccount: PublicKey
1265
+ }): Promise<TransactionInstruction | null> {
1266
+ if (await accountExists(state, connection, tokenAccount)) {
1267
+ return null
1268
+ }
1269
+
1270
+ const expectedAta = getAssociatedTokenAddressSync(mint, owner, true, tokenProgram)
1271
+ if (!expectedAta.equals(tokenAccount)) {
1272
+ return null
1273
+ }
1274
+
1275
+ state.existingAccounts.set(tokenAccount.toBase58(), true)
1276
+ return createAssociatedTokenAccountIdempotentInstruction(payer, tokenAccount, owner, mint, tokenProgram)
1277
+ }
1278
+
1279
+ async function ensureOrderbookPositionSetup(
1280
+ orderbook: Orderbook,
1281
+ buckets: InstructionBuckets,
1282
+ setupContext: StrategySetupContext,
1283
+ ) {
1284
+ const state = await loadStrategySetupState(setupContext)
959
1285
  if (!state) {
960
1286
  return
961
1287
  }
@@ -965,16 +1291,13 @@ async function ensureOrderbookPositionSetup(
965
1291
  return
966
1292
  }
967
1293
 
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
- )
977
- }
1294
+ const priceIdPt = resolvePriceIdFromMintToUnderlyingOrThrow({
1295
+ prices: state.prices,
1296
+ sourceMint: orderbook.mintPt,
1297
+ targetMint: state.strategyVault.state.underlyingMint,
1298
+ label: `orderbook setup (${orderbook.selfAddress.toBase58()})`,
1299
+ })
1300
+ trackRequiredPriceIds(state.requiredPriceIds, priceIdPt)
978
1301
 
979
1302
  buckets.setupInstructions.push(
980
1303
  state.strategyVault.ixWrapperManageVaultSettings({
@@ -987,7 +1310,7 @@ async function ensureOrderbookPositionSetup(
987
1310
  userEscrowIdx: 0,
988
1311
  mint: orderbook.vault.mintSy,
989
1312
  offerIdxVec: [],
990
- priceIdPt: exponentVaults.priceId("Simple", { priceId: ptPrice.priceId }),
1313
+ priceIdPt,
991
1314
  baseMint: state.strategyVault.state.underlyingMint,
992
1315
  }]),
993
1316
  ],
@@ -998,8 +1321,12 @@ async function ensureOrderbookPositionSetup(
998
1321
  }),
999
1322
  )
1000
1323
 
1001
- state.trackedOrderbooks.add(orderbookKey)
1002
- state.nextStrategyPositionIndex += 1
1324
+ recordPlannedOrderbookEntry(state, {
1325
+ orderbook: orderbook.selfAddress,
1326
+ mint: orderbook.vault.mintSy,
1327
+ priceIdPt,
1328
+ baseMint: state.strategyVault.state.underlyingMint,
1329
+ })
1003
1330
  }
1004
1331
 
1005
1332
  async function ensureYieldPositionSetup(
@@ -1017,14 +1344,13 @@ async function ensureYieldPositionSetup(
1017
1344
  return
1018
1345
  }
1019
1346
 
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()})`)
1027
- }
1347
+ const priceIdPt = resolvePriceIdFromMintToUnderlyingOrThrow({
1348
+ prices: state.prices,
1349
+ sourceMint: coreVault.mintPt,
1350
+ targetMint: state.strategyVault.state.underlyingMint,
1351
+ label: `core vault setup (${coreVault.selfAddress.toBase58()})`,
1352
+ })
1353
+ trackRequiredPriceIds(state.requiredPriceIds, priceIdPt)
1028
1354
 
1029
1355
  const yieldPosition = coreVault.pda.yieldPosition({ owner: setupContext.owner, vault: coreVault.selfAddress })
1030
1356
  if (!(await accountExists(state, setupContext.connection, yieldPosition))) {
@@ -1066,7 +1392,7 @@ async function ensureYieldPositionSetup(
1066
1392
  exponentVaults.vaultSettingsAction("AddYieldPositionEntry", {
1067
1393
  yieldPosition,
1068
1394
  vault: coreVault.selfAddress,
1069
- priceIdPt: exponentVaults.priceId("Simple", { priceId: ptPrice.priceId }),
1395
+ priceIdPt,
1070
1396
  }),
1071
1397
  ],
1072
1398
  remainingAccounts: [
@@ -1076,8 +1402,11 @@ async function ensureYieldPositionSetup(
1076
1402
  }),
1077
1403
  )
1078
1404
 
1079
- state.trackedYieldVaults.add(coreVaultKey)
1080
- state.nextStrategyPositionIndex += 1
1405
+ recordPlannedYieldPosition(state, {
1406
+ yieldPosition,
1407
+ vault: coreVault.selfAddress,
1408
+ priceIdPt,
1409
+ })
1081
1410
  }
1082
1411
 
1083
1412
  async function wrapVaultSignedSetupInstruction({
@@ -1120,12 +1449,17 @@ async function wrapVaultSignedSetupInstruction({
1120
1449
  setupContext.vaultAddress
1121
1450
  && (!resolvedLeadingAccounts || !resolvedPreHookAccounts || !resolvedPostHookAccounts)
1122
1451
  ) {
1123
- const hooks = await resolveHookAccounts(
1124
- setupContext.connection,
1125
- resolvedPolicyPda,
1126
- setupContext.vaultAddress,
1127
- setupContext.signer,
1128
- )
1452
+ // Cache hook resolution on the context to avoid redundant RPC calls
1453
+ // when wrapping multiple setup instructions in the same build.
1454
+ if (!setupContext.resolvedHooksPromise) {
1455
+ setupContext.resolvedHooksPromise = resolveHookAccounts(
1456
+ setupContext.connection,
1457
+ resolvedPolicyPda,
1458
+ setupContext.vaultAddress,
1459
+ setupContext.signer,
1460
+ )
1461
+ }
1462
+ const hooks = await setupContext.resolvedHooksPromise
1129
1463
  resolvedLeadingAccounts ??= hooks.leadingAccounts
1130
1464
  resolvedPreHookAccounts ??= hooks.preHookAccounts
1131
1465
  resolvedPostHookAccounts ??= hooks.postHookAccounts
@@ -1180,14 +1514,13 @@ async function ensureTrackedTokenAccountSetup({
1180
1514
  return
1181
1515
  }
1182
1516
 
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
- }
1517
+ const priceId = resolvePriceIdFromMintToUnderlyingOrThrow({
1518
+ prices: state.prices,
1519
+ sourceMint: tokenMint,
1520
+ targetMint: state.strategyVault.state.underlyingMint,
1521
+ label: `token position setup (${tokenMint.toBase58()})`,
1522
+ })
1523
+ trackRequiredPriceIds(state.requiredPriceIds, priceId)
1191
1524
 
1192
1525
  const maybeAtaIx = await maybeCreateOwnedAtaSetupInstruction({
1193
1526
  state,
@@ -1213,7 +1546,7 @@ async function ensureTrackedTokenAccountSetup({
1213
1546
  balances: [{
1214
1547
  tokenAccount,
1215
1548
  mint: tokenMint,
1216
- priceId: exponentVaults.priceId("Simple", { priceId: price.priceId }),
1549
+ priceId,
1217
1550
  }],
1218
1551
  }]),
1219
1552
  ],
@@ -1222,8 +1555,11 @@ async function ensureTrackedTokenAccountSetup({
1222
1555
  ],
1223
1556
  }),
1224
1557
  )
1225
- state.tokenPositionIndexByMint.set(tokenMintKey, state.nextStrategyPositionIndex)
1226
- state.nextStrategyPositionIndex += 1
1558
+ recordPlannedTokenAccountEntry(state, {
1559
+ tokenMint,
1560
+ tokenAccount,
1561
+ priceId,
1562
+ })
1227
1563
  } else {
1228
1564
  buckets.setupInstructions.push(
1229
1565
  state.strategyVault.ixWrapperManagerUpdatePosition({
@@ -1234,7 +1570,7 @@ async function ensureTrackedTokenAccountSetup({
1234
1570
  balance: {
1235
1571
  tokenAccount,
1236
1572
  mint: tokenMint,
1237
- priceId: exponentVaults.priceId("Simple", { priceId: price.priceId }),
1573
+ priceId,
1238
1574
  },
1239
1575
  },
1240
1576
  remainingAccounts: [
@@ -1242,6 +1578,11 @@ async function ensureTrackedTokenAccountSetup({
1242
1578
  ],
1243
1579
  }),
1244
1580
  )
1581
+ recordPlannedTokenAccountBalance(state, {
1582
+ tokenMint,
1583
+ tokenAccount,
1584
+ priceId,
1585
+ })
1245
1586
  }
1246
1587
 
1247
1588
  state.trackedTokenAccounts.add(tokenAccountKey)
@@ -1260,6 +1601,8 @@ async function buildVaultInstructions(
1260
1601
  preHookAccounts?: PublicKey[] | AccountMeta[],
1261
1602
  postHookAccounts?: PublicKey[] | AccountMeta[],
1262
1603
  squadsProgram: PublicKey = SQUADS_PROGRAM_ID,
1604
+ autoManagePositions: boolean = true,
1605
+ sharedSetupContext?: StrategySetupContext,
1263
1606
  ): Promise<InstructionBuckets> {
1264
1607
  const buckets: InstructionBuckets = {
1265
1608
  setupInstructions: [],
@@ -1269,7 +1612,7 @@ async function buildVaultInstructions(
1269
1612
  signers: [],
1270
1613
  addressLookupTableAddresses: [],
1271
1614
  }
1272
- const setupContext = createStrategySetupContext({
1615
+ const setupContext = sharedSetupContext ?? createStrategySetupContext({
1273
1616
  connection,
1274
1617
  env: LOCAL_ENV,
1275
1618
  owner,
@@ -1282,9 +1625,20 @@ async function buildVaultInstructions(
1282
1625
  leadingAccounts,
1283
1626
  preHookAccounts,
1284
1627
  postHookAccounts,
1628
+ autoManagePositions,
1285
1629
  })
1286
1630
 
1287
1631
  for (const ix of instructions) {
1632
+ if (isKaminoVaultInstruction(ix)) {
1633
+ await buildKaminoVaultInstruction(ix, buckets, setupContext)
1634
+ continue
1635
+ }
1636
+
1637
+ if (isKaminoFarmInstruction(ix)) {
1638
+ await buildKaminoFarmInstruction(ix, buckets, setupContext)
1639
+ continue
1640
+ }
1641
+
1288
1642
  if (isOrderbookInstruction(ix)) {
1289
1643
  await buildOrderbookInstruction(ix, owner, connection, buckets, setupContext)
1290
1644
  continue
@@ -1320,19 +1674,19 @@ async function buildVaultInstructions(
1320
1674
  await buildInitUserMetadata(owner, connection, buckets)
1321
1675
  break
1322
1676
  case VaultAction.INIT_OBLIGATION:
1323
- await buildInitObligation(ix, owner, connection, buckets)
1677
+ await buildInitObligation(ix, owner, connection, buckets, setupContext)
1324
1678
  break
1325
1679
  case VaultAction.DEPOSIT:
1326
- await buildDeposit(ix, owner, connection, signer, buckets)
1680
+ await buildDeposit(ix, owner, connection, signer, buckets, setupContext)
1327
1681
  break
1328
1682
  case VaultAction.WITHDRAW:
1329
- await buildWithdraw(ix, owner, connection, signer, buckets)
1683
+ await buildWithdraw(ix, owner, connection, signer, buckets, setupContext)
1330
1684
  break
1331
1685
  case VaultAction.BORROW:
1332
- await buildBorrow(ix, owner, connection, signer, buckets)
1686
+ await buildBorrow(ix, owner, connection, signer, buckets, setupContext)
1333
1687
  break
1334
1688
  case VaultAction.REPAY:
1335
- await buildRepay(ix, owner, connection, signer, buckets)
1689
+ await buildRepay(ix, owner, connection, signer, buckets, setupContext)
1336
1690
  break
1337
1691
  }
1338
1692
  }
@@ -1340,6 +1694,832 @@ async function buildVaultInstructions(
1340
1694
  return buckets
1341
1695
  }
1342
1696
 
1697
+ const KAMINO_VAULT_EVENT_AUTHORITY = emitEventAuthority(KAMINO_VAULT_PROGRAM_ID)
1698
+ const KAMINO_FARM_USER_STATE_SIZE = 920
1699
+ const KAMINO_STAKE_ALL_AMOUNT = new BN("18446744073709551615")
1700
+
1701
+ type KaminoVaultContext = {
1702
+ index: Awaited<ReturnType<typeof fetchKaminoVaultIndex>>
1703
+ tokenAta: PublicKey
1704
+ sharesAta: PublicKey
1705
+ }
1706
+
1707
+ type KaminoFarmContext = {
1708
+ farm: ReturnType<typeof decodeKaminoFarmState>
1709
+ farmState: PublicKey
1710
+ userState: PublicKey
1711
+ delegatee: PublicKey
1712
+ sourceAta: PublicKey
1713
+ scopePrices: PublicKey | null
1714
+ }
1715
+
1716
+ function toBn(value: BN | bigint | number): BN {
1717
+ if (BN.isBN(value)) {
1718
+ return value
1719
+ }
1720
+ return new BN(value.toString())
1721
+ }
1722
+
1723
+ function encodeU64InstructionData(discriminator: Buffer, value: BN | bigint | number): Buffer {
1724
+ return Buffer.concat([discriminator, toBn(value).toArrayLike(Buffer, "le", 8)])
1725
+ }
1726
+
1727
+ function encodeU128InstructionData(discriminator: Buffer, value: BN | bigint | number): Buffer {
1728
+ return Buffer.concat([discriminator, toBn(value).toArrayLike(Buffer, "le", 16)])
1729
+ }
1730
+
1731
+ function getKaminoFarmUserStateAddress(delegatee: PublicKey, farmState: PublicKey): PublicKey {
1732
+ return getKaminoFarmsObligationFarm(delegatee, farmState, KAMINO_FARMS_PROGRAM_ID)
1733
+ }
1734
+
1735
+ async function accountExistsMaybeTracked(
1736
+ setupContext: StrategySetupContext | undefined,
1737
+ address: PublicKey,
1738
+ ): Promise<boolean> {
1739
+ if (!setupContext) {
1740
+ return false
1741
+ }
1742
+
1743
+ const state = await loadStrategySetupState(setupContext)
1744
+ if (state) {
1745
+ return accountExists(state, setupContext.connection, address)
1746
+ }
1747
+
1748
+ return (await setupContext.connection.getAccountInfo(address)) !== null
1749
+ }
1750
+
1751
+ function matchesKaminoVaultInterfaceAccounts(
1752
+ entry: ExponentPrice,
1753
+ interfaceAccounts: PublicKey[],
1754
+ ): boolean {
1755
+ return (
1756
+ entry.interfaceAccounts.length === interfaceAccounts.length
1757
+ && entry.interfaceAccounts.every((account, index) => account.equals(interfaceAccounts[index]!))
1758
+ )
1759
+ }
1760
+
1761
+ // ExponentPrices stores explicit wire discriminators, which can drift from the
1762
+ // generated TypeScript enum ordinals. Accept both so Kamino vault share
1763
+ // tracking works across current program/IDL combinations.
1764
+ function isKaminoVaultPriceType(priceType: number): boolean {
1765
+ return priceType === KAMINO_VAULT_PRICE_TYPE_WIRE || priceType === exponentVaults.PriceType.KaminoVault
1766
+ }
1767
+
1768
+ function resolveKaminoVaultPriceEntry(params: {
1769
+ prices: ExponentPrices
1770
+ sharesMint: PublicKey
1771
+ depositTokenMint: PublicKey
1772
+ interfaceAccounts: PublicKey[]
1773
+ }): ExponentPrice {
1774
+ const candidates = params.prices.prices.filter((entry): entry is ExponentPrice => entry !== null).filter((entry) =>
1775
+ isKaminoVaultPriceType(entry.priceType)
1776
+ && entry.priceMint.equals(params.sharesMint)
1777
+ && entry.underlyingMint.equals(params.depositTokenMint)
1778
+ && matchesKaminoVaultInterfaceAccounts(entry, params.interfaceAccounts),
1779
+ )
1780
+
1781
+ if (candidates.length === 0) {
1782
+ const interfaceAccountsLabel = params.interfaceAccounts.map((account) => account.toBase58()).join(", ")
1783
+ throw new Error(
1784
+ `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.`,
1785
+ )
1786
+ }
1787
+
1788
+ if (candidates.length > 1) {
1789
+ throw new Error(
1790
+ `Multiple Exponent KaminoVault prices matched shares mint ${params.sharesMint.toBase58()} and deposit mint ${params.depositTokenMint.toBase58()}`,
1791
+ )
1792
+ }
1793
+
1794
+ return candidates[0]!
1795
+ }
1796
+
1797
+ function resolveKaminoVaultTrackedPriceId(params: {
1798
+ state: StrategySetupState
1799
+ depositTokenMint: PublicKey
1800
+ sharePriceId: bigint
1801
+ label: string
1802
+ }): ClientPriceId {
1803
+ if (params.depositTokenMint.equals(params.state.strategyVault.state.underlyingMint)) {
1804
+ return { __kind: "Simple", priceId: params.sharePriceId }
1805
+ }
1806
+
1807
+ const reservePriceId = resolvePriceIdFromMintToUnderlyingOrThrow({
1808
+ prices: params.state.prices,
1809
+ sourceMint: params.depositTokenMint,
1810
+ targetMint: params.state.strategyVault.state.underlyingMint,
1811
+ label: params.label,
1812
+ })
1813
+ const reservePriceIds = extractPriceIds(reservePriceId).map((id) => BigInt(id))
1814
+ return { __kind: "Multiply", priceIds: [...reservePriceIds, params.sharePriceId] }
1815
+ }
1816
+
1817
+ async function resolveKaminoVaultContext(
1818
+ vaultAddress: PublicKey,
1819
+ setupContext: StrategySetupContext,
1820
+ ): Promise<KaminoVaultContext> {
1821
+ const rawIndex = await fetchKaminoVaultIndex({
1822
+ connection: setupContext.connection,
1823
+ kaminoVaultAccount: vaultAddress,
1824
+ })
1825
+ const reserveAddresses = rawIndex.reserves.map((reserve) => reserve.reserveAddress)
1826
+ const [vaultInfo, tokenMintInfo, sharesMintInfo, reserveInfos] = await Promise.all([
1827
+ setupContext.connection.getAccountInfo(vaultAddress),
1828
+ setupContext.connection.getAccountInfo(rawIndex.tokenMint),
1829
+ setupContext.connection.getAccountInfo(rawIndex.sharesMint),
1830
+ setupContext.connection.getMultipleAccountsInfo(reserveAddresses),
1831
+ ])
1832
+ if (!vaultInfo?.data) {
1833
+ throw new Error(`Kamino vault account not found: ${vaultAddress.toBase58()}`)
1834
+ }
1835
+
1836
+ const decodedReserves = reserveInfos.map((reserveInfo, index) => {
1837
+ if (!reserveInfo?.data) {
1838
+ throw new Error(`Missing Kamino reserve account ${reserveAddresses[index]!.toBase58()}`)
1839
+ }
1840
+ return Reserve.decode(reserveInfo.data)
1841
+ })
1842
+ const collateralMintInfos = await setupContext.connection.getMultipleAccountsInfo(
1843
+ decodedReserves.map((reserve) => reserve.collateral.mintPubkey),
1844
+ )
1845
+ const normalizedReserves = reserveAddresses.map((reserveAddress, index) => {
1846
+ const reserveAccount = decodedReserves[index]!
1847
+ const rawReserve = rawIndex.reserves[index] as typeof rawIndex.reserves[number] & {
1848
+ reserve?: PublicKey
1849
+ marketAddress?: PublicKey
1850
+ ctokenVault?: PublicKey
1851
+ lendingMarketAuthority?: PublicKey
1852
+ pythOracle?: PublicKey
1853
+ switchboardPriceOracle?: PublicKey
1854
+ switchboardTwapOracle?: PublicKey
1855
+ scopePrices?: PublicKey
1856
+ reserveLiquiditySupply?: PublicKey
1857
+ reserveCollateralMint?: PublicKey
1858
+ reserveCollateralTokenProgram?: PublicKey
1859
+ }
1860
+ const allocationOffset =
1861
+ KAMINO_VAULT_ACCOUNT_DISCRIMINATOR_LEN
1862
+ + KAMINO_VAULT_ALLOCATION_STRATEGY_OFFSET
1863
+ + (index * KAMINO_VAULT_ALLOCATION_SIZE)
1864
+ const ctokenVaultOffset = allocationOffset + KAMINO_VAULT_ALLOCATION_CTOKEN_VAULT_OFFSET
1865
+ const [lendingMarketAuthority] = PublicKey.findProgramAddressSync(
1866
+ [Buffer.from("lma"), reserveAccount.lendingMarket.toBuffer()],
1867
+ KAMINO_LENDING_PROGRAM_ID,
1868
+ )
1869
+
1870
+ return {
1871
+ reserveAddress,
1872
+ marketAddress: rawReserve.marketAddress ?? rawReserve.reserve ?? reserveAccount.lendingMarket,
1873
+ ctokenVault:
1874
+ rawReserve.ctokenVault
1875
+ ?? new PublicKey(vaultInfo.data.subarray(ctokenVaultOffset, ctokenVaultOffset + 32)),
1876
+ lendingMarketAuthority: rawReserve.lendingMarketAuthority ?? lendingMarketAuthority,
1877
+ pythOracle: rawReserve.pythOracle ?? reserveAccount.config.tokenInfo.pythConfiguration.price,
1878
+ switchboardPriceOracle:
1879
+ rawReserve.switchboardPriceOracle
1880
+ ?? reserveAccount.config.tokenInfo.switchboardConfiguration.priceAggregator,
1881
+ switchboardTwapOracle:
1882
+ rawReserve.switchboardTwapOracle
1883
+ ?? reserveAccount.config.tokenInfo.switchboardConfiguration.twapAggregator,
1884
+ scopePrices: rawReserve.scopePrices ?? reserveAccount.config.tokenInfo.scopeConfiguration.priceFeed,
1885
+ reserveLiquiditySupply: rawReserve.reserveLiquiditySupply ?? reserveAccount.liquidity.supplyVault,
1886
+ reserveCollateralMint: rawReserve.reserveCollateralMint ?? reserveAccount.collateral.mintPubkey,
1887
+ reserveCollateralTokenProgram:
1888
+ rawReserve.reserveCollateralTokenProgram ?? collateralMintInfos[index]?.owner ?? PublicKey.default,
1889
+ }
1890
+ })
1891
+ const index = {
1892
+ ...rawIndex,
1893
+ reserves: normalizedReserves,
1894
+ tokenProgram: (rawIndex as typeof rawIndex & { tokenProgram?: PublicKey }).tokenProgram ?? tokenMintInfo?.owner ?? TOKEN_PROGRAM_ID,
1895
+ sharesTokenProgram:
1896
+ (rawIndex as typeof rawIndex & { sharesTokenProgram?: PublicKey }).sharesTokenProgram
1897
+ ?? sharesMintInfo?.owner
1898
+ ?? TOKEN_PROGRAM_ID,
1899
+ vaultLookupTable:
1900
+ (rawIndex as typeof rawIndex & { vaultLookupTable?: PublicKey }).vaultLookupTable ?? PublicKey.default,
1901
+ }
1902
+ const tokenAta = getAssociatedTokenAddressSync(index.tokenMint, setupContext.owner, true, index.tokenProgram)
1903
+ const sharesAta = getAssociatedTokenAddressSync(index.sharesMint, setupContext.owner, true, index.sharesTokenProgram)
1904
+ return { index, tokenAta, sharesAta }
1905
+ }
1906
+
1907
+ async function queueKaminoVaultSharesTracking(params: {
1908
+ setupContext: StrategySetupContext
1909
+ buckets: InstructionBuckets
1910
+ kaminoVaultAddress: PublicKey
1911
+ vaultContext: KaminoVaultContext
1912
+ }) {
1913
+ const state = await loadStrategySetupState(params.setupContext)
1914
+ if (!state) {
1915
+ return
1916
+ }
1917
+
1918
+ const sharesMintKey = params.vaultContext.index.sharesMint.toBase58()
1919
+ const sharesAtaKey = params.vaultContext.sharesAta.toBase58()
1920
+
1921
+ if (state.trackedTokenAccounts.has(sharesAtaKey)) {
1922
+ return
1923
+ }
1924
+
1925
+ const tokenEntryAccount = state.tokenEntryAccountByMint.get(sharesMintKey)
1926
+ if (tokenEntryAccount) {
1927
+ throw new Error(
1928
+ `Kamino Vault shares mint ${sharesMintKey} is already configured as a token entry on ${tokenEntryAccount}`,
1929
+ )
1930
+ }
1931
+
1932
+ const interfaceAccounts = [
1933
+ params.kaminoVaultAddress,
1934
+ ...params.vaultContext.index.reserves.map((reserve) => reserve.reserveAddress),
1935
+ ]
1936
+ const priceEntry = resolveKaminoVaultPriceEntry({
1937
+ prices: state.prices,
1938
+ sharesMint: params.vaultContext.index.sharesMint,
1939
+ depositTokenMint: params.vaultContext.index.tokenMint,
1940
+ interfaceAccounts,
1941
+ })
1942
+ const resolvedPriceId = resolveKaminoVaultTrackedPriceId({
1943
+ state,
1944
+ depositTokenMint: params.vaultContext.index.tokenMint,
1945
+ sharePriceId: priceEntry.priceId,
1946
+ label: `Kamino Vault shares tracking (${params.kaminoVaultAddress.toBase58()})`,
1947
+ })
1948
+ const remainingAccounts = uniqueRemainingAccounts([
1949
+ { pubkey: params.vaultContext.sharesAta, isSigner: false, isWritable: false },
1950
+ { pubkey: priceEntry.priceInterfaceAccounts, isSigner: false, isWritable: false },
1951
+ ...priceEntry.interfaceAccounts.map((account) => ({
1952
+ pubkey: account,
1953
+ isSigner: false,
1954
+ isWritable: false,
1955
+ })),
1956
+ ...buildTrackedAumRemainingAccounts(state),
1957
+ ])
1958
+
1959
+ // The hook validates Kamino vault deposits against the currently tracked
1960
+ // shares ATA, so this registration must happen before the Squads sync step.
1961
+ params.buckets.preInstructions.push(
1962
+ state.strategyVault.ixWrapperManagerUpdatePosition({
1963
+ manager: params.setupContext.signer,
1964
+ update: exponentVaults.positionUpdate("TrackKaminoVaultShares", {
1965
+ sharesMint: params.vaultContext.index.sharesMint,
1966
+ depositTokenMint: params.vaultContext.index.tokenMint,
1967
+ sharesTokenAccount: params.vaultContext.sharesAta,
1968
+ priceInterfaceAccounts: priceEntry.priceInterfaceAccounts,
1969
+ }),
1970
+ remainingAccounts,
1971
+ }),
1972
+ )
1973
+
1974
+ trackRequiredPriceIds(state.requiredPriceIds, resolvedPriceId)
1975
+ if (state.tokenPositionIndexByMint.get(sharesMintKey) === undefined) {
1976
+ recordPlannedTokenAccountEntry(state, {
1977
+ tokenMint: params.vaultContext.index.sharesMint,
1978
+ tokenAccount: params.vaultContext.sharesAta,
1979
+ priceId: resolvedPriceId,
1980
+ })
1981
+ } else {
1982
+ recordPlannedTokenAccountBalance(state, {
1983
+ tokenMint: params.vaultContext.index.sharesMint,
1984
+ tokenAccount: params.vaultContext.sharesAta,
1985
+ priceId: resolvedPriceId,
1986
+ })
1987
+ }
1988
+ }
1989
+
1990
+ async function resolveKaminoVaultValidationAccounts(params: {
1991
+ setupContext: StrategySetupContext
1992
+ kaminoVaultAddress: PublicKey
1993
+ vaultContext: KaminoVaultContext
1994
+ }): Promise<AccountMeta[]> {
1995
+ const state = await loadStrategySetupState(params.setupContext)
1996
+ if (!state) {
1997
+ return []
1998
+ }
1999
+
2000
+ const priceEntry = resolveKaminoVaultPriceEntry({
2001
+ prices: state.prices,
2002
+ sharesMint: params.vaultContext.index.sharesMint,
2003
+ depositTokenMint: params.vaultContext.index.tokenMint,
2004
+ interfaceAccounts: [
2005
+ params.kaminoVaultAddress,
2006
+ ...params.vaultContext.index.reserves.map((reserve) => reserve.reserveAddress),
2007
+ ],
2008
+ })
2009
+
2010
+ return [
2011
+ { pubkey: priceEntry.priceInterfaceAccounts, isSigner: false, isWritable: false },
2012
+ ]
2013
+ }
2014
+
2015
+ function buildKaminoVaultDepositInstruction(params: {
2016
+ owner: PublicKey
2017
+ kaminoVaultAddress: PublicKey
2018
+ vaultContext: KaminoVaultContext
2019
+ amount: BN
2020
+ validationAccounts?: AccountMeta[]
2021
+ }): TransactionInstruction {
2022
+ const reserveAccounts = params.vaultContext.index.reserves.flatMap((reserve) => [
2023
+ { pubkey: reserve.reserveAddress, isSigner: false, isWritable: true },
2024
+ { pubkey: reserve.ctokenVault, isSigner: false, isWritable: true },
2025
+ { pubkey: reserve.marketAddress, isSigner: false, isWritable: false },
2026
+ { pubkey: reserve.lendingMarketAuthority, isSigner: false, isWritable: false },
2027
+ { pubkey: reserve.reserveLiquiditySupply, isSigner: false, isWritable: true },
2028
+ { pubkey: reserve.reserveCollateralMint, isSigner: false, isWritable: true },
2029
+ { pubkey: reserve.reserveCollateralTokenProgram, isSigner: false, isWritable: false },
2030
+ { pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, isSigner: false, isWritable: false },
2031
+ { pubkey: KAMINO_VAULT_EVENT_AUTHORITY, isSigner: false, isWritable: false },
2032
+ { pubkey: KAMINO_VAULT_PROGRAM_ID, isSigner: false, isWritable: false },
2033
+ ])
2034
+
2035
+ return new TransactionInstruction({
2036
+ programId: KAMINO_VAULT_PROGRAM_ID,
2037
+ keys: [
2038
+ { pubkey: params.owner, isSigner: true, isWritable: true },
2039
+ { pubkey: params.kaminoVaultAddress, isSigner: false, isWritable: true },
2040
+ { pubkey: params.vaultContext.index.tokenVault, isSigner: false, isWritable: true },
2041
+ { pubkey: params.vaultContext.index.tokenMint, isSigner: false, isWritable: false },
2042
+ { pubkey: params.vaultContext.index.baseVaultAuthority, isSigner: false, isWritable: false },
2043
+ { pubkey: params.vaultContext.index.sharesMint, isSigner: false, isWritable: true },
2044
+ { pubkey: params.vaultContext.tokenAta, isSigner: false, isWritable: true },
2045
+ { pubkey: params.vaultContext.sharesAta, isSigner: false, isWritable: true },
2046
+ { pubkey: KAMINO_LENDING_PROGRAM_ID, isSigner: false, isWritable: false },
2047
+ { pubkey: params.vaultContext.index.tokenProgram, isSigner: false, isWritable: false },
2048
+ { pubkey: params.vaultContext.index.sharesTokenProgram, isSigner: false, isWritable: false },
2049
+ { pubkey: KAMINO_VAULT_EVENT_AUTHORITY, isSigner: false, isWritable: false },
2050
+ { pubkey: KAMINO_VAULT_PROGRAM_ID, isSigner: false, isWritable: false },
2051
+ ...reserveAccounts,
2052
+ ...params.vaultContext.index.reserves.map((reserve) => ({
2053
+ pubkey: reserve.reserveAddress,
2054
+ isSigner: false,
2055
+ isWritable: true,
2056
+ })),
2057
+ ...(params.validationAccounts ?? []),
2058
+ ],
2059
+ data: encodeU64InstructionData(KAMINO_VAULT_DISCRIMINATORS.deposit, params.amount),
2060
+ })
2061
+ }
2062
+
2063
+ function resolveKaminoVaultWithdrawReserve(
2064
+ ix: KaminoVaultWithdrawInstruction,
2065
+ vaultContext: KaminoVaultContext,
2066
+ ): KaminoVaultContext["index"]["reserves"][number] {
2067
+ if (ix.reserve) {
2068
+ const reserve = vaultContext.index.reserves.find((entry) => entry.reserveAddress.equals(ix.reserve!))
2069
+ if (!reserve) {
2070
+ throw new Error(
2071
+ `Kamino Vault ${ix.vault.toBase58()} does not use reserve ${ix.reserve.toBase58()}`,
2072
+ )
2073
+ }
2074
+ return reserve
2075
+ }
2076
+
2077
+ if (vaultContext.index.reserves.length !== 1) {
2078
+ throw new Error(
2079
+ `Kamino Vault ${ix.vault.toBase58()} uses ${vaultContext.index.reserves.length} reserves; specify withdraw.reserve`,
2080
+ )
2081
+ }
2082
+
2083
+ return vaultContext.index.reserves[0]!
2084
+ }
2085
+
2086
+ function buildKaminoVaultWithdrawInstruction(params: {
2087
+ owner: PublicKey
2088
+ ix: KaminoVaultWithdrawInstruction
2089
+ vaultContext: KaminoVaultContext
2090
+ validationAccounts?: AccountMeta[]
2091
+ }): TransactionInstruction {
2092
+ const reserve = resolveKaminoVaultWithdrawReserve(params.ix, params.vaultContext)
2093
+ const [globalConfig] = PublicKey.findProgramAddressSync(
2094
+ [KAMINO_VAULT_GLOBAL_CONFIG_SEED],
2095
+ KAMINO_VAULT_PROGRAM_ID,
2096
+ )
2097
+ return new TransactionInstruction({
2098
+ programId: KAMINO_VAULT_PROGRAM_ID,
2099
+ keys: [
2100
+ { pubkey: params.owner, isSigner: true, isWritable: true },
2101
+ { pubkey: params.ix.vault, isSigner: false, isWritable: true },
2102
+ { pubkey: globalConfig, isSigner: false, isWritable: false },
2103
+ { pubkey: params.vaultContext.index.tokenVault, isSigner: false, isWritable: true },
2104
+ { pubkey: params.vaultContext.index.baseVaultAuthority, isSigner: false, isWritable: false },
2105
+ { pubkey: params.vaultContext.tokenAta, isSigner: false, isWritable: true },
2106
+ { pubkey: params.vaultContext.index.tokenMint, isSigner: false, isWritable: true },
2107
+ { pubkey: params.vaultContext.sharesAta, isSigner: false, isWritable: true },
2108
+ { pubkey: params.vaultContext.index.sharesMint, isSigner: false, isWritable: true },
2109
+ { pubkey: params.vaultContext.index.tokenProgram, isSigner: false, isWritable: false },
2110
+ { pubkey: params.vaultContext.index.sharesTokenProgram, isSigner: false, isWritable: false },
2111
+ { pubkey: KAMINO_LENDING_PROGRAM_ID, isSigner: false, isWritable: false },
2112
+ { pubkey: KAMINO_VAULT_EVENT_AUTHORITY, isSigner: false, isWritable: false },
2113
+ { pubkey: KAMINO_VAULT_PROGRAM_ID, isSigner: false, isWritable: false },
2114
+ { pubkey: params.ix.vault, isSigner: false, isWritable: true },
2115
+ { pubkey: reserve.reserveAddress, isSigner: false, isWritable: true },
2116
+ { pubkey: reserve.ctokenVault, isSigner: false, isWritable: true },
2117
+ { pubkey: reserve.marketAddress, isSigner: false, isWritable: false },
2118
+ { pubkey: reserve.lendingMarketAuthority, isSigner: false, isWritable: false },
2119
+ { pubkey: reserve.reserveLiquiditySupply, isSigner: false, isWritable: true },
2120
+ { pubkey: reserve.reserveCollateralMint, isSigner: false, isWritable: true },
2121
+ { pubkey: reserve.reserveCollateralTokenProgram, isSigner: false, isWritable: false },
2122
+ { pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, isSigner: false, isWritable: false },
2123
+ { pubkey: KAMINO_VAULT_EVENT_AUTHORITY, isSigner: false, isWritable: false },
2124
+ { pubkey: KAMINO_VAULT_PROGRAM_ID, isSigner: false, isWritable: false },
2125
+ ...params.vaultContext.index.reserves.map((entry) => ({
2126
+ pubkey: entry.reserveAddress,
2127
+ isSigner: false,
2128
+ isWritable: true,
2129
+ })),
2130
+ ...(params.validationAccounts ?? []),
2131
+ ],
2132
+ data: encodeU64InstructionData(KAMINO_VAULT_DISCRIMINATORS.withdraw, params.ix.sharesAmount),
2133
+ })
2134
+ }
2135
+
2136
+ async function buildKaminoVaultInstruction(
2137
+ ix: KaminoVaultInstruction,
2138
+ buckets: InstructionBuckets,
2139
+ setupContext: StrategySetupContext,
2140
+ ): Promise<void> {
2141
+ const vaultContext = await resolveKaminoVaultContext(ix.vault, setupContext)
2142
+ const validationAccounts = await resolveKaminoVaultValidationAccounts({
2143
+ setupContext,
2144
+ kaminoVaultAddress: ix.vault,
2145
+ vaultContext,
2146
+ })
2147
+
2148
+ buckets.setupInstructions.push(
2149
+ createAssociatedTokenAccountIdempotentInstruction(
2150
+ setupContext.signer,
2151
+ vaultContext.sharesAta,
2152
+ setupContext.owner,
2153
+ vaultContext.index.sharesMint,
2154
+ vaultContext.index.sharesTokenProgram,
2155
+ ),
2156
+ )
2157
+
2158
+ if (setupContext.autoManagePositions) {
2159
+ await ensureTrackedTokenAccountSetup({
2160
+ tokenMint: vaultContext.index.tokenMint,
2161
+ tokenAccount: vaultContext.tokenAta,
2162
+ tokenProgram: vaultContext.index.tokenProgram,
2163
+ buckets,
2164
+ setupContext,
2165
+ })
2166
+ }
2167
+
2168
+ const vaultLookupTable = (vaultContext.index as typeof vaultContext.index & {
2169
+ vaultLookupTable?: PublicKey
2170
+ }).vaultLookupTable ?? PublicKey.default
2171
+
2172
+ if (!vaultLookupTable.equals(PublicKey.default)) {
2173
+ buckets.addressLookupTableAddresses.push(vaultLookupTable)
2174
+ }
2175
+
2176
+ if (ix.action === KaminoVaultAction.DEPOSIT) {
2177
+ buckets.syncInstructions.push(buildKaminoVaultDepositInstruction({
2178
+ owner: setupContext.owner,
2179
+ kaminoVaultAddress: ix.vault,
2180
+ vaultContext,
2181
+ amount: ix.amount,
2182
+ validationAccounts,
2183
+ }))
2184
+
2185
+ if (isAutoManagePositionsEnabled(setupContext)) {
2186
+ await queueKaminoVaultSharesTracking({
2187
+ setupContext,
2188
+ buckets,
2189
+ kaminoVaultAddress: ix.vault,
2190
+ vaultContext,
2191
+ })
2192
+ }
2193
+ return
2194
+ }
2195
+
2196
+ buckets.syncInstructions.push(buildKaminoVaultWithdrawInstruction({
2197
+ owner: setupContext.owner,
2198
+ ix,
2199
+ vaultContext,
2200
+ validationAccounts,
2201
+ }))
2202
+ }
2203
+
2204
+ function getOptionalReadonlyAccountMeta(account: PublicKey | null, placeholderProgram: PublicKey): AccountMeta {
2205
+ return {
2206
+ pubkey: account ?? placeholderProgram,
2207
+ isSigner: false,
2208
+ isWritable: false,
2209
+ }
2210
+ }
2211
+
2212
+ async function resolveKaminoFarmContext(
2213
+ ix: KaminoFarmInstruction,
2214
+ setupContext: StrategySetupContext,
2215
+ ): Promise<KaminoFarmContext> {
2216
+ const farmInfo = await setupContext.connection.getAccountInfo(ix.farmState)
2217
+ if (!farmInfo?.data) {
2218
+ throw new Error(`Kamino farm not found: ${ix.farmState.toBase58()}`)
2219
+ }
2220
+
2221
+ const farm = decodeKaminoFarmState(Buffer.from(farmInfo.data))
2222
+ const delegatee = ix.delegatee ?? setupContext.owner
2223
+ const userState = getKaminoFarmUserStateAddress(delegatee, ix.farmState)
2224
+ const sourceAta = getAssociatedTokenAddressSync(
2225
+ farm.underlyingMint,
2226
+ setupContext.owner,
2227
+ true,
2228
+ farm.tokenProgram,
2229
+ )
2230
+
2231
+ return {
2232
+ farm,
2233
+ farmState: ix.farmState,
2234
+ userState,
2235
+ delegatee,
2236
+ sourceAta,
2237
+ scopePrices: getKaminoFarmScopePricesAddress(farm),
2238
+ }
2239
+ }
2240
+
2241
+ function buildKaminoFarmInitializeUserRawInstruction(params: {
2242
+ owner: PublicKey
2243
+ delegatee: PublicKey
2244
+ userState: PublicKey
2245
+ farmState: PublicKey
2246
+ }): TransactionInstruction {
2247
+ return new TransactionInstruction({
2248
+ programId: KAMINO_FARMS_PROGRAM_ID,
2249
+ keys: [
2250
+ { pubkey: params.owner, isSigner: true, isWritable: true },
2251
+ { pubkey: params.owner, isSigner: true, isWritable: true },
2252
+ { pubkey: params.owner, isSigner: false, isWritable: false },
2253
+ { pubkey: params.delegatee, isSigner: false, isWritable: false },
2254
+ { pubkey: params.userState, isSigner: false, isWritable: true },
2255
+ { pubkey: params.farmState, isSigner: false, isWritable: true },
2256
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
2257
+ { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
2258
+ ],
2259
+ data: Buffer.from(KAMINO_FARM_DISCRIMINATORS.initializeUser),
2260
+ })
2261
+ }
2262
+
2263
+ async function ensureKaminoFarmUserSetup(params: {
2264
+ farmContext: KaminoFarmContext
2265
+ buckets: InstructionBuckets
2266
+ setupContext: StrategySetupContext
2267
+ }) {
2268
+ if (await accountExistsMaybeTracked(params.setupContext, params.farmContext.userState)) {
2269
+ return
2270
+ }
2271
+
2272
+ const requiredLamports = await params.setupContext.connection.getMinimumBalanceForRentExemption(
2273
+ KAMINO_FARM_USER_STATE_SIZE,
2274
+ )
2275
+ const ownerLamports = await params.setupContext.connection.getBalance(params.setupContext.owner)
2276
+ if (ownerLamports < requiredLamports) {
2277
+ params.buckets.setupInstructions.push(
2278
+ SystemProgram.transfer({
2279
+ fromPubkey: params.setupContext.signer,
2280
+ toPubkey: params.setupContext.owner,
2281
+ lamports: requiredLamports - ownerLamports,
2282
+ }),
2283
+ )
2284
+ }
2285
+
2286
+ params.buckets.syncInstructions.push(
2287
+ buildKaminoFarmInitializeUserRawInstruction({
2288
+ owner: params.setupContext.owner,
2289
+ delegatee: params.farmContext.delegatee,
2290
+ userState: params.farmContext.userState,
2291
+ farmState: params.farmContext.farmState,
2292
+ }),
2293
+ )
2294
+
2295
+ const state = await loadStrategySetupState(params.setupContext)
2296
+ state?.existingAccounts.set(params.farmContext.userState.toBase58(), true)
2297
+ }
2298
+
2299
+ function buildKaminoFarmStakeInstructionRaw(params: {
2300
+ owner: PublicKey
2301
+ farmContext: KaminoFarmContext
2302
+ amount: BN | "ALL"
2303
+ }): TransactionInstruction {
2304
+ const amount = params.amount === "ALL" ? KAMINO_STAKE_ALL_AMOUNT : params.amount
2305
+ return new TransactionInstruction({
2306
+ programId: KAMINO_FARMS_PROGRAM_ID,
2307
+ keys: [
2308
+ { pubkey: params.owner, isSigner: true, isWritable: true },
2309
+ { pubkey: params.farmContext.userState, isSigner: false, isWritable: true },
2310
+ { pubkey: params.farmContext.farmState, isSigner: false, isWritable: true },
2311
+ { pubkey: params.farmContext.farm.farmVault, isSigner: false, isWritable: true },
2312
+ { pubkey: params.farmContext.sourceAta, isSigner: false, isWritable: true },
2313
+ { pubkey: params.farmContext.farm.underlyingMint, isSigner: false, isWritable: false },
2314
+ getOptionalReadonlyAccountMeta(params.farmContext.scopePrices, KAMINO_FARMS_PROGRAM_ID),
2315
+ { pubkey: params.farmContext.farm.tokenProgram, isSigner: false, isWritable: false },
2316
+ ],
2317
+ data: encodeU64InstructionData(KAMINO_FARM_DISCRIMINATORS.stake, amount),
2318
+ })
2319
+ }
2320
+
2321
+ function buildKaminoFarmUnstakeInstructionRaw(params: {
2322
+ owner: PublicKey
2323
+ farmContext: KaminoFarmContext
2324
+ stakeSharesScaled: BN
2325
+ }): TransactionInstruction {
2326
+ return new TransactionInstruction({
2327
+ programId: KAMINO_FARMS_PROGRAM_ID,
2328
+ keys: [
2329
+ { pubkey: params.owner, isSigner: true, isWritable: true },
2330
+ { pubkey: params.farmContext.userState, isSigner: false, isWritable: true },
2331
+ { pubkey: params.farmContext.farmState, isSigner: false, isWritable: true },
2332
+ getOptionalReadonlyAccountMeta(params.farmContext.scopePrices, KAMINO_FARMS_PROGRAM_ID),
2333
+ ],
2334
+ data: encodeU128InstructionData(KAMINO_FARM_DISCRIMINATORS.unstake, params.stakeSharesScaled),
2335
+ })
2336
+ }
2337
+
2338
+ function buildKaminoFarmWithdrawUnstakedDepositsRawInstruction(params: {
2339
+ owner: PublicKey
2340
+ farmContext: KaminoFarmContext
2341
+ }): TransactionInstruction {
2342
+ return new TransactionInstruction({
2343
+ programId: KAMINO_FARMS_PROGRAM_ID,
2344
+ keys: [
2345
+ { pubkey: params.owner, isSigner: true, isWritable: true },
2346
+ { pubkey: params.farmContext.userState, isSigner: false, isWritable: true },
2347
+ { pubkey: params.farmContext.farmState, isSigner: false, isWritable: true },
2348
+ { pubkey: params.farmContext.sourceAta, isSigner: false, isWritable: true },
2349
+ { pubkey: params.farmContext.farm.farmVault, isSigner: false, isWritable: true },
2350
+ { pubkey: params.farmContext.farm.farmVaultsAuthority, isSigner: false, isWritable: false },
2351
+ { pubkey: params.farmContext.farm.tokenProgram, isSigner: false, isWritable: false },
2352
+ ],
2353
+ data: Buffer.from(KAMINO_FARM_DISCRIMINATORS.withdrawUnstakedDeposits),
2354
+ })
2355
+ }
2356
+
2357
+ function buildKaminoFarmHarvestRewardRawInstruction(params: {
2358
+ owner: PublicKey
2359
+ farmContext: KaminoFarmContext
2360
+ rewardIndex: number
2361
+ rewardAta: PublicKey
2362
+ }): TransactionInstruction {
2363
+ const rewardInfo = params.farmContext.farm.rewardInfos[params.rewardIndex]
2364
+ if (!rewardInfo) {
2365
+ throw new Error(
2366
+ `Reward index ${params.rewardIndex} is out of range for Kamino farm ${params.farmContext.farmState.toBase58()}`,
2367
+ )
2368
+ }
2369
+
2370
+ return new TransactionInstruction({
2371
+ programId: KAMINO_FARMS_PROGRAM_ID,
2372
+ keys: [
2373
+ { pubkey: params.owner, isSigner: true, isWritable: true },
2374
+ { pubkey: params.farmContext.userState, isSigner: false, isWritable: true },
2375
+ { pubkey: params.farmContext.farmState, isSigner: false, isWritable: true },
2376
+ { pubkey: params.farmContext.farm.globalConfig, isSigner: false, isWritable: false },
2377
+ { pubkey: rewardInfo.rewardMint, isSigner: false, isWritable: false },
2378
+ { pubkey: params.rewardAta, isSigner: false, isWritable: true },
2379
+ { pubkey: rewardInfo.rewardsVault, isSigner: false, isWritable: true },
2380
+ {
2381
+ pubkey: getKaminoFarmsRewardsTreasuryVault(
2382
+ rewardInfo.rewardMint,
2383
+ params.farmContext.farm.globalConfig,
2384
+ KAMINO_FARMS_PROGRAM_ID,
2385
+ ),
2386
+ isSigner: false,
2387
+ isWritable: true,
2388
+ },
2389
+ { pubkey: params.farmContext.farm.farmVaultsAuthority, isSigner: false, isWritable: false },
2390
+ getOptionalReadonlyAccountMeta(params.farmContext.scopePrices, KAMINO_FARMS_PROGRAM_ID),
2391
+ { pubkey: rewardInfo.tokenProgram, isSigner: false, isWritable: false },
2392
+ ],
2393
+ data: encodeU64InstructionData(KAMINO_FARM_DISCRIMINATORS.harvestReward, params.rewardIndex),
2394
+ })
2395
+ }
2396
+
2397
+ async function buildKaminoFarmInstruction(
2398
+ ix: KaminoFarmInstruction,
2399
+ buckets: InstructionBuckets,
2400
+ setupContext: StrategySetupContext,
2401
+ ): Promise<void> {
2402
+ const farmContext = await resolveKaminoFarmContext(ix, setupContext)
2403
+
2404
+ switch (ix.action) {
2405
+ case KaminoFarmAction.INITIALIZE_USER: {
2406
+ if (farmContext.farm.isDelegated && !farmContext.delegatee.equals(setupContext.owner)) {
2407
+ throw new Error(`Delegated Kamino farm initialization is not supported for ${ix.farmState.toBase58()}`)
2408
+ }
2409
+
2410
+ await ensureKaminoFarmUserSetup({ farmContext, buckets, setupContext })
2411
+ return
2412
+ }
2413
+ case KaminoFarmAction.STAKE: {
2414
+ if (farmContext.farm.isDelegated) {
2415
+ throw new Error(`Kamino farm ${ix.farmState.toBase58()} is delegated and cannot be staked directly`)
2416
+ }
2417
+ if (isAutoManagePositionsEnabled(setupContext)) {
2418
+ await ensureKaminoFarmUserSetup({ farmContext, buckets, setupContext })
2419
+ await ensureTrackedTokenAccountSetup({
2420
+ tokenMint: farmContext.farm.underlyingMint,
2421
+ tokenAccount: farmContext.sourceAta,
2422
+ tokenProgram: farmContext.farm.tokenProgram,
2423
+ buckets,
2424
+ setupContext,
2425
+ })
2426
+ }
2427
+
2428
+ buckets.syncInstructions.push(
2429
+ buildKaminoFarmStakeInstructionRaw({
2430
+ owner: setupContext.owner,
2431
+ farmContext,
2432
+ amount: ix.amount,
2433
+ }),
2434
+ )
2435
+
2436
+ if (isAutoManagePositionsEnabled(setupContext)) {
2437
+ const state = await loadStrategySetupState(setupContext)
2438
+ if (state) {
2439
+ recordPlannedKaminoFarmPosition(state, {
2440
+ farmState: ix.farmState,
2441
+ userState: farmContext.userState,
2442
+ globalConfig: farmContext.farm.globalConfig,
2443
+ scopePrices: farmContext.scopePrices,
2444
+ })
2445
+ }
2446
+ }
2447
+ return
2448
+ }
2449
+ case KaminoFarmAction.UNSTAKE: {
2450
+ if (farmContext.farm.isDelegated) {
2451
+ throw new Error(`Kamino farm ${ix.farmState.toBase58()} is delegated and cannot be unstaked directly`)
2452
+ }
2453
+
2454
+ buckets.syncInstructions.push(
2455
+ buildKaminoFarmUnstakeInstructionRaw({
2456
+ owner: setupContext.owner,
2457
+ farmContext,
2458
+ stakeSharesScaled: ix.stakeSharesScaled,
2459
+ }),
2460
+ )
2461
+ return
2462
+ }
2463
+ case KaminoFarmAction.WITHDRAW_UNSTAKED_DEPOSITS: {
2464
+ if (farmContext.farm.isDelegated) {
2465
+ throw new Error(
2466
+ `Kamino farm ${ix.farmState.toBase58()} is delegated and cannot withdraw unstaked deposits directly`,
2467
+ )
2468
+ }
2469
+
2470
+ buckets.syncInstructions.push(
2471
+ buildKaminoFarmWithdrawUnstakedDepositsRawInstruction({
2472
+ owner: setupContext.owner,
2473
+ farmContext,
2474
+ }),
2475
+ )
2476
+ return
2477
+ }
2478
+ case KaminoFarmAction.HARVEST_REWARD: {
2479
+ const rewardInfo = farmContext.farm.rewardInfos[ix.rewardIndex]
2480
+ if (!rewardInfo) {
2481
+ throw new Error(`Reward index ${ix.rewardIndex} is out of range for Kamino farm ${ix.farmState.toBase58()}`)
2482
+ }
2483
+
2484
+ const rewardAta = getAssociatedTokenAddressSync(
2485
+ rewardInfo.rewardMint,
2486
+ setupContext.owner,
2487
+ true,
2488
+ rewardInfo.tokenProgram,
2489
+ )
2490
+ if (isAutoManagePositionsEnabled(setupContext)) {
2491
+ await ensureTrackedTokenAccountSetup({
2492
+ tokenMint: rewardInfo.rewardMint,
2493
+ tokenAccount: rewardAta,
2494
+ tokenProgram: rewardInfo.tokenProgram,
2495
+ buckets,
2496
+ setupContext,
2497
+ })
2498
+ }
2499
+
2500
+ buckets.setupInstructions.push(
2501
+ createAssociatedTokenAccountIdempotentInstruction(
2502
+ setupContext.signer,
2503
+ rewardAta,
2504
+ setupContext.owner,
2505
+ rewardInfo.rewardMint,
2506
+ rewardInfo.tokenProgram,
2507
+ ),
2508
+ )
2509
+ buckets.syncInstructions.push(
2510
+ buildKaminoFarmHarvestRewardRawInstruction({
2511
+ owner: setupContext.owner,
2512
+ farmContext,
2513
+ rewardIndex: ix.rewardIndex,
2514
+ rewardAta,
2515
+ }),
2516
+ )
2517
+ return
2518
+ }
2519
+ }
2520
+ }
2521
+
2522
+
1343
2523
  async function buildTitanInstruction(
1344
2524
  ix: TitanSwapInstruction,
1345
2525
  buckets: InstructionBuckets,
@@ -1378,100 +2558,43 @@ async function buildTitanInstruction(
1378
2558
  setupContext,
1379
2559
  })
1380
2560
 
2561
+ if (ix.addressLookupTableAddresses?.length) {
2562
+ buckets.addressLookupTableAddresses.push(...ix.addressLookupTableAddresses)
2563
+ }
1381
2564
  buckets.syncInstructions.push(ix.instruction)
1382
2565
  }
1383
2566
 
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
2567
+ /**
2568
+ * Loopscale account index mapping per instruction (from the Loopscale IDL).
2569
+ * Actions not listed here (create_loan, close_loan, update_weight_matrix,
2570
+ * create_strategy, close_strategy, lock_loan, unlock_loan, refinance_ledger,
2571
+ * update_strategy) have no token accounts to track — the on-chain hook handles
2572
+ * TrackLoopscaleLoan/UntrackLoopscaleLoan mutations.
2573
+ */
2574
+ const LOOPSCALE_TOKEN_INDICES: Partial<Record<LoopscaleAction, { mint: number; account: number; program: number }>> = {
2575
+ [LoopscaleAction.DEPOSIT_COLLATERAL]: { mint: 6, account: 4, program: 9 },
2576
+ [LoopscaleAction.BORROW_PRINCIPAL]: { mint: 6, account: 7, program: 10 },
2577
+ [LoopscaleAction.REPAY_PRINCIPAL]: { mint: 6, account: 7, program: 10 },
2578
+ [LoopscaleAction.WITHDRAW_COLLATERAL]: { mint: 7, account: 4, program: 8 },
2579
+ [LoopscaleAction.DEPOSIT_STRATEGY]: { mint: 4, account: 6, program: 8 },
2580
+ [LoopscaleAction.WITHDRAW_STRATEGY]: { mint: 4, account: 6, program: 9 },
2581
+ }
1403
2582
 
1404
- /** Build a single Loopscale instruction (loan or strategy). Extracts token accounts for tracking. */
1405
- async function buildLoopscaleInstruction(
1406
- ix: LoopscaleInstruction,
1407
- buckets: InstructionBuckets,
1408
- setupContext: StrategySetupContext,
1409
- ): Promise<void> {
1410
- 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")
1417
- }
1418
- await ensureTrackedTokenAccountSetup({ tokenMint, tokenAccount, tokenProgram, buckets, setupContext })
1419
- break
1420
- }
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")
1427
- }
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")
1437
- }
1438
- await ensureTrackedTokenAccountSetup({ tokenMint, tokenAccount, tokenProgram, buckets, setupContext })
1439
- break
1440
- }
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")
1447
- }
1448
- await ensureTrackedTokenAccountSetup({ tokenMint, tokenAccount, tokenProgram, buckets, setupContext })
1449
- break
1450
- }
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")
1460
- }
1461
- await ensureTrackedTokenAccountSetup({ tokenMint, tokenAccount, tokenProgram, buckets, setupContext })
1462
- break
1463
- }
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")
1470
- }
1471
- await ensureTrackedTokenAccountSetup({ tokenMint, tokenAccount, tokenProgram, buckets, setupContext })
1472
- break
2583
+ /** Build a single Loopscale instruction (loan or strategy). Extracts token accounts for tracking. */
2584
+ async function buildLoopscaleInstruction(
2585
+ ix: LoopscaleInstruction,
2586
+ buckets: InstructionBuckets,
2587
+ setupContext: StrategySetupContext,
2588
+ ): Promise<void> {
2589
+ const indices = LOOPSCALE_TOKEN_INDICES[ix.action]
2590
+ if (indices) {
2591
+ const tokenMint = ix.instruction.keys[indices.mint]?.pubkey
2592
+ const tokenAccount = ix.instruction.keys[indices.account]?.pubkey
2593
+ const tokenProgram = ix.instruction.keys[indices.program]?.pubkey
2594
+ if (!tokenMint || !tokenAccount || !tokenProgram) {
2595
+ throw new Error(`Loopscale ${ix.action} instruction is missing expected token accounts`)
1473
2596
  }
1474
- // create_strategy and close_strategy have no token accounts to track
2597
+ await ensureTrackedTokenAccountSetup({ tokenMint, tokenAccount, tokenProgram, buckets, setupContext })
1475
2598
  }
1476
2599
 
1477
2600
  buckets.syncInstructions.push(ix.instruction)
@@ -1513,6 +2636,45 @@ async function buildOrderbookInstruction(
1513
2636
  // Action Builders (one per VaultAction)
1514
2637
  // ============================================================================
1515
2638
 
2639
+ function createKaminoInitUserMetadataInstruction(owner: PublicKey): TransactionInstruction {
2640
+ const userMetadata = getKaminoUserMetadata(owner, KAMINO_LENDING_PROGRAM_ID)
2641
+ return initUserMetadata(
2642
+ { userLookupTable: PublicKey.default },
2643
+ {
2644
+ owner,
2645
+ feePayer: owner,
2646
+ userMetadata,
2647
+ referrerUserMetadata: KAMINO_LENDING_PROGRAM_ID,
2648
+ rent: SYSVAR_RENT_PUBKEY,
2649
+ systemProgram: SystemProgram.programId,
2650
+ },
2651
+ )
2652
+ }
2653
+
2654
+ function createKaminoInitObligationInstruction(params: {
2655
+ market: KaminoMarket
2656
+ owner: PublicKey
2657
+ }): TransactionInstruction {
2658
+ const lendingMarket = KAMINO_MARKETS[params.market]
2659
+ const obligation = getKaminoLendObligation(params.owner, lendingMarket, KAMINO_LENDING_PROGRAM_ID)
2660
+ const userMetadata = getKaminoUserMetadata(params.owner, KAMINO_LENDING_PROGRAM_ID)
2661
+
2662
+ return initObligation(
2663
+ { args: { tag: 0, id: 0 } },
2664
+ {
2665
+ obligationOwner: params.owner,
2666
+ feePayer: params.owner,
2667
+ obligation,
2668
+ lendingMarket,
2669
+ seed1Account: SystemProgram.programId,
2670
+ seed2Account: SystemProgram.programId,
2671
+ ownerUserMetadata: userMetadata,
2672
+ rent: SYSVAR_RENT_PUBKEY,
2673
+ systemProgram: SystemProgram.programId,
2674
+ },
2675
+ )
2676
+ }
2677
+
1516
2678
  async function buildInitUserMetadata(
1517
2679
  owner: PublicKey,
1518
2680
  connection: Connection,
@@ -1522,49 +2684,317 @@ async function buildInitUserMetadata(
1522
2684
  const userMetadataAccount = await connection.getAccountInfo(userMetadata)
1523
2685
  if (userMetadataAccount) return
1524
2686
 
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
- )
2687
+ syncInstructions.push(createKaminoInitUserMetadataInstruction(owner))
1538
2688
  }
1539
2689
 
1540
2690
  async function buildInitObligation(
1541
2691
  ix: MarketInstruction,
1542
2692
  owner: PublicKey,
1543
2693
  connection: Connection,
1544
- { syncInstructions }: InstructionBuckets,
2694
+ { syncInstructions, postInstructions }: InstructionBuckets,
2695
+ setupContext?: StrategySetupContext,
1545
2696
  ) {
1546
2697
  const lendingMarket = KAMINO_MARKETS[ix.market]
1547
2698
  const obligation = getKaminoLendObligation(owner, lendingMarket, KAMINO_LENDING_PROGRAM_ID)
1548
- const userMetadata = getKaminoUserMetadata(owner, KAMINO_LENDING_PROGRAM_ID)
1549
2699
  const obligationAccount = await connection.getAccountInfo(obligation)
1550
2700
  if (obligationAccount) return
1551
2701
 
1552
- syncInstructions.push(
1553
- initObligation(
1554
- { args: { tag: 0, id: 0 } },
1555
- {
1556
- obligationOwner: owner,
1557
- feePayer: owner,
1558
- obligation,
2702
+ syncInstructions.push(createKaminoInitObligationInstruction({ market: ix.market, owner }))
2703
+ if (setupContext && isAutoManagePositionsEnabled(setupContext)) {
2704
+ await queueKaminoObligationTrackingAfterInit({
2705
+ market: ix.market,
2706
+ obligation,
2707
+ postInstructions,
2708
+ setupContext,
2709
+ })
2710
+ }
2711
+ }
2712
+
2713
+ async function queueKaminoObligationTrackingAfterInit(params: {
2714
+ market: KaminoMarket
2715
+ obligation: PublicKey
2716
+ postInstructions: TransactionInstruction[]
2717
+ setupContext: StrategySetupContext
2718
+ }) {
2719
+ const state = await loadStrategySetupState(params.setupContext)
2720
+ if (!state || state.trackedKaminoObligations.has(params.obligation.toBase58())) {
2721
+ return
2722
+ }
2723
+
2724
+ const quotePath = resolveBestKaminoQuotePath({
2725
+ prices: state.prices,
2726
+ vaultUnderlyingMint: state.strategyVault.state.underlyingMint,
2727
+ })
2728
+ trackRequiredPriceIds(state.requiredPriceIds, quotePath.quotePriceId)
2729
+ const remainingAccountsAmount = 1n
2730
+ const obligationEntry: exponentVaults.KaminoObligationEntry = {
2731
+ obligation: params.obligation,
2732
+ lendingProgramId: KAMINO_LENDING_PROGRAM_ID,
2733
+ quotePriceId: quotePath.quotePriceId,
2734
+ reservePriceMappings: [],
2735
+ reserveFarmMappings: [],
2736
+ minPriceStatusFlags: 0,
2737
+ }
2738
+ params.postInstructions.push(
2739
+ state.strategyVault.ixWrapperManagerUpdatePosition({
2740
+ manager: params.setupContext.signer,
2741
+ update: exponentVaults.positionUpdate("AddKaminoObligationEntry", [obligationEntry] as [exponentVaults.KaminoObligationEntry]),
2742
+ remainingAccounts: buildTrackedAumRemainingAccounts(state, [
2743
+ { pubkey: params.obligation, isSigner: false, isWritable: false },
2744
+ ]),
2745
+ }),
2746
+ )
2747
+
2748
+ recordPlannedKaminoObligation(state, {
2749
+ obligation: params.obligation,
2750
+ quotePriceId: quotePath.quotePriceId,
2751
+ quoteInputMint: quotePath.quoteInputMint,
2752
+ reservePriceMappings: [],
2753
+ remainingAccountsAmount,
2754
+ minPriceStatusFlags: 0,
2755
+ })
2756
+ }
2757
+
2758
+ async function ensureKaminoObligationSetup(params: {
2759
+ ix: ReserveInstruction
2760
+ reserveContext: ReserveContext
2761
+ buckets: InstructionBuckets
2762
+ setupContext?: StrategySetupContext
2763
+ }) {
2764
+ const { ix, reserveContext, buckets, setupContext } = params
2765
+ if (!setupContext || !isAutoManagePositionsEnabled(setupContext)) {
2766
+ return
2767
+ }
2768
+
2769
+ const state = await loadStrategySetupState(setupContext)
2770
+ if (!state) {
2771
+ return
2772
+ }
2773
+
2774
+ // Ensure user metadata exists
2775
+ const userMetadata = getKaminoUserMetadata(setupContext.owner, KAMINO_LENDING_PROGRAM_ID)
2776
+ if (!(await accountExists(state, setupContext.connection, userMetadata))) {
2777
+ buckets.setupInstructions.push(
2778
+ await wrapVaultSignedSetupInstruction({
2779
+ instruction: createKaminoInitUserMetadataInstruction(setupContext.owner),
2780
+ setupContext,
2781
+ }),
2782
+ )
2783
+ state.existingAccounts.set(userMetadata.toBase58(), true)
2784
+ }
2785
+
2786
+ // Ensure obligation exists and is tracked
2787
+ const obligationKey = reserveContext.obligation.toBase58()
2788
+ const obligationExistsOnChain = await accountExists(state, setupContext.connection, reserveContext.obligation)
2789
+ const trackedObligation = state.trackedKaminoObligations.get(obligationKey)
2790
+
2791
+ if (!trackedObligation) {
2792
+ if (obligationExistsOnChain) {
2793
+ throw new Error(
2794
+ `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.`,
2795
+ )
2796
+ }
2797
+
2798
+ buckets.setupInstructions.push(
2799
+ await wrapVaultSignedSetupInstruction({
2800
+ instruction: createKaminoInitObligationInstruction({ market: ix.market, owner: setupContext.owner }),
2801
+ setupContext,
2802
+ }),
2803
+ )
2804
+ state.existingAccounts.set(obligationKey, true)
2805
+
2806
+ const quotePath = resolveBestKaminoQuotePath({
2807
+ prices: state.prices,
2808
+ vaultUnderlyingMint: state.strategyVault.state.underlyingMint,
2809
+ })
2810
+ const reservePriceMapping = {
2811
+ reserve: reserveContext.reservePubkey,
2812
+ reservePriceId: resolveKaminoReservePriceIdOrThrow({
2813
+ prices: state.prices,
2814
+ reserveMint: reserveContext.reserveAccount.liquidity.mintPubkey,
2815
+ quoteInputMint: quotePath.quoteInputMint,
2816
+ }),
2817
+ }
2818
+ trackRequiredPriceIds(state.requiredPriceIds, quotePath.quotePriceId)
2819
+ trackRequiredPriceIds(state.requiredPriceIds, reservePriceMapping.reservePriceId)
2820
+ const remainingAccountsAmount = BigInt(1 + 1)
2821
+ const obligationEntry: exponentVaults.KaminoObligationEntry = {
2822
+ obligation: reserveContext.obligation,
2823
+ lendingProgramId: KAMINO_LENDING_PROGRAM_ID,
2824
+ quotePriceId: quotePath.quotePriceId,
2825
+ reservePriceMappings: [reservePriceMapping],
2826
+ reserveFarmMappings: [],
2827
+ minPriceStatusFlags: 0,
2828
+ }
2829
+
2830
+ buckets.setupInstructions.push(
2831
+ state.strategyVault.ixWrapperManagerUpdatePosition({
2832
+ manager: setupContext.signer,
2833
+ update: exponentVaults.positionUpdate("AddKaminoObligationEntry", [obligationEntry] as [exponentVaults.KaminoObligationEntry]),
2834
+ remainingAccounts: buildTrackedAumRemainingAccounts(state, [
2835
+ { pubkey: reserveContext.obligation, isSigner: false, isWritable: false },
2836
+ { pubkey: reserveContext.reservePubkey, isSigner: false, isWritable: false },
2837
+ ]),
2838
+ }),
2839
+ )
2840
+
2841
+ recordPlannedKaminoObligation(state, {
2842
+ obligation: reserveContext.obligation,
2843
+ quotePriceId: quotePath.quotePriceId,
2844
+ quoteInputMint: quotePath.quoteInputMint,
2845
+ reservePriceMappings: [reservePriceMapping],
2846
+ remainingAccountsAmount,
2847
+ minPriceStatusFlags: 0,
2848
+ })
2849
+ return
2850
+ }
2851
+
2852
+ // Obligation exists and is tracked — ensure the reserve is mapped
2853
+ await ensureKaminoReserveMapped({
2854
+ state,
2855
+ reserveContext,
2856
+ setupContext,
2857
+ buckets,
2858
+ trackedObligation,
2859
+ })
2860
+ }
2861
+
2862
+ /** Ensure a Kamino reserve is mapped on an already-tracked obligation. */
2863
+ async function ensureKaminoReserveMapped(params: {
2864
+ state: StrategySetupState
2865
+ reserveContext: ReserveContext
2866
+ setupContext: StrategySetupContext
2867
+ buckets: InstructionBuckets
2868
+ trackedObligation: TrackedKaminoObligationState
2869
+ }) {
2870
+ const { state, reserveContext, setupContext, buckets, trackedObligation } = params
2871
+
2872
+ if (trackedObligation.mappedReserves.has(reserveContext.reservePubkey.toBase58())) {
2873
+ return
2874
+ }
2875
+
2876
+ const reservePriceMapping = {
2877
+ reserve: reserveContext.reservePubkey,
2878
+ reservePriceId: resolveKaminoReservePriceIdOrThrow({
2879
+ prices: state.prices,
2880
+ reserveMint: reserveContext.reserveAccount.liquidity.mintPubkey,
2881
+ quoteInputMint: trackedObligation.quoteInputMint,
2882
+ }),
2883
+ }
2884
+ trackRequiredPriceIds(state.requiredPriceIds, reservePriceMapping.reservePriceId)
2885
+
2886
+ buckets.setupInstructions.push(...await buildKaminoPositionFreshnessInstructions({
2887
+ reservePubkey: reserveContext.reservePubkey,
2888
+ reserveAccount: reserveContext.reserveAccount,
2889
+ lendingMarket: reserveContext.lendingMarket,
2890
+ obligation: reserveContext.obligation,
2891
+ connection: setupContext.connection,
2892
+ }))
2893
+
2894
+ buckets.setupInstructions.push(
2895
+ state.strategyVault.ixWrapperManagerUpdatePosition({
2896
+ manager: setupContext.signer,
2897
+ update: exponentVaults.positionUpdate("UpsertKaminoObligationReservePriceMappings", {
2898
+ obligation: reserveContext.obligation,
2899
+ reservePriceMappings: [reservePriceMapping],
2900
+ reserveFarmMappings: [],
2901
+ }),
2902
+ remainingAccounts: buildTrackedAumRemainingAccounts(state, [
2903
+ { pubkey: reserveContext.reservePubkey, isSigner: false, isWritable: false },
2904
+ ]),
2905
+ }),
2906
+ )
2907
+
2908
+ recordPlannedKaminoReserveMappings(state, {
2909
+ obligation: reserveContext.obligation,
2910
+ quoteInputMint: trackedObligation.quoteInputMint,
2911
+ reservePriceMappings: [reservePriceMapping],
2912
+ })
2913
+ }
2914
+
2915
+ async function buildKaminoPositionFreshnessInstructions({
2916
+ reservePubkey,
2917
+ reserveAccount,
2918
+ lendingMarket,
2919
+ obligation,
2920
+ connection,
2921
+ }: {
2922
+ reservePubkey: PublicKey
2923
+ reserveAccount: Reserve
2924
+ lendingMarket: PublicKey
2925
+ obligation: PublicKey
2926
+ connection: Connection
2927
+ }): Promise<TransactionInstruction[]> {
2928
+ const instructions: TransactionInstruction[] = []
2929
+ const defaultKey = PublicKey.default
2930
+ const oracleOrSentinel = (key: PublicKey) =>
2931
+ key.equals(defaultKey) ? KAMINO_LENDING_PROGRAM_ID : key
2932
+
2933
+ const obligationState = await Obligation.fetch(connection, obligation)
2934
+ const otherReservePubkeys: PublicKey[] = []
2935
+ if (obligationState) {
2936
+ for (const deposit of obligationState.deposits) {
2937
+ if (!deposit.depositReserve.equals(defaultKey) && !deposit.depositReserve.equals(reservePubkey)) {
2938
+ otherReservePubkeys.push(deposit.depositReserve)
2939
+ }
2940
+ }
2941
+ for (const borrow of obligationState.borrows) {
2942
+ if (!borrow.borrowReserve.equals(defaultKey) && !borrow.borrowReserve.equals(reservePubkey)) {
2943
+ otherReservePubkeys.push(borrow.borrowReserve)
2944
+ }
2945
+ }
2946
+ }
2947
+
2948
+ const otherReserves: Array<{ pubkey: PublicKey; account: Reserve }> = []
2949
+ for (const otherReservePubkey of otherReservePubkeys) {
2950
+ const otherReserveAccount = await Reserve.fetch(connection, otherReservePubkey)
2951
+ if (!otherReserveAccount) {
2952
+ continue
2953
+ }
2954
+ otherReserves.push({ pubkey: otherReservePubkey, account: otherReserveAccount })
2955
+ }
2956
+
2957
+ for (const { pubkey, account } of otherReserves) {
2958
+ const tokenInfo = account.config.tokenInfo
2959
+ instructions.push(
2960
+ refreshReserve({
2961
+ reserve: pubkey,
1559
2962
  lendingMarket,
1560
- seed1Account: SystemProgram.programId,
1561
- seed2Account: SystemProgram.programId,
1562
- ownerUserMetadata: userMetadata,
1563
- rent: SYSVAR_RENT_PUBKEY,
1564
- systemProgram: SystemProgram.programId,
1565
- },
1566
- ),
2963
+ pythOracle: oracleOrSentinel(tokenInfo.pythConfiguration.price),
2964
+ switchboardPriceOracle: oracleOrSentinel(tokenInfo.switchboardConfiguration.priceAggregator),
2965
+ switchboardTwapOracle: oracleOrSentinel(tokenInfo.switchboardConfiguration.twapAggregator),
2966
+ scopePrices: oracleOrSentinel(tokenInfo.scopeConfiguration.priceFeed),
2967
+ }),
2968
+ )
2969
+ }
2970
+
2971
+ const tokenInfo = reserveAccount.config.tokenInfo
2972
+ instructions.push(
2973
+ refreshReserve({
2974
+ reserve: reservePubkey,
2975
+ lendingMarket,
2976
+ pythOracle: oracleOrSentinel(tokenInfo.pythConfiguration.price),
2977
+ switchboardPriceOracle: oracleOrSentinel(tokenInfo.switchboardConfiguration.priceAggregator),
2978
+ switchboardTwapOracle: oracleOrSentinel(tokenInfo.switchboardConfiguration.twapAggregator),
2979
+ scopePrices: oracleOrSentinel(tokenInfo.scopeConfiguration.priceFeed),
2980
+ }),
1567
2981
  )
2982
+
2983
+ const refreshObligationIx = refreshObligation({ lendingMarket, obligation })
2984
+ if (obligationState) {
2985
+ const depositReserves = obligationState.deposits
2986
+ .map((deposit) => deposit.depositReserve)
2987
+ .filter((reserve) => !reserve.equals(defaultKey))
2988
+ const borrowReserves = obligationState.borrows
2989
+ .map((borrow) => borrow.borrowReserve)
2990
+ .filter((reserve) => !reserve.equals(defaultKey))
2991
+ for (const reserve of [...depositReserves, ...borrowReserves]) {
2992
+ refreshObligationIx.keys.push({ pubkey: reserve, isSigner: false, isWritable: false })
2993
+ }
2994
+ }
2995
+ instructions.push(refreshObligationIx)
2996
+
2997
+ return instructions
1568
2998
  }
1569
2999
 
1570
3000
  async function buildDeposit(
@@ -1573,8 +3003,10 @@ async function buildDeposit(
1573
3003
  connection: Connection,
1574
3004
  signer: PublicKey,
1575
3005
  { setupInstructions, syncInstructions, preInstructions, postInstructions }: InstructionBuckets,
3006
+ setupContext?: StrategySetupContext,
1576
3007
  ) {
1577
3008
  const ctx = await resolveReserveContext(ix, owner, connection)
3009
+ await ensureKaminoObligationSetup({ ix, reserveContext: ctx, buckets: { setupInstructions, syncInstructions, preInstructions, postInstructions, signers: [], addressLookupTableAddresses: [] }, setupContext })
1578
3010
  const refreshes = await buildRefreshInstructions({
1579
3011
  ...ctx, owner, farmMode: FARM_COLLATERAL, signer, connection, needsScopeRefresh: false,
1580
3012
  })
@@ -1628,8 +3060,10 @@ async function buildWithdraw(
1628
3060
  connection: Connection,
1629
3061
  signer: PublicKey,
1630
3062
  { setupInstructions, syncInstructions, preInstructions, postInstructions }: InstructionBuckets,
3063
+ setupContext?: StrategySetupContext,
1631
3064
  ) {
1632
3065
  const ctx = await resolveReserveContext(ix, owner, connection)
3066
+ await ensureKaminoObligationSetup({ ix, reserveContext: ctx, buckets: { setupInstructions, syncInstructions, preInstructions, postInstructions, signers: [], addressLookupTableAddresses: [] }, setupContext })
1633
3067
  const refreshes = await buildRefreshInstructions({
1634
3068
  ...ctx, owner, farmMode: FARM_COLLATERAL, signer, connection, needsScopeRefresh: true,
1635
3069
  })
@@ -1690,8 +3124,10 @@ async function buildBorrow(
1690
3124
  connection: Connection,
1691
3125
  signer: PublicKey,
1692
3126
  { setupInstructions, syncInstructions, preInstructions, postInstructions }: InstructionBuckets,
3127
+ setupContext?: StrategySetupContext,
1693
3128
  ) {
1694
3129
  const ctx = await resolveReserveContext(ix, owner, connection)
3130
+ await ensureKaminoObligationSetup({ ix, reserveContext: ctx, buckets: { setupInstructions, syncInstructions, preInstructions, postInstructions, signers: [], addressLookupTableAddresses: [] }, setupContext })
1695
3131
  const refreshes = await buildRefreshInstructions({
1696
3132
  ...ctx, owner, farmMode: FARM_DEBT, signer, connection, needsScopeRefresh: true,
1697
3133
  })
@@ -1743,8 +3179,10 @@ async function buildRepay(
1743
3179
  connection: Connection,
1744
3180
  signer: PublicKey,
1745
3181
  { setupInstructions, syncInstructions, preInstructions, postInstructions }: InstructionBuckets,
3182
+ setupContext?: StrategySetupContext,
1746
3183
  ) {
1747
3184
  const ctx = await resolveReserveContext(ix, owner, connection)
3185
+ await ensureKaminoObligationSetup({ ix, reserveContext: ctx, buckets: { setupInstructions, syncInstructions, preInstructions, postInstructions, signers: [], addressLookupTableAddresses: [] }, setupContext })
1748
3186
  const refreshes = await buildRefreshInstructions({
1749
3187
  ...ctx, owner, farmMode: FARM_DEBT, signer, connection, needsScopeRefresh: false,
1750
3188
  })
@@ -2023,17 +3461,13 @@ export const orderbookAction = {
2023
3461
  /**
2024
3462
  * Post a limit order on the orderbook.
2025
3463
  * @param params - Order parameters
2026
- * @param params.offerIdx - Required offer index for position tracking. Must be unique per trader.
2027
3464
  */
2028
3465
  postOffer(params: {
2029
3466
  orderbook: PublicKey
2030
3467
  direction: OrderbookTradeDirection
2031
3468
  priceApy: number
2032
3469
  amount: bigint
2033
- /** Required offer index for position tracking. Must be unique per trader. */
2034
- offerIdx: number
2035
3470
  offerOption?: OrderbookOfferOption
2036
- virtualOffer?: boolean
2037
3471
  expirySeconds?: number
2038
3472
  mode?: OrderbookInstructionMode
2039
3473
  }): OrderbookPostOfferInstruction {
@@ -2044,9 +3478,7 @@ export const orderbookAction = {
2044
3478
  direction: params.direction,
2045
3479
  priceApy: params.priceApy,
2046
3480
  amount: params.amount,
2047
- offerIdx: params.offerIdx,
2048
3481
  offerOption: params.offerOption,
2049
- virtualOffer: params.virtualOffer,
2050
3482
  expirySeconds: params.expirySeconds,
2051
3483
  }
2052
3484
  },
@@ -2237,7 +3669,8 @@ export const coreAction = {
2237
3669
 
2238
3670
  /**
2239
3671
  * Initialize yield position for the Squads vault (owner).
2240
- * Required before buying YT or depositing YT.
3672
+ * Required before buying YT or depositing YT. When `autoManagePositions`
3673
+ * is enabled, the SDK also tracks the new yield position automatically.
2241
3674
  */
2242
3675
  initializeYieldPosition(params: { vault: PublicKey }): CoreInitializeYieldPositionInstruction {
2243
3676
  return {
@@ -2311,7 +3744,7 @@ export async function createOrderbookSyncTransaction({
2311
3744
  owner: PublicKey
2312
3745
  connection: Connection
2313
3746
  policyPda?: PublicKey
2314
- vaultPda: PublicKey
3747
+ vaultPda?: PublicKey
2315
3748
  signer: PublicKey
2316
3749
  accountIndex?: number
2317
3750
  constraintIndices?: number[]
@@ -2323,6 +3756,7 @@ export async function createOrderbookSyncTransaction({
2323
3756
  squadsProgram?: PublicKey
2324
3757
  env?: Environment
2325
3758
  }): Promise<VaultSyncTransactionResult> {
3759
+ vaultPda ??= owner
2326
3760
  const { setupInstructions, syncInstructions, preInstructions, postInstructions, signers, addressLookupTableAddresses } = await buildOrderbookInstructions(
2327
3761
  instructions,
2328
3762
  owner,
@@ -2445,6 +3879,7 @@ async function buildPostOffer(
2445
3879
  ) {
2446
3880
  const offerType = directionToOfferType(ix.direction)
2447
3881
  const option = ix.offerOption ?? "FillOrKill"
3882
+ const offerIdx = orderbook.getNextOfferIndex()
2448
3883
 
2449
3884
  if (setupContext) {
2450
3885
  await ensureOrderbookPositionSetup(orderbook, buckets, setupContext)
@@ -2480,10 +3915,10 @@ async function buildPostOffer(
2480
3915
  amount: ix.amount,
2481
3916
  offerType,
2482
3917
  offerOption: offerOptions(option, [false]),
2483
- virtualOffer: ix.virtualOffer ?? false,
3918
+ virtualOffer: true,
2484
3919
  expirySeconds: ix.expirySeconds ?? 3600,
2485
3920
  mintSy: orderbook.vault.mintSy,
2486
- offerIdx: ix.offerIdx,
3921
+ offerIdx,
2487
3922
  }),
2488
3923
  )
2489
3924
  return
@@ -2496,10 +3931,10 @@ async function buildPostOffer(
2496
3931
  amount: ix.amount,
2497
3932
  offerType,
2498
3933
  offerOption: offerOptions(option, [false]),
2499
- virtualOffer: ix.virtualOffer ?? false,
3934
+ virtualOffer: true,
2500
3935
  expirySeconds: ix.expirySeconds ?? 3600,
2501
3936
  mintSy: orderbook.vault.mintSy,
2502
- offerIdx: ix.offerIdx,
3937
+ offerIdx,
2503
3938
  })
2504
3939
 
2505
3940
  buckets.preInstructions.push(...setupIxs)
@@ -2616,6 +4051,52 @@ async function buildWithdrawFunds(
2616
4051
  // Core Instruction Builders (Strip/Merge)
2617
4052
  // ============================================================================
2618
4053
 
4054
+ async function queueYieldPositionTrackingAfterInit(params: {
4055
+ vault: Vault
4056
+ setupContext: StrategySetupContext
4057
+ postInstructions: TransactionInstruction[]
4058
+ }) {
4059
+ const state = await loadStrategySetupState(params.setupContext)
4060
+ if (!state || state.trackedYieldVaults.has(params.vault.selfAddress.toBase58())) {
4061
+ return
4062
+ }
4063
+
4064
+ const yieldPosition = params.vault.pda.yieldPosition({
4065
+ owner: params.setupContext.owner,
4066
+ vault: params.vault.selfAddress,
4067
+ })
4068
+ const priceIdPt = resolvePriceIdFromMintToUnderlyingOrThrow({
4069
+ prices: state.prices,
4070
+ sourceMint: params.vault.mintPt,
4071
+ targetMint: state.strategyVault.state.underlyingMint,
4072
+ label: `yield position setup (${params.vault.selfAddress.toBase58()})`,
4073
+ })
4074
+ trackRequiredPriceIds(state.requiredPriceIds, priceIdPt)
4075
+
4076
+ params.postInstructions.push(
4077
+ state.strategyVault.ixWrapperManageVaultSettings({
4078
+ manager: params.setupContext.signer,
4079
+ actions: [
4080
+ exponentVaults.vaultSettingsAction("AddYieldPositionEntry", {
4081
+ yieldPosition,
4082
+ vault: params.vault.selfAddress,
4083
+ priceIdPt,
4084
+ }),
4085
+ ],
4086
+ remainingAccounts: [
4087
+ { pubkey: params.vault.selfAddress, isSigner: false, isWritable: false },
4088
+ { pubkey: yieldPosition, isSigner: false, isWritable: false },
4089
+ ],
4090
+ }),
4091
+ )
4092
+
4093
+ recordPlannedYieldPosition(state, {
4094
+ yieldPosition,
4095
+ vault: params.vault.selfAddress,
4096
+ priceIdPt,
4097
+ })
4098
+ }
4099
+
2619
4100
  /** Build a single core instruction (strip/merge) */
2620
4101
  async function buildCoreInstruction(
2621
4102
  ix: CoreInstruction,
@@ -2667,7 +4148,7 @@ async function buildCoreInstruction(
2667
4148
  await buildDepositYt(ix, vault, owner, buckets)
2668
4149
  break
2669
4150
  case CoreAction.INITIALIZE_YIELD_POSITION:
2670
- await buildInitializeYieldPosition(ix, vault, owner, buckets)
4151
+ await buildInitializeYieldPosition(ix, vault, owner, buckets, setupContext)
2671
4152
  break
2672
4153
  }
2673
4154
  }
@@ -2676,10 +4157,18 @@ async function buildInitializeYieldPosition(
2676
4157
  ix: CoreInitializeYieldPositionInstruction,
2677
4158
  vault: Vault,
2678
4159
  owner: PublicKey,
2679
- { syncInstructions }: InstructionBuckets,
4160
+ { syncInstructions, postInstructions }: InstructionBuckets,
4161
+ setupContext?: StrategySetupContext,
2680
4162
  ) {
2681
4163
  const initIx = vault.ixInitializeYieldPosition({ owner })
2682
4164
  syncInstructions.push(initIx)
4165
+ if (setupContext && isAutoManagePositionsEnabled(setupContext)) {
4166
+ await queueYieldPositionTrackingAfterInit({
4167
+ vault,
4168
+ setupContext,
4169
+ postInstructions,
4170
+ })
4171
+ }
2683
4172
  }
2684
4173
 
2685
4174
  async function buildStrip(
@@ -2859,9 +4348,17 @@ export const titanAction = {
2859
4348
  /**
2860
4349
  * Wrap a Titan SwapRouteV2 instruction for vault execution.
2861
4350
  * @param params.instruction - Pre-built TransactionInstruction from Titan's router API
4351
+ * @param params.addressLookupTableAddresses - Optional ALT addresses returned by Titan for this route
2862
4352
  */
2863
- swap(params: { instruction: TransactionInstruction }): TitanSwapInstruction {
2864
- return { action: TitanAction.SWAP, instruction: params.instruction }
4353
+ swap(params: {
4354
+ instruction: TransactionInstruction
4355
+ addressLookupTableAddresses?: PublicKey[]
4356
+ }): TitanSwapInstruction {
4357
+ return {
4358
+ action: TitanAction.SWAP,
4359
+ instruction: params.instruction,
4360
+ addressLookupTableAddresses: params.addressLookupTableAddresses,
4361
+ }
2865
4362
  },
2866
4363
  }
2867
4364
 
@@ -2870,7 +4367,7 @@ export const titanAction = {
2870
4367
  // ============================================================================
2871
4368
 
2872
4369
  /**
2873
- * Builder for Loopscale vault action descriptors.
4370
+ * Builder for Loopscale action descriptors used in Exponent sync transactions.
2874
4371
  *
2875
4372
  * Loans (BORROWER side): create/close loan, deposit/withdraw collateral, borrow/repay principal.
2876
4373
  * Strategies (LENDER side): create/close strategy, deposit/withdraw into strategy.
@@ -2990,7 +4487,9 @@ export const clmmAction = {
2990
4487
  /**
2991
4488
  * Create a new LP position on the CLMM with a specified tick range.
2992
4489
  * The LP position keypair is generated internally — retrieve it from
2993
- * `result.signers[0]` after calling `createVaultSyncTransaction`.
4490
+ * `result.signers[0]` after calling `createVaultSyncTransaction`. When
4491
+ * `autoManagePositions` is enabled, the SDK also tracks the new LP
4492
+ * position automatically after the deposit sync instruction succeeds.
2994
4493
  *
2995
4494
  * @param params.market - CLMM MarketThree account address
2996
4495
  * @param params.ptInIntent - Maximum PT to deposit
@@ -3158,6 +4657,86 @@ export { SwapDirection }
3158
4657
  /** Cache for loaded MarketThree instances to avoid redundant fetches. */
3159
4658
  const marketThreeCache = new Map<string, MarketThree>()
3160
4659
 
4660
+ async function ensureTrackedClmmPosition(params: {
4661
+ lpPosition: PublicKey
4662
+ setupContext?: StrategySetupContext
4663
+ }) {
4664
+ if (!params.setupContext || !isAutoManagePositionsEnabled(params.setupContext)) {
4665
+ return
4666
+ }
4667
+
4668
+ const state = await loadStrategySetupState(params.setupContext)
4669
+ if (!state || state.trackedClmmPositions.has(params.lpPosition.toBase58())) {
4670
+ return
4671
+ }
4672
+
4673
+ const existsOnChain = await accountExists(state, params.setupContext.connection, params.lpPosition)
4674
+ if (existsOnChain) {
4675
+ throw new Error(
4676
+ `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.`,
4677
+ )
4678
+ }
4679
+
4680
+ throw new Error(
4681
+ `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.`,
4682
+ )
4683
+ }
4684
+
4685
+ async function queueClmmPositionTrackingAfterDeposit(params: {
4686
+ market: MarketThree
4687
+ lpPosition: PublicKey
4688
+ postInstructions: TransactionInstruction[]
4689
+ setupContext?: StrategySetupContext
4690
+ }) {
4691
+ if (!params.setupContext || !isAutoManagePositionsEnabled(params.setupContext)) {
4692
+ return
4693
+ }
4694
+
4695
+ const state = await loadStrategySetupState(params.setupContext)
4696
+ if (!state || state.trackedClmmPositions.has(params.lpPosition.toBase58())) {
4697
+ return
4698
+ }
4699
+
4700
+ const priceIdPt = resolvePriceIdFromMintToUnderlyingOrThrow({
4701
+ prices: state.prices,
4702
+ sourceMint: params.market.mintPt,
4703
+ targetMint: state.strategyVault.state.underlyingMint,
4704
+ label: `CLMM PT position setup (${params.market.selfAddress.toBase58()})`,
4705
+ })
4706
+ trackRequiredPriceIds(state.requiredPriceIds, priceIdPt)
4707
+ const priceIdSy = resolvePriceIdFromMintToUnderlyingOrThrow({
4708
+ prices: state.prices,
4709
+ sourceMint: params.market.mintSy,
4710
+ targetMint: state.strategyVault.state.underlyingMint,
4711
+ label: `CLMM SY position setup (${params.market.selfAddress.toBase58()})`,
4712
+ })
4713
+ trackRequiredPriceIds(state.requiredPriceIds, priceIdSy)
4714
+
4715
+ params.postInstructions.push(
4716
+ state.strategyVault.ixWrapperManageVaultSettings({
4717
+ manager: params.setupContext.signer,
4718
+ actions: [exponentVaults.vaultSettingsAction("AddClmmPositionEntry", [{
4719
+ lpPosition: params.lpPosition,
4720
+ market: params.market.selfAddress,
4721
+ priceIdPt,
4722
+ priceIdSy,
4723
+ }])],
4724
+ remainingAccounts: [
4725
+ { pubkey: params.lpPosition, isSigner: false, isWritable: false },
4726
+ { pubkey: params.market.selfAddress, isSigner: false, isWritable: false },
4727
+ ],
4728
+ }),
4729
+ )
4730
+
4731
+ recordPlannedClmmPosition(state, {
4732
+ lpPosition: params.lpPosition,
4733
+ market: params.market.selfAddress,
4734
+ priceIdPt,
4735
+ priceIdSy,
4736
+ ticksKey: params.market.ticksKey,
4737
+ })
4738
+ }
4739
+
3161
4740
  /**
3162
4741
  * Resolve a high-level CLMM action descriptor into raw Solana instructions.
3163
4742
  * Loads the MarketThree from cache, derives token accounts from the vault
@@ -3201,12 +4780,14 @@ async function buildClmmInstruction(
3201
4780
 
3202
4781
  switch (ix.action) {
3203
4782
  case ClmmAction.DEPOSIT_LIQUIDITY:
3204
- buildClmmDepositLiquidity(ix, market, owner, buckets)
4783
+ await buildClmmDepositLiquidity(ix, market, owner, buckets, setupContext)
3205
4784
  break
3206
4785
  case ClmmAction.ADD_LIQUIDITY:
4786
+ await ensureTrackedClmmPosition({ lpPosition: ix.lpPosition, setupContext })
3207
4787
  buildClmmAddLiquidity(ix, market, owner, buckets)
3208
4788
  break
3209
4789
  case ClmmAction.WITHDRAW_LIQUIDITY:
4790
+ await ensureTrackedClmmPosition({ lpPosition: ix.lpPosition, setupContext })
3210
4791
  buildClmmWithdrawLiquidity(ix, market, owner, buckets)
3211
4792
  break
3212
4793
  case ClmmAction.TRADE_PT:
@@ -3225,6 +4806,7 @@ async function buildClmmInstruction(
3225
4806
  await buildClmmSellYt(ix, market, owner, buckets, setupContext)
3226
4807
  break
3227
4808
  case ClmmAction.CLAIM_FARM_EMISSION:
4809
+ await ensureTrackedClmmPosition({ lpPosition: ix.lpPosition, setupContext })
3228
4810
  buildClmmClaimFarmEmission(ix, market, owner, buckets)
3229
4811
  break
3230
4812
  }
@@ -3234,12 +4816,13 @@ async function buildClmmInstruction(
3234
4816
  * Create a new LP position. Generates the keypair internally and adds it
3235
4817
  * to `buckets.signers` so consumers can include it in the transaction.
3236
4818
  */
3237
- function buildClmmDepositLiquidity(
4819
+ async function buildClmmDepositLiquidity(
3238
4820
  ix: ClmmDepositLiquidityInstruction,
3239
4821
  market: MarketThree,
3240
4822
  owner: PublicKey,
3241
4823
  buckets: InstructionBuckets,
3242
- ): void {
4824
+ setupContext?: StrategySetupContext,
4825
+ ): Promise<void> {
3243
4826
  const ptSrc = getAssociatedTokenAddressSync(market.mintPt, owner, true, TOKEN_PROGRAM_ID)
3244
4827
  const sySrc = getAssociatedTokenAddressSync(market.mintSy, owner, true, TOKEN_PROGRAM_ID)
3245
4828
 
@@ -3255,6 +4838,12 @@ function buildClmmDepositLiquidity(
3255
4838
 
3256
4839
  buckets.syncInstructions.push(depositIx)
3257
4840
  buckets.signers.push(lpPositionKeypair)
4841
+ await queueClmmPositionTrackingAfterDeposit({
4842
+ market,
4843
+ lpPosition: lpPositionKeypair.publicKey,
4844
+ postInstructions: buckets.postInstructions,
4845
+ setupContext,
4846
+ })
3258
4847
  }
3259
4848
 
3260
4849
  /** Add liquidity to an existing LP position. */
@@ -3396,6 +4985,7 @@ async function buildClmmBuyYt(
3396
4985
 
3397
4986
  const { ixs, setupIxs } = market.ixBuyYt({
3398
4987
  trader: owner,
4988
+ payer: setupContext?.signer,
3399
4989
  ytOut: ix.ytOut,
3400
4990
  maxSyIn: ix.maxSyIn,
3401
4991
  lnImpliedApyLimit: ix.lnImpliedApyLimit,
@@ -3426,6 +5016,7 @@ async function buildClmmSellYt(
3426
5016
 
3427
5017
  const { ixs, setupIxs } = market.ixSellYt({
3428
5018
  trader: owner,
5019
+ payer: setupContext?.signer,
3429
5020
  ytIn: ix.ytIn,
3430
5021
  minSyOut: ix.minSyOut,
3431
5022
  lnImpliedApyLimit: ix.lnImpliedApyLimit,