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