@carrot-protocol/clend-vaults-rpc 0.0.2-pub1-dev-a741874

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,1354 @@
1
+ // rpc.ts
2
+ import { AnchorProvider, BN, Program, web3 } from "@coral-xyz/anchor";
3
+ import { Connection } from "@solana/web3.js";
4
+ import {
5
+ AuthorityType,
6
+ createInitializeInstruction,
7
+ createSetAuthorityInstruction,
8
+ getAssociatedTokenAddressSync,
9
+ MintLayout,
10
+ RawMint,
11
+ TOKEN_2022_PROGRAM_ID,
12
+ TOKEN_PROGRAM_ID,
13
+ unpackMint,
14
+ } from "@solana/spl-token";
15
+ import { Decimal } from "decimal.js";
16
+
17
+ import { AddAssetArgs, ClendVaultsProgram } from "./program";
18
+ import {
19
+ CLEND_VAULTS_PROGRAM_ID,
20
+ getVaultPda,
21
+ JUPITER_SWAP_PROGRAM_ID,
22
+ } from "./addresses";
23
+
24
+ import {
25
+ getBankPda,
26
+ getBankLiquidityVaultPda,
27
+ getBankLiquidityVaultAuthorityPda,
28
+ CLEND_PROGRAM_ID,
29
+ getClendAccountRemainingAccounts,
30
+ ClendClient,
31
+ getClendAccountActiveBanks,
32
+ Bank,
33
+ ISwapper,
34
+ getTokenProgramForMint,
35
+ getTokenProgramForMintFromRpc,
36
+ BankFlags,
37
+ } from "@carrot-protocol/clend-rpc";
38
+ import {
39
+ Vault,
40
+ VaultAsset,
41
+ VaultAssetReserve,
42
+ VaultClendAccount,
43
+ VaultEquity,
44
+ parseVaultAccount,
45
+ } from "./state";
46
+ import { ClendVaultsJupSwapper } from "./swapper";
47
+ import {
48
+ amountToUi,
49
+ calculateLendingAccountEquity,
50
+ calculateVaultNav,
51
+ convertSharesToAsset,
52
+ uiToAmount,
53
+ } from "./math";
54
+
55
+ export type Wallet = AnchorProvider["wallet"];
56
+ type AccountMeta = web3.AccountMeta;
57
+
58
+ export class ClendVaultsClient {
59
+ readonly connection: Connection;
60
+ readonly program: ClendVaultsProgram;
61
+ readonly clendClient: ClendClient;
62
+ readonly swapper: ISwapper;
63
+ readonly skipPreflight: boolean;
64
+
65
+ constructor(
66
+ provider: AnchorProvider,
67
+ skipPreflight: boolean = false,
68
+ swapperOverride?: ISwapper,
69
+ ) {
70
+ this.connection = provider.connection;
71
+ this.program = new ClendVaultsProgram(provider);
72
+ this.skipPreflight = skipPreflight;
73
+ this.clendClient = new ClendClient(
74
+ provider.connection,
75
+ provider.wallet as any,
76
+ skipPreflight,
77
+ 0,
78
+ );
79
+ this.swapper =
80
+ swapperOverride || new ClendVaultsJupSwapper(this.connection);
81
+ }
82
+
83
+ address(): web3.PublicKey {
84
+ return this.program.program.provider.publicKey!;
85
+ }
86
+
87
+ async getVault(vault: web3.PublicKey): Promise<Vault> {
88
+ const vaultAccountInfo = await this.connection.getAccountInfo(vault);
89
+ if (!vaultAccountInfo) {
90
+ throw new Error(`Vault account not found: ${vault.toString()}`);
91
+ }
92
+ return parseVaultAccount(vault, vaultAccountInfo, this.connection);
93
+ }
94
+
95
+ async getVaultEquity(vault: Vault): Promise<VaultEquity> {
96
+ let vaultEquity = 0;
97
+ const clendAccountEquity: { address: web3.PublicKey; equity: number }[] =
98
+ [];
99
+
100
+ // calculate equity for each clend account
101
+ for (const clendAccount of vault.clendAccounts) {
102
+ const clendAccountData = await this.clendClient.getClendAccount(
103
+ clendAccount.address,
104
+ );
105
+ if (!clendAccountData) {
106
+ throw new Error(
107
+ `Clend account not found: ${clendAccount.address.toString()}`,
108
+ );
109
+ }
110
+
111
+ const lendingAccountEquity = calculateLendingAccountEquity(
112
+ clendAccountData.lendingAccount,
113
+ );
114
+ clendAccountEquity.push({
115
+ address: clendAccount.address,
116
+ equity: lendingAccountEquity,
117
+ });
118
+ vaultEquity += lendingAccountEquity;
119
+ }
120
+
121
+ // calculate equity for each reserve
122
+ const reserveEquity: { address: web3.PublicKey; equity: number }[] = [];
123
+ for (const asset of vault.assets) {
124
+ const mint = asset.mint.address;
125
+ const priceUi = await this.clendClient.getPythOraclePrice(mint);
126
+ const rEquity = priceUi * asset.reserve.amountUi;
127
+ reserveEquity.push({ address: asset.reserve.address, equity: rEquity });
128
+ vaultEquity += rEquity;
129
+ }
130
+
131
+ return {
132
+ address: vault.address,
133
+ vaultEquity,
134
+ clendAccountEquity,
135
+ reserveEquity,
136
+ };
137
+ }
138
+
139
+ async initVault(
140
+ sharesMint: web3.PublicKey,
141
+ managementFeeBps: number,
142
+ ): Promise<{ txSig: string; vault: web3.PublicKey }> {
143
+ const { vault, ixns: initVaultIxns } = await this.program.initVault(
144
+ sharesMint,
145
+ this.address(),
146
+ managementFeeBps,
147
+ );
148
+
149
+ const initializeTxSig = await this.send(initVaultIxns);
150
+
151
+ return { txSig: initializeTxSig, vault };
152
+ }
153
+
154
+ async addAsset(
155
+ vault: web3.PublicKey,
156
+ assetMint: web3.PublicKey,
157
+ assetOracle: web3.PublicKey,
158
+ addAssetArgs: AddAssetArgs,
159
+ ): Promise<string> {
160
+ const ixns = await this.program.addAsset(
161
+ vault,
162
+ this.address(),
163
+ assetMint,
164
+ assetOracle,
165
+ TOKEN_PROGRAM_ID,
166
+ addAssetArgs,
167
+ );
168
+
169
+ const txSig = await this.send(ixns);
170
+
171
+ return txSig;
172
+ }
173
+
174
+ async addClendAccount(
175
+ vault: web3.PublicKey,
176
+ clendGroup: web3.PublicKey,
177
+ ): Promise<{ txSig: string; clendAccount: web3.PublicKey }> {
178
+ const clendAccount = web3.Keypair.generate();
179
+
180
+ const addClendAccountIxns = await this.program.addClendAccount(
181
+ vault,
182
+ this.address(),
183
+ clendGroup,
184
+ clendAccount,
185
+ );
186
+
187
+ const txSig = await this.send([...addClendAccountIxns], [clendAccount]);
188
+
189
+ return { txSig, clendAccount: clendAccount.publicKey };
190
+ }
191
+
192
+ async updateVaultManager(
193
+ vault: web3.PublicKey,
194
+ newManager: web3.PublicKey,
195
+ ): Promise<web3.TransactionInstruction[]> {
196
+ return this.program.updateVaultManager(vault, this.address(), newManager);
197
+ }
198
+
199
+ async issue(
200
+ vault: web3.PublicKey,
201
+ assetMint: web3.PublicKey,
202
+ amount: BN,
203
+ ): Promise<string> {
204
+ const ixns = await this.prepareIssueIxns(
205
+ this.address(),
206
+ vault,
207
+ assetMint,
208
+ amount,
209
+ );
210
+
211
+ const txSig = await this.send(ixns);
212
+
213
+ return txSig;
214
+ }
215
+
216
+ async prepareIssueIxns(
217
+ user: web3.PublicKey,
218
+ vault: web3.PublicKey,
219
+ assetMint: web3.PublicKey,
220
+ amount: BN,
221
+ ): Promise<web3.TransactionInstruction[]> {
222
+ // get vault and asset data
223
+ const vaultData = await this.getVault(vault);
224
+ const assetData = vaultData.assets.find((a) =>
225
+ a.mint.address.equals(assetMint),
226
+ )!;
227
+
228
+ const remainingAccounts = await buildEquityRemainingAccounts(
229
+ vaultData,
230
+ this.clendClient,
231
+ );
232
+
233
+ const ixns = await this.program.issue(
234
+ vault,
235
+ vaultData.sharesMint,
236
+ user,
237
+ assetMint,
238
+ assetData.oracle,
239
+ amount,
240
+ TOKEN_PROGRAM_ID,
241
+ TOKEN_2022_PROGRAM_ID,
242
+ remainingAccounts,
243
+ );
244
+
245
+ return ixns;
246
+ }
247
+
248
+ async redeem(
249
+ vault: web3.PublicKey,
250
+ assetMint: web3.PublicKey,
251
+ sharesAmount: BN,
252
+ ): Promise<string> {
253
+ const ixns = await this.prepareRedeemIxns(
254
+ this.address(),
255
+ vault,
256
+ assetMint,
257
+ sharesAmount,
258
+ );
259
+
260
+ const txSig = await this.send(ixns);
261
+
262
+ return txSig;
263
+ }
264
+
265
+ async prepareRedeemIxns(
266
+ user: web3.PublicKey,
267
+ vault: web3.PublicKey,
268
+ assetMint: web3.PublicKey,
269
+ amount: BN,
270
+ ): Promise<web3.TransactionInstruction[]> {
271
+ const vaultData = await this.getVault(vault);
272
+ const assetData = vaultData.assets.find((a) =>
273
+ a.mint.address.equals(assetMint),
274
+ )!;
275
+
276
+ const remainingAccounts = await buildEquityRemainingAccounts(
277
+ vaultData,
278
+ this.clendClient,
279
+ );
280
+
281
+ return this.program.redeem(
282
+ vault,
283
+ vaultData.sharesMint,
284
+ user,
285
+ assetMint,
286
+ assetData.oracle,
287
+ amount,
288
+ TOKEN_PROGRAM_ID,
289
+ TOKEN_2022_PROGRAM_ID,
290
+ remainingAccounts,
291
+ );
292
+ }
293
+
294
+ async clendAccountDeposit(
295
+ vault: web3.PublicKey,
296
+ clendAccount: web3.PublicKey,
297
+ assetMint: web3.PublicKey,
298
+ amount: BN,
299
+ ): Promise<string> {
300
+ // fetch clend account data
301
+ const clendAccountData =
302
+ await this.clendClient.getClendAccount(clendAccount);
303
+ if (!clendAccountData) {
304
+ throw new Error(`Clend account not found: ${clendAccount.toString()}`);
305
+ }
306
+
307
+ // fetch vault data
308
+ const vaultData = await this.getVault(vault);
309
+ const assetData = vaultData.assets.find((a) =>
310
+ a.mint.address.equals(assetMint),
311
+ )!;
312
+
313
+ // remaining accounts for protocol call
314
+ const ra = getClendAccountRemainingAccounts([]);
315
+
316
+ const ixns = await this.program.clendAccountDeposit(
317
+ vault,
318
+ this.address(),
319
+ clendAccountData.group,
320
+ clendAccount,
321
+ assetMint,
322
+ TOKEN_PROGRAM_ID,
323
+ amount,
324
+ assetData.reserve.address,
325
+ ra,
326
+ );
327
+
328
+ const txSig = await this.send(ixns);
329
+
330
+ return txSig;
331
+ }
332
+
333
+ async clendAccountWithdraw(
334
+ vault: web3.PublicKey,
335
+ clendAccount: web3.PublicKey,
336
+ assetMint: web3.PublicKey,
337
+ amount: BN,
338
+ ): Promise<string> {
339
+ // fetch clend account data
340
+ const clendAccountData =
341
+ await this.clendClient.getClendAccount(clendAccount);
342
+ if (!clendAccountData) {
343
+ throw new Error(`Clend account not found: ${clendAccount.toString()}`);
344
+ }
345
+
346
+ // fetch active banks
347
+ const clendAccountActiveBanks =
348
+ getClendAccountActiveBanks(clendAccountData);
349
+
350
+ const activeBankData: Bank[] = [];
351
+ for (const bank of clendAccountActiveBanks) {
352
+ const bankData = await this.clendClient.getBank(bank);
353
+ activeBankData.push(bankData);
354
+ }
355
+
356
+ const ra = getClendAccountRemainingAccounts(activeBankData);
357
+
358
+ const ixns = await this.program.clendAccountWithdraw(
359
+ vault,
360
+ this.address(),
361
+ clendAccountData.group,
362
+ clendAccount,
363
+ assetMint,
364
+ TOKEN_PROGRAM_ID,
365
+ amount,
366
+ ra,
367
+ );
368
+
369
+ const txSig = await this.send(ixns);
370
+
371
+ return txSig;
372
+ }
373
+
374
+ async clendAccountRepay(
375
+ vault: web3.PublicKey,
376
+ clendAccount: web3.PublicKey,
377
+ assetMint: web3.PublicKey,
378
+ amount: BN | number,
379
+ ): Promise<string> {
380
+ const clendAccountData =
381
+ await this.clendClient.getClendAccount(clendAccount);
382
+ if (!clendAccountData) {
383
+ throw new Error(`Clend account not found: ${clendAccount.toString()}`);
384
+ }
385
+
386
+ const ra = getClendAccountRemainingAccounts([]);
387
+
388
+ const ixns = await this.program.clendAccountRepay(
389
+ vault,
390
+ this.address(),
391
+ clendAccountData.group,
392
+ clendAccount,
393
+ assetMint,
394
+ TOKEN_PROGRAM_ID,
395
+ amount,
396
+ ra,
397
+ );
398
+
399
+ const txSig = await this.send(ixns);
400
+
401
+ return txSig;
402
+ }
403
+
404
+ async clendAccountBorrow(
405
+ vault: web3.PublicKey,
406
+ clendAccount: web3.PublicKey,
407
+ assetMint: web3.PublicKey,
408
+ amount: BN,
409
+ ): Promise<string> {
410
+ const clendAccountData =
411
+ await this.clendClient.getClendAccount(clendAccount);
412
+ if (!clendAccountData) {
413
+ throw new Error(`Clend account not found: ${clendAccount.toString()}`);
414
+ }
415
+
416
+ // fetch active banks
417
+ const clendAccountActiveBanks =
418
+ getClendAccountActiveBanks(clendAccountData);
419
+
420
+ // add target bank if not already in list
421
+ // this would be the case for a first time borrow
422
+ const targetBank = getBankPda(clendAccountData.group, assetMint);
423
+ const alreadyInBankList = clendAccountActiveBanks.some((c) =>
424
+ c.equals(targetBank),
425
+ );
426
+ if (!alreadyInBankList) {
427
+ clendAccountActiveBanks.push(targetBank);
428
+ }
429
+
430
+ const activeBankData: Bank[] = [];
431
+ for (const bank of clendAccountActiveBanks) {
432
+ const bankData = await this.clendClient.getBank(bank);
433
+ activeBankData.push(bankData);
434
+ }
435
+
436
+ const ra = getClendAccountRemainingAccounts(activeBankData);
437
+
438
+ const ixns = await this.program.clendAccountBorrow(
439
+ vault,
440
+ this.address(),
441
+ clendAccountData.group,
442
+ clendAccount,
443
+ assetMint,
444
+ TOKEN_PROGRAM_ID,
445
+ amount,
446
+ ra,
447
+ );
448
+
449
+ const txSig = await this.send(ixns);
450
+
451
+ return txSig;
452
+ }
453
+
454
+ async clendAccountClaimEmissions(
455
+ vault: web3.PublicKey,
456
+ clendGroup: web3.PublicKey,
457
+ clendAccount: web3.PublicKey,
458
+ bank: web3.PublicKey,
459
+ assetMint: web3.PublicKey,
460
+ emissionsMint: web3.PublicKey,
461
+ ): Promise<web3.TransactionInstruction[]> {
462
+ const emissionsVault = getBankLiquidityVaultPda(bank);
463
+ const emissionsAuthority = getBankLiquidityVaultAuthorityPda(bank);
464
+
465
+ // destination is an ATA owned by the vault PDA
466
+ const destinationAccount = getAssociatedTokenAddressSync(
467
+ emissionsMint,
468
+ vault,
469
+ true,
470
+ TOKEN_PROGRAM_ID,
471
+ );
472
+
473
+ return this.program.clendAccountClaimEmissions(
474
+ vault,
475
+ this.address(),
476
+ clendGroup,
477
+ clendAccount,
478
+ bank,
479
+ assetMint,
480
+ emissionsMint,
481
+ destinationAccount,
482
+ TOKEN_PROGRAM_ID,
483
+ emissionsVault,
484
+ emissionsAuthority,
485
+ CLEND_PROGRAM_ID,
486
+ );
487
+ }
488
+
489
+ // adjust the leverage of the clend account
490
+ async clendAccountAdjustLeverage(
491
+ clendAccount: web3.PublicKey,
492
+ collateralMint: web3.PublicKey,
493
+ debtMint: web3.PublicKey,
494
+ targetLeverage: number,
495
+ slippageBps: number,
496
+ ): Promise<string> {
497
+ const clendAccountState =
498
+ await this.clendClient.getClendAccount(clendAccount);
499
+ if (!clendAccountState) {
500
+ throw new Error(`Clend account not found: ${clendAccount.toString()}`);
501
+ }
502
+
503
+ const adjustLeverageTxSig = await this.clendClient.adjustLeverage(
504
+ clendAccountState.group,
505
+ clendAccount,
506
+ collateralMint,
507
+ debtMint,
508
+ targetLeverage,
509
+ slippageBps,
510
+ this.swapper,
511
+ );
512
+
513
+ return adjustLeverageTxSig;
514
+ }
515
+
516
+ // create instructions for redeeming shares
517
+ // however this will pull from a clend account balance
518
+ // the equity value of the shares desired for redemption
519
+ async prepareRedeemFromClendAccountIxns(
520
+ user: web3.PublicKey,
521
+ vault: web3.PublicKey,
522
+ clendAccount: web3.PublicKey,
523
+ outputAssetMint: web3.PublicKey,
524
+ collateralMint: web3.PublicKey,
525
+ debtMint: web3.PublicKey,
526
+ totalSharesToRedeem: BN,
527
+ sharesToRedeemFromClendAccount: BN,
528
+ slippageBps: number,
529
+ ): Promise<{ ixns: web3.TransactionInstruction[]; luts: web3.PublicKey[] }> {
530
+ const vaultState = await this.getVault(vault);
531
+ const outputAssetState = vaultState.assets.find((a) =>
532
+ a.mint.address.equals(outputAssetMint),
533
+ );
534
+ if (!outputAssetState) {
535
+ throw new Error(
536
+ `output asset mint ${outputAssetMint.toString()} not found in vault ${vault.toString()}`,
537
+ );
538
+ }
539
+
540
+ // get price of output asset
541
+ const priceUi = await this.clendClient.getPythOraclePrice(
542
+ outputAssetState.mint.address,
543
+ );
544
+
545
+ const vaultEquity = await this.getVaultEquity(vaultState);
546
+
547
+ const sharesSupplyUi = amountToUi(
548
+ vaultState.sharesSupply,
549
+ vaultState.sharesDecimals,
550
+ );
551
+ const sharesToRedeemFromClendAccountUi = amountToUi(
552
+ sharesToRedeemFromClendAccount,
553
+ vaultState.sharesDecimals,
554
+ );
555
+ const vaultNav = calculateVaultNav(vaultEquity.vaultEquity, sharesSupplyUi);
556
+
557
+ // TODO: will need to add fees to this
558
+ const desiredOutputAmount = convertSharesToAsset(
559
+ vaultNav,
560
+ sharesToRedeemFromClendAccountUi,
561
+ outputAssetState.mint.decimals,
562
+ priceUi,
563
+ );
564
+
565
+ // check if clend account is listed in vault config
566
+ const clendAccountIsValid = vaultState.clendAccounts.some((c) =>
567
+ c.address.equals(clendAccount),
568
+ );
569
+ if (!clendAccountIsValid) {
570
+ throw new Error(
571
+ `Clend account ${clendAccount.toString()} not found in vault ${vault.toString()} config`,
572
+ );
573
+ }
574
+
575
+ // fetch clend account state
576
+ const clendAccountState =
577
+ await this.clendClient.getClendAccount(clendAccount);
578
+ if (!clendAccountState) {
579
+ throw new Error(
580
+ `error fetching clend account state: ${clendAccount.toString()}`,
581
+ );
582
+ }
583
+
584
+ // we need to know how many ixns will go before the leverage ixns
585
+ // this is for the flash loan index positioning
586
+ let prependedIxCount: number = 0;
587
+ let cuIxCount: number = 0; // TODO: in the clend libs we already account for this
588
+ prependedIxCount += cuIxCount;
589
+
590
+ // get the redemption ixns
591
+ // these are run after the withdraw leverage ixns
592
+ const redeemIxns = await this.prepareRedeemIxns(
593
+ user,
594
+ vault,
595
+ outputAssetMint,
596
+ totalSharesToRedeem,
597
+ );
598
+
599
+ // calculate parameters for withdraw leverage
600
+ // based on output asset mint
601
+ let debtToRepay: BN;
602
+ let collateralToWithdraw: BN;
603
+ let debtBankData: Bank;
604
+ let collateralBankData: Bank;
605
+ switch (outputAssetMint.toString()) {
606
+ case collateralMint.toString():
607
+ // get params for withdraw leverage
608
+ const {
609
+ debtToRepay: debtToRepayCollateral,
610
+ collateralToWithdraw: collateralToWithdrawCollateral,
611
+ debtBankData: debtBankDataCollateral,
612
+ collateralBankData: collateralBankDataCollateral,
613
+ } = await this.clendClient.getNetWithdrawLeverageCollateralParams(
614
+ clendAccountState.group,
615
+ clendAccount,
616
+ collateralMint,
617
+ debtMint,
618
+ desiredOutputAmount,
619
+ false,
620
+ );
621
+ debtToRepay = debtToRepayCollateral;
622
+ collateralToWithdraw = collateralToWithdrawCollateral;
623
+ debtBankData = debtBankDataCollateral;
624
+ collateralBankData = collateralBankDataCollateral;
625
+ break;
626
+ case debtMint.toString():
627
+ // get params for withdraw leverage
628
+ const {
629
+ debtToRepay: debtToRepayDebt,
630
+ collateralToWithdraw: collateralToWithdrawDebt,
631
+ debtBankData: debtBankDataDebt,
632
+ collateralBankData: collateralBankDataDebt,
633
+ } = await this.clendClient.getNetWithdrawLeverageDebtParams(
634
+ clendAccountState.group,
635
+ clendAccount,
636
+ collateralMint,
637
+ debtMint,
638
+ desiredOutputAmount,
639
+ false,
640
+ );
641
+ debtToRepay = debtToRepayDebt;
642
+ collateralToWithdraw = collateralToWithdrawDebt;
643
+ debtBankData = debtBankDataDebt;
644
+ collateralBankData = collateralBankDataDebt;
645
+ break;
646
+ default:
647
+ throw new Error(
648
+ `invalid output token mint: ${outputAssetMint.toString()}`,
649
+ );
650
+ }
651
+
652
+ // compose instructions for user to sign
653
+ let ixns: web3.TransactionInstruction[] = [];
654
+ let luts: web3.PublicKey[] = [];
655
+ switch (outputAssetMint.toString()) {
656
+ case collateralMint.toString():
657
+ const { ixns: collateralIxns, luts: collateralLuts } =
658
+ await this.getWithdrawLeverageCollateralIxns(
659
+ vault,
660
+ vaultState.manager,
661
+ user,
662
+ clendAccountState.group,
663
+ clendAccount,
664
+ collateralMint,
665
+ debtMint,
666
+ collateralBankData.mintDecimals,
667
+ debtBankData.mintDecimals,
668
+ false,
669
+ collateralToWithdraw,
670
+ desiredOutputAmount,
671
+ debtToRepay,
672
+ slippageBps,
673
+ prependedIxCount,
674
+ this.swapper,
675
+ );
676
+ ixns = collateralIxns;
677
+ luts = collateralLuts;
678
+ break;
679
+ case debtMint.toString():
680
+ const { ixns: debtIxns, luts: debtLuts } =
681
+ await this.getWithdrawLeverageDebtIxns(
682
+ vault,
683
+ vaultState.manager,
684
+ user,
685
+ clendAccountState.group,
686
+ clendAccount,
687
+ collateralMint,
688
+ debtMint,
689
+ collateralBankData.mintDecimals,
690
+ debtBankData.mintDecimals,
691
+ false,
692
+ debtToRepay,
693
+ collateralToWithdraw,
694
+ slippageBps,
695
+ prependedIxCount,
696
+ this.swapper,
697
+ );
698
+ ixns = debtIxns;
699
+ luts = debtLuts;
700
+ break;
701
+ default:
702
+ throw new Error(
703
+ `invalid output token mint: ${outputAssetMint.toString()}`,
704
+ );
705
+ }
706
+
707
+ const allIxns: web3.TransactionInstruction[] = [...ixns, ...redeemIxns];
708
+
709
+ return { ixns: allIxns, luts };
710
+ }
711
+
712
+ async getWithdrawLeverageCollateralIxns(
713
+ vault: web3.PublicKey,
714
+ vaultManager: web3.PublicKey,
715
+ user: web3.PublicKey,
716
+ clendGroup: web3.PublicKey,
717
+ clendAccount: web3.PublicKey,
718
+ collateralMint: web3.PublicKey,
719
+ debtMint: web3.PublicKey,
720
+ collateralDecimals: number,
721
+ debtDecimals: number,
722
+ withdrawAll: boolean,
723
+ collateralToWithdraw: BN,
724
+ desiredNetCollateralToReceive: BN,
725
+ debtToRepay: BN,
726
+ slippageBps: number,
727
+ additionalIxnCount: number,
728
+ swapperOverride?: ISwapper,
729
+ ): Promise<{ ixns: web3.TransactionInstruction[]; luts: web3.PublicKey[] }> {
730
+ // override swapper if provided
731
+ let activeSwapper = this.swapper;
732
+ if (swapperOverride) {
733
+ activeSwapper = swapperOverride;
734
+ }
735
+
736
+ const clendAccountData =
737
+ await this.clendClient.getClendAccount(clendAccount);
738
+ if (!clendAccountData) {
739
+ throw new Error(`Clend account not found: ${clendAccount.toString()}`);
740
+ }
741
+
742
+ let activeBanks = getClendAccountActiveBanks(clendAccountData);
743
+ const activeBankData = await this.clendClient.getBanks(activeBanks);
744
+ const remainingAccounts = getClendAccountRemainingAccounts(activeBankData);
745
+
746
+ // this is for partial withdraws
747
+ // overriden if withdrawAll is true
748
+ let collateralToSwap = collateralToWithdraw.sub(
749
+ desiredNetCollateralToReceive,
750
+ );
751
+
752
+ // again for partial withdraws
753
+ let swapQuote = await activeSwapper.getQuote({
754
+ payer: vault,
755
+ inputMint: collateralMint,
756
+ inputMintDecimals: collateralDecimals,
757
+ outputMint: debtMint,
758
+ outputMintDecimals: debtDecimals,
759
+ inputAmount: collateralToSwap,
760
+ slippageBps,
761
+ swapMode: "ExactIn",
762
+ });
763
+
764
+ if (withdrawAll) {
765
+ // Step 1: Get the current price via a nominal 1-token quote.
766
+ const oneToken = uiToAmount(1, collateralDecimals);
767
+ const nominalQuote = await activeSwapper.getQuote({
768
+ payer: vault,
769
+ inputMint: collateralMint,
770
+ inputMintDecimals: collateralDecimals,
771
+ outputMint: debtMint,
772
+ outputMintDecimals: debtDecimals,
773
+ inputAmount: oneToken,
774
+ slippageBps,
775
+ swapMode: "ExactIn",
776
+ });
777
+ const outAmountUi = amountToUi(
778
+ nominalQuote.otherAmountThreshold,
779
+ debtDecimals,
780
+ );
781
+
782
+ // Step 2: Estimate the ideal collateral needed to cover the debt.
783
+ const debtToRepayUi = new Decimal(
784
+ amountToUi(debtToRepay, debtDecimals).toString(),
785
+ );
786
+ const collateralToSwapUi_ideal = debtToRepayUi.div(outAmountUi);
787
+ const collateralToSwap_ideal = uiToAmount(
788
+ collateralToSwapUi_ideal.toNumber(),
789
+ collateralDecimals,
790
+ );
791
+ collateralToSwap = collateralToSwap_ideal;
792
+ //collateralToSwap = adjustAmountForSlippage(
793
+ // collateralToSwap_ideal,
794
+ // slippageBps,
795
+ //);
796
+
797
+ // Step 4 (Final Quote): Get the real quote for the buffered amount.
798
+ swapQuote = await activeSwapper.getQuote({
799
+ payer: user,
800
+ inputMint: collateralMint,
801
+ inputMintDecimals: collateralDecimals,
802
+ outputMint: debtMint,
803
+ outputMintDecimals: debtDecimals,
804
+ inputAmount: collateralToSwap,
805
+ slippageBps,
806
+ swapMode: "ExactIn",
807
+ });
808
+
809
+ // check its sufficient
810
+ if (swapQuote.outAmount.lt(debtToRepay)) {
811
+ throw new Error(
812
+ `Quote is insufficient to cover debt after slippage. Try increasing slippage tolerance.
813
+ ${swapQuote.otherAmountThreshold.toString()} < ${debtToRepay.toString()}`,
814
+ );
815
+ }
816
+ }
817
+
818
+ // 3. The final amount to repay is the minimum we are guaranteed to get from the swap.
819
+ const finalDebtToRepay = swapQuote.outAmount;
820
+
821
+ const swapIxns = await activeSwapper.getSwapIxns(swapQuote);
822
+
823
+ const collateralTokenProgram = await getTokenProgramForMintFromRpc(
824
+ this.connection,
825
+ collateralMint,
826
+ );
827
+ const debtTokenProgram = await getTokenProgramForMintFromRpc(
828
+ this.connection,
829
+ debtMint,
830
+ );
831
+
832
+ const withdrawIx = await this.program.clendAccountWithdraw(
833
+ vault,
834
+ vaultManager,
835
+ clendGroup,
836
+ clendAccount,
837
+ collateralMint,
838
+ collateralTokenProgram,
839
+ collateralToWithdraw,
840
+ remainingAccounts,
841
+ );
842
+
843
+ // if withdrawAll == true we need to make sure we withdraw emissions from both banks before withdrawing all the bank tokens
844
+ const emissionsIxns: web3.TransactionInstruction[] = [];
845
+ if (withdrawAll) {
846
+ // get bank data
847
+ const collateralBankData = activeBankData.find((b) =>
848
+ b.mint.equals(collateralMint),
849
+ );
850
+ if (!collateralBankData) {
851
+ throw new Error(
852
+ `Collateral bank not found: ${collateralMint.toString()}`,
853
+ );
854
+ }
855
+ const debtBankData = activeBankData.find((b) => b.mint.equals(debtMint));
856
+ if (!debtBankData) {
857
+ throw new Error(`Debt bank not found: ${debtMint.toString()}`);
858
+ }
859
+
860
+ //// collateral bank emissions
861
+ //if (
862
+ // !collateralBankData.emissionsMint.equals(web3.PublicKey.default) &&
863
+ // (collateralBankData.flags === BankFlags.LendingEmissionsActive ||
864
+ // collateralBankData.flags ===
865
+ // BankFlags.LendingAndBorrowingEmissionsActive)
866
+ //) {
867
+ // const emissionsMint = collateralBankData.emissionsMint;
868
+ // const emissionsMintTokenProgram = await getTokenProgramForMintFromRpc(
869
+ // this.connection,
870
+ // emissionsMint,
871
+ // );
872
+ // const withdrawEmissionsIx = await this.program.clendAccountClaimEmissions(
873
+
874
+ // clendGroup,
875
+ // clendAccount,
876
+ // clendAccountData.authority,
877
+ // collateralBank,
878
+ // emissionsMint,
879
+ // emissionsMintTokenProgram,
880
+ // );
881
+ // emissionsIxns.push(...withdrawEmissionsIx);
882
+ //}
883
+
884
+ //// debt bank emissions
885
+ //if (
886
+ // !debtBankData.emissionsMint.equals(web3.PublicKey.default) &&
887
+ // (debtBankData.flags === BankFlags.BorrowEmissionsActive ||
888
+ // debtBankData.flags === BankFlags.LendingAndBorrowingEmissionsActive)
889
+ //) {
890
+ // const emissionsMint = debtBankData.emissionsMint;
891
+ // const emissionsMintTokenProgram = await getTokenProgramForMintFromRpc(
892
+ // this.connection,
893
+ // emissionsMint,
894
+ // );
895
+ // const withdrawEmissionsIx = await this.program.clendAccountWithdrawEmissions(
896
+ // clendGroup,
897
+ // clendAccount,
898
+ // clendAccountData.authority,
899
+ // debtBank,
900
+ // emissionsMint,
901
+ // emissionsMintTokenProgram,
902
+ // );
903
+ // emissionsIxns.push(...withdrawEmissionsIx);
904
+ //}
905
+ }
906
+
907
+ // Create repay instruction with the amount from the 'ExactIn' swap
908
+ const repayIx = await this.program.clendAccountRepay(
909
+ vault,
910
+ vaultManager,
911
+ clendGroup,
912
+ clendAccount,
913
+ debtMint,
914
+ debtTokenProgram,
915
+ finalDebtToRepay,
916
+ remainingAccounts,
917
+ );
918
+
919
+ const ixnsWithoutFlashLoan: web3.TransactionInstruction[] = [
920
+ ...emissionsIxns,
921
+ ...withdrawIx,
922
+ ...swapIxns.ixns,
923
+ ...repayIx,
924
+ ];
925
+
926
+ const cuIxns = 2;
927
+ const endIndex = new BN(
928
+ cuIxns + ixnsWithoutFlashLoan.length + 1 + additionalIxnCount,
929
+ );
930
+ const remainingAccountsForFlashLoan =
931
+ getClendAccountRemainingAccounts(activeBankData);
932
+
933
+ const { beginFlashLoanIx, endFlashLoanIx } =
934
+ await this.clendClient.instructions.createFlashLoanInstructions(
935
+ clendAccount,
936
+ user,
937
+ endIndex,
938
+ remainingAccountsForFlashLoan,
939
+ );
940
+
941
+ const instructions = [
942
+ beginFlashLoanIx,
943
+ ...ixnsWithoutFlashLoan,
944
+ endFlashLoanIx,
945
+ ];
946
+
947
+ return { ixns: instructions, luts: swapIxns.luts };
948
+ }
949
+
950
+ async getWithdrawLeverageDebtIxns(
951
+ vault: web3.PublicKey,
952
+ vaultManager: web3.PublicKey,
953
+ user: web3.PublicKey,
954
+ clendGroup: web3.PublicKey,
955
+ clendAccount: web3.PublicKey,
956
+ collateralMint: web3.PublicKey,
957
+ debtMint: web3.PublicKey,
958
+ collateralDecimals: number,
959
+ debtDecimals: number,
960
+ withdrawAll: boolean,
961
+ debtToRepay: BN,
962
+ collateralToWithdraw: BN,
963
+ slippageBps: number,
964
+ additionalIxnCount: number,
965
+ swapperOverride?: ISwapper,
966
+ ): Promise<{ ixns: web3.TransactionInstruction[]; luts: web3.PublicKey[] }> {
967
+ // override swapper if provided
968
+ let activeSwapper = this.swapper;
969
+ if (swapperOverride) {
970
+ activeSwapper = swapperOverride;
971
+ }
972
+
973
+ const clendAccountData =
974
+ await this.clendClient.getClendAccount(clendAccount);
975
+ if (!clendAccountData) {
976
+ throw new Error(`Clend account not found: ${clendAccount.toString()}`);
977
+ }
978
+
979
+ let activeBanks = getClendAccountActiveBanks(clendAccountData);
980
+ const activeBankData = await this.clendClient.getBanks(activeBanks);
981
+ const remainingAccounts = getClendAccountRemainingAccounts(activeBankData);
982
+
983
+ // Get a fresh 'ExactIn' quote for swapping the total collateral to withdraw
984
+ const swapQuote = await activeSwapper.getQuote({
985
+ payer: vault,
986
+ inputMint: collateralMint,
987
+ inputMintDecimals: collateralDecimals,
988
+ outputMint: debtMint,
989
+ outputMintDecimals: debtDecimals,
990
+ inputAmount: collateralToWithdraw,
991
+ slippageBps,
992
+ swapMode: "ExactIn",
993
+ });
994
+
995
+ // The minimum amount of debt token we are guaranteed to receive from the swap
996
+ const minDebtReceivedFromSwap = swapQuote.outAmount;
997
+
998
+ // Determine the final amount to repay. It's the lesser of what we need and what we're guaranteed to get.
999
+ const finalDebtToRepay = BN.min(debtToRepay, minDebtReceivedFromSwap);
1000
+
1001
+ // Get Swap Instructions from the swapper
1002
+ const swapIxns = await activeSwapper.getSwapIxns(swapQuote);
1003
+
1004
+ // --- ATAs and Clend Instructions ---
1005
+ const collateralTokenProgram = getTokenProgramForMint(collateralMint);
1006
+ const debtTokenProgram = getTokenProgramForMint(debtMint);
1007
+ // Create withdraw instruction: Withdraw the total collateral calculated by the params function
1008
+ const withdrawIx = await this.program.clendAccountWithdraw(
1009
+ vault,
1010
+ vaultManager,
1011
+ clendGroup,
1012
+ clendAccount,
1013
+ collateralMint,
1014
+ collateralTokenProgram,
1015
+ collateralToWithdraw,
1016
+ remainingAccounts,
1017
+ );
1018
+
1019
+ // if withdrawAll == true we need to make sure we withdraw emissions from both banks before withdrawing all the bank tokens
1020
+ const emissionsIxns: web3.TransactionInstruction[] = [];
1021
+ if (withdrawAll) {
1022
+ // get bank data
1023
+ const collateralBankData = activeBankData.find((b) =>
1024
+ b.mint.equals(collateralMint),
1025
+ );
1026
+ if (!collateralBankData) {
1027
+ throw new Error(
1028
+ `Collateral bank not found: ${collateralMint.toString()}`,
1029
+ );
1030
+ }
1031
+ const debtBankData = activeBankData.find((b) => b.mint.equals(debtMint));
1032
+ if (!debtBankData) {
1033
+ throw new Error(`Debt bank not found: ${debtMint.toString()}`);
1034
+ }
1035
+
1036
+ //// collateral bank emissions
1037
+ //if (
1038
+ // !collateralBankData.emissionsMint.equals(web3.PublicKey.default) &&
1039
+ // (collateralBankData.flags === BankFlags.LendingEmissionsActive ||
1040
+ // collateralBankData.flags ===
1041
+ // BankFlags.LendingAndBorrowingEmissionsActive)
1042
+ //) {
1043
+ // const emissionsMint = collateralBankData.emissionsMint;
1044
+ // const emissionsMintTokenProgram = await getTokenProgramForMintFromRpc(
1045
+ // this.connection,
1046
+ // emissionsMint,
1047
+ // );
1048
+ // const withdrawEmissionsIx = await this.instructions.withdrawEmissions(
1049
+ // clendGroup,
1050
+ // clendAccount,
1051
+ // clendAccountData.authority,
1052
+ // collateralBank,
1053
+ // emissionsMint,
1054
+ // emissionsMintTokenProgram,
1055
+ // );
1056
+ // emissionsIxns.push(...withdrawEmissionsIx);
1057
+ //}
1058
+
1059
+ //// debt bank emissions
1060
+ //if (
1061
+ // !debtBankData.emissionsMint.equals(web3.PublicKey.default) &&
1062
+ // (debtBankData.flags === BankFlags.BorrowEmissionsActive ||
1063
+ // debtBankData.flags === BankFlags.LendingAndBorrowingEmissionsActive)
1064
+ //) {
1065
+ // const emissionsMint = debtBankData.emissionsMint;
1066
+ // const emissionsMintTokenProgram = await getTokenProgramForMintFromRpc(
1067
+ // this.connection,
1068
+ // emissionsMint,
1069
+ // );
1070
+ // const withdrawEmissionsIx = await this.instructions.withdrawEmissions(
1071
+ // clendGroup,
1072
+ // clendAccount,
1073
+ // clendAccountData.authority,
1074
+ // debtBank,
1075
+ // emissionsMint,
1076
+ // emissionsMintTokenProgram,
1077
+ // );
1078
+ // emissionsIxns.push(...withdrawEmissionsIx);
1079
+ //}
1080
+ }
1081
+
1082
+ // Create repay instruction: Repay the debt portion using the guaranteed swap output
1083
+ const repayIx = await this.program.clendAccountRepay(
1084
+ vault,
1085
+ vaultManager,
1086
+ clendGroup,
1087
+ clendAccount,
1088
+ debtMint,
1089
+ debtTokenProgram,
1090
+ finalDebtToRepay, // Use the slippage-protected amount from the final quote
1091
+ remainingAccounts,
1092
+ );
1093
+
1094
+ // Assemble instructions
1095
+ const ixnsWithoutFlashLoan: web3.TransactionInstruction[] = [
1096
+ ...emissionsIxns,
1097
+ ...withdrawIx,
1098
+ ...swapIxns.ixns,
1099
+ ...repayIx,
1100
+ ];
1101
+
1102
+ // Flash Loan Wrapping
1103
+ const cuIxns = 2;
1104
+ const endIndex = new BN(
1105
+ cuIxns + ixnsWithoutFlashLoan.length + 1 + additionalIxnCount,
1106
+ );
1107
+ const remainingAccountsForFlashLoan =
1108
+ getClendAccountRemainingAccounts(activeBankData);
1109
+
1110
+ const { beginFlashLoanIx, endFlashLoanIx } =
1111
+ await this.clendClient.instructions.createFlashLoanInstructions(
1112
+ clendAccount,
1113
+ user,
1114
+ endIndex,
1115
+ remainingAccountsForFlashLoan,
1116
+ );
1117
+
1118
+ const instructions = [
1119
+ beginFlashLoanIx,
1120
+ ...ixnsWithoutFlashLoan,
1121
+ endFlashLoanIx,
1122
+ ];
1123
+
1124
+ return { ixns: instructions, luts: swapIxns.luts };
1125
+ }
1126
+
1127
+ async swap(
1128
+ vault: web3.PublicKey,
1129
+ inputMint: web3.PublicKey,
1130
+ outputMint: web3.PublicKey,
1131
+ inAmount: BN,
1132
+ ): Promise<string> {
1133
+ const vaultAssetInReserve = getAssociatedTokenAddressSync(
1134
+ inputMint,
1135
+ vault,
1136
+ true,
1137
+ TOKEN_PROGRAM_ID,
1138
+ );
1139
+ const vaultAssetOutReserve = getAssociatedTokenAddressSync(
1140
+ outputMint,
1141
+ vault,
1142
+ true,
1143
+ TOKEN_PROGRAM_ID,
1144
+ );
1145
+
1146
+ const quote = await this.swapper.getQuote({
1147
+ payer: vault,
1148
+ inputMint: inputMint,
1149
+ inputMintDecimals: 6,
1150
+ outputMint: outputMint,
1151
+ outputMintDecimals: 6,
1152
+ inputAmount: inAmount,
1153
+ swapMode: "ExactIn",
1154
+ slippageBps: 100,
1155
+ });
1156
+
1157
+ // TODO: no setup ixns
1158
+ const swapIxns = await this.swapper.getSwapIxns(quote);
1159
+ const swapIx = swapIxns.ixns.find((ix) =>
1160
+ ix.programId.equals(JUPITER_SWAP_PROGRAM_ID),
1161
+ )!;
1162
+ const swapRemainingAccounts: web3.AccountMeta[] = swapIx.keys.map(
1163
+ (key) => ({
1164
+ pubkey: key.pubkey,
1165
+ // If the account is the vault PDA, mark it as NOT a signer for the client transaction.
1166
+ isSigner: key.pubkey.equals(vault) ? false : key.isSigner,
1167
+ isWritable: key.isWritable,
1168
+ }),
1169
+ );
1170
+ const swapData = swapIx.data;
1171
+
1172
+ const ixns = await this.program.swap(
1173
+ vault,
1174
+ this.address(),
1175
+ inputMint,
1176
+ outputMint,
1177
+ vaultAssetInReserve,
1178
+ vaultAssetOutReserve,
1179
+ swapData,
1180
+ swapRemainingAccounts,
1181
+ JUPITER_SWAP_PROGRAM_ID,
1182
+ );
1183
+ const txSig = await this.send(ixns);
1184
+
1185
+ return txSig;
1186
+ }
1187
+
1188
+ async distributeFees(
1189
+ vault: web3.PublicKey,
1190
+ sharesDestinationOwner: web3.PublicKey,
1191
+ ): Promise<string> {
1192
+ // fetch vault data
1193
+ const vaultData = await this.getVault(vault);
1194
+ const manager = vaultData.manager;
1195
+ const shares = vaultData.sharesMint;
1196
+
1197
+ const ixns = await this.program.distributeFees(
1198
+ vault,
1199
+ manager,
1200
+ shares,
1201
+ sharesDestinationOwner,
1202
+ );
1203
+
1204
+ const txSig = await this.send(ixns);
1205
+
1206
+ return txSig;
1207
+ }
1208
+
1209
+ async send(
1210
+ ixns: web3.TransactionInstruction[],
1211
+ additionalSigners: web3.Signer[] = [],
1212
+ ): Promise<string> {
1213
+ const { blockhash } = await this.connection.getLatestBlockhash("confirmed");
1214
+ const msg = new web3.TransactionMessage({
1215
+ payerKey: this.address(),
1216
+ recentBlockhash: blockhash,
1217
+ instructions: ixns,
1218
+ }).compileToV0Message();
1219
+
1220
+ const tx = new web3.VersionedTransaction(msg);
1221
+
1222
+ const signedTx =
1223
+ await this.program.program.provider.wallet!.signTransaction(tx);
1224
+ signedTx.sign(additionalSigners);
1225
+
1226
+ try {
1227
+ const txSig = await this.connection.sendRawTransaction(
1228
+ signedTx.serialize(),
1229
+ {
1230
+ skipPreflight: this.skipPreflight,
1231
+ },
1232
+ );
1233
+
1234
+ // will throw an error if not found or tx errored
1235
+ await this.confirmTx(txSig);
1236
+
1237
+ return txSig;
1238
+ } catch (e) {
1239
+ if (e instanceof web3.SendTransactionError) {
1240
+ throw new Error(`tx failed: ${e}`);
1241
+ }
1242
+ throw e;
1243
+ }
1244
+ }
1245
+
1246
+ async confirmTx(txSig: string): Promise<void> {
1247
+ const maxAttempts = 90;
1248
+ const sleepDurationMs = 1000; // 1 sec
1249
+
1250
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
1251
+ const txResult = await this.connection.getSignatureStatuses([txSig], {
1252
+ searchTransactionHistory: false,
1253
+ });
1254
+
1255
+ if (txResult.value.length > 0 && txResult.value[0] !== null) {
1256
+ const txSigResult = txResult.value[0];
1257
+ if (txSigResult.err !== null) {
1258
+ throw new Error(
1259
+ `tx returned an error: ${txSigResult.err.toString()}`,
1260
+ );
1261
+ }
1262
+ return; // Transaction confirmed successfully
1263
+ }
1264
+
1265
+ if (attempt < maxAttempts - 1) {
1266
+ // sleep before next attempt
1267
+ await new Promise((resolve) => setTimeout(resolve, sleepDurationMs));
1268
+ }
1269
+ }
1270
+
1271
+ // If we've exhausted all attempts, throw the error
1272
+ throw new Error(`tx sig ${txSig} not found after ${maxAttempts} attempts`);
1273
+ }
1274
+ }
1275
+
1276
+ export async function buildEquityRemainingAccounts(
1277
+ vaultData: Vault,
1278
+ clendClient: ClendClient,
1279
+ ): Promise<AccountMeta[]> {
1280
+ // gather all asset mints, oracles, reserves, and clend accounts
1281
+ const assetMints: web3.PublicKey[] = vaultData.assets.map(
1282
+ (a) => a.mint.address,
1283
+ );
1284
+ const assetOracles: web3.PublicKey[] = vaultData.assets.map((a) => a.oracle);
1285
+ const assetReserves: web3.PublicKey[] = vaultData.assets.map(
1286
+ (a) => a.reserve.address,
1287
+ );
1288
+ const clendAccounts: web3.PublicKey[] = vaultData.clendAccounts.map(
1289
+ (ca) => ca.address,
1290
+ );
1291
+
1292
+ const base: AccountMeta[] = [
1293
+ // mints
1294
+ ...assetMints.map((a) => ({
1295
+ pubkey: a,
1296
+ isSigner: false,
1297
+ isWritable: false,
1298
+ })),
1299
+ // oracles
1300
+ ...assetOracles.map((a) => ({
1301
+ pubkey: a,
1302
+ isSigner: false,
1303
+ isWritable: false,
1304
+ })),
1305
+ // reserves
1306
+ ...assetReserves.map((a) => ({
1307
+ pubkey: a,
1308
+ isSigner: false,
1309
+ isWritable: false,
1310
+ })),
1311
+ // clend account addrs
1312
+ ...clendAccounts.map((ca) => ({
1313
+ pubkey: ca,
1314
+ isSigner: false,
1315
+ isWritable: false,
1316
+ })),
1317
+ ];
1318
+
1319
+ // include each clend account's active banks + their oracles
1320
+ const bankAndOracleMetas: AccountMeta[] = [];
1321
+ for (const ca of clendAccounts) {
1322
+ const state = await clendClient.getClendAccount(ca);
1323
+ if (!state) {
1324
+ throw new Error(`Clend account not found: ${ca.toString()}`);
1325
+ }
1326
+ const activeBanks = getClendAccountActiveBanks(state);
1327
+
1328
+ const activeBankState: Bank[] = [];
1329
+ for (const bank of activeBanks) {
1330
+ const bankData = await clendClient.getBank(bank);
1331
+ activeBankState.push(bankData);
1332
+ }
1333
+ const rem = getClendAccountRemainingAccounts(activeBankState);
1334
+ bankAndOracleMetas.push(...rem);
1335
+ }
1336
+
1337
+ // de-duplicate metas by pubkey, preserving writability where needed
1338
+ const byPk = new Map<string, AccountMeta>();
1339
+ const absorb = (m: AccountMeta) => {
1340
+ const k = m.pubkey.toBase58();
1341
+ const prev = byPk.get(k);
1342
+ if (!prev) byPk.set(k, m);
1343
+ else
1344
+ byPk.set(k, {
1345
+ pubkey: m.pubkey,
1346
+ isSigner: prev.isSigner || m.isSigner,
1347
+ isWritable: prev.isWritable || m.isWritable,
1348
+ });
1349
+ };
1350
+ [...base, ...bankAndOracleMetas].forEach(absorb);
1351
+
1352
+ const remAccounts = [...byPk.values()];
1353
+ return remAccounts;
1354
+ }