@kamino-finance/klend-sdk 5.10.6 → 5.10.8

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.
Files changed (80) hide show
  1. package/dist/classes/action.d.ts +7 -3
  2. package/dist/classes/action.d.ts.map +1 -1
  3. package/dist/classes/action.js +28 -13
  4. package/dist/classes/action.js.map +1 -1
  5. package/dist/classes/manager.d.ts +20 -18
  6. package/dist/classes/manager.d.ts.map +1 -1
  7. package/dist/classes/manager.js +27 -20
  8. package/dist/classes/manager.js.map +1 -1
  9. package/dist/classes/market.d.ts +11 -0
  10. package/dist/classes/market.d.ts.map +1 -1
  11. package/dist/classes/market.js +26 -0
  12. package/dist/classes/market.js.map +1 -1
  13. package/dist/classes/obligation.d.ts +14 -0
  14. package/dist/classes/obligation.d.ts.map +1 -1
  15. package/dist/classes/obligation.js +25 -0
  16. package/dist/classes/obligation.js.map +1 -1
  17. package/dist/classes/types_utils.d.ts +11 -0
  18. package/dist/classes/types_utils.d.ts.map +1 -0
  19. package/dist/classes/types_utils.js +21 -0
  20. package/dist/classes/types_utils.js.map +1 -0
  21. package/dist/classes/utils.d.ts +7 -0
  22. package/dist/classes/utils.d.ts.map +1 -1
  23. package/dist/classes/utils.js +21 -0
  24. package/dist/classes/utils.js.map +1 -1
  25. package/dist/classes/vault.d.ts +23 -14
  26. package/dist/classes/vault.d.ts.map +1 -1
  27. package/dist/classes/vault.js +65 -33
  28. package/dist/classes/vault.js.map +1 -1
  29. package/dist/client_kamino_manager.d.ts.map +1 -1
  30. package/dist/client_kamino_manager.js +25 -2
  31. package/dist/client_kamino_manager.js.map +1 -1
  32. package/dist/lending_operations/index.d.ts +1 -0
  33. package/dist/lending_operations/index.d.ts.map +1 -1
  34. package/dist/lending_operations/index.js +1 -0
  35. package/dist/lending_operations/index.js.map +1 -1
  36. package/dist/lending_operations/repay_with_collateral_operations.d.ts +5 -5
  37. package/dist/lending_operations/repay_with_collateral_operations.d.ts.map +1 -1
  38. package/dist/lending_operations/repay_with_collateral_operations.js +3 -2
  39. package/dist/lending_operations/repay_with_collateral_operations.js.map +1 -1
  40. package/dist/lending_operations/swap_collateral_operations.d.ts +102 -0
  41. package/dist/lending_operations/swap_collateral_operations.d.ts.map +1 -0
  42. package/dist/lending_operations/swap_collateral_operations.js +306 -0
  43. package/dist/lending_operations/swap_collateral_operations.js.map +1 -0
  44. package/dist/leverage/operations.d.ts +2 -2
  45. package/dist/leverage/operations.d.ts.map +1 -1
  46. package/dist/leverage/operations.js +4 -0
  47. package/dist/leverage/operations.js.map +1 -1
  48. package/dist/leverage/types.d.ts +5 -5
  49. package/dist/leverage/types.d.ts.map +1 -1
  50. package/dist/leverage/utils.d.ts +5 -5
  51. package/dist/leverage/utils.d.ts.map +1 -1
  52. package/dist/leverage/utils.js.map +1 -1
  53. package/dist/utils/constants.d.ts +1 -0
  54. package/dist/utils/constants.d.ts.map +1 -1
  55. package/dist/utils/constants.js +2 -1
  56. package/dist/utils/constants.js.map +1 -1
  57. package/dist/utils/pubkey.d.ts +3 -0
  58. package/dist/utils/pubkey.d.ts.map +1 -1
  59. package/dist/utils/pubkey.js +16 -2
  60. package/dist/utils/pubkey.js.map +1 -1
  61. package/dist/utils/seeds.d.ts +1 -1
  62. package/dist/utils/seeds.js +1 -1
  63. package/package.json +4 -4
  64. package/src/classes/action.ts +37 -19
  65. package/src/classes/manager.ts +40 -23
  66. package/src/classes/market.ts +35 -1
  67. package/src/classes/obligation.ts +75 -0
  68. package/src/classes/types_utils.ts +19 -0
  69. package/src/classes/utils.ts +22 -1
  70. package/src/classes/vault.ts +93 -41
  71. package/src/client_kamino_manager.ts +43 -4
  72. package/src/lending_operations/index.ts +1 -0
  73. package/src/lending_operations/repay_with_collateral_operations.ts +10 -9
  74. package/src/lending_operations/swap_collateral_operations.ts +586 -0
  75. package/src/leverage/operations.ts +14 -10
  76. package/src/leverage/types.ts +6 -6
  77. package/src/leverage/utils.ts +8 -8
  78. package/src/utils/constants.ts +2 -0
  79. package/src/utils/pubkey.ts +19 -2
  80. package/src/utils/seeds.ts +1 -1
@@ -0,0 +1,586 @@
1
+ import {
2
+ ElevationGroupDescription,
3
+ FeeCalculation,
4
+ KaminoAction,
5
+ KaminoMarket,
6
+ KaminoObligation,
7
+ KaminoReserve,
8
+ } from '../classes';
9
+ import { getFlashLoanInstructions, SwapIxsProvider, SwapQuoteProvider } from '../leverage';
10
+ import {
11
+ createAtasIdempotent,
12
+ DEFAULT_MAX_COMPUTE_UNITS,
13
+ getAssociatedTokenAddress,
14
+ getComputeBudgetAndPriorityFeeIxns,
15
+ PublicKeySet,
16
+ ScopeRefresh,
17
+ U64_MAX,
18
+ uniqueAccounts,
19
+ WRAPPED_SOL_MINT,
20
+ } from '../utils';
21
+ import { AddressLookupTableAccount, PublicKey, TransactionInstruction } from '@solana/web3.js';
22
+ import Decimal from 'decimal.js';
23
+ import { createCloseAccountInstruction, TOKEN_PROGRAM_ID } from '@solana/spl-token';
24
+
25
+ /**
26
+ * Inputs to the `getSwapCollIxns()` operation.
27
+ */
28
+ export interface SwapCollIxnsInputs<QuoteResponse> {
29
+ /**
30
+ * The amount of source collateral to be swapped-in for the target collateral.
31
+ * This value will be treated exactly (i.e. slippage is not applied here) and thus must not exceed the collateral's
32
+ * total amount.
33
+ */
34
+ sourceCollSwapAmount: Decimal;
35
+
36
+ /**
37
+ * If true, the source collateral will be closed - whatever amount is left after withdrawing `sourceCollSwapAmount`
38
+ * will be transferred to the user.
39
+ */
40
+ isClosingSourceColl: boolean;
41
+
42
+ /**
43
+ * The mint of the source collateral token (i.e. the current one).
44
+ */
45
+ sourceCollTokenMint: PublicKey;
46
+
47
+ /**
48
+ * The mint of the target collateral token (i.e. the new one).
49
+ */
50
+ targetCollTokenMint: PublicKey;
51
+
52
+ /**
53
+ * An elevation group ID that the obligation should end up with after the collateral swap - it will be requested by
54
+ * this operation (if different from the obligation's current elevation group).
55
+ */
56
+ newElevationGroup: number;
57
+
58
+ // Note: the undocumented inputs below all have their most usual meaning used across the SDK.
59
+
60
+ market: KaminoMarket;
61
+ obligation: KaminoObligation;
62
+ referrer: PublicKey;
63
+ currentSlot: number;
64
+ budgetAndPriorityFeeIxns?: TransactionInstruction[];
65
+ scopeRefresh?: ScopeRefresh;
66
+ quoter: SwapQuoteProvider<QuoteResponse>;
67
+ swapper: SwapIxsProvider<QuoteResponse>;
68
+ logger?: (msg: string, ...extra: any[]) => void;
69
+ }
70
+
71
+ /**
72
+ * Outputs from the `getSwapCollIxns()` operation.
73
+ */
74
+ export interface SwapCollIxnsOutputs<QuoteResponse> {
75
+ /**
76
+ * Instructions for on-chain execution.
77
+ */
78
+ ixs: TransactionInstruction[];
79
+
80
+ /**
81
+ * Required LUTs.
82
+ */
83
+ lookupTables: AddressLookupTableAccount[];
84
+
85
+ /**
86
+ * Informational-only details of the token amounts/fees/rates that were used during construction of `ixs`.
87
+ */
88
+ simulationDetails: {
89
+ /**
90
+ * Details related to the flash-loan operation needed during collateral swap.
91
+ */
92
+ flashLoan: {
93
+ /**
94
+ * The amount of flash-borrowed target collateral.
95
+ * It is also *exactly the amount of target collateral that gets added to the obligation*.
96
+ */
97
+ targetCollFlashBorrowedAmount: Decimal;
98
+
99
+ /**
100
+ * The flash-repaid amount.
101
+ * Simply a `targetCollFlashBorrowedAmount` + any flash-loan fees.
102
+ */
103
+ targetCollFlashRepaidAmount: Decimal;
104
+ };
105
+
106
+ /**
107
+ * Details related to the external DEX's swap operation (i.e. `swapper` input) needed during collateral swap.
108
+ */
109
+ externalSwap: {
110
+ /**
111
+ * The amount swapped-in to an external DEX.
112
+ * It is also *exactly the amount of source collateral that gets removed from the obligation* (i.e. echoed back
113
+ * `sourceCollSwapAmount` input).
114
+ */
115
+ sourceCollSwapInAmount: Decimal;
116
+
117
+ /**
118
+ * The amount swapped-out from an external DEX.
119
+ * Please note that this field will be equal to the `flashBorrow.targetCollFlashRepaidAmount`, but an actual
120
+ * on-chain swap-out is subject to slippage.
121
+ */
122
+ targetCollSwapOutAmount: Decimal;
123
+
124
+ /**
125
+ * The verbatim response coming from the input `quoter`.
126
+ */
127
+ quoteResponse?: QuoteResponse;
128
+ };
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Constructs instructions needed to partially/fully swap the given source collateral for some other collateral type.
134
+ */
135
+ export async function getSwapCollIxns<QuoteResponse>(
136
+ inputs: SwapCollIxnsInputs<QuoteResponse>
137
+ ): Promise<SwapCollIxnsOutputs<QuoteResponse>> {
138
+ const [args, context] = extractArgsAndContext(inputs);
139
+
140
+ // Conceptually, we need to construct the following ixns:
141
+ // 0. any set-up, like budgeting and ATAs
142
+ // 1. `flash-borrowed target coll = targetCollReserve.flashBorrow()`
143
+ // 2. `targetCollReserve.deposit(flash-borrowed target coll)`
144
+ // 3. `sourceCollReserve.withdraw(requested amount to be coll-swapped)`
145
+ // 4. `externally-swapped target coll = externalDex.swap(withdrawn current coll)`
146
+ // 5. `flashRepay(externally-swapped target coll)`
147
+ // However, there is a cyclic dependency:
148
+ // - To construct 4. (specifically, to query the external swap quote), we need to know all accounts used by Kamino's
149
+ // own ixns.
150
+ // - To construct 1. (i.e. flash-borrow), we need to know the target collateral swap-out from 4.
151
+
152
+ // Construct the Klend's own ixns with a fake swap-out (only to learn the klend accounts used):
153
+ const fakeKlendIxns = await getKlendIxns(args, FAKE_TARGET_COLL_SWAP_OUT_AMOUNT, context);
154
+ const klendAccounts = uniqueAccounts(listIxns(fakeKlendIxns));
155
+
156
+ // Construct the external swap ixns (and learn the actual swap-out amount):
157
+ const externalSwapIxns = await getExternalSwapIxns(args, klendAccounts, context);
158
+
159
+ // We now have the full information needed to simulate the end-state, so let's check that the operation is legal:
160
+ context.logger(
161
+ `Expected to swap ${args.sourceCollSwapAmount} ${context.sourceCollReserve.symbol} collateral into ${externalSwapIxns.swapOutAmount} ${context.targetCollReserve.symbol} collateral`
162
+ );
163
+ checkResultingObligationValid(args, externalSwapIxns.swapOutAmount, context);
164
+
165
+ // Construct the Klend's own ixns with an actual swap-out amount:
166
+ const klendIxns = await getKlendIxns(args, externalSwapIxns.swapOutAmount, context);
167
+
168
+ return {
169
+ ixs: listIxns(klendIxns, externalSwapIxns.ixns),
170
+ lookupTables: externalSwapIxns.luts,
171
+ simulationDetails: {
172
+ flashLoan: {
173
+ targetCollFlashBorrowedAmount: klendIxns.simulationDetails.targetCollFlashBorrowedAmount,
174
+ targetCollFlashRepaidAmount: externalSwapIxns.swapOutAmount,
175
+ },
176
+ externalSwap: {
177
+ sourceCollSwapInAmount: args.sourceCollSwapAmount, // repeated `/inputs.sourceCollSwapAmount`, only for clarity
178
+ targetCollSwapOutAmount: externalSwapIxns.swapOutAmount, // repeated `../flashLoan.targetCollFlashRepaidAmount`, only for clarity
179
+ quoteResponse: externalSwapIxns.simulationDetails.quoteResponse,
180
+ },
181
+ },
182
+ };
183
+ }
184
+
185
+ type SwapCollArgs = {
186
+ sourceCollSwapAmount: Decimal;
187
+ isClosingSourceColl: boolean;
188
+ newElevationGroup: ElevationGroupDescription | null;
189
+ };
190
+
191
+ type SwapCollContext<QuoteResponse> = {
192
+ budgetAndPriorityFeeIxns: TransactionInstruction[];
193
+ market: KaminoMarket;
194
+ sourceCollReserve: KaminoReserve;
195
+ targetCollReserve: KaminoReserve;
196
+ obligation: KaminoObligation;
197
+ quoter: SwapQuoteProvider<QuoteResponse>;
198
+ swapper: SwapIxsProvider<QuoteResponse>;
199
+ referrer: PublicKey;
200
+ currentSlot: number;
201
+ scopeRefresh: ScopeRefresh | undefined;
202
+ logger: (msg: string, ...extra: any[]) => void;
203
+ };
204
+
205
+ function extractArgsAndContext<QuoteResponse>(
206
+ inputs: SwapCollIxnsInputs<QuoteResponse>
207
+ ): [SwapCollArgs, SwapCollContext<QuoteResponse>] {
208
+ if (inputs.sourceCollTokenMint.equals(inputs.targetCollTokenMint)) {
209
+ throw new Error(`Cannot swap from/to the same collateral`);
210
+ }
211
+ if (inputs.sourceCollSwapAmount.lte(0)) {
212
+ throw new Error(`Cannot swap a negative amount`);
213
+ }
214
+ return [
215
+ {
216
+ sourceCollSwapAmount: inputs.sourceCollSwapAmount,
217
+ isClosingSourceColl: inputs.isClosingSourceColl,
218
+ newElevationGroup: inputs.market.getExistingElevationGroup(inputs.newElevationGroup, 'Newly-requested'),
219
+ },
220
+ {
221
+ budgetAndPriorityFeeIxns:
222
+ inputs.budgetAndPriorityFeeIxns || getComputeBudgetAndPriorityFeeIxns(DEFAULT_MAX_COMPUTE_UNITS),
223
+ sourceCollReserve: inputs.market.getExistingReserveByMint(inputs.sourceCollTokenMint, 'Current collateral'),
224
+ targetCollReserve: inputs.market.getExistingReserveByMint(inputs.targetCollTokenMint, 'Target collateral'),
225
+ logger: console.log,
226
+ market: inputs.market,
227
+ obligation: inputs.obligation,
228
+ quoter: inputs.quoter,
229
+ swapper: inputs.swapper,
230
+ referrer: inputs.referrer,
231
+ scopeRefresh: inputs.scopeRefresh,
232
+ currentSlot: inputs.currentSlot,
233
+ },
234
+ ];
235
+ }
236
+
237
+ const FAKE_TARGET_COLL_SWAP_OUT_AMOUNT = new Decimal(1); // see the lengthy `getSwapCollIxns()` impl comment
238
+
239
+ type SwapCollKlendIxns = {
240
+ setupIxns: TransactionInstruction[];
241
+ targetCollFlashBorrowIxn: TransactionInstruction;
242
+ depositTargetCollIxns: TransactionInstruction[];
243
+ withdrawSourceCollIxns: TransactionInstruction[];
244
+ targetCollFlashRepayIxn: TransactionInstruction;
245
+ cleanupIxns: TransactionInstruction[];
246
+ simulationDetails: {
247
+ targetCollFlashBorrowedAmount: Decimal;
248
+ };
249
+ };
250
+
251
+ async function getKlendIxns(
252
+ args: SwapCollArgs,
253
+ targetCollSwapOutAmount: Decimal,
254
+ context: SwapCollContext<any>
255
+ ): Promise<SwapCollKlendIxns> {
256
+ const { ataCreationIxns, targetCollAta } = getAtaCreationIxns(context);
257
+ const setupIxns = [...context.budgetAndPriorityFeeIxns, ...ataCreationIxns];
258
+
259
+ const targetCollFlashBorrowedAmount = calculateTargetCollFlashBorrowedAmount(targetCollSwapOutAmount, context);
260
+ const { targetCollFlashBorrowIxn, targetCollFlashRepayIxn } = getTargetCollFlashLoanIxns(
261
+ targetCollFlashBorrowedAmount,
262
+ setupIxns.length,
263
+ targetCollAta,
264
+ context
265
+ );
266
+
267
+ const depositTargetCollIxns = await getDepositTargetCollIxns(targetCollFlashBorrowedAmount, context);
268
+ const withdrawSourceCollIxns = await getWithdrawSourceCollIxns(
269
+ args,
270
+ depositTargetCollIxns.removesElevationGroup,
271
+ context
272
+ );
273
+
274
+ const cleanupIxns = getAtaCloseIxns(context);
275
+
276
+ return {
277
+ setupIxns,
278
+ targetCollFlashBorrowIxn,
279
+ depositTargetCollIxns: depositTargetCollIxns.ixns,
280
+ withdrawSourceCollIxns,
281
+ targetCollFlashRepayIxn,
282
+ cleanupIxns,
283
+ simulationDetails: {
284
+ targetCollFlashBorrowedAmount,
285
+ },
286
+ };
287
+ }
288
+
289
+ function calculateTargetCollFlashBorrowedAmount(
290
+ targetCollFlashRepaidAmount: Decimal,
291
+ context: SwapCollContext<any>
292
+ ): Decimal {
293
+ const { protocolFees, referrerFees } = context.targetCollReserve.calculateFees(
294
+ targetCollFlashRepaidAmount.mul(context.targetCollReserve.getMintFactor()),
295
+ context.targetCollReserve.getFlashLoanFee(),
296
+ FeeCalculation.Inclusive, // denotes that the amount parameter above means "to be repaid" (not "borrowed")
297
+ context.market.state.referralFeeBps,
298
+ !context.referrer.equals(PublicKey.default)
299
+ );
300
+ const targetCollFlashLoanFee = protocolFees.add(referrerFees).div(context.targetCollReserve.getMintFactor());
301
+ return targetCollFlashRepaidAmount.sub(targetCollFlashLoanFee);
302
+ }
303
+
304
+ function getAtaCreationIxns(context: SwapCollContext<any>) {
305
+ const atasAndAtaCreationIxns = createAtasIdempotent(context.obligation.state.owner, [
306
+ {
307
+ mint: context.sourceCollReserve.getLiquidityMint(),
308
+ tokenProgram: context.sourceCollReserve.getLiquidityTokenProgram(),
309
+ },
310
+ {
311
+ mint: context.targetCollReserve.getLiquidityMint(),
312
+ tokenProgram: context.targetCollReserve.getLiquidityTokenProgram(),
313
+ },
314
+ ]);
315
+ return {
316
+ ataCreationIxns: atasAndAtaCreationIxns.map((tuple) => tuple.createAtaIx),
317
+ targetCollAta: atasAndAtaCreationIxns[1].ata,
318
+ };
319
+ }
320
+
321
+ function getAtaCloseIxns(context: SwapCollContext<any>) {
322
+ const ataCloseIxns = [];
323
+ if (
324
+ context.sourceCollReserve.getLiquidityMint().equals(WRAPPED_SOL_MINT) ||
325
+ context.targetCollReserve.getLiquidityMint().equals(WRAPPED_SOL_MINT)
326
+ ) {
327
+ const owner = context.obligation.state.owner;
328
+ const wsolAta = getAssociatedTokenAddress(WRAPPED_SOL_MINT, owner, false);
329
+ ataCloseIxns.push(createCloseAccountInstruction(wsolAta, owner, owner, [], TOKEN_PROGRAM_ID));
330
+ }
331
+ return ataCloseIxns;
332
+ }
333
+
334
+ function getTargetCollFlashLoanIxns(
335
+ targetCollAmount: Decimal,
336
+ flashBorrowIxnIndex: number,
337
+ destinationAta: PublicKey,
338
+ context: SwapCollContext<any>
339
+ ) {
340
+ const { flashBorrowIxn: targetCollFlashBorrowIxn, flashRepayIxn: targetCollFlashRepayIxn } = getFlashLoanInstructions(
341
+ {
342
+ borrowIxnIndex: flashBorrowIxnIndex,
343
+ walletPublicKey: context.obligation.state.owner,
344
+ lendingMarketAuthority: context.market.getLendingMarketAuthority(),
345
+ lendingMarketAddress: context.market.getAddress(),
346
+ reserve: context.targetCollReserve,
347
+ amountLamports: targetCollAmount.mul(context.targetCollReserve.getMintFactor()),
348
+ destinationAta,
349
+ // TODO(referrals): once we support referrals, we will have to replace the placeholder args below:
350
+ referrerAccount: context.market.programId,
351
+ referrerTokenState: context.market.programId,
352
+ programId: context.market.programId,
353
+ }
354
+ );
355
+ return { targetCollFlashBorrowIxn, targetCollFlashRepayIxn };
356
+ }
357
+
358
+ type DepositTargetCollIxns = {
359
+ removesElevationGroup: boolean;
360
+ ixns: TransactionInstruction[];
361
+ };
362
+
363
+ async function getDepositTargetCollIxns(
364
+ targetCollAmount: Decimal,
365
+ context: SwapCollContext<any>
366
+ ): Promise<DepositTargetCollIxns> {
367
+ const removesElevationGroup = mustRemoveElevationGroupBeforeDeposit(context);
368
+ const depositCollAction = await KaminoAction.buildDepositTxns(
369
+ context.market,
370
+ targetCollAmount.mul(context.targetCollReserve.getMintFactor()).toString(), // in lamports
371
+ context.targetCollReserve.getLiquidityMint(),
372
+ context.obligation.state.owner,
373
+ context.obligation,
374
+ 0, // no extra compute budget
375
+ false, // we do not need ATA ixns here (we construct and close them ourselves)
376
+ removesElevationGroup, // we may need to (temporarily) remove the elevation group; the same or a different one will be set on withdraw, if requested
377
+ false, // we are dealing with an existing obligation, no need to create user metadata
378
+ context.referrer,
379
+ context.currentSlot,
380
+ context.scopeRefresh,
381
+ removesElevationGroup ? 0 : undefined // only applicable when removing the group
382
+ );
383
+ return {
384
+ ixns: KaminoAction.actionToIxs(depositCollAction),
385
+ removesElevationGroup,
386
+ };
387
+ }
388
+
389
+ function mustRemoveElevationGroupBeforeDeposit(context: SwapCollContext<any>): boolean {
390
+ if (context.obligation.deposits.has(context.targetCollReserve.address)) {
391
+ return false; // the target collateral already was a reserve in the obligation, so we do not affect any potential elevation group
392
+ }
393
+ const currentElevationGroupId = context.obligation.state.elevationGroup;
394
+ if (currentElevationGroupId == 0) {
395
+ return false; // simply nothing to remove
396
+ }
397
+ if (!context.targetCollReserve.state.config.elevationGroups.includes(currentElevationGroupId)) {
398
+ return true; // the target collateral reserve is NOT in the obligation's group - must remove the group
399
+ }
400
+ const currentElevationGroup = context.market.getElevationGroup(currentElevationGroupId);
401
+ if (context.obligation.deposits.size >= currentElevationGroup.maxReservesAsCollateral) {
402
+ return true; // the obligation is already at its elevation group's deposits count limit - must remove the group
403
+ }
404
+ return false; // the obligation has some elevation group and the new collateral can be added to it
405
+ }
406
+
407
+ async function getWithdrawSourceCollIxns(
408
+ args: SwapCollArgs,
409
+ depositRemovedElevationGroup: boolean,
410
+ context: SwapCollContext<any>
411
+ ): Promise<TransactionInstruction[]> {
412
+ const withdrawnSourceCollLamports = args.isClosingSourceColl
413
+ ? U64_MAX
414
+ : args.sourceCollSwapAmount.mul(context.sourceCollReserve.getMintFactor()).toString();
415
+ const requestedElevationGroup = elevationGroupIdToRequestAfterWithdraw(args, depositRemovedElevationGroup, context);
416
+ const withdrawCollAction = await KaminoAction.buildWithdrawTxns(
417
+ context.market,
418
+ withdrawnSourceCollLamports,
419
+ context.sourceCollReserve.getLiquidityMint(),
420
+ context.obligation.state.owner,
421
+ context.obligation,
422
+ 0, // no extra compute budget
423
+ false, // we do not need ATA ixns here (we construct and close them ourselves)
424
+ requestedElevationGroup !== undefined, // the `elevationGroupIdToRequestAfterWithdraw()` has already decided on this
425
+ false, // we are dealing with an existing obligation, no need to create user metadata
426
+ context.referrer,
427
+ context.currentSlot,
428
+ undefined, // we have refreshed scope already, during depositing
429
+ requestedElevationGroup,
430
+ context.obligation.deposits.has(context.targetCollReserve.address) // if our obligation already had the target coll...
431
+ ? undefined // ... then we need no customizations here, but otherwise...
432
+ : {
433
+ addedDepositReserves: [context.targetCollReserve.address], // ... we need to inform our infra that the obligation now has one more reserve that needs refreshing.
434
+ }
435
+ );
436
+ return KaminoAction.actionToIxs(withdrawCollAction);
437
+ }
438
+
439
+ function elevationGroupIdToRequestAfterWithdraw(
440
+ args: SwapCollArgs,
441
+ depositRemovedElevationGroup: boolean,
442
+ context: SwapCollContext<any>
443
+ ): number | undefined {
444
+ const obligationInitialElevationGroup = context.obligation.state.elevationGroup;
445
+ const requestedElevationGroupId = args.newElevationGroup?.elevationGroup ?? 0;
446
+ if (requestedElevationGroupId === 0) {
447
+ // the user doesn't want any elevation group, and...
448
+ if (obligationInitialElevationGroup === 0) {
449
+ return undefined; // ... he already didn't have it - fine!
450
+ }
451
+ if (depositRemovedElevationGroup) {
452
+ return undefined; // ... our deposit already forced us to remove it - fine!
453
+ }
454
+ return 0; // ... but he *did have one*, and our deposit didn't need to remove it - so we remove it now, just to satisfy him
455
+ } else {
456
+ // the user wants some elevation group, and...
457
+ if (depositRemovedElevationGroup) {
458
+ return requestedElevationGroupId; // ...our deposit forced us to remove it - so we now request the new one, whatever it is
459
+ }
460
+ if (obligationInitialElevationGroup === requestedElevationGroupId) {
461
+ return undefined; // ...and he already had exactly this one - fine!
462
+ }
463
+ return requestedElevationGroupId; // ...and he had some different one - so we request the new one
464
+ }
465
+ }
466
+
467
+ type ExternalSwapIxns<QuoteResponse> = {
468
+ swapOutAmount: Decimal;
469
+ ixns: TransactionInstruction[];
470
+ luts: AddressLookupTableAccount[];
471
+ simulationDetails: {
472
+ quoteResponse?: QuoteResponse;
473
+ };
474
+ };
475
+
476
+ async function getExternalSwapIxns<QuoteResponse>(
477
+ args: SwapCollArgs,
478
+ klendAccounts: PublicKey[],
479
+ context: SwapCollContext<QuoteResponse>
480
+ ): Promise<ExternalSwapIxns<QuoteResponse>> {
481
+ const externalSwapInputs = {
482
+ inputAmountLamports: args.sourceCollSwapAmount.mul(context.sourceCollReserve.getMintFactor()),
483
+ inputMint: context.sourceCollReserve.getLiquidityMint(),
484
+ outputMint: context.targetCollReserve.getLiquidityMint(),
485
+ amountDebtAtaBalance: undefined, // only used for kTokens
486
+ };
487
+ const externalSwapQuote = await context.quoter(externalSwapInputs, klendAccounts);
488
+ const swapOutAmount = externalSwapQuote.priceAInB.mul(args.sourceCollSwapAmount);
489
+ const externalSwapIxnsAndLuts = await context.swapper(externalSwapInputs, klendAccounts, externalSwapQuote);
490
+ // Note: we can ignore the returned `preActionIxs` field - we do not request any of them from the swapper.
491
+ return {
492
+ swapOutAmount,
493
+ ixns: externalSwapIxnsAndLuts.swapIxs,
494
+ luts: externalSwapIxnsAndLuts.lookupTables,
495
+ simulationDetails: {
496
+ quoteResponse: externalSwapQuote.quoteResponse,
497
+ },
498
+ };
499
+ }
500
+
501
+ function checkResultingObligationValid(
502
+ args: SwapCollArgs,
503
+ targetCollAmount: Decimal,
504
+ context: SwapCollContext<any>
505
+ ): void {
506
+ // The newly-requested elevation group must have its conditions satisfied:
507
+ if (args.newElevationGroup !== null) {
508
+ // Note: we cannot use the existing `isLoanEligibleForElevationGroup()`, since it operates on a `KaminoObligation`,
509
+ // and our instance is stale (we want to assert on the state *after* potential changes).
510
+
511
+ // Let's start with the (simpler) debt reserve - it cannot change during a coll-swap:
512
+ const debtReserveAddresses = [...context.obligation.borrows.keys()];
513
+ if (debtReserveAddresses.length > 1) {
514
+ throw new Error(
515
+ `The obligation with ${debtReserveAddresses.length} debt reserves cannot request any elevation group`
516
+ );
517
+ }
518
+ if (debtReserveAddresses.length == 1) {
519
+ const debtReserveAddress = debtReserveAddresses[0];
520
+ if (!args.newElevationGroup.debtReserve.equals(debtReserveAddress)) {
521
+ throw new Error(
522
+ `The obligation with debt reserve ${debtReserveAddress.toBase58()} cannot request elevation group ${
523
+ args.newElevationGroup.elevationGroup
524
+ }`
525
+ );
526
+ }
527
+ }
528
+
529
+ // Now the coll reserves: this requires first finding out the resulting set of deposits:
530
+ const collReserveAddresses = new PublicKeySet([
531
+ ...context.obligation.deposits.keys(),
532
+ context.targetCollReserve.address,
533
+ ]);
534
+ if (args.isClosingSourceColl) {
535
+ collReserveAddresses.remove(context.sourceCollReserve.address);
536
+ }
537
+ if (collReserveAddresses.size() > args.newElevationGroup.maxReservesAsCollateral) {
538
+ throw new Error(
539
+ `The obligation with ${collReserveAddresses.size()} collateral reserves cannot request elevation group ${
540
+ args.newElevationGroup.elevationGroup
541
+ }`
542
+ );
543
+ }
544
+ for (const collReserveAddress of collReserveAddresses.toArray()) {
545
+ if (!args.newElevationGroup.collateralReserves.contains(collReserveAddress)) {
546
+ throw new Error(
547
+ `The obligation with collateral reserve ${collReserveAddress.toBase58()} cannot request elevation group ${
548
+ args.newElevationGroup.elevationGroup
549
+ }`
550
+ );
551
+ }
552
+ }
553
+ }
554
+
555
+ // The LTV cannot be exceeded:
556
+ const effectiveWithdrawAmount = args.isClosingSourceColl
557
+ ? context.obligation.getDepositAmountByReserve(context.sourceCollReserve)
558
+ : args.sourceCollSwapAmount;
559
+ const resultingStats = context.obligation.getPostSwapCollObligationStats({
560
+ withdrawAmountLamports: effectiveWithdrawAmount.mul(context.sourceCollReserve.getMintFactor()),
561
+ withdrawReserveAddress: context.sourceCollReserve.address,
562
+ depositAmountLamports: targetCollAmount.mul(context.targetCollReserve.getMintFactor()),
563
+ depositReserveAddress: context.targetCollReserve.address,
564
+ market: context.market,
565
+ newElevationGroup: args.newElevationGroup?.elevationGroup ?? 0,
566
+ slot: context.currentSlot,
567
+ });
568
+ const maxLtv = resultingStats.borrowLimit.div(resultingStats.userTotalCollateralDeposit);
569
+ if (resultingStats.loanToValue > maxLtv) {
570
+ throw new Error(
571
+ `Swapping collateral ${effectiveWithdrawAmount} ${context.sourceCollReserve.symbol} into ${targetCollAmount} ${context.targetCollReserve.symbol} would result in the obligation's LTV ${resultingStats.loanToValue} exceeding its max LTV ${maxLtv}`
572
+ );
573
+ }
574
+ }
575
+
576
+ function listIxns(klendIxns: SwapCollKlendIxns, externalSwapIxns?: TransactionInstruction[]): TransactionInstruction[] {
577
+ return [
578
+ ...klendIxns.setupIxns,
579
+ klendIxns.targetCollFlashBorrowIxn,
580
+ ...klendIxns.depositTargetCollIxns,
581
+ ...klendIxns.withdrawSourceCollIxns,
582
+ ...(externalSwapIxns || []),
583
+ klendIxns.targetCollFlashRepayIxn,
584
+ ...klendIxns.cleanupIxns,
585
+ ];
586
+ }
@@ -52,8 +52,8 @@ import {
52
52
  DepsoitLeverageIxsResponse,
53
53
  PriceAinBProvider,
54
54
  SwapInputs,
55
- SwapQuoteIxs,
56
- SwapQuoteIxsProvider,
55
+ SwapIxs,
56
+ SwapIxsProvider,
57
57
  WithdrawLeverageCalcsResult,
58
58
  WithdrawLeverageInitialInputs,
59
59
  WithdrawLeverageIxsResponse,
@@ -335,7 +335,7 @@ export async function getDepositWithLeverageIxns<QuoteResponse>({
335
335
  quoter,
336
336
  });
337
337
 
338
- let depositSwapper: SwapQuoteIxsProvider<QuoteResponse>;
338
+ let depositSwapper: SwapIxsProvider<QuoteResponse>;
339
339
 
340
340
  if (!initialInputs.collIsKtoken) {
341
341
  depositSwapper = swapper;
@@ -411,7 +411,7 @@ async function buildDepositWithLeverageIxns(
411
411
  scopeFeed: string | undefined,
412
412
  calcs: DepositLeverageCalcsResult,
413
413
  budgetAndPriorityFeeIxs: TransactionInstruction[] | undefined,
414
- swapQuoteIxs: SwapQuoteIxs,
414
+ swapQuoteIxs: SwapIxs,
415
415
  strategy: StrategyWithAddress | undefined,
416
416
  collIsKtoken: boolean,
417
417
  elevationGroupOverride?: number
@@ -506,6 +506,7 @@ async function buildDepositWithLeverageIxns(
506
506
  !collIsKtoken ? collReserve.stats.decimals : debtReserve.stats.decimals
507
507
  ),
508
508
  destinationAta: !collIsKtoken ? collTokenAta : debtTokenAta,
509
+ // TODO(referrals): once we support referrals, we will have to replace the placeholder args below:
509
510
  referrerAccount: market.programId,
510
511
  referrerTokenState: market.programId,
511
512
  programId: market.programId,
@@ -748,7 +749,7 @@ export async function getWithdrawWithLeverageIxns<QuoteResponse>({
748
749
  quoter,
749
750
  });
750
751
 
751
- let withdrawSwapper: SwapQuoteIxsProvider<QuoteResponse>;
752
+ let withdrawSwapper: SwapIxsProvider<QuoteResponse>;
752
753
 
753
754
  if (initialInputs.collIsKtoken) {
754
755
  if (kamino === undefined) {
@@ -821,7 +822,7 @@ export async function buildWithdrawWithLeverageIxns(
821
822
  scopeFeed: string | undefined,
822
823
  calcs: WithdrawLeverageCalcsResult,
823
824
  budgetAndPriorityFeeIxs: TransactionInstruction[] | undefined,
824
- swapQuoteIxs: SwapQuoteIxs,
825
+ swapQuoteIxs: SwapIxs,
825
826
  strategy: StrategyWithAddress | undefined,
826
827
  collIsKtoken: boolean
827
828
  ): Promise<TransactionInstruction[]> {
@@ -917,6 +918,7 @@ export async function buildWithdrawWithLeverageIxns(
917
918
  reserve: debtReserve!,
918
919
  amountLamports: toLamports(calcs.repayAmount, debtReserve!.stats.decimals),
919
920
  destinationAta: debtTokenAta,
921
+ // TODO(referrals): once we support referrals, we will have to replace the placeholder args below:
920
922
  referrerAccount: market.programId,
921
923
  referrerTokenState: market.programId,
922
924
  programId: market.programId,
@@ -1258,7 +1260,7 @@ export async function getAdjustLeverageIxns<QuoteResponse>({
1258
1260
 
1259
1261
  // leverage increased so we need to deposit and borrow more
1260
1262
  if (initialInputs.isDeposit) {
1261
- let depositSwapper: SwapQuoteIxsProvider<QuoteResponse>;
1263
+ let depositSwapper: SwapIxsProvider<QuoteResponse>;
1262
1264
 
1263
1265
  if (initialInputs.collIsKtoken) {
1264
1266
  if (kamino === undefined) {
@@ -1312,7 +1314,7 @@ export async function getAdjustLeverageIxns<QuoteResponse>({
1312
1314
  } else {
1313
1315
  console.log('Decreasing leverage');
1314
1316
 
1315
- let withdrawSwapper: SwapQuoteIxsProvider<QuoteResponse>;
1317
+ let withdrawSwapper: SwapIxsProvider<QuoteResponse>;
1316
1318
 
1317
1319
  if (initialInputs.collIsKtoken) {
1318
1320
  if (kamino === undefined) {
@@ -1374,7 +1376,7 @@ async function buildIncreaseLeverageIxns(
1374
1376
  strategy: StrategyWithAddress | undefined,
1375
1377
  scopeFeed: string | undefined,
1376
1378
  collIsKtoken: boolean,
1377
- swapQuoteIxs: SwapQuoteIxs,
1379
+ swapQuoteIxs: SwapIxs,
1378
1380
  budgetAndPriorityFeeIxns: TransactionInstruction[] | undefined
1379
1381
  ): Promise<TransactionInstruction[]> {
1380
1382
  const collReserve = kaminoMarket.getReserveByMint(collTokenMint);
@@ -1455,6 +1457,7 @@ async function buildIncreaseLeverageIxns(
1455
1457
  !collIsKtoken ? collReserve!.stats.decimals : debtReserve!.stats.decimals
1456
1458
  ),
1457
1459
  destinationAta: !collIsKtoken ? collTokenAta : debtTokenAta,
1460
+ // TODO(referrals): once we support referrals, we will have to replace the placeholder args below:
1458
1461
  referrerAccount: kaminoMarket.programId,
1459
1462
  referrerTokenState: kaminoMarket.programId,
1460
1463
  programId: kaminoMarket.programId,
@@ -1539,7 +1542,7 @@ async function buildDecreaseLeverageIxns(
1539
1542
  strategy: StrategyWithAddress | undefined,
1540
1543
  scopeFeed: string | undefined,
1541
1544
  collIsKtoken: boolean,
1542
- swapQuoteIxs: SwapQuoteIxs,
1545
+ swapQuoteIxs: SwapIxs,
1543
1546
  budgetAndPriorityFeeIxns: TransactionInstruction[] | undefined
1544
1547
  ): Promise<TransactionInstruction[]> {
1545
1548
  const collReserve = kaminoMarket.getReserveByMint(collTokenMint);
@@ -1626,6 +1629,7 @@ async function buildDecreaseLeverageIxns(
1626
1629
  reserve: debtReserve!,
1627
1630
  amountLamports: toLamports(Decimal.abs(calcs.adjustBorrowPosition), debtReserve!.stats.decimals),
1628
1631
  destinationAta: debtTokenAta,
1632
+ // TODO(referrals): once we support referrals, we will have to replace the placeholder args below:
1629
1633
  referrerAccount: kaminoMarket.programId,
1630
1634
  referrerTokenState: kaminoMarket.programId,
1631
1635
  programId: kaminoMarket.programId,