@hobba-io/core 0.1.0
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/LICENSE +15 -0
- package/README.md +87 -0
- package/dist/amountUtils.d.ts +15 -0
- package/dist/amountUtils.d.ts.map +1 -0
- package/dist/amountUtils.js +59 -0
- package/dist/amountUtils.js.map +1 -0
- package/dist/constants.d.ts +148 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +231 -0
- package/dist/constants.js.map +1 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +29 -0
- package/dist/errors.js.map +1 -0
- package/dist/executeAction.d.ts +36 -0
- package/dist/executeAction.d.ts.map +1 -0
- package/dist/executeAction.js +606 -0
- package/dist/executeAction.js.map +1 -0
- package/dist/executeDepositBorrow.d.ts +49 -0
- package/dist/executeDepositBorrow.d.ts.map +1 -0
- package/dist/executeDepositBorrow.js +444 -0
- package/dist/executeDepositBorrow.js.map +1 -0
- package/dist/idl/hobba.json +5072 -0
- package/dist/idl.d.ts +783 -0
- package/dist/idl.d.ts.map +1 -0
- package/dist/idl.js +7 -0
- package/dist/idl.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/juplend/accountResolvers.d.ts +106 -0
- package/dist/juplend/accountResolvers.d.ts.map +1 -0
- package/dist/juplend/accountResolvers.js +148 -0
- package/dist/juplend/accountResolvers.js.map +1 -0
- package/dist/juplend/operateRemaining.d.ts +11 -0
- package/dist/juplend/operateRemaining.d.ts.map +1 -0
- package/dist/juplend/operateRemaining.js +46 -0
- package/dist/juplend/operateRemaining.js.map +1 -0
- package/dist/juplend/positionInit.d.ts +34 -0
- package/dist/juplend/positionInit.d.ts.map +1 -0
- package/dist/juplend/positionInit.js +56 -0
- package/dist/juplend/positionInit.js.map +1 -0
- package/dist/juplend/positionParser.d.ts +8 -0
- package/dist/juplend/positionParser.d.ts.map +1 -0
- package/dist/juplend/positionParser.js +30 -0
- package/dist/juplend/positionParser.js.map +1 -0
- package/dist/juplend/sdkApi.d.ts +39 -0
- package/dist/juplend/sdkApi.d.ts.map +1 -0
- package/dist/juplend/sdkApi.js +68 -0
- package/dist/juplend/sdkApi.js.map +1 -0
- package/dist/juplend/tickMath.d.ts +19 -0
- package/dist/juplend/tickMath.d.ts.map +1 -0
- package/dist/juplend/tickMath.js +91 -0
- package/dist/juplend/tickMath.js.map +1 -0
- package/dist/juplend/tickUtils.d.ts +20 -0
- package/dist/juplend/tickUtils.d.ts.map +1 -0
- package/dist/juplend/tickUtils.js +115 -0
- package/dist/juplend/tickUtils.js.map +1 -0
- package/dist/juplend/vaultConfigParser.d.ts +25 -0
- package/dist/juplend/vaultConfigParser.d.ts.map +1 -0
- package/dist/juplend/vaultConfigParser.js +38 -0
- package/dist/juplend/vaultConfigParser.js.map +1 -0
- package/dist/kamino/accountResolvers.d.ts +110 -0
- package/dist/kamino/accountResolvers.d.ts.map +1 -0
- package/dist/kamino/accountResolvers.js +220 -0
- package/dist/kamino/accountResolvers.js.map +1 -0
- package/dist/kamino/obligationParser.d.ts +13 -0
- package/dist/kamino/obligationParser.d.ts.map +1 -0
- package/dist/kamino/obligationParser.js +92 -0
- package/dist/kamino/obligationParser.js.map +1 -0
- package/dist/kamino/refreshInstructions.d.ts +4 -0
- package/dist/kamino/refreshInstructions.d.ts.map +1 -0
- package/dist/kamino/refreshInstructions.js +32 -0
- package/dist/kamino/refreshInstructions.js.map +1 -0
- package/dist/limits.d.ts +87 -0
- package/dist/limits.d.ts.map +1 -0
- package/dist/limits.js +174 -0
- package/dist/limits.js.map +1 -0
- package/dist/marketConfig.d.ts +35 -0
- package/dist/marketConfig.d.ts.map +1 -0
- package/dist/marketConfig.js +85 -0
- package/dist/marketConfig.js.map +1 -0
- package/dist/oracle/scopePrice.d.ts +7 -0
- package/dist/oracle/scopePrice.d.ts.map +1 -0
- package/dist/oracle/scopePrice.js +31 -0
- package/dist/oracle/scopePrice.js.map +1 -0
- package/dist/pdas.d.ts +33 -0
- package/dist/pdas.d.ts.map +1 -0
- package/dist/pdas.js +116 -0
- package/dist/pdas.js.map +1 -0
- package/dist/position.d.ts +45 -0
- package/dist/position.d.ts.map +1 -0
- package/dist/position.js +68 -0
- package/dist/position.js.map +1 -0
- package/dist/positionMetrics.d.ts +108 -0
- package/dist/positionMetrics.d.ts.map +1 -0
- package/dist/positionMetrics.js +238 -0
- package/dist/positionMetrics.js.map +1 -0
- package/dist/program.d.ts +12 -0
- package/dist/program.d.ts.map +1 -0
- package/dist/program.js +24 -0
- package/dist/program.js.map +1 -0
- package/dist/quote.d.ts +90 -0
- package/dist/quote.d.ts.map +1 -0
- package/dist/quote.js +198 -0
- package/dist/quote.js.map +1 -0
- package/dist/rates/borrowRates.d.ts +44 -0
- package/dist/rates/borrowRates.d.ts.map +1 -0
- package/dist/rates/borrowRates.js +229 -0
- package/dist/rates/borrowRates.js.map +1 -0
- package/dist/rates/collateralInfo.d.ts +21 -0
- package/dist/rates/collateralInfo.d.ts.map +1 -0
- package/dist/rates/collateralInfo.js +104 -0
- package/dist/rates/collateralInfo.js.map +1 -0
- package/dist/rates/constants.d.ts +22 -0
- package/dist/rates/constants.d.ts.map +1 -0
- package/dist/rates/constants.js +70 -0
- package/dist/rates/constants.js.map +1 -0
- package/dist/rates/index.d.ts +11 -0
- package/dist/rates/index.d.ts.map +1 -0
- package/dist/rates/index.js +11 -0
- package/dist/rates/index.js.map +1 -0
- package/dist/rates/kaminoApi.d.ts +24 -0
- package/dist/rates/kaminoApi.d.ts.map +1 -0
- package/dist/rates/kaminoApi.js +98 -0
- package/dist/rates/kaminoApi.js.map +1 -0
- package/dist/rates/supplyRates.d.ts +23 -0
- package/dist/rates/supplyRates.d.ts.map +1 -0
- package/dist/rates/supplyRates.js +67 -0
- package/dist/rates/supplyRates.js.map +1 -0
- package/dist/transactionBuilder.d.ts +5 -0
- package/dist/transactionBuilder.d.ts.map +1 -0
- package/dist/transactionBuilder.js +287 -0
- package/dist/transactionBuilder.js.map +1 -0
- package/dist/txHelpers.d.ts +16 -0
- package/dist/txHelpers.d.ts.map +1 -0
- package/dist/txHelpers.js +82 -0
- package/dist/txHelpers.js.map +1 -0
- package/dist/userState.d.ts +21 -0
- package/dist/userState.d.ts.map +1 -0
- package/dist/userState.js +60 -0
- package/dist/userState.js.map +1 -0
- package/dist/vault/vaultAccounts.d.ts +82 -0
- package/dist/vault/vaultAccounts.d.ts.map +1 -0
- package/dist/vault/vaultAccounts.js +275 -0
- package/dist/vault/vaultAccounts.js.map +1 -0
- package/dist/wallet.d.ts +12 -0
- package/dist/wallet.d.ts.map +1 -0
- package/dist/wallet.js +2 -0
- package/dist/wallet.js.map +1 -0
- package/package.json +63 -0
- package/src/amountUtils.ts +69 -0
- package/src/constants.ts +263 -0
- package/src/errors.ts +33 -0
- package/src/executeAction.ts +884 -0
- package/src/executeDepositBorrow.ts +677 -0
- package/src/idl/hobba.json +5072 -0
- package/src/idl.ts +7 -0
- package/src/index.ts +21 -0
- package/src/juplend/accountResolvers.ts +207 -0
- package/src/juplend/operateRemaining.ts +81 -0
- package/src/juplend/positionInit.ts +86 -0
- package/src/juplend/positionParser.ts +41 -0
- package/src/juplend/sdkApi.ts +112 -0
- package/src/juplend/tickMath.ts +82 -0
- package/src/juplend/tickUtils.ts +163 -0
- package/src/juplend/vaultConfigParser.ts +45 -0
- package/src/kamino/accountResolvers.ts +299 -0
- package/src/kamino/obligationParser.ts +123 -0
- package/src/kamino/refreshInstructions.ts +45 -0
- package/src/limits.ts +222 -0
- package/src/marketConfig.ts +139 -0
- package/src/oracle/scopePrice.ts +31 -0
- package/src/pdas.ts +226 -0
- package/src/position.ts +131 -0
- package/src/positionMetrics.ts +387 -0
- package/src/program.ts +30 -0
- package/src/quote.ts +310 -0
- package/src/rates/borrowRates.ts +270 -0
- package/src/rates/collateralInfo.ts +136 -0
- package/src/rates/constants.ts +87 -0
- package/src/rates/index.ts +38 -0
- package/src/rates/kaminoApi.ts +134 -0
- package/src/rates/supplyRates.ts +102 -0
- package/src/transactionBuilder.ts +324 -0
- package/src/txHelpers.ts +107 -0
- package/src/userState.ts +91 -0
- package/src/vault/vaultAccounts.ts +365 -0
- package/src/wallet.ts +12 -0
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Connection,
|
|
3
|
+
PublicKey,
|
|
4
|
+
SystemProgram,
|
|
5
|
+
TransactionInstruction,
|
|
6
|
+
} from "@solana/web3.js";
|
|
7
|
+
import {
|
|
8
|
+
createAssociatedTokenAccountInstruction,
|
|
9
|
+
getAssociatedTokenAddress,
|
|
10
|
+
createSyncNativeInstruction,
|
|
11
|
+
NATIVE_MINT,
|
|
12
|
+
} from "@solana/spl-token";
|
|
13
|
+
import type { Program } from "@coral-xyz/anchor";
|
|
14
|
+
import BN from "bn.js";
|
|
15
|
+
import {
|
|
16
|
+
CBBTC_MINT,
|
|
17
|
+
SOL_MINT,
|
|
18
|
+
USDC_MINT,
|
|
19
|
+
JUPLEND_POS_TICK_CURRENT,
|
|
20
|
+
JUPLEND_POS_TICK_FINAL,
|
|
21
|
+
JUPLEND_POS_TICK_ID_CURRENT,
|
|
22
|
+
JUPLEND_POS_TICK_ID_FINAL,
|
|
23
|
+
LENDING_PROVIDER_JUPLEND,
|
|
24
|
+
DEFAULT_LENDING_PROVIDER,
|
|
25
|
+
resolveJuplendVaultId,
|
|
26
|
+
deriveJuplendVaultConfigPda,
|
|
27
|
+
deriveJuplendVaultStatePda,
|
|
28
|
+
} from "./constants";
|
|
29
|
+
import {
|
|
30
|
+
deriveUserStatePda,
|
|
31
|
+
deriveAllowlistEntryPda,
|
|
32
|
+
deriveUserTokenAccounts,
|
|
33
|
+
deriveObligationPda,
|
|
34
|
+
deriveUserMetadataPda,
|
|
35
|
+
deriveFarmPdas,
|
|
36
|
+
deriveJuplendPositionPda,
|
|
37
|
+
deriveJuplendPositionMintPda,
|
|
38
|
+
derivePositionMetadataPda,
|
|
39
|
+
} from "./pdas";
|
|
40
|
+
import {
|
|
41
|
+
getInitializeUserAccounts,
|
|
42
|
+
getKaminoInitializeUserLenderAccounts,
|
|
43
|
+
getDepositAccounts,
|
|
44
|
+
getKaminoDepositLenderAccounts,
|
|
45
|
+
getBorrowAccounts,
|
|
46
|
+
getKaminoBorrowLenderAccounts,
|
|
47
|
+
fixFarmWritable,
|
|
48
|
+
} from "./kamino/accountResolvers";
|
|
49
|
+
import {
|
|
50
|
+
buildRefreshReserveIx,
|
|
51
|
+
buildRefreshObligationIx,
|
|
52
|
+
} from "./kamino/refreshInstructions";
|
|
53
|
+
import { resolveMainMarketConfigByMint } from "./marketConfig";
|
|
54
|
+
import { resolveKaminoMarketConfig } from "./position";
|
|
55
|
+
import { parseUserStateData } from "./userState";
|
|
56
|
+
import { fetchMarketLut } from "./txHelpers";
|
|
57
|
+
import {
|
|
58
|
+
getCreateJuplendPositionAccounts,
|
|
59
|
+
getInitializeUserJuplendAccounts,
|
|
60
|
+
getJuplendInitializeUserLenderAccounts,
|
|
61
|
+
} from "./juplend/positionInit";
|
|
62
|
+
import {
|
|
63
|
+
getJuplendDepositAccounts,
|
|
64
|
+
getJuplendDepositLenderAccounts,
|
|
65
|
+
getJuplendBorrowAccounts,
|
|
66
|
+
getJuplendBorrowLenderAccounts,
|
|
67
|
+
getJuplendDepositBorrowAccounts,
|
|
68
|
+
getJuplendDepositBorrowLenderAccounts,
|
|
69
|
+
} from "./juplend/accountResolvers";
|
|
70
|
+
import { buildFallbackOperateRemaining } from "./juplend/operateRemaining";
|
|
71
|
+
import {
|
|
72
|
+
fetchOperateContext,
|
|
73
|
+
deserializeRemaining,
|
|
74
|
+
deserializeOtherIxs,
|
|
75
|
+
type SdkOperateResponse,
|
|
76
|
+
} from "./juplend/sdkApi";
|
|
77
|
+
import { sendLegacyTx, sendVersionedTx } from "./transactionBuilder";
|
|
78
|
+
import { buildHobbaProgram } from "./program";
|
|
79
|
+
import { collateralLabel } from "./amountUtils";
|
|
80
|
+
import type { HobbaSigner } from "./wallet";
|
|
81
|
+
|
|
82
|
+
export interface ExecuteDepositBorrowParams {
|
|
83
|
+
/** RPC connection used for reads, simulation, and submission. */
|
|
84
|
+
connection: Connection;
|
|
85
|
+
/** Wallet able to sign legacy + versioned transactions (publicKey required). */
|
|
86
|
+
signer: HobbaSigner;
|
|
87
|
+
/** Collateral mint — cbBTC or SOL. */
|
|
88
|
+
collateralMint: PublicKey;
|
|
89
|
+
/** Deposit amount in collateral base units (cbBTC: 1e8, SOL: 1e9). */
|
|
90
|
+
depositAmount: BN;
|
|
91
|
+
/** Borrow amount in USDC base units (1e6). */
|
|
92
|
+
borrowAmount: BN;
|
|
93
|
+
/**
|
|
94
|
+
* Base URL for the Hobba backend that serves `/api/juplend-operate` and
|
|
95
|
+
* `/api/lut`. Defaults to "" (same-origin relative), which is what the app uses.
|
|
96
|
+
* The widget passes the partner-configured `apiBaseUrl`.
|
|
97
|
+
*/
|
|
98
|
+
apiBaseUrl?: string;
|
|
99
|
+
/** Progress callback for UI status text. */
|
|
100
|
+
onStep?: (step: string) => void;
|
|
101
|
+
/** Optional pre-built Anchor program; built from the IDL + connection if omitted. */
|
|
102
|
+
program?: Program;
|
|
103
|
+
/**
|
|
104
|
+
* Origin the user signs up through (0 = hobba). Recorded immutably on the
|
|
105
|
+
* user's on-chain state at initialization; the origin's OriginConfig PDA
|
|
106
|
+
* must already be registered by the deployer. Only used for new users.
|
|
107
|
+
*/
|
|
108
|
+
origin?: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface ExecuteDepositBorrowResult {
|
|
112
|
+
depositSig: string;
|
|
113
|
+
borrowSig: string;
|
|
114
|
+
/** Lender the flow ran against (Kamino runs as a single combined tx). */
|
|
115
|
+
lender: "juplend" | "kamino";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Build wSOL wrapping instructions if needed (collateral === SOL).
|
|
119
|
+
async function buildWrapSolIxs(
|
|
120
|
+
owner: PublicKey,
|
|
121
|
+
wsolAta: PublicKey,
|
|
122
|
+
lamports: BN,
|
|
123
|
+
safeGetAccountInfo: (addr: PublicKey) => Promise<any>,
|
|
124
|
+
): Promise<TransactionInstruction[]> {
|
|
125
|
+
const ixs: TransactionInstruction[] = [];
|
|
126
|
+
const ataInfo = await safeGetAccountInfo(wsolAta);
|
|
127
|
+
if (!ataInfo) {
|
|
128
|
+
ixs.push(createAssociatedTokenAccountInstruction(owner, wsolAta, owner, NATIVE_MINT));
|
|
129
|
+
}
|
|
130
|
+
// Transfer SOL to wSOL ATA then sync
|
|
131
|
+
ixs.push(
|
|
132
|
+
SystemProgram.transfer({
|
|
133
|
+
fromPubkey: owner,
|
|
134
|
+
toPubkey: wsolAta,
|
|
135
|
+
lamports: BigInt(lamports.toString()),
|
|
136
|
+
}),
|
|
137
|
+
createSyncNativeInstruction(wsolAta),
|
|
138
|
+
);
|
|
139
|
+
return ixs;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Run the full JupLend deposit + borrow flow (the default Hobba lending provider).
|
|
144
|
+
*
|
|
145
|
+
* Lifted verbatim from the app's `DepositBorrowForm.handleJuplendFlow` so the app
|
|
146
|
+
* and the embeddable widget share one implementation. Sequence:
|
|
147
|
+
* TX1 (new users only): create JupLend position + initialize user + ATAs (legacy)
|
|
148
|
+
* TX2: deposit collateral (versioned, LUT, tick-rebuild recovery)
|
|
149
|
+
* TX3: borrow USDC (versioned, LUT, tick-rebuild recovery)
|
|
150
|
+
*/
|
|
151
|
+
export async function executeDepositBorrow(
|
|
152
|
+
params: ExecuteDepositBorrowParams,
|
|
153
|
+
): Promise<ExecuteDepositBorrowResult> {
|
|
154
|
+
const {
|
|
155
|
+
connection,
|
|
156
|
+
signer,
|
|
157
|
+
collateralMint: selectedMint,
|
|
158
|
+
depositAmount,
|
|
159
|
+
borrowAmount,
|
|
160
|
+
apiBaseUrl = "",
|
|
161
|
+
onStep,
|
|
162
|
+
origin = 0,
|
|
163
|
+
} = params;
|
|
164
|
+
|
|
165
|
+
if (!signer.publicKey || !signer.signTransaction) {
|
|
166
|
+
throw new Error("Wallet not connected");
|
|
167
|
+
}
|
|
168
|
+
const owner = signer.publicKey;
|
|
169
|
+
const program = params.program ?? buildHobbaProgram(connection, owner);
|
|
170
|
+
const base = apiBaseUrl.replace(/\/+$/, "");
|
|
171
|
+
const setStep = (s: string) => onStep?.(s);
|
|
172
|
+
const label = collateralLabel(selectedMint);
|
|
173
|
+
const isSol = selectedMint.equals(SOL_MINT);
|
|
174
|
+
|
|
175
|
+
const safeGetAccountInfo = async (addr: PublicKey) => {
|
|
176
|
+
try {
|
|
177
|
+
return await connection.getAccountInfo(addr);
|
|
178
|
+
} catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const [userStatePda] = deriveUserStatePda(owner, selectedMint);
|
|
184
|
+
const [allowlistEntryPda] = deriveAllowlistEntryPda(owner);
|
|
185
|
+
const tokens = await deriveUserTokenAccounts(owner, userStatePda, selectedMint);
|
|
186
|
+
|
|
187
|
+
const userStateInfo = await safeGetAccountInfo(userStatePda);
|
|
188
|
+
|
|
189
|
+
// Lender routing: existing users follow their UserState's provider byte;
|
|
190
|
+
// new users (no UserState yet) get the default lender (JupLend).
|
|
191
|
+
const isJuplendLender =
|
|
192
|
+
userStateInfo && userStateInfo.data.length > 105
|
|
193
|
+
? userStateInfo.data[105] === LENDING_PROVIDER_JUPLEND
|
|
194
|
+
: DEFAULT_LENDING_PROVIDER === LENDING_PROVIDER_JUPLEND;
|
|
195
|
+
|
|
196
|
+
if (!isJuplendLender) {
|
|
197
|
+
const sig = await executeKaminoDepositBorrow({
|
|
198
|
+
connection,
|
|
199
|
+
signer,
|
|
200
|
+
program,
|
|
201
|
+
selectedMint,
|
|
202
|
+
depositAmount,
|
|
203
|
+
borrowAmount,
|
|
204
|
+
base,
|
|
205
|
+
setStep,
|
|
206
|
+
owner,
|
|
207
|
+
userStatePda,
|
|
208
|
+
allowlistEntryPda,
|
|
209
|
+
tokens,
|
|
210
|
+
userStateInfo,
|
|
211
|
+
safeGetAccountInfo,
|
|
212
|
+
isSol,
|
|
213
|
+
origin,
|
|
214
|
+
});
|
|
215
|
+
return { depositSig: sig, borrowSig: sig, lender: "kamino" };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Resolve vault ID, config, and state for selected collateral
|
|
219
|
+
const juplendVaultId = resolveJuplendVaultId(selectedMint);
|
|
220
|
+
const juplendVaultConfig = deriveJuplendVaultConfigPda(juplendVaultId);
|
|
221
|
+
const juplendVaultState = deriveJuplendVaultStatePda(juplendVaultId);
|
|
222
|
+
|
|
223
|
+
if (!userStateInfo) {
|
|
224
|
+
// TX1: Create JupLend position + Initialize user + ATAs
|
|
225
|
+
setStep("Initializing position...");
|
|
226
|
+
|
|
227
|
+
// Read next_position_id from vault state (offset 115, u32 LE)
|
|
228
|
+
const vaultStateInfo = await connection.getAccountInfo(juplendVaultState);
|
|
229
|
+
if (!vaultStateInfo) throw new Error("JupLend vault state not found");
|
|
230
|
+
const nextPositionId = vaultStateInfo.data.readUInt32LE(115);
|
|
231
|
+
|
|
232
|
+
const [positionPda] = deriveJuplendPositionPda(juplendVaultId, nextPositionId);
|
|
233
|
+
const [positionMintPda] = deriveJuplendPositionMintPda(juplendVaultId, nextPositionId);
|
|
234
|
+
const positionTokenAccount = await getAssociatedTokenAddress(positionMintPda, userStatePda, true);
|
|
235
|
+
const [positionMetadata] = derivePositionMetadataPda(positionMintPda);
|
|
236
|
+
|
|
237
|
+
// ATA creation
|
|
238
|
+
const ataIxs: TransactionInstruction[] = [];
|
|
239
|
+
const [pdaCollateralAtaInfo, pdaUsdcAtaInfo, userUsdcAtaInfo] = await Promise.all([
|
|
240
|
+
safeGetAccountInfo(tokens.pdaCollateralAta),
|
|
241
|
+
safeGetAccountInfo(tokens.pdaUsdcAta),
|
|
242
|
+
safeGetAccountInfo(tokens.userUsdcAta),
|
|
243
|
+
]);
|
|
244
|
+
if (!pdaCollateralAtaInfo) {
|
|
245
|
+
ataIxs.push(createAssociatedTokenAccountInstruction(owner, tokens.pdaCollateralAta, userStatePda, selectedMint));
|
|
246
|
+
}
|
|
247
|
+
if (!pdaUsdcAtaInfo) {
|
|
248
|
+
ataIxs.push(createAssociatedTokenAccountInstruction(owner, tokens.pdaUsdcAta, userStatePda, USDC_MINT));
|
|
249
|
+
}
|
|
250
|
+
if (!userUsdcAtaInfo) {
|
|
251
|
+
ataIxs.push(createAssociatedTokenAccountInstruction(owner, tokens.userUsdcAta, owner, USDC_MINT));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const createPositionIx = await program.methods
|
|
255
|
+
.createJuplendPosition(juplendVaultId, nextPositionId)
|
|
256
|
+
.accounts(getCreateJuplendPositionAccounts(
|
|
257
|
+
owner, userStatePda, positionPda, positionMintPda, positionTokenAccount, positionMetadata, allowlistEntryPda, selectedMint, juplendVaultState,
|
|
258
|
+
))
|
|
259
|
+
.instruction();
|
|
260
|
+
|
|
261
|
+
const juplendInitLender = getJuplendInitializeUserLenderAccounts(positionPda, juplendVaultConfig);
|
|
262
|
+
const initIx = await program.methods
|
|
263
|
+
.initializeUser(1, origin)
|
|
264
|
+
.accounts(getInitializeUserJuplendAccounts(
|
|
265
|
+
owner, userStatePda, allowlistEntryPda, selectedMint, origin,
|
|
266
|
+
))
|
|
267
|
+
.remainingAccounts(juplendInitLender)
|
|
268
|
+
.instruction();
|
|
269
|
+
|
|
270
|
+
await sendLegacyTx(connection, [...ataIxs, createPositionIx, initIx], signer, 400_000);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Read position address from userState (obligation field stores position for JupLend)
|
|
274
|
+
const updatedUserState = await connection.getAccountInfo(userStatePda);
|
|
275
|
+
if (!updatedUserState) throw new Error("User state not found after init");
|
|
276
|
+
const positionAddress = new PublicKey(updatedUserState.data.slice(41, 73));
|
|
277
|
+
|
|
278
|
+
// Read position for tick + derive position token account
|
|
279
|
+
const positionInfo = await connection.getAccountInfo(positionAddress);
|
|
280
|
+
if (!positionInfo) throw new Error("Position account not found");
|
|
281
|
+
|
|
282
|
+
const vaultId = positionInfo.data.readUInt16LE(8);
|
|
283
|
+
const positionId = positionInfo.data.readUInt32LE(10);
|
|
284
|
+
const [positionMintPda] = deriveJuplendPositionMintPda(vaultId, positionId);
|
|
285
|
+
const positionTokenAccount = await getAssociatedTokenAddress(positionMintPda, userStatePda, true);
|
|
286
|
+
|
|
287
|
+
const juplendPdas = {
|
|
288
|
+
userStatePda,
|
|
289
|
+
allowlistEntryPda,
|
|
290
|
+
obligationPda: positionAddress,
|
|
291
|
+
userMetadataPda: userStatePda,
|
|
292
|
+
userCollateralAta: tokens.userCollateralAta,
|
|
293
|
+
userUsdcAta: tokens.userUsdcAta,
|
|
294
|
+
pdaCollateralAta: tokens.pdaCollateralAta,
|
|
295
|
+
pdaUsdcAta: tokens.pdaUsdcAta,
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// Fetch JupLend LUT
|
|
299
|
+
const lutResp = await fetch(`${base}/api/lut?market=juplend`);
|
|
300
|
+
const lutData = await lutResp.json();
|
|
301
|
+
if (!lutData.address) throw new Error("JupLend LUT not found. Run stream-vault-oracles.ts first.");
|
|
302
|
+
const juplendLut = new PublicKey(lutData.address);
|
|
303
|
+
|
|
304
|
+
const wantsDeposit = depositAmount.gtn(0);
|
|
305
|
+
const wantsBorrow = borrowAmount.gtn(0);
|
|
306
|
+
if (!wantsDeposit && !wantsBorrow) {
|
|
307
|
+
throw new Error("Nothing to execute: deposit and borrow amounts are both zero");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// wSOL wrapping for deposit
|
|
311
|
+
// Wrap amount + 1 because juplend_deposit transfers amount + 1 (JupLend rounding)
|
|
312
|
+
let wrapIxs: TransactionInstruction[] = [];
|
|
313
|
+
if (isSol && wantsDeposit) {
|
|
314
|
+
wrapIxs = await buildWrapSolIxs(owner, tokens.userCollateralAta, depositAmount.addn(1), safeGetAccountInfo);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ---- Combined path: both sides in ONE versioned tx via juplend_deposit_borrow ----
|
|
318
|
+
// Single operate CPI: ~150 bytes smaller and one wallet popup instead of two.
|
|
319
|
+
// If the operate-context API is unavailable we fall back to the split TX2+TX3 flow
|
|
320
|
+
// below — the hardcoded cbBTC fallback list is only valid for the deposit-only
|
|
321
|
+
// shape (its final tick ignores debt), so the combined ix never runs on fallback
|
|
322
|
+
// accounts.
|
|
323
|
+
if (wantsDeposit && wantsBorrow) {
|
|
324
|
+
setStep(`Depositing ${label} & borrowing USDC...`);
|
|
325
|
+
|
|
326
|
+
// Static prefix: wrap + ATA creates. Independent of the SDK context, computed
|
|
327
|
+
// once and reused by the tick-error rebuild. TX1 already created these ATAs for
|
|
328
|
+
// brand-new users; the existence checks self-heal older positions.
|
|
329
|
+
const staticPrefix: TransactionInstruction[] = [...wrapIxs];
|
|
330
|
+
const [pdaColInfo, pdaUsdcInfo, userUsdcInfo] = await Promise.all([
|
|
331
|
+
safeGetAccountInfo(tokens.pdaCollateralAta),
|
|
332
|
+
safeGetAccountInfo(tokens.pdaUsdcAta),
|
|
333
|
+
safeGetAccountInfo(tokens.userUsdcAta),
|
|
334
|
+
]);
|
|
335
|
+
if (!pdaColInfo) {
|
|
336
|
+
staticPrefix.push(createAssociatedTokenAccountInstruction(owner, tokens.pdaCollateralAta, userStatePda, selectedMint));
|
|
337
|
+
}
|
|
338
|
+
if (!pdaUsdcInfo) {
|
|
339
|
+
staticPrefix.push(createAssociatedTokenAccountInstruction(owner, tokens.pdaUsdcAta, userStatePda, USDC_MINT));
|
|
340
|
+
}
|
|
341
|
+
if (!userUsdcInfo) {
|
|
342
|
+
staticPrefix.push(createAssociatedTokenAccountInstruction(owner, tokens.userUsdcAta, owner, USDC_MINT));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const fetchCombinedCtx = () => fetchOperateContext({
|
|
346
|
+
vaultId, positionId,
|
|
347
|
+
colAmount: depositAmount.toString(),
|
|
348
|
+
debtAmount: borrowAmount.toString(),
|
|
349
|
+
signerPda: userStatePda.toBase58(),
|
|
350
|
+
}, base);
|
|
351
|
+
|
|
352
|
+
// SDK-dependent suffix, rebuilt from a fresh operate context on tick errors.
|
|
353
|
+
const buildCombinedSuffix = async (ctx: SdkOperateResponse): Promise<TransactionInstruction[]> => {
|
|
354
|
+
const otherIxs = deserializeOtherIxs(ctx, userStatePda, owner);
|
|
355
|
+
const combRemaining = deserializeRemaining(ctx);
|
|
356
|
+
// vault_config (operate index 0) and position (index 5) are named accounts
|
|
357
|
+
// on this handler (the program splices them back into the operate list);
|
|
358
|
+
// strip them from the remaining list and pass them as named accounts.
|
|
359
|
+
const combOperateRest = combRemaining.filter((_, i) => i !== 0 && i !== 5);
|
|
360
|
+
const combinedIx = await program.methods
|
|
361
|
+
.juplendDepositBorrow(depositAmount, borrowAmount)
|
|
362
|
+
.accounts(getJuplendDepositBorrowAccounts(
|
|
363
|
+
owner, juplendPdas, selectedMint, juplendVaultConfig, positionAddress,
|
|
364
|
+
))
|
|
365
|
+
.remainingAccounts([...getJuplendDepositBorrowLenderAccounts(), ...combOperateRest])
|
|
366
|
+
.instruction();
|
|
367
|
+
return [...otherIxs, combinedIx];
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
let combinedCtx: SdkOperateResponse | null = null;
|
|
371
|
+
try {
|
|
372
|
+
combinedCtx = await fetchCombinedCtx();
|
|
373
|
+
} catch (sdkErr: any) {
|
|
374
|
+
console.warn("[deposit-borrow] combined operate context unavailable, falling back to split flow:", sdkErr.message);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (combinedCtx) {
|
|
378
|
+
const suffix = await buildCombinedSuffix(combinedCtx);
|
|
379
|
+
const sig = await sendVersionedTx(
|
|
380
|
+
connection, [...staticPrefix, ...suffix], signer, [juplendLut], 1_400_000, false,
|
|
381
|
+
async () => {
|
|
382
|
+
const fresh = await fetchCombinedCtx().catch(() => null);
|
|
383
|
+
if (!fresh) return null;
|
|
384
|
+
return [...staticPrefix, ...(await buildCombinedSuffix(fresh))];
|
|
385
|
+
},
|
|
386
|
+
);
|
|
387
|
+
return { depositSig: sig, borrowSig: sig, lender: "juplend" };
|
|
388
|
+
}
|
|
389
|
+
// Operate context unavailable — continue into the split flow.
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
let depositSig: string | null = null;
|
|
393
|
+
let borrowSig: string | null = null;
|
|
394
|
+
|
|
395
|
+
if (wantsDeposit) {
|
|
396
|
+
// TX2: Deposit — try SDK first, fallback to hardcoded remaining for cbBTC only
|
|
397
|
+
setStep(`Depositing ${label}...`);
|
|
398
|
+
|
|
399
|
+
let depositRemaining: { pubkey: PublicKey; isSigner: boolean; isWritable: boolean }[];
|
|
400
|
+
let depositOtherIxs: TransactionInstruction[] = [];
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
const sdkDepositResult = await fetchOperateContext({
|
|
404
|
+
vaultId, positionId,
|
|
405
|
+
colAmount: depositAmount.toString(),
|
|
406
|
+
debtAmount: "0",
|
|
407
|
+
signerPda: userStatePda.toBase58(),
|
|
408
|
+
}, base);
|
|
409
|
+
depositRemaining = deserializeRemaining(sdkDepositResult);
|
|
410
|
+
depositOtherIxs = deserializeOtherIxs(sdkDepositResult, userStatePda, owner);
|
|
411
|
+
} catch (sdkErr: any) {
|
|
412
|
+
console.warn("[deposit] SDK failed:", sdkErr.message);
|
|
413
|
+
if (!selectedMint.equals(CBBTC_MINT)) {
|
|
414
|
+
throw new Error(`SDK API failed for deposit on vault ${vaultId}: ${sdkErr.message}`);
|
|
415
|
+
}
|
|
416
|
+
// cbBTC fallback with hardcoded remaining
|
|
417
|
+
depositRemaining = buildFallbackOperateRemaining(
|
|
418
|
+
positionAddress, positionTokenAccount,
|
|
419
|
+
JUPLEND_POS_TICK_CURRENT, JUPLEND_POS_TICK_FINAL,
|
|
420
|
+
JUPLEND_POS_TICK_ID_CURRENT, JUPLEND_POS_TICK_ID_FINAL,
|
|
421
|
+
).remaining;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const juplendDepositLender = getJuplendDepositLenderAccounts(juplendPdas);
|
|
425
|
+
const depositIx = await program.methods.deposit(depositAmount)
|
|
426
|
+
.accounts(getJuplendDepositAccounts(owner, juplendPdas, selectedMint))
|
|
427
|
+
.remainingAccounts([...juplendDepositLender, ...depositRemaining]).instruction();
|
|
428
|
+
|
|
429
|
+
// Pre-sign rebuilder: refetch SDK and rebuild deposit ix on VaultInvalidTick
|
|
430
|
+
const rebuildDepositOnTickError = async (): Promise<TransactionInstruction[] | null> => {
|
|
431
|
+
const fresh = await fetchOperateContext({
|
|
432
|
+
vaultId, positionId,
|
|
433
|
+
colAmount: depositAmount.toString(), debtAmount: "0",
|
|
434
|
+
signerPda: userStatePda.toBase58(),
|
|
435
|
+
}, base).catch(() => null);
|
|
436
|
+
if (!fresh) return null;
|
|
437
|
+
const freshRemaining = deserializeRemaining(fresh);
|
|
438
|
+
const freshOtherIxs = deserializeOtherIxs(fresh, userStatePda, owner);
|
|
439
|
+
const freshIx = await program.methods.deposit(depositAmount)
|
|
440
|
+
.accounts(getJuplendDepositAccounts(owner, juplendPdas, selectedMint))
|
|
441
|
+
.remainingAccounts([...juplendDepositLender, ...freshRemaining]).instruction();
|
|
442
|
+
return [...wrapIxs, ...freshOtherIxs, freshIx];
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
depositSig = await sendVersionedTx(
|
|
446
|
+
connection, [...wrapIxs, ...depositOtherIxs, depositIx], signer, [juplendLut], 600_000, false, rebuildDepositOnTickError,
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (wantsBorrow) {
|
|
451
|
+
// TX3: Borrow with SDK-computed remaining (SDK computes correct final tick for debt)
|
|
452
|
+
setStep("Borrowing USDC...");
|
|
453
|
+
|
|
454
|
+
const sdkResult = await fetchOperateContext({
|
|
455
|
+
vaultId,
|
|
456
|
+
positionId,
|
|
457
|
+
colAmount: "0",
|
|
458
|
+
debtAmount: borrowAmount.toString(),
|
|
459
|
+
signerPda: userStatePda.toBase58(),
|
|
460
|
+
}, base);
|
|
461
|
+
|
|
462
|
+
const borrowRemaining = deserializeRemaining(sdkResult);
|
|
463
|
+
const otherIxs = deserializeOtherIxs(sdkResult, userStatePda, owner);
|
|
464
|
+
|
|
465
|
+
const juplendLender = getJuplendBorrowLenderAccounts(juplendPdas);
|
|
466
|
+
const borrowIx = await program.methods.borrow(borrowAmount, 0)
|
|
467
|
+
.accounts(getJuplendBorrowAccounts(owner, juplendPdas, selectedMint))
|
|
468
|
+
.remainingAccounts([...juplendLender, ...borrowRemaining]).instruction();
|
|
469
|
+
|
|
470
|
+
const rebuildBorrowOnTickError = async (): Promise<TransactionInstruction[] | null> => {
|
|
471
|
+
const fresh = await fetchOperateContext({
|
|
472
|
+
vaultId, positionId,
|
|
473
|
+
colAmount: "0", debtAmount: borrowAmount.toString(),
|
|
474
|
+
signerPda: userStatePda.toBase58(),
|
|
475
|
+
}, base).catch(() => null);
|
|
476
|
+
if (!fresh) return null;
|
|
477
|
+
const freshRemaining = deserializeRemaining(fresh);
|
|
478
|
+
const freshOtherIxs = deserializeOtherIxs(fresh, userStatePda, owner);
|
|
479
|
+
const freshIx = await program.methods.borrow(borrowAmount, 0)
|
|
480
|
+
.accounts(getJuplendBorrowAccounts(owner, juplendPdas, selectedMint))
|
|
481
|
+
.remainingAccounts([...juplendLender, ...freshRemaining]).instruction();
|
|
482
|
+
return [...freshOtherIxs, freshIx];
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
borrowSig = await sendVersionedTx(
|
|
486
|
+
connection, [...otherIxs, borrowIx], signer, [juplendLut], 800_000, false, rebuildBorrowOnTickError,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return {
|
|
491
|
+
depositSig: (depositSig ?? borrowSig)!,
|
|
492
|
+
borrowSig: (borrowSig ?? depositSig)!,
|
|
493
|
+
lender: "juplend",
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// --- Kamino deposit & borrow (lifted from app page.tsx handleDepositBorrow) ---
|
|
498
|
+
// Single versioned tx: [ATA creates] → [initializeUser] → [wrap SOL] →
|
|
499
|
+
// refresh_reserve(col) → refresh_reserve(usdc)? → refresh_obligation? →
|
|
500
|
+
// deposit → refresh_reserve(col) → refresh_obligation → borrow.
|
|
501
|
+
async function executeKaminoDepositBorrow(args: {
|
|
502
|
+
connection: Connection;
|
|
503
|
+
signer: HobbaSigner;
|
|
504
|
+
program: Program;
|
|
505
|
+
selectedMint: PublicKey;
|
|
506
|
+
depositAmount: BN;
|
|
507
|
+
borrowAmount: BN;
|
|
508
|
+
base: string;
|
|
509
|
+
setStep: (s: string) => void;
|
|
510
|
+
owner: PublicKey;
|
|
511
|
+
userStatePda: PublicKey;
|
|
512
|
+
allowlistEntryPda: PublicKey;
|
|
513
|
+
tokens: Awaited<ReturnType<typeof deriveUserTokenAccounts>>;
|
|
514
|
+
userStateInfo: Awaited<ReturnType<Connection["getAccountInfo"]>>;
|
|
515
|
+
safeGetAccountInfo: (addr: PublicKey) => Promise<any>;
|
|
516
|
+
isSol: boolean;
|
|
517
|
+
origin: number;
|
|
518
|
+
}): Promise<string> {
|
|
519
|
+
const {
|
|
520
|
+
connection,
|
|
521
|
+
signer,
|
|
522
|
+
program,
|
|
523
|
+
selectedMint,
|
|
524
|
+
depositAmount,
|
|
525
|
+
borrowAmount,
|
|
526
|
+
base,
|
|
527
|
+
setStep,
|
|
528
|
+
owner,
|
|
529
|
+
userStatePda,
|
|
530
|
+
allowlistEntryPda,
|
|
531
|
+
tokens,
|
|
532
|
+
userStateInfo,
|
|
533
|
+
safeGetAccountInfo,
|
|
534
|
+
isSol,
|
|
535
|
+
origin,
|
|
536
|
+
} = args;
|
|
537
|
+
|
|
538
|
+
setStep("Processing deposit & borrow...");
|
|
539
|
+
|
|
540
|
+
const effectiveUserState = userStateInfo
|
|
541
|
+
? parseUserStateData(userStateInfo.data as Buffer)
|
|
542
|
+
: null;
|
|
543
|
+
const mc = effectiveUserState
|
|
544
|
+
? resolveKaminoMarketConfig(effectiveUserState.lendingMarket, selectedMint)
|
|
545
|
+
: resolveMainMarketConfigByMint(selectedMint);
|
|
546
|
+
|
|
547
|
+
const obligPda =
|
|
548
|
+
effectiveUserState?.obligation ?? deriveObligationPda(userStatePda, mc.market)[0];
|
|
549
|
+
const [userMetadataPda] = deriveUserMetadataPda(userStatePda);
|
|
550
|
+
const { obligationFarmUserStatePda, obligationFarmDebtUserStatePda } = deriveFarmPdas(
|
|
551
|
+
obligPda,
|
|
552
|
+
mc.collateralFarmState,
|
|
553
|
+
mc.usdcReserveFarmDebt,
|
|
554
|
+
);
|
|
555
|
+
const pdas = {
|
|
556
|
+
userStatePda,
|
|
557
|
+
allowlistEntryPda,
|
|
558
|
+
obligationPda: obligPda,
|
|
559
|
+
userMetadataPda,
|
|
560
|
+
obligationFarmUserStatePda,
|
|
561
|
+
obligationFarmDebtUserStatePda,
|
|
562
|
+
...tokens,
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
const needsInit = !userStateInfo;
|
|
566
|
+
|
|
567
|
+
const obligInfo = await connection.getAccountInfo(obligPda);
|
|
568
|
+
const obligationExists = obligInfo !== null && obligInfo.data.length > 0;
|
|
569
|
+
const refreshReserves: PublicKey[] = [];
|
|
570
|
+
if (obligInfo && obligInfo.data.length > 1208) {
|
|
571
|
+
const depositReserveKey = new PublicKey(obligInfo.data.slice(96, 128));
|
|
572
|
+
if (!depositReserveKey.equals(PublicKey.default)) refreshReserves.push(mc.collateralReserve);
|
|
573
|
+
const borrowReserveKey = new PublicKey(obligInfo.data.slice(1208, 1240));
|
|
574
|
+
if (!borrowReserveKey.equals(PublicKey.default)) refreshReserves.push(mc.usdcReserve);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const allIxs: TransactionInstruction[] = [];
|
|
578
|
+
|
|
579
|
+
const [pdaColInfo, pdaUsdcInfo, userUsdcInfo] = await Promise.all([
|
|
580
|
+
safeGetAccountInfo(tokens.pdaCollateralAta),
|
|
581
|
+
safeGetAccountInfo(tokens.pdaUsdcAta),
|
|
582
|
+
safeGetAccountInfo(tokens.userUsdcAta),
|
|
583
|
+
]);
|
|
584
|
+
if (!pdaColInfo)
|
|
585
|
+
allIxs.push(
|
|
586
|
+
createAssociatedTokenAccountInstruction(owner, tokens.pdaCollateralAta, userStatePda, selectedMint),
|
|
587
|
+
);
|
|
588
|
+
if (!pdaUsdcInfo)
|
|
589
|
+
allIxs.push(
|
|
590
|
+
createAssociatedTokenAccountInstruction(owner, tokens.pdaUsdcAta, userStatePda, USDC_MINT),
|
|
591
|
+
);
|
|
592
|
+
if (!userUsdcInfo)
|
|
593
|
+
allIxs.push(
|
|
594
|
+
createAssociatedTokenAccountInstruction(owner, tokens.userUsdcAta, owner, USDC_MINT),
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
if (needsInit) {
|
|
598
|
+
const initIx = await program.methods
|
|
599
|
+
.initializeUser(0, origin)
|
|
600
|
+
.accounts(getInitializeUserAccounts(owner, pdas, mc, origin))
|
|
601
|
+
.remainingAccounts(getKaminoInitializeUserLenderAccounts(pdas, mc))
|
|
602
|
+
.instruction();
|
|
603
|
+
allIxs.push(initIx);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const wantsDeposit = depositAmount.gtn(0);
|
|
607
|
+
const wantsBorrow = borrowAmount.gtn(0);
|
|
608
|
+
|
|
609
|
+
if (isSol && wantsDeposit) {
|
|
610
|
+
const wrapIxs = await buildWrapSolIxs(owner, tokens.userCollateralAta, depositAmount, safeGetAccountInfo);
|
|
611
|
+
allIxs.push(...wrapIxs);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Combined deposit+borrow layout (fits the 1232-byte versioned-tx cap):
|
|
615
|
+
// refresh_reserve(col)
|
|
616
|
+
// refresh_reserve(usdc) — only when borrowing (or existing USDC debt)
|
|
617
|
+
// refresh_obligation — only if obligation already existed with entries
|
|
618
|
+
// deposit — if any
|
|
619
|
+
// refresh_reserve(col) + refresh_obligation — only if also borrowing
|
|
620
|
+
// borrow — if any
|
|
621
|
+
if (wantsDeposit || wantsBorrow) {
|
|
622
|
+
allIxs.push(buildRefreshReserveIx(mc.collateralReserve, mc.market, mc.scopePrices));
|
|
623
|
+
// refresh_obligation reads every reserve listed in the obligation and rejects
|
|
624
|
+
// stale ones, so a deposit-only tx on an obligation with prior debt must
|
|
625
|
+
// still refresh USDC.
|
|
626
|
+
const obligationHasUsdcBorrow = refreshReserves.includes(mc.usdcReserve);
|
|
627
|
+
if (wantsBorrow || obligationHasUsdcBorrow) {
|
|
628
|
+
allIxs.push(buildRefreshReserveIx(mc.usdcReserve, mc.market, mc.scopePrices));
|
|
629
|
+
}
|
|
630
|
+
if (!needsInit && obligationExists && refreshReserves.length > 0) {
|
|
631
|
+
allIxs.push(buildRefreshObligationIx(mc.market, obligPda, refreshReserves));
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (wantsDeposit) {
|
|
636
|
+
const depIx = await program.methods
|
|
637
|
+
.deposit(depositAmount)
|
|
638
|
+
.accounts(getDepositAccounts(owner, pdas, mc))
|
|
639
|
+
.remainingAccounts(getKaminoDepositLenderAccounts(pdas, mc))
|
|
640
|
+
.instruction();
|
|
641
|
+
if (mc.collateralFarmState)
|
|
642
|
+
fixFarmWritable(depIx.keys, obligationFarmUserStatePda, mc.collateralFarmState);
|
|
643
|
+
allIxs.push(depIx);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (wantsBorrow) {
|
|
647
|
+
// Deposit marks the collateral reserve stale (even within the same slot),
|
|
648
|
+
// so it must be re-refreshed before refresh_obligation accepts it.
|
|
649
|
+
if (wantsDeposit) {
|
|
650
|
+
allIxs.push(buildRefreshReserveIx(mc.collateralReserve, mc.market, mc.scopePrices));
|
|
651
|
+
}
|
|
652
|
+
// Refresh obligation AFTER deposit (so borrow's health check sees the new
|
|
653
|
+
// collateral). Include USDC reserve only if the obligation already had a borrow.
|
|
654
|
+
const preBorrowReserves = refreshReserves.includes(mc.usdcReserve)
|
|
655
|
+
? refreshReserves
|
|
656
|
+
: [mc.collateralReserve];
|
|
657
|
+
allIxs.push(buildRefreshObligationIx(mc.market, obligPda, preBorrowReserves));
|
|
658
|
+
|
|
659
|
+
const borIx = await program.methods
|
|
660
|
+
.borrow(borrowAmount, 0)
|
|
661
|
+
.accounts(getBorrowAccounts(owner, pdas, mc, false))
|
|
662
|
+
.remainingAccounts(getKaminoBorrowLenderAccounts(pdas, mc, false))
|
|
663
|
+
.instruction();
|
|
664
|
+
if (mc.usdcReserveFarmDebt)
|
|
665
|
+
fixFarmWritable(borIx.keys, obligationFarmDebtUserStatePda, mc.usdcReserveFarmDebt);
|
|
666
|
+
allIxs.push(borIx);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const marketLut = await fetchMarketLut(base, "main");
|
|
670
|
+
return await sendVersionedTx(
|
|
671
|
+
connection,
|
|
672
|
+
allIxs,
|
|
673
|
+
signer,
|
|
674
|
+
marketLut ? [marketLut] : [],
|
|
675
|
+
1_400_000,
|
|
676
|
+
);
|
|
677
|
+
}
|