@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/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
+ }