@carrot-protocol/clend-rpc 0.0.1-mrgn-fork1-dev-7be6ef2
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/.prettierignore +1 -0
- package/makefile +12 -0
- package/package.json +32 -0
- package/src/addresses.ts +206 -0
- package/src/idl/clend.ts +7509 -0
- package/src/index.ts +31 -0
- package/src/instructions.ts +466 -0
- package/src/jupUtils.ts +347 -0
- package/src/jupiterUtils.ts +288 -0
- package/src/logger.ts +21 -0
- package/src/math.ts +684 -0
- package/src/mockJupiterUtils.ts +109 -0
- package/src/rpc.ts +1296 -0
- package/src/state.ts +512 -0
- package/src/utils.ts +249 -0
- package/test/bank.test.ts +95 -0
- package/test/interest-rate.test.ts +114 -0
- package/test/leverage.test.ts +867 -0
- package/test/token-amounts.test.ts +73 -0
- package/tsconfig.json +17 -0
package/src/rpc.ts
ADDED
|
@@ -0,0 +1,1296 @@
|
|
|
1
|
+
import { AnchorProvider, Wallet, web3, BN } from "@coral-xyz/anchor";
|
|
2
|
+
import Decimal from "decimal.js";
|
|
3
|
+
import { Instructions, GroupConfig } from "./instructions";
|
|
4
|
+
import { getAssociatedTokenAddressSync } from "@solana/spl-token";
|
|
5
|
+
import {
|
|
6
|
+
wrappedI80F48toBigNumber,
|
|
7
|
+
parseBankConfig,
|
|
8
|
+
getClendAccountRemainingAccounts,
|
|
9
|
+
getClendAccountActiveBanks,
|
|
10
|
+
getTokenProgramForMint,
|
|
11
|
+
getTokenDecimalsForMint,
|
|
12
|
+
} from "./utils";
|
|
13
|
+
import { getFeeStatePda, getBankPda } from "./addresses";
|
|
14
|
+
import {
|
|
15
|
+
BankConfigCompact,
|
|
16
|
+
ClendGroup,
|
|
17
|
+
Bank,
|
|
18
|
+
ClendAccount,
|
|
19
|
+
FeeState,
|
|
20
|
+
Balance,
|
|
21
|
+
newClendAccountBalance,
|
|
22
|
+
InterestRateConfigCompact,
|
|
23
|
+
InterestRateConfigOpt,
|
|
24
|
+
} from "./state";
|
|
25
|
+
import {
|
|
26
|
+
uiToAmount,
|
|
27
|
+
amountToUi,
|
|
28
|
+
computeAdjustLeverageAmounts,
|
|
29
|
+
computeLoopingAmounts,
|
|
30
|
+
computeMaxLeverage,
|
|
31
|
+
calculateWeightedLeverage,
|
|
32
|
+
} from "./math";
|
|
33
|
+
import {
|
|
34
|
+
IJupiterUtils,
|
|
35
|
+
JupiterUtils,
|
|
36
|
+
SwapInputs,
|
|
37
|
+
SwapQuote,
|
|
38
|
+
SwapIxs,
|
|
39
|
+
} from "./jupiterUtils";
|
|
40
|
+
import { QuoteResponse, SwapMode } from "@jup-ag/api";
|
|
41
|
+
import { computeWithdrawLeverageAmounts } from "./math";
|
|
42
|
+
|
|
43
|
+
export class ClendClient {
|
|
44
|
+
readonly connection: web3.Connection;
|
|
45
|
+
readonly provider: AnchorProvider;
|
|
46
|
+
readonly instructions: Instructions;
|
|
47
|
+
private jupiterUtils: IJupiterUtils;
|
|
48
|
+
|
|
49
|
+
constructor(
|
|
50
|
+
connection: web3.Connection,
|
|
51
|
+
wallet: Wallet,
|
|
52
|
+
skipPreflight: boolean = false,
|
|
53
|
+
jupiterUtils?: IJupiterUtils,
|
|
54
|
+
) {
|
|
55
|
+
this.provider = new AnchorProvider(connection, wallet, {
|
|
56
|
+
commitment: "processed",
|
|
57
|
+
skipPreflight,
|
|
58
|
+
});
|
|
59
|
+
this.connection = connection;
|
|
60
|
+
this.instructions = new Instructions(this.provider);
|
|
61
|
+
this.jupiterUtils = jupiterUtils || new JupiterUtils();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
address(): web3.PublicKey {
|
|
65
|
+
return this.provider.wallet.publicKey;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get Jupiter price for a token pair
|
|
70
|
+
*/
|
|
71
|
+
async getJupiterPrice(mint: web3.PublicKey | string): Promise<number> {
|
|
72
|
+
return this.jupiterUtils.getJupiterPrice(mint);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get Jupiter quote for a swap
|
|
77
|
+
*/
|
|
78
|
+
async getJupiterQuote(
|
|
79
|
+
inputMint: web3.PublicKey,
|
|
80
|
+
outputMint: web3.PublicKey,
|
|
81
|
+
inputMintDecimals: number,
|
|
82
|
+
outputMintDecimals: number,
|
|
83
|
+
inputAmountLamports: Decimal,
|
|
84
|
+
slippageBps: number,
|
|
85
|
+
swapMode?: SwapMode,
|
|
86
|
+
): Promise<SwapQuote<QuoteResponse>> {
|
|
87
|
+
return this.jupiterUtils.getJupiterQuote(
|
|
88
|
+
inputMint,
|
|
89
|
+
outputMint,
|
|
90
|
+
inputMintDecimals,
|
|
91
|
+
outputMintDecimals,
|
|
92
|
+
inputAmountLamports,
|
|
93
|
+
slippageBps,
|
|
94
|
+
swapMode,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get Jupiter swap instructions
|
|
100
|
+
*/
|
|
101
|
+
async getJupiterSwap(
|
|
102
|
+
payer: web3.PublicKey,
|
|
103
|
+
inputs: SwapInputs,
|
|
104
|
+
quote: SwapQuote<QuoteResponse>,
|
|
105
|
+
): Promise<SwapIxs> {
|
|
106
|
+
return this.jupiterUtils.getJupiterSwap(payer, inputs, quote);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async send(
|
|
110
|
+
ixns: web3.TransactionInstruction[],
|
|
111
|
+
additionalSigners: web3.Signer[] = [],
|
|
112
|
+
additionalLuts: web3.PublicKey[] = [],
|
|
113
|
+
): Promise<string> {
|
|
114
|
+
const { blockhash, lastValidBlockHeight } =
|
|
115
|
+
await this.connection.getLatestBlockhash({
|
|
116
|
+
commitment: "confirmed",
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// dedup
|
|
120
|
+
const luts = new Set([...additionalLuts]);
|
|
121
|
+
const lutAccounts: web3.AddressLookupTableAccount[] = [];
|
|
122
|
+
for (const lut of luts) {
|
|
123
|
+
const account = (await this.connection.getAddressLookupTable(lut)).value!;
|
|
124
|
+
lutAccounts.push(account);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const msg = new web3.TransactionMessage({
|
|
128
|
+
payerKey: this.address(),
|
|
129
|
+
recentBlockhash: blockhash,
|
|
130
|
+
instructions: [...ixns],
|
|
131
|
+
}).compileToV0Message(lutAccounts);
|
|
132
|
+
|
|
133
|
+
const tx = new web3.VersionedTransaction(msg);
|
|
134
|
+
const signedTx = await this.provider.wallet.signTransaction(tx);
|
|
135
|
+
signedTx.sign(additionalSigners);
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const txSig = await this.connection.sendRawTransaction(
|
|
139
|
+
signedTx.serialize(),
|
|
140
|
+
{
|
|
141
|
+
skipPreflight: this.provider.opts.skipPreflight, // set at client init
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// will throw an error if not found or tx errored
|
|
146
|
+
await this.confirmTx(txSig, blockhash, lastValidBlockHeight);
|
|
147
|
+
|
|
148
|
+
return txSig;
|
|
149
|
+
} catch (e) {
|
|
150
|
+
if (e instanceof web3.SendTransactionError) {
|
|
151
|
+
throw new Error(`tx failed: ${e.logs}`);
|
|
152
|
+
}
|
|
153
|
+
throw e;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async confirmTx(
|
|
158
|
+
txSig: string,
|
|
159
|
+
blockhash: string,
|
|
160
|
+
lastValidBlockHeight: number,
|
|
161
|
+
): Promise<void> {
|
|
162
|
+
const result = await this.connection.confirmTransaction(
|
|
163
|
+
{
|
|
164
|
+
signature: txSig,
|
|
165
|
+
lastValidBlockHeight,
|
|
166
|
+
blockhash,
|
|
167
|
+
},
|
|
168
|
+
"confirmed",
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// check error
|
|
172
|
+
const err = result.value.err;
|
|
173
|
+
if (err !== null) {
|
|
174
|
+
throw new Error(`tx returned an error: ${JSON.stringify(err)}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Group Operations
|
|
179
|
+
async initializeGroup(
|
|
180
|
+
clendGroup?: web3.Keypair,
|
|
181
|
+
): Promise<{ clendGroup: web3.PublicKey; txSig: string }> {
|
|
182
|
+
const feeState = getFeeStatePda();
|
|
183
|
+
|
|
184
|
+
if (!clendGroup) {
|
|
185
|
+
clendGroup = web3.Keypair.generate();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const ix = await this.instructions.initializeGroup(
|
|
189
|
+
clendGroup,
|
|
190
|
+
this.address(),
|
|
191
|
+
feeState,
|
|
192
|
+
);
|
|
193
|
+
const txSig = await this.send([ix], [clendGroup]);
|
|
194
|
+
|
|
195
|
+
return { clendGroup: clendGroup.publicKey, txSig };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async configureGroup(
|
|
199
|
+
clendGroup: web3.PublicKey,
|
|
200
|
+
config: GroupConfig,
|
|
201
|
+
): Promise<string> {
|
|
202
|
+
const ix = await this.instructions.configureGroup(
|
|
203
|
+
clendGroup,
|
|
204
|
+
this.address(),
|
|
205
|
+
config,
|
|
206
|
+
);
|
|
207
|
+
return this.send([ix]);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Fee State Operations
|
|
211
|
+
async initGlobalFeeState(
|
|
212
|
+
feeWallet: web3.PublicKey,
|
|
213
|
+
bankInitFlatSolFee: number,
|
|
214
|
+
programFeeFixed: BN,
|
|
215
|
+
programFeeRate: BN,
|
|
216
|
+
): Promise<string> {
|
|
217
|
+
const initIx = await this.instructions.initGlobalFeeState(
|
|
218
|
+
this.address(),
|
|
219
|
+
feeWallet,
|
|
220
|
+
bankInitFlatSolFee,
|
|
221
|
+
programFeeFixed,
|
|
222
|
+
programFeeRate,
|
|
223
|
+
);
|
|
224
|
+
return this.send([initIx]);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Bank Operations
|
|
228
|
+
async addBank(
|
|
229
|
+
clendGroup: web3.PublicKey,
|
|
230
|
+
bankMint: web3.PublicKey,
|
|
231
|
+
pythOracle: web3.PublicKey,
|
|
232
|
+
pythOracleFeedId: web3.PublicKey,
|
|
233
|
+
bankConfig: BankConfigCompact,
|
|
234
|
+
interestRateConfig: InterestRateConfigCompact,
|
|
235
|
+
): Promise<{ bank: web3.PublicKey; txSig: string }> {
|
|
236
|
+
const tokenProgram = getTokenProgramForMint(bankMint);
|
|
237
|
+
|
|
238
|
+
const feeStateAccountData = await this.getFeeState();
|
|
239
|
+
const globalFeeWallet = feeStateAccountData.globalFeeWallet;
|
|
240
|
+
|
|
241
|
+
const { bank, ixns } = await this.instructions.addBank(
|
|
242
|
+
clendGroup,
|
|
243
|
+
this.address(),
|
|
244
|
+
globalFeeWallet,
|
|
245
|
+
bankMint,
|
|
246
|
+
tokenProgram,
|
|
247
|
+
pythOracle,
|
|
248
|
+
pythOracleFeedId,
|
|
249
|
+
bankConfig,
|
|
250
|
+
interestRateConfig,
|
|
251
|
+
);
|
|
252
|
+
const txSig = await this.send(ixns);
|
|
253
|
+
return { bank, txSig };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Account fetching Operations
|
|
257
|
+
async getClendGroup(clendGroup: web3.PublicKey): Promise<ClendGroup> {
|
|
258
|
+
const accountInfo = await this.connection.getAccountInfo(clendGroup);
|
|
259
|
+
if (accountInfo === null) {
|
|
260
|
+
throw new Error(`clend group not found: ${clendGroup.toString()}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const data = this.instructions.program.coder.accounts.decode(
|
|
264
|
+
"clendGroup",
|
|
265
|
+
accountInfo.data,
|
|
266
|
+
);
|
|
267
|
+
return {
|
|
268
|
+
admin: new web3.PublicKey(data.admin),
|
|
269
|
+
flags: data.flags,
|
|
270
|
+
padding: data.padding,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async getBank(bank: web3.PublicKey): Promise<Bank> {
|
|
275
|
+
const accountInfo = await this.connection.getAccountInfo(bank);
|
|
276
|
+
if (accountInfo === null) {
|
|
277
|
+
throw new Error(`bank not found: ${bank.toString()}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const data = this.instructions.program.coder.accounts.decode(
|
|
281
|
+
"bank",
|
|
282
|
+
accountInfo.data,
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
key: bank,
|
|
287
|
+
mint: new web3.PublicKey(data.mint),
|
|
288
|
+
mintDecimals: data.mintDecimals,
|
|
289
|
+
group: new web3.PublicKey(data.group),
|
|
290
|
+
assetShareValue: data.assetShareValue,
|
|
291
|
+
liabilityShareValue: data.liabilityShareValue,
|
|
292
|
+
liquidityVault: new web3.PublicKey(data.liquidityVault),
|
|
293
|
+
liquidityVaultBump: data.liquidityVaultBump,
|
|
294
|
+
liquidityVaultAuthorityBump: data.liquidityVaultAuthorityBump,
|
|
295
|
+
insuranceVault: new web3.PublicKey(data.insuranceVault),
|
|
296
|
+
insuranceVaultBump: data.insuranceVaultBump,
|
|
297
|
+
insuranceVaultAuthorityBump: data.insuranceVaultAuthorityBump,
|
|
298
|
+
collectedInsuranceFeesOutstanding: data.collectedInsuranceFeesOutstanding,
|
|
299
|
+
feeVault: new web3.PublicKey(data.feeVault),
|
|
300
|
+
feeVaultBump: data.feeVaultBump,
|
|
301
|
+
feeVaultAuthorityBump: data.feeVaultAuthorityBump,
|
|
302
|
+
collectedGroupFeesOutstanding: data.collectedGroupFeesOutstanding,
|
|
303
|
+
totalLiabilityShares: data.totalLiabilityShares,
|
|
304
|
+
totalAssetShares: data.totalAssetShares,
|
|
305
|
+
lastUpdate: new BN(data.lastUpdate),
|
|
306
|
+
config: parseBankConfig(data.config),
|
|
307
|
+
flags: new BN(data.flags),
|
|
308
|
+
emissionsRate: new BN(data.emissionsRate),
|
|
309
|
+
emissionsRemaining: data.emissionsRemaining,
|
|
310
|
+
emissionsMint: new web3.PublicKey(data.emissionsMint),
|
|
311
|
+
collectedProgramFeesOutstanding: data.collectedProgramFeesOutstanding,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async getClendAccount(
|
|
316
|
+
clendAccount: web3.PublicKey,
|
|
317
|
+
): Promise<ClendAccount | undefined> {
|
|
318
|
+
const accountInfo = await this.connection.getAccountInfo(clendAccount);
|
|
319
|
+
if (accountInfo === null) {
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const data = this.instructions.program.coder.accounts.decode(
|
|
324
|
+
"clendAccount",
|
|
325
|
+
accountInfo.data,
|
|
326
|
+
);
|
|
327
|
+
const balances: Balance[] = [];
|
|
328
|
+
for (const balance of data.lendingAccount.balances) {
|
|
329
|
+
balances.push(newClendAccountBalance(balance));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
authority: new web3.PublicKey(data.authority),
|
|
334
|
+
group: new web3.PublicKey(data.group),
|
|
335
|
+
accountFlags: data.accountFlags,
|
|
336
|
+
lendingAccount: {
|
|
337
|
+
balances,
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async getFeeState(): Promise<FeeState> {
|
|
343
|
+
const feeStatePda = getFeeStatePda();
|
|
344
|
+
const accountInfo = await this.connection.getAccountInfo(feeStatePda);
|
|
345
|
+
if (accountInfo === null) {
|
|
346
|
+
throw new Error(`fee state not found: ${feeStatePda.toString()}`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const data = this.instructions.program.coder.accounts.decode(
|
|
350
|
+
"feeState",
|
|
351
|
+
accountInfo.data,
|
|
352
|
+
);
|
|
353
|
+
return {
|
|
354
|
+
key: feeStatePda,
|
|
355
|
+
globalFeeAdmin: new web3.PublicKey(data.globalFeeAdmin),
|
|
356
|
+
globalFeeWallet: new web3.PublicKey(data.globalFeeWallet),
|
|
357
|
+
bankInitFlatSolFee: data.bankInitFlatSolFee,
|
|
358
|
+
programFeeFixed: wrappedI80F48toBigNumber(data.programFeeFixed),
|
|
359
|
+
programFeeRate: wrappedI80F48toBigNumber(data.programFeeRate),
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Account Operations
|
|
364
|
+
async initializeClendAccount(
|
|
365
|
+
clendGroup: web3.PublicKey,
|
|
366
|
+
): Promise<{ clendAccount: web3.PublicKey; txSig: string }> {
|
|
367
|
+
const clendAccount = web3.Keypair.generate();
|
|
368
|
+
|
|
369
|
+
const ix = await this.instructions.initializeClendAccount(
|
|
370
|
+
clendGroup,
|
|
371
|
+
this.address(),
|
|
372
|
+
clendAccount,
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
const txSig = await this.send([ix], [clendAccount]);
|
|
376
|
+
|
|
377
|
+
return { clendAccount: clendAccount.publicKey, txSig };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Lending Operations
|
|
381
|
+
async deposit(
|
|
382
|
+
clendGroup: web3.PublicKey,
|
|
383
|
+
clendAccount: web3.PublicKey,
|
|
384
|
+
mint: web3.PublicKey,
|
|
385
|
+
amount: BN,
|
|
386
|
+
remainingAccounts: web3.AccountMeta[] = [],
|
|
387
|
+
): Promise<string> {
|
|
388
|
+
const bank = getBankPda(clendGroup, mint);
|
|
389
|
+
|
|
390
|
+
// Get bank data to access the liquidityVault and mint
|
|
391
|
+
const bankData = await this.getBank(bank);
|
|
392
|
+
|
|
393
|
+
// Get token program
|
|
394
|
+
const tokenProgram = getTokenProgramForMint(bankData.mint);
|
|
395
|
+
|
|
396
|
+
// Derive the associated token account for the signer
|
|
397
|
+
const signerTokenAccount = getAssociatedTokenAddressSync(
|
|
398
|
+
mint,
|
|
399
|
+
this.address(),
|
|
400
|
+
true,
|
|
401
|
+
tokenProgram,
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
const ix = await this.instructions.deposit(
|
|
405
|
+
clendGroup,
|
|
406
|
+
clendAccount,
|
|
407
|
+
this.address(),
|
|
408
|
+
bank,
|
|
409
|
+
signerTokenAccount,
|
|
410
|
+
tokenProgram,
|
|
411
|
+
amount,
|
|
412
|
+
remainingAccounts,
|
|
413
|
+
);
|
|
414
|
+
return this.send([ix]);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async withdraw(
|
|
418
|
+
clendGroup: web3.PublicKey,
|
|
419
|
+
clendAccount: web3.PublicKey,
|
|
420
|
+
mint: web3.PublicKey,
|
|
421
|
+
amount: BN,
|
|
422
|
+
withdrawAll: boolean | null = null,
|
|
423
|
+
): Promise<string> {
|
|
424
|
+
const bank = getBankPda(clendGroup, mint);
|
|
425
|
+
|
|
426
|
+
// Get token program
|
|
427
|
+
const tokenProgram = getTokenProgramForMint(mint);
|
|
428
|
+
|
|
429
|
+
// Derive the associated token account for the signer
|
|
430
|
+
const destinationTokenAccount = getAssociatedTokenAddressSync(
|
|
431
|
+
mint,
|
|
432
|
+
this.address(),
|
|
433
|
+
true,
|
|
434
|
+
tokenProgram,
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
// get required remaining accounts based on account balance
|
|
438
|
+
const clendAccountData = await this.getClendAccount(clendAccount);
|
|
439
|
+
let activeBanks = getClendAccountActiveBanks(clendAccountData!);
|
|
440
|
+
|
|
441
|
+
// If withdrawAll is true, remove the target bank from activeBanks
|
|
442
|
+
// as we wont have a remaining balance after the withdraw operation
|
|
443
|
+
if (withdrawAll === true) {
|
|
444
|
+
activeBanks = activeBanks.filter((b) => !b.equals(bank));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// fetch all active banks
|
|
448
|
+
const activeBankData: Bank[] = [];
|
|
449
|
+
for (const bank of activeBanks) {
|
|
450
|
+
const bankData = await this.getBank(bank);
|
|
451
|
+
activeBankData.push(bankData);
|
|
452
|
+
}
|
|
453
|
+
const remainingAccounts = getClendAccountRemainingAccounts(activeBankData);
|
|
454
|
+
|
|
455
|
+
const ix = await this.instructions.withdraw(
|
|
456
|
+
clendGroup,
|
|
457
|
+
clendAccount,
|
|
458
|
+
this.address(),
|
|
459
|
+
bank,
|
|
460
|
+
destinationTokenAccount,
|
|
461
|
+
tokenProgram,
|
|
462
|
+
amount,
|
|
463
|
+
withdrawAll,
|
|
464
|
+
remainingAccounts,
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
return this.send([ix]);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async borrow(
|
|
471
|
+
clendGroup: web3.PublicKey,
|
|
472
|
+
clendAccount: web3.PublicKey,
|
|
473
|
+
mint: web3.PublicKey,
|
|
474
|
+
amount: BN,
|
|
475
|
+
): Promise<string> {
|
|
476
|
+
const bank = getBankPda(clendGroup, mint);
|
|
477
|
+
|
|
478
|
+
// Get bank data to access the liquidityVault and mint
|
|
479
|
+
const bankData = await this.getBank(bank);
|
|
480
|
+
|
|
481
|
+
// Get token program
|
|
482
|
+
const tokenProgram = getTokenProgramForMint(bankData.mint);
|
|
483
|
+
|
|
484
|
+
// Derive the associated token account for the signer
|
|
485
|
+
const destinationTokenAccount = getAssociatedTokenAddressSync(
|
|
486
|
+
mint,
|
|
487
|
+
this.address(),
|
|
488
|
+
true,
|
|
489
|
+
tokenProgram,
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
// fetch user clendAccount data
|
|
493
|
+
const clendAccountData = await this.getClendAccount(clendAccount);
|
|
494
|
+
let clendAccountActiveBanks = getClendAccountActiveBanks(clendAccountData!);
|
|
495
|
+
|
|
496
|
+
// add bank to active banks if not already in the array
|
|
497
|
+
clendAccountActiveBanks = Array.from(
|
|
498
|
+
new Set([...clendAccountActiveBanks, bank]),
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
const activeBankData: Bank[] = [];
|
|
502
|
+
for (const bank of clendAccountActiveBanks) {
|
|
503
|
+
const bankData = await this.getBank(bank);
|
|
504
|
+
activeBankData.push(bankData);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const remainingAccounts = getClendAccountRemainingAccounts(activeBankData);
|
|
508
|
+
|
|
509
|
+
const ix = await this.instructions.borrow(
|
|
510
|
+
clendGroup,
|
|
511
|
+
clendAccount,
|
|
512
|
+
this.address(),
|
|
513
|
+
bank,
|
|
514
|
+
destinationTokenAccount,
|
|
515
|
+
tokenProgram,
|
|
516
|
+
amount,
|
|
517
|
+
remainingAccounts,
|
|
518
|
+
);
|
|
519
|
+
return this.send([ix]);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async repay(
|
|
523
|
+
clendGroup: web3.PublicKey,
|
|
524
|
+
clendAccount: web3.PublicKey,
|
|
525
|
+
mint: web3.PublicKey,
|
|
526
|
+
amount: BN,
|
|
527
|
+
repayAll: boolean | null = null,
|
|
528
|
+
): Promise<string> {
|
|
529
|
+
const bank = getBankPda(clendGroup, mint);
|
|
530
|
+
|
|
531
|
+
// Get bank data to access the liquidityVault and mint
|
|
532
|
+
const bankData = await this.getBank(bank);
|
|
533
|
+
|
|
534
|
+
// Get token program
|
|
535
|
+
const tokenProgram = getTokenProgramForMint(bankData.mint);
|
|
536
|
+
|
|
537
|
+
// Derive the associated token account for the signer
|
|
538
|
+
const userTokenAccount = getAssociatedTokenAddressSync(
|
|
539
|
+
bankData.mint,
|
|
540
|
+
this.address(),
|
|
541
|
+
true,
|
|
542
|
+
tokenProgram,
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
const clendAccountData = await this.getClendAccount(clendAccount);
|
|
546
|
+
const clendAccountActiveBanks = getClendAccountActiveBanks(
|
|
547
|
+
clendAccountData!,
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
const activeBankData: Bank[] = [];
|
|
551
|
+
for (const bank of clendAccountActiveBanks) {
|
|
552
|
+
const bankData = await this.getBank(bank);
|
|
553
|
+
activeBankData.push(bankData);
|
|
554
|
+
}
|
|
555
|
+
const remainingAccounts = getClendAccountRemainingAccounts(activeBankData);
|
|
556
|
+
|
|
557
|
+
const ix = await this.instructions.repay(
|
|
558
|
+
clendGroup,
|
|
559
|
+
clendAccount,
|
|
560
|
+
this.address(),
|
|
561
|
+
bank,
|
|
562
|
+
userTokenAccount,
|
|
563
|
+
tokenProgram,
|
|
564
|
+
amount,
|
|
565
|
+
repayAll,
|
|
566
|
+
remainingAccounts,
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
return this.send([ix]);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async editGlobalFeeWallet(
|
|
573
|
+
newGlobalFeeWallet: web3.PublicKey,
|
|
574
|
+
): Promise<string> {
|
|
575
|
+
const ix = await this.instructions.editGlobalFeeWallet(
|
|
576
|
+
this.address(),
|
|
577
|
+
newGlobalFeeWallet,
|
|
578
|
+
);
|
|
579
|
+
return this.send([ix]);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
async configureBankInterestRate(
|
|
583
|
+
clendGroup: web3.PublicKey,
|
|
584
|
+
bank: web3.PublicKey,
|
|
585
|
+
interestRateConfig: InterestRateConfigOpt,
|
|
586
|
+
): Promise<string> {
|
|
587
|
+
const ix = await this.instructions.configureBankInterestRate(
|
|
588
|
+
clendGroup,
|
|
589
|
+
this.address(),
|
|
590
|
+
bank,
|
|
591
|
+
interestRateConfig,
|
|
592
|
+
);
|
|
593
|
+
return this.send([ix]);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async depositLeverage(
|
|
597
|
+
clendGroup: web3.PublicKey,
|
|
598
|
+
clendAccount: web3.PublicKey | null,
|
|
599
|
+
collateralMint: web3.PublicKey,
|
|
600
|
+
debtMint: web3.PublicKey,
|
|
601
|
+
depositAmount: BN,
|
|
602
|
+
targetLeverage: number,
|
|
603
|
+
slippageBps: number,
|
|
604
|
+
): Promise<string> {
|
|
605
|
+
// Get token programs
|
|
606
|
+
const collateralTokenProgram = getTokenProgramForMint(collateralMint);
|
|
607
|
+
const debtTokenProgram = getTokenProgramForMint(debtMint);
|
|
608
|
+
|
|
609
|
+
// Get decimals
|
|
610
|
+
const collateralDecimals = getTokenDecimalsForMint(collateralMint);
|
|
611
|
+
const debtDecimals = getTokenDecimalsForMint(debtMint);
|
|
612
|
+
|
|
613
|
+
// Get bank addrs
|
|
614
|
+
const collateralBank = getBankPda(clendGroup, collateralMint);
|
|
615
|
+
const debtBank = getBankPda(clendGroup, debtMint);
|
|
616
|
+
|
|
617
|
+
// Get bank data
|
|
618
|
+
const collateralBankData = await this.getBank(collateralBank);
|
|
619
|
+
const debtBankData = await this.getBank(debtBank);
|
|
620
|
+
|
|
621
|
+
const { maxLeverage, ltv } = computeMaxLeverage(
|
|
622
|
+
wrappedI80F48toBigNumber(collateralBankData.config.assetWeightInit),
|
|
623
|
+
wrappedI80F48toBigNumber(debtBankData.config.liabilityWeightInit),
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
// Derive token accounts
|
|
627
|
+
const userCollateralAta = getAssociatedTokenAddressSync(
|
|
628
|
+
collateralMint,
|
|
629
|
+
this.address(),
|
|
630
|
+
true,
|
|
631
|
+
collateralTokenProgram,
|
|
632
|
+
);
|
|
633
|
+
const userDebtAta = getAssociatedTokenAddressSync(
|
|
634
|
+
debtMint,
|
|
635
|
+
this.address(),
|
|
636
|
+
true,
|
|
637
|
+
debtTokenProgram,
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
// Calculate borrow amount based on target leverage
|
|
641
|
+
const depositAmountUI = amountToUi(depositAmount, collateralDecimals);
|
|
642
|
+
|
|
643
|
+
// Get current prices from Jupiter
|
|
644
|
+
const collateralPrice = await this.getJupiterPrice(collateralMint);
|
|
645
|
+
const debtPrice = await this.getJupiterPrice(debtMint);
|
|
646
|
+
|
|
647
|
+
// Calculate collateral value in debt token terms
|
|
648
|
+
const { borrowAmountUi } = computeLoopingAmounts(
|
|
649
|
+
depositAmountUI,
|
|
650
|
+
targetLeverage,
|
|
651
|
+
collateralPrice,
|
|
652
|
+
debtPrice,
|
|
653
|
+
wrappedI80F48toBigNumber(collateralBankData.config.assetWeightInit),
|
|
654
|
+
wrappedI80F48toBigNumber(debtBankData.config.liabilityWeightInit),
|
|
655
|
+
);
|
|
656
|
+
const borrowAmount = uiToAmount(borrowAmountUi, debtDecimals);
|
|
657
|
+
|
|
658
|
+
// Get Jupiter quote for swapping debt token to collateral token
|
|
659
|
+
const swapMode = SwapMode.ExactIn;
|
|
660
|
+
const swapQuote = await this.getJupiterQuote(
|
|
661
|
+
debtMint,
|
|
662
|
+
collateralMint,
|
|
663
|
+
debtDecimals,
|
|
664
|
+
collateralDecimals,
|
|
665
|
+
new Decimal(borrowAmount.toString()),
|
|
666
|
+
slippageBps,
|
|
667
|
+
swapMode,
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
let activeBanks: web3.PublicKey[] = [];
|
|
671
|
+
const additionalSigners: web3.Keypair[] = [];
|
|
672
|
+
let initClendAccountIx: web3.TransactionInstruction | undefined = undefined;
|
|
673
|
+
if (clendAccount === null) {
|
|
674
|
+
console.log("Creating init clend account ix");
|
|
675
|
+
|
|
676
|
+
// create clend account
|
|
677
|
+
const clendAccountKp = web3.Keypair.generate();
|
|
678
|
+
const ix = await this.instructions.initializeClendAccount(
|
|
679
|
+
clendGroup,
|
|
680
|
+
this.address(),
|
|
681
|
+
clendAccountKp,
|
|
682
|
+
);
|
|
683
|
+
initClendAccountIx = ix;
|
|
684
|
+
additionalSigners.push(clendAccountKp);
|
|
685
|
+
clendAccount = clendAccountKp.publicKey;
|
|
686
|
+
} else {
|
|
687
|
+
// Get account data to determine active banks
|
|
688
|
+
const clendAccountData = await this.getClendAccount(clendAccount);
|
|
689
|
+
if (!clendAccountData) {
|
|
690
|
+
throw new Error(`Clend account not found: ${clendAccount.toString()}`);
|
|
691
|
+
}
|
|
692
|
+
activeBanks.push(...getClendAccountActiveBanks(clendAccountData));
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Add both banks to active banks if not already in the array
|
|
696
|
+
// dedup with set
|
|
697
|
+
activeBanks = Array.from(
|
|
698
|
+
new Set([...activeBanks, debtBank, collateralBank]), // debt first as we borrow first
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
const activeBankData: Bank[] = [];
|
|
702
|
+
for (const bank of activeBanks) {
|
|
703
|
+
const bankData = await this.getBank(bank);
|
|
704
|
+
activeBankData.push(bankData);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const remainingAccounts = getClendAccountRemainingAccounts(activeBankData);
|
|
708
|
+
|
|
709
|
+
// Get swap instructions
|
|
710
|
+
const swapInputs = {
|
|
711
|
+
inputAmountLamports: new Decimal(borrowAmount.toString()),
|
|
712
|
+
inputMint: debtMint,
|
|
713
|
+
outputMint: collateralMint,
|
|
714
|
+
};
|
|
715
|
+
const swapIxs = await this.getJupiterSwap(
|
|
716
|
+
this.address(),
|
|
717
|
+
swapInputs,
|
|
718
|
+
swapQuote,
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
// Calculate additional collateral from swap
|
|
722
|
+
const additionalCollateralAmount = new BN(
|
|
723
|
+
swapQuote.quoteResponse!.outAmount,
|
|
724
|
+
);
|
|
725
|
+
const totalCollateralAmount = depositAmount.add(additionalCollateralAmount);
|
|
726
|
+
|
|
727
|
+
// Create transaction instructions in correct order
|
|
728
|
+
const borrowIx = await this.instructions.borrow(
|
|
729
|
+
clendGroup,
|
|
730
|
+
clendAccount,
|
|
731
|
+
this.address(),
|
|
732
|
+
debtBank,
|
|
733
|
+
userDebtAta,
|
|
734
|
+
debtTokenProgram,
|
|
735
|
+
borrowAmount,
|
|
736
|
+
remainingAccounts,
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
const depositIx = await this.instructions.deposit(
|
|
740
|
+
clendGroup,
|
|
741
|
+
clendAccount,
|
|
742
|
+
this.address(),
|
|
743
|
+
collateralBank,
|
|
744
|
+
userCollateralAta,
|
|
745
|
+
collateralTokenProgram,
|
|
746
|
+
totalCollateralAmount,
|
|
747
|
+
remainingAccounts,
|
|
748
|
+
);
|
|
749
|
+
|
|
750
|
+
// add lending and swap instructions
|
|
751
|
+
const ixnsWithoutFlashLoan: web3.TransactionInstruction[] = [
|
|
752
|
+
borrowIx,
|
|
753
|
+
...swapIxs.swapIxs,
|
|
754
|
+
depositIx,
|
|
755
|
+
];
|
|
756
|
+
|
|
757
|
+
// If the clend account is being created, we need to add 1 to the endIndex
|
|
758
|
+
const { addInitClendAccountIncrement, initClendAccountIxns } =
|
|
759
|
+
initClendAccountIx
|
|
760
|
+
? {
|
|
761
|
+
addInitClendAccountIncrement: 1,
|
|
762
|
+
initClendAccountIxns: [initClendAccountIx],
|
|
763
|
+
}
|
|
764
|
+
: { addInitClendAccountIncrement: 0, initClendAccountIxns: [] };
|
|
765
|
+
const startFlashLoanIncrement = 1;
|
|
766
|
+
const endIndex = new BN(
|
|
767
|
+
ixnsWithoutFlashLoan.length +
|
|
768
|
+
addInitClendAccountIncrement +
|
|
769
|
+
startFlashLoanIncrement,
|
|
770
|
+
);
|
|
771
|
+
|
|
772
|
+
const { beginFlashLoanIx, endFlashLoanIx } =
|
|
773
|
+
await this.instructions.createFlashLoanInstructions(
|
|
774
|
+
clendAccount,
|
|
775
|
+
this.address(),
|
|
776
|
+
endIndex,
|
|
777
|
+
remainingAccounts,
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
const ixns: web3.TransactionInstruction[] = [
|
|
781
|
+
...initClendAccountIxns,
|
|
782
|
+
beginFlashLoanIx,
|
|
783
|
+
...ixnsWithoutFlashLoan,
|
|
784
|
+
endFlashLoanIx,
|
|
785
|
+
];
|
|
786
|
+
|
|
787
|
+
// Combine all instructions
|
|
788
|
+
return this.send(ixns, additionalSigners, swapIxs.lookupTables);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
async withdrawLeverage(
|
|
792
|
+
clendGroup: web3.PublicKey,
|
|
793
|
+
clendAccount: web3.PublicKey,
|
|
794
|
+
collateralMint: web3.PublicKey,
|
|
795
|
+
debtMint: web3.PublicKey,
|
|
796
|
+
withdrawAmount: BN,
|
|
797
|
+
withdrawAll: boolean,
|
|
798
|
+
slippageBps: number,
|
|
799
|
+
): Promise<string> {
|
|
800
|
+
console.log("\n=== WITHDRAW LEVERAGE ===");
|
|
801
|
+
console.log(`withdrawAll: ${withdrawAll}`);
|
|
802
|
+
console.log(`withdrawAmount: ${withdrawAmount.toString()}`);
|
|
803
|
+
console.log(`slippageBps: ${slippageBps}`);
|
|
804
|
+
|
|
805
|
+
// Get token programs
|
|
806
|
+
const collateralTokenProgram = getTokenProgramForMint(collateralMint);
|
|
807
|
+
const debtTokenProgram = getTokenProgramForMint(debtMint);
|
|
808
|
+
|
|
809
|
+
// Get bank data
|
|
810
|
+
const collateralBank = getBankPda(clendGroup, collateralMint);
|
|
811
|
+
const debtBank = getBankPda(clendGroup, debtMint);
|
|
812
|
+
const collateralBankData = await this.getBank(collateralBank);
|
|
813
|
+
const debtBankData = await this.getBank(debtBank);
|
|
814
|
+
|
|
815
|
+
// Get decimals
|
|
816
|
+
const collateralDecimals = collateralBankData.mintDecimals;
|
|
817
|
+
const debtDecimals = debtBankData.mintDecimals;
|
|
818
|
+
|
|
819
|
+
// Derive token accounts
|
|
820
|
+
const userCollateralAta = getAssociatedTokenAddressSync(
|
|
821
|
+
collateralMint,
|
|
822
|
+
this.address(),
|
|
823
|
+
true,
|
|
824
|
+
collateralTokenProgram,
|
|
825
|
+
);
|
|
826
|
+
const userDebtAta = getAssociatedTokenAddressSync(
|
|
827
|
+
debtMint,
|
|
828
|
+
this.address(),
|
|
829
|
+
true,
|
|
830
|
+
debtTokenProgram,
|
|
831
|
+
);
|
|
832
|
+
|
|
833
|
+
// Get account data
|
|
834
|
+
const clendAccountData = await this.getClendAccount(clendAccount);
|
|
835
|
+
if (!clendAccountData) {
|
|
836
|
+
throw new Error(`Clend account not found: ${clendAccount.toString()}`);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Find the debt and collateral positions
|
|
840
|
+
let debtBalance: Balance | undefined;
|
|
841
|
+
let collateralBalance: Balance | undefined;
|
|
842
|
+
|
|
843
|
+
for (const balance of clendAccountData.lendingAccount.balances) {
|
|
844
|
+
if (balance.bankPk && balance.bankPk.equals(debtBank)) {
|
|
845
|
+
debtBalance = balance;
|
|
846
|
+
}
|
|
847
|
+
if (balance.bankPk && balance.bankPk.equals(collateralBank)) {
|
|
848
|
+
collateralBalance = balance;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
if (!debtBalance || !collateralBalance) {
|
|
853
|
+
throw new Error("Could not find debt or collateral position in account");
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Get the current collateral and debt amounts
|
|
857
|
+
const collateralAmount = wrappedI80F48toBigNumber(
|
|
858
|
+
collateralBalance.assetShares,
|
|
859
|
+
);
|
|
860
|
+
const debtAmount = wrappedI80F48toBigNumber(debtBalance.liabilityShares);
|
|
861
|
+
console.log(`collateralAmount: ${collateralAmount}`);
|
|
862
|
+
console.log(`debtAmount: ${debtAmount}`);
|
|
863
|
+
|
|
864
|
+
// For UI display and calculations
|
|
865
|
+
const collateralAmountUI = amountToUi(collateralAmount, collateralDecimals);
|
|
866
|
+
const debtAmountUI = amountToUi(debtAmount, debtDecimals);
|
|
867
|
+
|
|
868
|
+
// Get current prices
|
|
869
|
+
const collateralPrice = await this.getJupiterPrice(collateralMint);
|
|
870
|
+
const debtPrice = await this.getJupiterPrice(debtMint);
|
|
871
|
+
|
|
872
|
+
// Determine collateral to withdraw and debt to repay
|
|
873
|
+
let collateralToWithdraw: BN;
|
|
874
|
+
let debtToRepay: BN;
|
|
875
|
+
|
|
876
|
+
if (withdrawAll) {
|
|
877
|
+
// If withdrawing all, use full collateral and debt amounts
|
|
878
|
+
collateralToWithdraw = new BN(Math.floor(collateralAmount));
|
|
879
|
+
debtToRepay = new BN(Math.floor(debtAmount));
|
|
880
|
+
} else {
|
|
881
|
+
// For partial withdrawal, calculate debt to maintain leverage
|
|
882
|
+
const withdrawAmountUI = amountToUi(withdrawAmount, collateralDecimals);
|
|
883
|
+
|
|
884
|
+
// Get the weights for leverage calculation
|
|
885
|
+
const collateralWeight = wrappedI80F48toBigNumber(
|
|
886
|
+
collateralBankData.config.assetWeightInit,
|
|
887
|
+
);
|
|
888
|
+
const debtWeight = wrappedI80F48toBigNumber(
|
|
889
|
+
debtBankData.config.liabilityWeightInit,
|
|
890
|
+
);
|
|
891
|
+
|
|
892
|
+
// Calculate leverage-preserving withdrawal
|
|
893
|
+
const result = computeWithdrawLeverageAmounts(
|
|
894
|
+
collateralAmountUI,
|
|
895
|
+
debtAmountUI,
|
|
896
|
+
withdrawAmountUI,
|
|
897
|
+
collateralPrice,
|
|
898
|
+
debtPrice,
|
|
899
|
+
collateralWeight,
|
|
900
|
+
debtWeight,
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
console.log(`Partial withdrawal calculations:`);
|
|
904
|
+
console.log(`Current leverage: ${result.currentLeverage}`);
|
|
905
|
+
console.log(`Debt to repay: ${result.debtToRepay}`);
|
|
906
|
+
console.log(`New predicted leverage: ${result.newLeverage}`);
|
|
907
|
+
|
|
908
|
+
// Set the amounts to withdraw and repay
|
|
909
|
+
collateralToWithdraw = withdrawAmount;
|
|
910
|
+
debtToRepay = uiToAmount(result.debtToRepay, debtDecimals);
|
|
911
|
+
|
|
912
|
+
// Add a small buffer for interest accrual
|
|
913
|
+
debtToRepay = debtToRepay.add(new BN(1));
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Get remaining accounts for flash loan
|
|
917
|
+
let activeBanks = getClendAccountActiveBanks(clendAccountData);
|
|
918
|
+
|
|
919
|
+
// If withdrawAll is true, remove both banks from activeBanks
|
|
920
|
+
if (withdrawAll) {
|
|
921
|
+
activeBanks = activeBanks.filter(
|
|
922
|
+
(b) => !b.equals(collateralBank) && !b.equals(debtBank),
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const activeBankData: Bank[] = [];
|
|
927
|
+
for (const bank of activeBanks) {
|
|
928
|
+
const bankData = await this.getBank(bank);
|
|
929
|
+
activeBankData.push(bankData);
|
|
930
|
+
}
|
|
931
|
+
const remainingAccounts = getClendAccountRemainingAccounts(activeBankData);
|
|
932
|
+
|
|
933
|
+
// Get Jupiter quote for swapping collateral to debt token (ExactOut mode)
|
|
934
|
+
console.log(`Getting Jupiter quote for swapping collateral to debt...`);
|
|
935
|
+
const swapMode = SwapMode.ExactOut;
|
|
936
|
+
const swapQuote = await this.getJupiterQuote(
|
|
937
|
+
collateralMint,
|
|
938
|
+
debtMint,
|
|
939
|
+
collateralDecimals,
|
|
940
|
+
debtDecimals,
|
|
941
|
+
new Decimal(debtToRepay.toString()),
|
|
942
|
+
slippageBps,
|
|
943
|
+
swapMode,
|
|
944
|
+
);
|
|
945
|
+
|
|
946
|
+
// Calculate how much collateral we need to swap for debt repayment
|
|
947
|
+
const collateralToSwap = new BN(swapQuote.quoteResponse!.inAmount);
|
|
948
|
+
console.log(`Collateral needed for swap: ${collateralToSwap.toString()}`);
|
|
949
|
+
console.log(`Collateral to withdraw: ${collateralToWithdraw.toString()}`);
|
|
950
|
+
|
|
951
|
+
// Get swap instructions
|
|
952
|
+
const swapInputs = {
|
|
953
|
+
inputAmountLamports: new Decimal(collateralToSwap.toString()),
|
|
954
|
+
inputMint: collateralMint,
|
|
955
|
+
outputMint: debtMint,
|
|
956
|
+
};
|
|
957
|
+
const swapIxs = await this.getJupiterSwap(
|
|
958
|
+
this.address(),
|
|
959
|
+
swapInputs,
|
|
960
|
+
swapQuote,
|
|
961
|
+
);
|
|
962
|
+
|
|
963
|
+
// Create withdraw instruction
|
|
964
|
+
console.log(`Creating withdrawal instruction...`);
|
|
965
|
+
const withdrawIx = await this.instructions.withdraw(
|
|
966
|
+
clendGroup,
|
|
967
|
+
clendAccount,
|
|
968
|
+
this.address(),
|
|
969
|
+
collateralBank,
|
|
970
|
+
userCollateralAta,
|
|
971
|
+
collateralTokenProgram,
|
|
972
|
+
collateralToWithdraw,
|
|
973
|
+
withdrawAll,
|
|
974
|
+
remainingAccounts,
|
|
975
|
+
);
|
|
976
|
+
|
|
977
|
+
// Create repay instruction
|
|
978
|
+
console.log(`Creating repay instruction...`);
|
|
979
|
+
const repayIx = await this.instructions.repay(
|
|
980
|
+
clendGroup,
|
|
981
|
+
clendAccount,
|
|
982
|
+
this.address(),
|
|
983
|
+
debtBank,
|
|
984
|
+
userDebtAta,
|
|
985
|
+
debtTokenProgram,
|
|
986
|
+
debtToRepay,
|
|
987
|
+
withdrawAll,
|
|
988
|
+
remainingAccounts,
|
|
989
|
+
);
|
|
990
|
+
|
|
991
|
+
// Assemble all instructions without flash loan
|
|
992
|
+
const ixnsWithoutFlashLoan: web3.TransactionInstruction[] = [
|
|
993
|
+
withdrawIx,
|
|
994
|
+
...swapIxs.swapIxs,
|
|
995
|
+
repayIx,
|
|
996
|
+
];
|
|
997
|
+
|
|
998
|
+
// Create flash loan instructions
|
|
999
|
+
const endIndex = new BN(ixnsWithoutFlashLoan.length + 1);
|
|
1000
|
+
const { beginFlashLoanIx, endFlashLoanIx } =
|
|
1001
|
+
await this.instructions.createFlashLoanInstructions(
|
|
1002
|
+
clendAccount,
|
|
1003
|
+
this.address(),
|
|
1004
|
+
endIndex,
|
|
1005
|
+
remainingAccounts,
|
|
1006
|
+
);
|
|
1007
|
+
|
|
1008
|
+
// Assemble all instructions in the correct order
|
|
1009
|
+
const instructions = [
|
|
1010
|
+
beginFlashLoanIx,
|
|
1011
|
+
...ixnsWithoutFlashLoan,
|
|
1012
|
+
endFlashLoanIx,
|
|
1013
|
+
];
|
|
1014
|
+
|
|
1015
|
+
// Send transaction
|
|
1016
|
+
console.log(`Sending transaction...`);
|
|
1017
|
+
return this.send(instructions, [], swapIxs.lookupTables);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
async adjustLeverage(
|
|
1021
|
+
clendGroup: web3.PublicKey,
|
|
1022
|
+
clendAccount: web3.PublicKey,
|
|
1023
|
+
collateralMint: web3.PublicKey,
|
|
1024
|
+
debtMint: web3.PublicKey,
|
|
1025
|
+
targetLeverage: number,
|
|
1026
|
+
slippageBps: number,
|
|
1027
|
+
): Promise<string> {
|
|
1028
|
+
// Get token programs
|
|
1029
|
+
const collateralTokenProgram = getTokenProgramForMint(collateralMint);
|
|
1030
|
+
const debtTokenProgram = getTokenProgramForMint(debtMint);
|
|
1031
|
+
|
|
1032
|
+
// Get bank data
|
|
1033
|
+
const collateralBank = getBankPda(clendGroup, collateralMint);
|
|
1034
|
+
const debtBank = getBankPda(clendGroup, debtMint);
|
|
1035
|
+
const collateralBankData = await this.getBank(collateralBank);
|
|
1036
|
+
const debtBankData = await this.getBank(debtBank);
|
|
1037
|
+
|
|
1038
|
+
// Derive token accounts
|
|
1039
|
+
const userCollateralAta = getAssociatedTokenAddressSync(
|
|
1040
|
+
collateralMint,
|
|
1041
|
+
this.address(),
|
|
1042
|
+
true,
|
|
1043
|
+
collateralTokenProgram,
|
|
1044
|
+
);
|
|
1045
|
+
const userDebtAta = getAssociatedTokenAddressSync(
|
|
1046
|
+
debtMint,
|
|
1047
|
+
this.address(),
|
|
1048
|
+
true,
|
|
1049
|
+
debtTokenProgram,
|
|
1050
|
+
);
|
|
1051
|
+
|
|
1052
|
+
// Get account data to determine current positions
|
|
1053
|
+
const clendAccountData = await this.getClendAccount(clendAccount);
|
|
1054
|
+
if (!clendAccountData) {
|
|
1055
|
+
throw new Error(`Clend account not found: ${clendAccount.toString()}`);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// Find the debt and collateral positions
|
|
1059
|
+
let debtBalance: Balance | undefined;
|
|
1060
|
+
let collateralBalance: Balance | undefined;
|
|
1061
|
+
|
|
1062
|
+
for (const balance of clendAccountData.lendingAccount.balances) {
|
|
1063
|
+
if (balance.bankPk && balance.bankPk.equals(debtBank)) {
|
|
1064
|
+
debtBalance = balance;
|
|
1065
|
+
}
|
|
1066
|
+
if (balance.bankPk && balance.bankPk.equals(collateralBank)) {
|
|
1067
|
+
collateralBalance = balance;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
if (!debtBalance || !collateralBalance) {
|
|
1072
|
+
throw new Error("Could not find debt or collateral position in account");
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// Calculate current leverage
|
|
1076
|
+
const collateralDecimals = collateralBankData.mintDecimals;
|
|
1077
|
+
const debtDecimals = debtBankData.mintDecimals;
|
|
1078
|
+
|
|
1079
|
+
// Get current prices from Jupiter
|
|
1080
|
+
const collateralPrice = await this.getJupiterPrice(collateralMint);
|
|
1081
|
+
const debtPrice = await this.getJupiterPrice(debtMint);
|
|
1082
|
+
|
|
1083
|
+
// Convert to UI amounts for calculations
|
|
1084
|
+
const collateralAmountUI =
|
|
1085
|
+
wrappedI80F48toBigNumber(collateralBalance.assetShares) /
|
|
1086
|
+
10 ** collateralDecimals;
|
|
1087
|
+
const debtAmountUI =
|
|
1088
|
+
wrappedI80F48toBigNumber(debtBalance.liabilityShares) /
|
|
1089
|
+
10 ** debtDecimals;
|
|
1090
|
+
|
|
1091
|
+
// Calculate current leverage and values
|
|
1092
|
+
const collateralValue = collateralAmountUI * collateralPrice;
|
|
1093
|
+
const debtValue = debtAmountUI * debtPrice;
|
|
1094
|
+
const currentLeverage = calculateWeightedLeverage(
|
|
1095
|
+
collateralAmountUI,
|
|
1096
|
+
collateralPrice,
|
|
1097
|
+
wrappedI80F48toBigNumber(collateralBankData.config.assetWeightInit),
|
|
1098
|
+
debtAmountUI,
|
|
1099
|
+
debtPrice,
|
|
1100
|
+
wrappedI80F48toBigNumber(debtBankData.config.liabilityWeightInit),
|
|
1101
|
+
);
|
|
1102
|
+
|
|
1103
|
+
console.log(`=== ADJUST LEVERAGE DIAGNOSTICS ===`);
|
|
1104
|
+
console.log(
|
|
1105
|
+
`Current collateral: ${collateralAmountUI} (${collateralValue} USD)`,
|
|
1106
|
+
);
|
|
1107
|
+
console.log(`Current debt: ${debtAmountUI} (${debtValue} USD)`);
|
|
1108
|
+
console.log(`Current leverage: ${currentLeverage}`);
|
|
1109
|
+
console.log(`Target leverage: ${targetLeverage}`);
|
|
1110
|
+
|
|
1111
|
+
const { collateralDelta, debtDelta, isIncrease } =
|
|
1112
|
+
computeAdjustLeverageAmounts(
|
|
1113
|
+
collateralAmountUI,
|
|
1114
|
+
debtAmountUI,
|
|
1115
|
+
collateralPrice,
|
|
1116
|
+
debtPrice,
|
|
1117
|
+
currentLeverage,
|
|
1118
|
+
targetLeverage,
|
|
1119
|
+
);
|
|
1120
|
+
|
|
1121
|
+
console.log(`isIncrease from calculation: ${isIncrease}`);
|
|
1122
|
+
console.log(`collateralDelta: ${collateralDelta}`);
|
|
1123
|
+
console.log(`debtDelta: ${debtDelta}`);
|
|
1124
|
+
|
|
1125
|
+
// Get remaining accounts for flash loan
|
|
1126
|
+
let activeBanks = getClendAccountActiveBanks(clendAccountData);
|
|
1127
|
+
|
|
1128
|
+
// Add both banks to active banks if not already in the array
|
|
1129
|
+
activeBanks = Array.from(
|
|
1130
|
+
new Set([...activeBanks, collateralBank, debtBank]),
|
|
1131
|
+
);
|
|
1132
|
+
|
|
1133
|
+
const activeBankData: Bank[] = [];
|
|
1134
|
+
for (const bank of activeBanks) {
|
|
1135
|
+
const bankData = await this.getBank(bank);
|
|
1136
|
+
activeBankData.push(bankData);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
const remainingAccounts = getClendAccountRemainingAccounts(activeBankData);
|
|
1140
|
+
|
|
1141
|
+
// Prepare transaction instructions
|
|
1142
|
+
const instructions: web3.TransactionInstruction[] = [];
|
|
1143
|
+
let swapLookupTables: web3.PublicKey[] = [];
|
|
1144
|
+
|
|
1145
|
+
if (isIncrease) {
|
|
1146
|
+
console.log(`Increasing leverage`);
|
|
1147
|
+
|
|
1148
|
+
// Calculate amounts for increasing leverage
|
|
1149
|
+
const additionalDebtAmountUI = debtDelta * debtPrice;
|
|
1150
|
+
const additionalDebtAmount = uiToAmount(
|
|
1151
|
+
additionalDebtAmountUI,
|
|
1152
|
+
debtDecimals,
|
|
1153
|
+
);
|
|
1154
|
+
|
|
1155
|
+
// Get Jupiter quote for swapping debt token to collateral token
|
|
1156
|
+
const swapMode = SwapMode.ExactIn;
|
|
1157
|
+
const swapQuote = await this.getJupiterQuote(
|
|
1158
|
+
debtMint,
|
|
1159
|
+
collateralMint,
|
|
1160
|
+
debtDecimals,
|
|
1161
|
+
collateralDecimals,
|
|
1162
|
+
new Decimal(additionalDebtAmount.toString()),
|
|
1163
|
+
slippageBps,
|
|
1164
|
+
swapMode,
|
|
1165
|
+
);
|
|
1166
|
+
|
|
1167
|
+
// Get swap instructions
|
|
1168
|
+
const swapInputs = {
|
|
1169
|
+
inputAmountLamports: new Decimal(additionalDebtAmount.toString()),
|
|
1170
|
+
inputMint: debtMint,
|
|
1171
|
+
outputMint: collateralMint,
|
|
1172
|
+
};
|
|
1173
|
+
const swapIxs = await this.getJupiterSwap(
|
|
1174
|
+
this.address(),
|
|
1175
|
+
swapInputs,
|
|
1176
|
+
swapQuote,
|
|
1177
|
+
);
|
|
1178
|
+
|
|
1179
|
+
// Borrow more debt
|
|
1180
|
+
const borrowIx = await this.instructions.borrow(
|
|
1181
|
+
clendGroup,
|
|
1182
|
+
clendAccount,
|
|
1183
|
+
this.address(),
|
|
1184
|
+
debtBank,
|
|
1185
|
+
userDebtAta,
|
|
1186
|
+
debtTokenProgram,
|
|
1187
|
+
additionalDebtAmount,
|
|
1188
|
+
remainingAccounts,
|
|
1189
|
+
);
|
|
1190
|
+
|
|
1191
|
+
// Calculate additional collateral from swap
|
|
1192
|
+
const additionalCollateralAmount = new BN(
|
|
1193
|
+
swapQuote.quoteResponse!.outAmount,
|
|
1194
|
+
);
|
|
1195
|
+
|
|
1196
|
+
// Deposit additional collateral
|
|
1197
|
+
const depositIx = await this.instructions.deposit(
|
|
1198
|
+
clendGroup,
|
|
1199
|
+
clendAccount,
|
|
1200
|
+
this.address(),
|
|
1201
|
+
collateralBank,
|
|
1202
|
+
userCollateralAta,
|
|
1203
|
+
collateralTokenProgram,
|
|
1204
|
+
additionalCollateralAmount,
|
|
1205
|
+
remainingAccounts,
|
|
1206
|
+
);
|
|
1207
|
+
|
|
1208
|
+
instructions.push(borrowIx, ...swapIxs.swapIxs, depositIx);
|
|
1209
|
+
swapLookupTables = swapIxs.lookupTables;
|
|
1210
|
+
} else {
|
|
1211
|
+
console.log(`Decreasing leverage`);
|
|
1212
|
+
// Calculate amounts for decreasing leverage
|
|
1213
|
+
const debtToReduceAmount = new BN(
|
|
1214
|
+
Math.floor(debtDelta * 10 ** debtDecimals),
|
|
1215
|
+
);
|
|
1216
|
+
|
|
1217
|
+
// Get Jupiter quote for swapping collateral token to debt token with ExactOut
|
|
1218
|
+
const swapMode = SwapMode.ExactOut;
|
|
1219
|
+
const swapQuote = await this.getJupiterQuote(
|
|
1220
|
+
collateralMint,
|
|
1221
|
+
debtMint,
|
|
1222
|
+
collateralDecimals,
|
|
1223
|
+
debtDecimals,
|
|
1224
|
+
new Decimal(debtToReduceAmount.toString()),
|
|
1225
|
+
slippageBps,
|
|
1226
|
+
swapMode,
|
|
1227
|
+
);
|
|
1228
|
+
|
|
1229
|
+
// Calculate how much collateral we need to swap to repay the debt
|
|
1230
|
+
const collateralToSwap = new BN(swapQuote.quoteResponse!.inAmount);
|
|
1231
|
+
|
|
1232
|
+
// Get swap instructions
|
|
1233
|
+
const swapInputs = {
|
|
1234
|
+
inputAmountLamports: new Decimal(collateralToSwap.toString()),
|
|
1235
|
+
inputMint: collateralMint,
|
|
1236
|
+
outputMint: debtMint,
|
|
1237
|
+
};
|
|
1238
|
+
const swapIxs = await this.getJupiterSwap(
|
|
1239
|
+
this.address(),
|
|
1240
|
+
swapInputs,
|
|
1241
|
+
swapQuote,
|
|
1242
|
+
);
|
|
1243
|
+
|
|
1244
|
+
// Withdraw collateral
|
|
1245
|
+
const withdrawIx = await this.instructions.withdraw(
|
|
1246
|
+
clendGroup,
|
|
1247
|
+
clendAccount,
|
|
1248
|
+
this.address(),
|
|
1249
|
+
collateralBank,
|
|
1250
|
+
userCollateralAta,
|
|
1251
|
+
collateralTokenProgram,
|
|
1252
|
+
collateralToSwap,
|
|
1253
|
+
false,
|
|
1254
|
+
remainingAccounts,
|
|
1255
|
+
);
|
|
1256
|
+
console.log("withdraw ix created");
|
|
1257
|
+
|
|
1258
|
+
// Repay debt
|
|
1259
|
+
const repayIx = await this.instructions.repay(
|
|
1260
|
+
clendGroup,
|
|
1261
|
+
clendAccount,
|
|
1262
|
+
this.address(),
|
|
1263
|
+
debtBank,
|
|
1264
|
+
userDebtAta,
|
|
1265
|
+
debtTokenProgram,
|
|
1266
|
+
debtToReduceAmount,
|
|
1267
|
+
false,
|
|
1268
|
+
remainingAccounts,
|
|
1269
|
+
);
|
|
1270
|
+
console.log("repay ix created");
|
|
1271
|
+
|
|
1272
|
+
instructions.push(withdrawIx, ...swapIxs.swapIxs, repayIx);
|
|
1273
|
+
swapLookupTables = swapIxs.lookupTables;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
const endIndex = new BN(instructions.length + 1);
|
|
1277
|
+
|
|
1278
|
+
const { beginFlashLoanIx, endFlashLoanIx } =
|
|
1279
|
+
await this.instructions.createFlashLoanInstructions(
|
|
1280
|
+
clendAccount,
|
|
1281
|
+
this.address(),
|
|
1282
|
+
endIndex,
|
|
1283
|
+
remainingAccounts,
|
|
1284
|
+
);
|
|
1285
|
+
|
|
1286
|
+
// Create a new array with the flash loan instructions wrapping the existing instructions
|
|
1287
|
+
const finalInstructions = [
|
|
1288
|
+
beginFlashLoanIx,
|
|
1289
|
+
...instructions,
|
|
1290
|
+
endFlashLoanIx,
|
|
1291
|
+
];
|
|
1292
|
+
|
|
1293
|
+
// Send transaction
|
|
1294
|
+
return this.send(finalInstructions, [], swapLookupTables);
|
|
1295
|
+
}
|
|
1296
|
+
}
|