@kamino-finance/klend-sdk 5.11.17 → 5.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/dist/classes/action.d.ts +9 -1
  2. package/dist/classes/action.d.ts.map +1 -1
  3. package/dist/classes/action.js +15 -1
  4. package/dist/classes/action.js.map +1 -1
  5. package/dist/classes/fraction.d.ts +6 -5
  6. package/dist/classes/fraction.d.ts.map +1 -1
  7. package/dist/classes/fraction.js +13 -8
  8. package/dist/classes/fraction.js.map +1 -1
  9. package/dist/classes/manager.d.ts +1 -1
  10. package/dist/classes/manager.d.ts.map +1 -1
  11. package/dist/classes/manager.js +10 -0
  12. package/dist/classes/manager.js.map +1 -1
  13. package/dist/classes/obligation.d.ts +54 -1
  14. package/dist/classes/obligation.d.ts.map +1 -1
  15. package/dist/classes/obligation.js +47 -1
  16. package/dist/classes/obligation.js.map +1 -1
  17. package/dist/classes/obligationOrder.d.ts +206 -0
  18. package/dist/classes/obligationOrder.d.ts.map +1 -0
  19. package/dist/classes/obligationOrder.js +359 -0
  20. package/dist/classes/obligationOrder.js.map +1 -0
  21. package/dist/classes/reserve.d.ts +5 -5
  22. package/dist/classes/reserve.d.ts.map +1 -1
  23. package/dist/classes/reserve.js +80 -264
  24. package/dist/classes/reserve.js.map +1 -1
  25. package/dist/classes/types.d.ts +14 -1
  26. package/dist/classes/types.d.ts.map +1 -1
  27. package/dist/classes/utils.d.ts +9 -0
  28. package/dist/classes/utils.d.ts.map +1 -1
  29. package/dist/classes/utils.js +21 -0
  30. package/dist/classes/utils.js.map +1 -1
  31. package/dist/classes/vault.js +1 -1
  32. package/dist/classes/vault.js.map +1 -1
  33. package/dist/client_kamino_manager.js +6 -6
  34. package/dist/client_kamino_manager.js.map +1 -1
  35. package/dist/idl.json +244 -23
  36. package/dist/idl_codegen/accounts/LendingMarket.d.ts +12 -6
  37. package/dist/idl_codegen/accounts/LendingMarket.d.ts.map +1 -1
  38. package/dist/idl_codegen/accounts/LendingMarket.js +15 -3
  39. package/dist/idl_codegen/accounts/LendingMarket.js.map +1 -1
  40. package/dist/idl_codegen/accounts/Obligation.d.ts +27 -6
  41. package/dist/idl_codegen/accounts/Obligation.d.ts.map +1 -1
  42. package/dist/idl_codegen/accounts/Obligation.js +26 -9
  43. package/dist/idl_codegen/accounts/Obligation.js.map +1 -1
  44. package/dist/idl_codegen/errors/custom.d.ts +36 -4
  45. package/dist/idl_codegen/errors/custom.d.ts.map +1 -1
  46. package/dist/idl_codegen/errors/custom.js +64 -8
  47. package/dist/idl_codegen/errors/custom.js.map +1 -1
  48. package/dist/idl_codegen/instructions/index.d.ts +2 -0
  49. package/dist/idl_codegen/instructions/index.d.ts.map +1 -1
  50. package/dist/idl_codegen/instructions/index.js +3 -1
  51. package/dist/idl_codegen/instructions/index.js.map +1 -1
  52. package/dist/idl_codegen/instructions/setObligationOrder.d.ts +14 -0
  53. package/dist/idl_codegen/instructions/setObligationOrder.d.ts.map +1 -0
  54. package/dist/idl_codegen/instructions/setObligationOrder.js +62 -0
  55. package/dist/idl_codegen/instructions/setObligationOrder.js.map +1 -0
  56. package/dist/idl_codegen/types/ObligationOrder.d.ts +284 -0
  57. package/dist/idl_codegen/types/ObligationOrder.d.ts.map +1 -0
  58. package/dist/idl_codegen/types/ObligationOrder.js +205 -0
  59. package/dist/idl_codegen/types/ObligationOrder.js.map +1 -0
  60. package/dist/idl_codegen/types/ReserveConfig.d.ts +26 -11
  61. package/dist/idl_codegen/types/ReserveConfig.d.ts.map +1 -1
  62. package/dist/idl_codegen/types/ReserveConfig.js +15 -10
  63. package/dist/idl_codegen/types/ReserveConfig.js.map +1 -1
  64. package/dist/idl_codegen/types/UpdateConfigMode.d.ts +34 -21
  65. package/dist/idl_codegen/types/UpdateConfigMode.d.ts.map +1 -1
  66. package/dist/idl_codegen/types/UpdateConfigMode.js +58 -34
  67. package/dist/idl_codegen/types/UpdateConfigMode.js.map +1 -1
  68. package/dist/idl_codegen/types/UpdateLendingMarketMode.d.ts +13 -0
  69. package/dist/idl_codegen/types/UpdateLendingMarketMode.d.ts.map +1 -1
  70. package/dist/idl_codegen/types/UpdateLendingMarketMode.js +25 -1
  71. package/dist/idl_codegen/types/UpdateLendingMarketMode.js.map +1 -1
  72. package/dist/idl_codegen/types/index.d.ts +6 -4
  73. package/dist/idl_codegen/types/index.d.ts.map +1 -1
  74. package/dist/idl_codegen/types/index.js +3 -1
  75. package/dist/idl_codegen/types/index.js.map +1 -1
  76. package/dist/idl_codegen/zero_padding/ObligationZP.d.ts +13 -6
  77. package/dist/idl_codegen/zero_padding/ObligationZP.d.ts.map +1 -1
  78. package/dist/idl_codegen/zero_padding/ObligationZP.js +25 -11
  79. package/dist/idl_codegen/zero_padding/ObligationZP.js.map +1 -1
  80. package/dist/utils/instruction.d.ts +1 -1
  81. package/dist/utils/instruction.d.ts.map +1 -1
  82. package/dist/utils/instruction.js +5 -1
  83. package/dist/utils/instruction.js.map +1 -1
  84. package/dist/utils/managerTypes.js +2 -2
  85. package/dist/utils/managerTypes.js.map +1 -1
  86. package/package.json +1 -1
  87. package/src/classes/action.ts +26 -1
  88. package/src/classes/fraction.ts +16 -12
  89. package/src/classes/manager.ts +13 -0
  90. package/src/classes/obligation.ts +77 -1
  91. package/src/classes/obligationOrder.ts +514 -0
  92. package/src/classes/reserve.ts +257 -336
  93. package/src/classes/types.ts +15 -1
  94. package/src/classes/utils.ts +21 -0
  95. package/src/classes/vault.ts +1 -1
  96. package/src/client_kamino_manager.ts +6 -6
  97. package/src/idl.json +244 -23
  98. package/src/idl_codegen/accounts/LendingMarket.ts +23 -7
  99. package/src/idl_codegen/accounts/Obligation.ts +50 -13
  100. package/src/idl_codegen/errors/custom.ts +68 -6
  101. package/src/idl_codegen/instructions/index.ts +5 -0
  102. package/src/idl_codegen/instructions/setObligationOrder.ts +45 -0
  103. package/src/idl_codegen/types/ObligationOrder.ts +347 -0
  104. package/src/idl_codegen/types/ReserveConfig.ts +31 -16
  105. package/src/idl_codegen/types/UpdateConfigMode.ts +69 -39
  106. package/src/idl_codegen/types/UpdateLendingMarketMode.ts +30 -0
  107. package/src/idl_codegen/types/index.ts +15 -6
  108. package/src/idl_codegen/zero_padding/ObligationZP.ts +37 -15
  109. package/src/utils/instruction.ts +5 -1
  110. package/src/utils/managerTypes.ts +2 -2
@@ -0,0 +1,514 @@
1
+ /* eslint-disable max-classes-per-file */
2
+ import { Fraction } from './fraction';
3
+ import { ObligationOrder } from '../idl_codegen/types';
4
+ import { getSingleElement, orThrow, roundNearest } from './utils';
5
+ import Decimal from 'decimal.js';
6
+ import BN from 'bn.js';
7
+ import { KaminoObligation, Position } from './obligation';
8
+ import { TokenAmount } from './types';
9
+ import { ONE_HUNDRED_PCT_IN_BPS } from '../utils';
10
+
11
+ // Polymorphic parts of an order:
12
+
13
+ /**
14
+ * A condition "activating" an order.
15
+ *
16
+ * When a {@link KaminoObligationOrder.condition} is met by an obligation, the corresponding
17
+ * {@link KaminoObligationOrder.opportunity} becomes available to liquidators.
18
+ */
19
+ interface OrderCondition {
20
+ /**
21
+ * An abstract parameter of the condition, meaningful in context of the condition's type.
22
+ */
23
+ threshold(): Decimal;
24
+
25
+ /**
26
+ * Returns a potential hit on this condition.
27
+ */
28
+ evaluate(obligation: KaminoObligation): ConditionHit | null;
29
+ }
30
+
31
+ /**
32
+ * An "opportunity" of an order - i.e. a type and size of a trade made available by the order (provided that its
33
+ * {@link KaminoObligationOrder.condition} is met).
34
+ */
35
+ export interface OrderOpportunity {
36
+ /**
37
+ * An abstract parameter of the condition, meaningful in context of the condition's type.
38
+ */
39
+ parameter(): Decimal;
40
+
41
+ /**
42
+ * Returns the highest-valued {@link TokenAmount} that can be repaid (among the given borrows) using this opportunity.
43
+ */
44
+ getMaxRepay(borrows: Array<Position>): TokenAmount;
45
+ }
46
+
47
+ // All condition types:
48
+
49
+ /**
50
+ * A condition met when obligation's overall "User LTV" is strictly higher than the given threshold.
51
+ */
52
+ export class UserLtvAbove implements OrderCondition {
53
+ private readonly minUserLtvExclusive: Decimal;
54
+
55
+ constructor(minUserLtvExclusive: Decimal.Value) {
56
+ this.minUserLtvExclusive = new Decimal(minUserLtvExclusive);
57
+ }
58
+
59
+ threshold(): Decimal {
60
+ return this.minUserLtvExclusive;
61
+ }
62
+
63
+ evaluate(obligation: KaminoObligation): ConditionHit | null {
64
+ // Note: below we deliberately use the LTV-related methods of `KaminoObligation` (instead of the precomputed fields
65
+ // of the `ObligationStats`), since we care about using the same LTV computation as the KLend smart contract.
66
+ // Please see their docs for details.
67
+ return evaluateStopLoss(obligation.loanToValue(), this.minUserLtvExclusive, obligation.liquidationLtv());
68
+ }
69
+ }
70
+
71
+ /**
72
+ * A condition met when obligation's overall "User LTV" is strictly lower than the given threshold.
73
+ */
74
+ export class UserLtvBelow implements OrderCondition {
75
+ private readonly maxUserLtvExclusive: Decimal;
76
+
77
+ constructor(maxUserLtvExclusive: Decimal.Value) {
78
+ this.maxUserLtvExclusive = new Decimal(maxUserLtvExclusive);
79
+ }
80
+
81
+ threshold(): Decimal {
82
+ return this.maxUserLtvExclusive;
83
+ }
84
+
85
+ evaluate(obligation: KaminoObligation): ConditionHit | null {
86
+ // Note: below we deliberately use the `KaminoObligation.loanToValue()` method (instead of the precomputed field
87
+ // `ObligationStats.loanToValue`), since we care about using the same LTV computation as the KLend smart contract.
88
+ // Please see the method's docs for details.
89
+ return evaluateTakeProfit(obligation.loanToValue(), this.maxUserLtvExclusive);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * A condition met when the obligation's collateral token price (expressed in the debt token) is strictly higher than
95
+ * the given threshold.
96
+ *
97
+ * May only be applied to single-collateral, single-debt obligations.
98
+ */
99
+ export class DebtCollPriceRatioAbove implements OrderCondition {
100
+ private readonly minDebtCollPriceRatioExclusive: Decimal;
101
+
102
+ constructor(minDebtCollPriceRatioExclusive: Decimal.Value) {
103
+ this.minDebtCollPriceRatioExclusive = new Decimal(minDebtCollPriceRatioExclusive);
104
+ }
105
+
106
+ threshold(): Decimal {
107
+ return this.minDebtCollPriceRatioExclusive;
108
+ }
109
+
110
+ evaluate(obligation: KaminoObligation): ConditionHit | null {
111
+ const priceRatio = calculateDebtCollPriceRatio(obligation);
112
+ return evaluateStopLoss(
113
+ priceRatio,
114
+ this.minDebtCollPriceRatioExclusive,
115
+ // For single-debt-single-coll obligations, the price ratio is directly proportional
116
+ // to LTV - so we can calculate the "liquidation price ratio" simply by scaling the
117
+ // current value by the ratio of unhealthy/current LTV:
118
+ priceRatio.mul(obligation.refreshedStats.liquidationLtv).div(obligation.refreshedStats.loanToValue)
119
+ );
120
+ }
121
+ }
122
+
123
+ /**
124
+ * A condition met when the obligation's collateral token price (expressed in the debt token) is strictly higher than
125
+ * the given threshold.
126
+ *
127
+ * May only be applied to single-collateral, single-debt obligations.
128
+ */
129
+ export class DebtCollPriceRatioBelow implements OrderCondition {
130
+ private readonly maxDebtCollPriceRatioExclusive: Decimal;
131
+
132
+ constructor(maxDebtCollPriceRatioExclusive: Decimal.Value) {
133
+ this.maxDebtCollPriceRatioExclusive = new Decimal(maxDebtCollPriceRatioExclusive);
134
+ }
135
+
136
+ threshold(): Decimal {
137
+ return this.maxDebtCollPriceRatioExclusive;
138
+ }
139
+
140
+ evaluate(obligation: KaminoObligation): ConditionHit | null {
141
+ return evaluateTakeProfit(calculateDebtCollPriceRatio(obligation), this.maxDebtCollPriceRatioExclusive);
142
+ }
143
+ }
144
+
145
+ // All opportunity types:
146
+
147
+ /**
148
+ * An opportunity for repaying the given amount of the obligation's debt token.
149
+ *
150
+ * May only be applied to single-debt obligations.
151
+ */
152
+ export class DeleverageDebtAmount implements OrderOpportunity {
153
+ private readonly amount: Decimal;
154
+
155
+ constructor(amount: Decimal.Value) {
156
+ this.amount = new Decimal(amount);
157
+ }
158
+
159
+ parameter(): Decimal {
160
+ return this.amount;
161
+ }
162
+
163
+ getMaxRepay(borrows: Array<Position>): TokenAmount {
164
+ const singleBorrow = getSingleElement(borrows, 'Opportunity type requires a single borrow');
165
+ return {
166
+ mint: singleBorrow.mintAddress,
167
+ amount: Decimal.min(singleBorrow.amount, this.amount),
168
+ };
169
+ }
170
+ }
171
+
172
+ /**
173
+ * An opportunity for repaying all debt(s) of an obligation.
174
+ */
175
+ export class DeleverageAllDebt implements OrderOpportunity {
176
+ /**
177
+ * The only legal value for the {@link parameter()} of this opportunity type.
178
+ */
179
+ static FRACTION_MAX = new Fraction(Fraction.MAX_F_BN).toDecimal();
180
+
181
+ constructor(fixed_parameter?: Decimal.Value) {
182
+ if (fixed_parameter !== undefined && !new Decimal(fixed_parameter).eq(DeleverageAllDebt.FRACTION_MAX)) {
183
+ throw new Error(
184
+ `invalid DeleverageAllDebt parameter: ${fixed_parameter} (if given, must be FRACTION_MAX = ${DeleverageAllDebt.FRACTION_MAX})`
185
+ );
186
+ }
187
+ }
188
+
189
+ parameter(): Decimal {
190
+ return DeleverageAllDebt.FRACTION_MAX;
191
+ }
192
+
193
+ getMaxRepay(borrows: Array<Position>): TokenAmount {
194
+ if (borrows.length === 0) {
195
+ throw new Error(`Opportunity type not valid on obligation with no borrows`);
196
+ }
197
+ const highestValueBorrow = borrows
198
+ .sort((left, right) => left.marketValueRefreshed.comparedTo(right.marketValueRefreshed))
199
+ .at(-1)!;
200
+ return {
201
+ mint: highestValueBorrow.mintAddress,
202
+ amount: highestValueBorrow.amount,
203
+ };
204
+ }
205
+ }
206
+
207
+ // A couple of internal interfaces:
208
+
209
+ interface OrderConditionConstructor {
210
+ new (threshold: Decimal): OrderCondition;
211
+ }
212
+
213
+ interface OrderOpportunityConstructor {
214
+ new (parameter: Decimal): OrderOpportunity;
215
+ }
216
+
217
+ // Internal type ID mappings:
218
+
219
+ const CONDITION_TO_TYPE_ID = new Map<OrderConditionConstructor, number>([
220
+ // Note: the special value of 0 (Never) is represented in the SDK by `KaminoObligationOrder === null`.
221
+ [UserLtvAbove, 1],
222
+ [UserLtvBelow, 2],
223
+ [DebtCollPriceRatioAbove, 3],
224
+ [DebtCollPriceRatioBelow, 4],
225
+ ]);
226
+
227
+ const OPPORTUNITY_TO_TYPE_ID = new Map<OrderOpportunityConstructor, number>([
228
+ [DeleverageDebtAmount, 0],
229
+ [DeleverageAllDebt, 1],
230
+ ]);
231
+
232
+ const TYPE_ID_TO_CONDITION = new Map([...CONDITION_TO_TYPE_ID].map(([type, id]) => [id, type]));
233
+ const TYPE_ID_TO_OPPORTUNITY = new Map([...OPPORTUNITY_TO_TYPE_ID].map(([type, id]) => [id, type]));
234
+
235
+ // Core types:
236
+
237
+ /**
238
+ * A business wrapper around the on-chain {@link ObligationOrder} account data.
239
+ */
240
+ export class KaminoObligationOrder {
241
+ /**
242
+ * An on-chain data representing a `null` order.
243
+ */
244
+ static NULL_STATE = new ObligationOrder({
245
+ conditionType: 0,
246
+ conditionThresholdSf: new BN(0),
247
+ opportunityType: 0,
248
+ opportunityParameterSf: new BN(0),
249
+ minExecutionBonusBps: 0,
250
+ maxExecutionBonusBps: 0,
251
+ padding1: Array(10).fill(0),
252
+ padding2: Array(5).fill(new BN(0)),
253
+ });
254
+
255
+ /**
256
+ * The order's condition.
257
+ */
258
+ readonly condition: OrderCondition;
259
+
260
+ /**
261
+ * The order's opportunity.
262
+ */
263
+ readonly opportunity: OrderOpportunity;
264
+
265
+ /**
266
+ * The minimum bonus rate (e.g. `0.01` meaning "1%") offered to a liquidator executing this order when its condition
267
+ * threshold has been barely crossed.
268
+ */
269
+ readonly minExecutionBonusRate: Decimal;
270
+
271
+ /**
272
+ * The maximum bonus rate (e.g. `0.04` meaning "4%") offered to a liquidator executing this order when its condition
273
+ * threshold has already been exceeded by a very large margin (to be specific: maximum possible margin - e.g. for
274
+ * LTV-based stop-loss order, that would be when the obligation's LTV is approaching its liquidation LTV).
275
+ */
276
+ readonly maxExecutionBonusRate: Decimal;
277
+
278
+ /**
279
+ * Direct constructor.
280
+ *
281
+ * Please see {@link fromState()} if you are constructing an instance representing existing on-chain data.
282
+ */
283
+ constructor(
284
+ condition: OrderCondition,
285
+ opportunity: OrderOpportunity,
286
+ minExecutionBonusRate: Decimal,
287
+ maxExecutionBonusRate: Decimal = minExecutionBonusRate
288
+ ) {
289
+ this.condition = condition;
290
+ this.opportunity = opportunity;
291
+ this.minExecutionBonusRate = minExecutionBonusRate;
292
+ this.maxExecutionBonusRate = maxExecutionBonusRate;
293
+ }
294
+
295
+ /**
296
+ * Returns the highest-valued {@link AvailableOrderExecution} currently offered by this order.
297
+ *
298
+ * May return `undefined` when the order's condition is not met.
299
+ */
300
+ findMaxAvailableExecution(obligation: KaminoObligation): AvailableOrderExecution | undefined {
301
+ const conditionHit = this.condition.evaluate(obligation);
302
+ if (conditionHit === null) {
303
+ return undefined; // condition not met - cannot execute
304
+ }
305
+ const maxRepay = this.opportunity.getMaxRepay(obligation.getBorrows());
306
+ const repayBorrow = obligation.getBorrowByMint(maxRepay.mint)!;
307
+ const maxRepayValue = tokenAmountToValue(maxRepay, repayBorrow);
308
+ const executionBonusRate = this.calculateExecutionBonusRate(conditionHit, obligation);
309
+ const executionBonusFactor = new Decimal(1).add(executionBonusRate);
310
+ const maxWithdrawValue = maxRepayValue.mul(executionBonusFactor);
311
+ const [actualWithdrawValue, withdrawDeposit] = obligation
312
+ .getDeposits()
313
+ .map((deposit): [Decimal, Position] => {
314
+ const availableWithdrawValue = Decimal.min(deposit.marketValueRefreshed, maxWithdrawValue);
315
+ return [availableWithdrawValue, deposit];
316
+ })
317
+ .sort(([leftValue, leftDeposit], [rightValue, rightDeposit]) => {
318
+ const valueComparison = leftValue.comparedTo(rightValue);
319
+ if (valueComparison !== 0) {
320
+ return valueComparison;
321
+ }
322
+ // Just for deterministic selection in case of multiple equally-good deposits: pick the one with lower mint pubkey (mostly for test stability purposes)
323
+ return leftDeposit.mintAddress.toBase58().localeCompare(rightDeposit.mintAddress.toBase58());
324
+ })
325
+ .at(-1)!;
326
+ const actualRepayValue = actualWithdrawValue.div(executionBonusFactor);
327
+ return {
328
+ repay: valueToTokenAmount(actualRepayValue, repayBorrow),
329
+ withdraw: valueToTokenAmount(actualWithdrawValue, withdrawDeposit),
330
+ bonusRate: executionBonusRate,
331
+ };
332
+ }
333
+
334
+ /**
335
+ * Constructs an instance based on the given on-chain data.
336
+ *
337
+ * Returns `null` if the input represents just an empty slot in the orders' array.
338
+ */
339
+ static fromState(state: ObligationOrder): KaminoObligationOrder | null {
340
+ if (state.conditionType === KaminoObligationOrder.NULL_STATE.conditionType) {
341
+ return null; // In practice an entire null order is zeroed, but technically the "condition == never" is enough to consider the order "not active"
342
+ }
343
+ const conditionConstructor =
344
+ TYPE_ID_TO_CONDITION.get(state.conditionType) ?? orThrow(`Unknown condition type ${state.conditionType}`);
345
+ const condition = new conditionConstructor(new Fraction(state.conditionThresholdSf).toDecimal());
346
+ const opportunityConstructor =
347
+ TYPE_ID_TO_OPPORTUNITY.get(state.opportunityType) ?? orThrow(`Unknown opportunity type ${state.opportunityType}`);
348
+ const opportunity = new opportunityConstructor(new Fraction(state.opportunityParameterSf).toDecimal());
349
+ const minExecutionBonusRate = Fraction.fromBps(state.minExecutionBonusBps).toDecimal();
350
+ const maxExecutionBonusRate = Fraction.fromBps(state.maxExecutionBonusBps).toDecimal();
351
+ return new KaminoObligationOrder(condition, opportunity, minExecutionBonusRate, maxExecutionBonusRate);
352
+ }
353
+
354
+ /**
355
+ * Returns the on-chain state represented by this instance.
356
+ *
357
+ * See {@link NULL_STATE} for the state of a `null` order.
358
+ */
359
+ toState(): ObligationOrder {
360
+ return new ObligationOrder({
361
+ ...KaminoObligationOrder.NULL_STATE.toEncodable(),
362
+ conditionType:
363
+ CONDITION_TO_TYPE_ID.get(Object.getPrototypeOf(this.condition).constructor) ??
364
+ orThrow(`Unknown condition ${this.condition.constructor}`),
365
+ conditionThresholdSf: Fraction.fromDecimal(this.condition.threshold()).getValue(),
366
+ opportunityType:
367
+ OPPORTUNITY_TO_TYPE_ID.get(Object.getPrototypeOf(this.opportunity).constructor) ??
368
+ orThrow(`Unknown opportunity ${this.opportunity.constructor}`),
369
+ opportunityParameterSf: Fraction.fromDecimal(this.opportunity.parameter()).getValue(),
370
+ minExecutionBonusBps: roundNearest(this.minExecutionBonusRate.mul(ONE_HUNDRED_PCT_IN_BPS)).toNumber(),
371
+ maxExecutionBonusBps: roundNearest(this.maxExecutionBonusRate.mul(ONE_HUNDRED_PCT_IN_BPS)).toNumber(),
372
+ });
373
+ }
374
+
375
+ /**
376
+ * Calculates the given order's actual execution bonus rate.
377
+ *
378
+ * The min-max bonus range is configured by the user on a per-order basis, and the actual value is interpolated based
379
+ * on the given obligation's state at the moment of order execution.
380
+ * In short: the minimum bonus applies if the order is executed precisely at the point when the condition is met.
381
+ * Then, as the distance from condition's threshold grows, the bonus approaches the configured maximum.
382
+ *
383
+ * On top of that, similar to regular liquidation, the bonus cannot exceed the ceiled limit of `1.0 - user_no_bf_ltv`
384
+ * (which ensures that order execution improves LTV).
385
+ */
386
+ private calculateExecutionBonusRate(conditionHit: ConditionHit, obligation: KaminoObligation): Decimal {
387
+ const interpolatedBonusRate = interpolateBonusRate(
388
+ conditionHit.normalizedDistanceFromThreshold,
389
+ this.minExecutionBonusRate,
390
+ this.maxExecutionBonusRate
391
+ );
392
+ // Note: instead of the existing `obligation.noBfLoanToValue()`, here we use a formula consistent with the
393
+ // `obligation.refreshedStats.loanToValue` (that we use in other parts of obligation orders' SDK logic):
394
+ const userNoBfLtv = obligation.refreshedStats.userTotalBorrow.div(
395
+ obligation.refreshedStats.userTotalCollateralDeposit
396
+ );
397
+ // In order to ensure that LTV improves on order execution, we apply the same heuristic formula as for the regular
398
+ // liquidations:
399
+ const diffToBadDebt = new Decimal(1).sub(userNoBfLtv);
400
+ return Decimal.min(interpolatedBonusRate, diffToBadDebt);
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Numeric details on why an order's condition was met.
406
+ */
407
+ export type ConditionHit = {
408
+ /**
409
+ * The distance between the current value (e.g. "current LTV") and the condition's
410
+ * threshold (e.g. "when LTV > 70%"), normalized with regard to the most extreme value
411
+ * (e.g. "liquidation LTV = 90%").
412
+ *
413
+ * Following the above example:
414
+ * - when current LTV = 70% (i.e. at condition threshold), this normalized distance is `0`,
415
+ * - when current LTV = 90% (i.e. at liquidation point), this normalized distance is `1`,
416
+ * - when current LTV = 82% (i.e. some number in-between), this normalized distance is `0.6`.
417
+ *
418
+ * In other words: this is a `[0; 1]` measure of how hard the condition threshold is crossed.
419
+ */
420
+ normalizedDistanceFromThreshold: Decimal;
421
+ };
422
+
423
+ /**
424
+ * A potential exchange of tokens resulting from order execution.
425
+ */
426
+ export type AvailableOrderExecution = {
427
+ /**
428
+ * How much (and of what token) to repay.
429
+ */
430
+ repay: TokenAmount;
431
+
432
+ /**
433
+ * How much (and of what other token) can be withdrawn in exchange.
434
+ *
435
+ * Note: This amount already *includes* the execution bonus (see `bonusRate` below), but does *not* take the protocol
436
+ * fee into account (see `ReserveConfig.protocolOrderExecutionFeePct`).
437
+ */
438
+ withdraw: TokenAmount;
439
+
440
+ /**
441
+ * The bonus rate (e.g. `0.01` meaning 1%), computed based on the configured execution bonus range and the safety
442
+ * ceiling.
443
+ *
444
+ * Note: this bonus is still subject to the protocol fee.
445
+ */
446
+ bonusRate: Decimal;
447
+ };
448
+
449
+ // Internal calculation functions:
450
+
451
+ function tokenAmountToValue(tokenAmount: TokenAmount, position: Position): Decimal {
452
+ if (tokenAmount.mint !== position.mintAddress) {
453
+ throw new Error(`Value of token amount ${tokenAmount} cannot be computed using data from ${position}`);
454
+ }
455
+ return tokenAmount.amount.mul(position.marketValueRefreshed).div(position.amount);
456
+ }
457
+
458
+ function valueToTokenAmount(value: Decimal, position: Position): TokenAmount {
459
+ const fractionalAmount = value.mul(position.amount).div(position.marketValueRefreshed);
460
+ return {
461
+ amount: roundNearest(fractionalAmount),
462
+ mint: position.mintAddress,
463
+ };
464
+ }
465
+
466
+ function evaluateStopLoss(
467
+ current_value: Decimal,
468
+ conditionThreshold: Decimal,
469
+ liquidationThreshold: Decimal
470
+ ): ConditionHit | null {
471
+ if (current_value.lte(conditionThreshold)) {
472
+ return null; // SL not hit
473
+ }
474
+ let normalizedDistanceFromThreshold;
475
+ if (conditionThreshold.gt(liquidationThreshold)) {
476
+ // A theoretically-impossible case (the user may of course set his order's condition
477
+ // threshold that high, but then the current value is above liquidation threshold, so
478
+ // liquidation logic should kick in and never reach this function). Anyway, let's
479
+ // interpret it as maximum distance from threshold:
480
+ normalizedDistanceFromThreshold = new Decimal(1);
481
+ } else {
482
+ // By now we know they are both > 0:
483
+ const currentDistance = current_value.sub(conditionThreshold);
484
+ const maximumDistance = liquidationThreshold.sub(conditionThreshold);
485
+ normalizedDistanceFromThreshold = currentDistance.div(maximumDistance);
486
+ }
487
+ return { normalizedDistanceFromThreshold };
488
+ }
489
+
490
+ function evaluateTakeProfit(currentValue: Decimal, conditionThreshold: Decimal): ConditionHit | null {
491
+ if (currentValue.gte(conditionThreshold)) {
492
+ return null; // TP not hit
493
+ }
494
+ const distanceTowards0 = conditionThreshold.sub(currentValue); // by now we know it is > 0
495
+ return { normalizedDistanceFromThreshold: distanceTowards0.div(conditionThreshold) };
496
+ }
497
+
498
+ function calculateDebtCollPriceRatio(obligation: KaminoObligation): Decimal {
499
+ const singleBorrow = getSingleElement(obligation.getBorrows(), 'Condition type requires a single borrow');
500
+ const singleDeposit = getSingleElement(obligation.getDeposits(), 'Condition type requires a single deposit');
501
+ return calculateTokenPrice(singleBorrow).div(calculateTokenPrice(singleDeposit));
502
+ }
503
+
504
+ function calculateTokenPrice(position: Position): Decimal {
505
+ return position.marketValueRefreshed.mul(position.mintFactor).div(position.amount);
506
+ }
507
+
508
+ function interpolateBonusRate(
509
+ normalizedDistanceFromThreshold: Decimal,
510
+ minBonusRate: Decimal,
511
+ maxBonusRate: Decimal
512
+ ): Decimal {
513
+ return minBonusRate.add(normalizedDistanceFromThreshold.mul(maxBonusRate.sub(minBonusRate)));
514
+ }