@kamino-finance/klend-sdk 5.12.8 → 5.13.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.
- package/dist/classes/action.d.ts +2 -2
- package/dist/classes/action.d.ts.map +1 -1
- package/dist/classes/action.js +5 -3
- package/dist/classes/action.js.map +1 -1
- package/dist/classes/manager.d.ts.map +1 -1
- package/dist/classes/manager.js.map +1 -1
- package/dist/classes/market.d.ts +10 -0
- package/dist/classes/market.d.ts.map +1 -1
- package/dist/classes/market.js +17 -10
- package/dist/classes/market.js.map +1 -1
- package/dist/classes/obligation.d.ts +2 -2
- package/dist/classes/obligation.js +1 -1
- package/dist/classes/obligationOrder.d.ts +30 -7
- package/dist/classes/obligationOrder.d.ts.map +1 -1
- package/dist/classes/obligationOrder.js +47 -12
- package/dist/classes/obligationOrder.js.map +1 -1
- package/dist/classes/utils.d.ts +0 -1
- package/dist/classes/utils.d.ts.map +1 -1
- package/dist/classes/utils.js +0 -7
- package/dist/classes/utils.js.map +1 -1
- package/dist/lib.d.ts +1 -0
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +1 -0
- package/dist/lib.js.map +1 -1
- package/dist/obligation_orders/common.d.ts +62 -0
- package/dist/obligation_orders/common.d.ts.map +1 -0
- package/dist/obligation_orders/common.js +20 -0
- package/dist/obligation_orders/common.js.map +1 -0
- package/dist/obligation_orders/index.d.ts +4 -0
- package/dist/obligation_orders/index.d.ts.map +1 -0
- package/dist/obligation_orders/index.js +20 -0
- package/dist/obligation_orders/index.js.map +1 -0
- package/dist/obligation_orders/internal.d.ts +6 -0
- package/dist/obligation_orders/internal.d.ts.map +1 -0
- package/dist/obligation_orders/internal.js +72 -0
- package/dist/obligation_orders/internal.js.map +1 -0
- package/dist/obligation_orders/ltv_based.d.ts +51 -0
- package/dist/obligation_orders/ltv_based.d.ts.map +1 -0
- package/dist/obligation_orders/ltv_based.js +107 -0
- package/dist/obligation_orders/ltv_based.js.map +1 -0
- package/dist/obligation_orders/price_based.d.ts +81 -0
- package/dist/obligation_orders/price_based.d.ts.map +1 -0
- package/dist/obligation_orders/price_based.js +167 -0
- package/dist/obligation_orders/price_based.js.map +1 -0
- package/dist/utils/validations.d.ts +5 -0
- package/dist/utils/validations.d.ts.map +1 -0
- package/dist/utils/validations.js +36 -0
- package/dist/utils/validations.js.map +1 -0
- package/package.json +1 -1
- package/src/classes/action.ts +6 -4
- package/src/classes/manager.ts +1 -4
- package/src/classes/market.ts +21 -11
- package/src/classes/obligation.ts +2 -2
- package/src/classes/obligationOrder.ts +57 -20
- package/src/classes/utils.ts +0 -7
- package/src/lib.ts +1 -0
- package/src/obligation_orders/common.ts +70 -0
- package/src/obligation_orders/index.ts +3 -0
- package/src/obligation_orders/internal.ts +92 -0
- package/src/obligation_orders/ltv_based.ts +143 -0
- package/src/obligation_orders/price_based.ts +256 -0
- package/src/utils/validations.ts +31 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/* eslint-disable max-classes-per-file */
|
|
2
2
|
import { Fraction } from './fraction';
|
|
3
3
|
import { ObligationOrder } from '../idl_codegen/types';
|
|
4
|
-
import {
|
|
4
|
+
import { orThrow, roundNearest } from './utils';
|
|
5
5
|
import Decimal from 'decimal.js';
|
|
6
6
|
import BN from 'bn.js';
|
|
7
7
|
import { KaminoObligation, Position } from './obligation';
|
|
8
8
|
import { TokenAmount } from './types';
|
|
9
9
|
import { ONE_HUNDRED_PCT_IN_BPS } from '../utils';
|
|
10
|
+
import { getSingleElement } from '../utils/validations';
|
|
10
11
|
|
|
11
12
|
// Polymorphic parts of an order:
|
|
12
13
|
|
|
@@ -16,7 +17,7 @@ import { ONE_HUNDRED_PCT_IN_BPS } from '../utils';
|
|
|
16
17
|
* When a {@link KaminoObligationOrder.condition} is met by an obligation, the corresponding
|
|
17
18
|
* {@link KaminoObligationOrder.opportunity} becomes available to liquidators.
|
|
18
19
|
*/
|
|
19
|
-
interface OrderCondition {
|
|
20
|
+
export interface OrderCondition {
|
|
20
21
|
/**
|
|
21
22
|
* An abstract parameter of the condition, meaningful in context of the condition's type.
|
|
22
23
|
*/
|
|
@@ -50,7 +51,7 @@ export interface OrderOpportunity {
|
|
|
50
51
|
* A condition met when obligation's overall "User LTV" is strictly higher than the given threshold.
|
|
51
52
|
*/
|
|
52
53
|
export class UserLtvAbove implements OrderCondition {
|
|
53
|
-
|
|
54
|
+
readonly minUserLtvExclusive: Decimal;
|
|
54
55
|
|
|
55
56
|
constructor(minUserLtvExclusive: Decimal.Value) {
|
|
56
57
|
this.minUserLtvExclusive = new Decimal(minUserLtvExclusive);
|
|
@@ -72,7 +73,7 @@ export class UserLtvAbove implements OrderCondition {
|
|
|
72
73
|
* A condition met when obligation's overall "User LTV" is strictly lower than the given threshold.
|
|
73
74
|
*/
|
|
74
75
|
export class UserLtvBelow implements OrderCondition {
|
|
75
|
-
|
|
76
|
+
readonly maxUserLtvExclusive: Decimal;
|
|
76
77
|
|
|
77
78
|
constructor(maxUserLtvExclusive: Decimal.Value) {
|
|
78
79
|
this.maxUserLtvExclusive = new Decimal(maxUserLtvExclusive);
|
|
@@ -97,7 +98,7 @@ export class UserLtvBelow implements OrderCondition {
|
|
|
97
98
|
* May only be applied to single-collateral, single-debt obligations.
|
|
98
99
|
*/
|
|
99
100
|
export class DebtCollPriceRatioAbove implements OrderCondition {
|
|
100
|
-
|
|
101
|
+
readonly minDebtCollPriceRatioExclusive: Decimal;
|
|
101
102
|
|
|
102
103
|
constructor(minDebtCollPriceRatioExclusive: Decimal.Value) {
|
|
103
104
|
this.minDebtCollPriceRatioExclusive = new Decimal(minDebtCollPriceRatioExclusive);
|
|
@@ -114,8 +115,10 @@ export class DebtCollPriceRatioAbove implements OrderCondition {
|
|
|
114
115
|
this.minDebtCollPriceRatioExclusive,
|
|
115
116
|
// For single-debt-single-coll obligations, the price ratio is directly proportional
|
|
116
117
|
// to LTV - so we can calculate the "liquidation price ratio" simply by scaling the
|
|
117
|
-
// current value by the ratio of unhealthy/current
|
|
118
|
-
priceRatio
|
|
118
|
+
// current value by the ratio of unhealthy/current borrow value:
|
|
119
|
+
priceRatio
|
|
120
|
+
.mul(obligation.refreshedStats.borrowLiquidationLimit)
|
|
121
|
+
.div(obligation.refreshedStats.userTotalBorrowBorrowFactorAdjusted)
|
|
119
122
|
);
|
|
120
123
|
}
|
|
121
124
|
}
|
|
@@ -127,7 +130,7 @@ export class DebtCollPriceRatioAbove implements OrderCondition {
|
|
|
127
130
|
* May only be applied to single-collateral, single-debt obligations.
|
|
128
131
|
*/
|
|
129
132
|
export class DebtCollPriceRatioBelow implements OrderCondition {
|
|
130
|
-
|
|
133
|
+
readonly maxDebtCollPriceRatioExclusive: Decimal;
|
|
131
134
|
|
|
132
135
|
constructor(maxDebtCollPriceRatioExclusive: Decimal.Value) {
|
|
133
136
|
this.maxDebtCollPriceRatioExclusive = new Decimal(maxDebtCollPriceRatioExclusive);
|
|
@@ -150,7 +153,7 @@ export class DebtCollPriceRatioBelow implements OrderCondition {
|
|
|
150
153
|
* May only be applied to single-debt obligations.
|
|
151
154
|
*/
|
|
152
155
|
export class DeleverageDebtAmount implements OrderOpportunity {
|
|
153
|
-
|
|
156
|
+
readonly amount: Decimal;
|
|
154
157
|
|
|
155
158
|
constructor(amount: Decimal.Value) {
|
|
156
159
|
this.amount = new Decimal(amount);
|
|
@@ -161,7 +164,7 @@ export class DeleverageDebtAmount implements OrderOpportunity {
|
|
|
161
164
|
}
|
|
162
165
|
|
|
163
166
|
getMaxRepay(borrows: Array<Position>): TokenAmount {
|
|
164
|
-
const singleBorrow = getSingleElement(borrows, '
|
|
167
|
+
const singleBorrow = getSingleElement(borrows, 'borrow');
|
|
165
168
|
return {
|
|
166
169
|
mint: singleBorrow.mintAddress,
|
|
167
170
|
amount: Decimal.min(singleBorrow.amount, this.amount),
|
|
@@ -372,6 +375,15 @@ export class KaminoObligationOrder {
|
|
|
372
375
|
});
|
|
373
376
|
}
|
|
374
377
|
|
|
378
|
+
/**
|
|
379
|
+
* Binds this order to the given slot.
|
|
380
|
+
*
|
|
381
|
+
* This is just a convenience method for easier interaction with {@link KaminoAction#buildSetObligationOrderIxn()}.
|
|
382
|
+
*/
|
|
383
|
+
atIndex(index: number): ObligationOrderAtIndex {
|
|
384
|
+
return new ObligationOrderAtIndex(index, this);
|
|
385
|
+
}
|
|
386
|
+
|
|
375
387
|
/**
|
|
376
388
|
* Calculates the given order's actual execution bonus rate.
|
|
377
389
|
*
|
|
@@ -389,18 +401,43 @@ export class KaminoObligationOrder {
|
|
|
389
401
|
this.minExecutionBonusRate,
|
|
390
402
|
this.maxExecutionBonusRate
|
|
391
403
|
);
|
|
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
404
|
// In order to ensure that LTV improves on order execution, we apply the same heuristic formula as for the regular
|
|
398
|
-
// liquidations
|
|
399
|
-
|
|
405
|
+
// liquidations. Please note that we deliberately use the `obligation.noBfLoanToValue()`, which is consistent with
|
|
406
|
+
// the smart contract's calculation:
|
|
407
|
+
const diffToBadDebt = new Decimal(1).sub(obligation.noBfLoanToValue());
|
|
400
408
|
return Decimal.min(interpolatedBonusRate, diffToBadDebt);
|
|
401
409
|
}
|
|
402
410
|
}
|
|
403
411
|
|
|
412
|
+
/**
|
|
413
|
+
* A single slot within {@link Obligation.orders} (which may contain an order or not).
|
|
414
|
+
*
|
|
415
|
+
* This is used as an argument to {@link KaminoAction.buildSetObligationOrderIxn()} to easily set or cancel an order.
|
|
416
|
+
*/
|
|
417
|
+
export class ObligationOrderAtIndex {
|
|
418
|
+
index: number;
|
|
419
|
+
order: KaminoObligationOrder | null;
|
|
420
|
+
|
|
421
|
+
constructor(index: number, order: KaminoObligationOrder | null) {
|
|
422
|
+
this.index = index;
|
|
423
|
+
this.order = order;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Creates an empty slot representation (suitable for cancelling an order).
|
|
428
|
+
*/
|
|
429
|
+
static empty(index: number): ObligationOrderAtIndex {
|
|
430
|
+
return new ObligationOrderAtIndex(index, null);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Returns the on-chain state of the order (potentially a zeroed account data, if the order is not set).
|
|
435
|
+
*/
|
|
436
|
+
orderState(): ObligationOrder {
|
|
437
|
+
return this.order !== null ? this.order.toState() : KaminoObligationOrder.NULL_STATE;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
404
441
|
/**
|
|
405
442
|
* Numeric details on why an order's condition was met.
|
|
406
443
|
*/
|
|
@@ -449,7 +486,7 @@ export type AvailableOrderExecution = {
|
|
|
449
486
|
// Internal calculation functions:
|
|
450
487
|
|
|
451
488
|
function tokenAmountToValue(tokenAmount: TokenAmount, position: Position): Decimal {
|
|
452
|
-
if (tokenAmount.mint
|
|
489
|
+
if (!tokenAmount.mint.equals(position.mintAddress)) {
|
|
453
490
|
throw new Error(`Value of token amount ${tokenAmount} cannot be computed using data from ${position}`);
|
|
454
491
|
}
|
|
455
492
|
return tokenAmount.amount.mul(position.marketValueRefreshed).div(position.amount);
|
|
@@ -496,8 +533,8 @@ function evaluateTakeProfit(currentValue: Decimal, conditionThreshold: Decimal):
|
|
|
496
533
|
}
|
|
497
534
|
|
|
498
535
|
function calculateDebtCollPriceRatio(obligation: KaminoObligation): Decimal {
|
|
499
|
-
const singleBorrow = getSingleElement(obligation.getBorrows(), '
|
|
500
|
-
const singleDeposit = getSingleElement(obligation.getDeposits(), '
|
|
536
|
+
const singleBorrow = getSingleElement(obligation.getBorrows(), 'borrow');
|
|
537
|
+
const singleDeposit = getSingleElement(obligation.getDeposits(), 'deposit');
|
|
501
538
|
return calculateTokenPrice(singleBorrow).div(calculateTokenPrice(singleDeposit));
|
|
502
539
|
}
|
|
503
540
|
|
package/src/classes/utils.ts
CHANGED
|
@@ -336,13 +336,6 @@ export function orThrow(message: string): never {
|
|
|
336
336
|
throw new Error(message);
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
-
export function getSingleElement<T>(array: T[], message: string): T {
|
|
340
|
-
if (array.length !== 1) {
|
|
341
|
-
throw new Error(`${message} (found ${array.length})`);
|
|
342
|
-
}
|
|
343
|
-
return array[0];
|
|
344
|
-
}
|
|
345
|
-
|
|
346
339
|
/**
|
|
347
340
|
* Returns an integer {@link Decimal} nearest to the given one.
|
|
348
341
|
*
|
package/src/lib.ts
CHANGED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { KaminoMarket, KaminoObligation } from '../classes';
|
|
2
|
+
import Decimal from 'decimal.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A basic context needed to interpret orders.
|
|
6
|
+
*/
|
|
7
|
+
export type OrderContext = {
|
|
8
|
+
kaminoMarket: KaminoMarket;
|
|
9
|
+
kaminoObligation: KaminoObligation;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A type of order for obligations that follow the "one optional stop-loss and one optional take-profit" convention.
|
|
14
|
+
*/
|
|
15
|
+
export enum OrderType {
|
|
16
|
+
StopLoss = 'StopLoss',
|
|
17
|
+
TakeProfit = 'TakeProfit',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A discriminator enum for {@link OrderAction};
|
|
22
|
+
*/
|
|
23
|
+
export enum OrderActionType {
|
|
24
|
+
FullRepay = 'FullRepay',
|
|
25
|
+
PartialRepay = 'PartialRepay',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* One of possible actions to take on a price-based order.
|
|
30
|
+
*/
|
|
31
|
+
export type OrderAction = FullRepay | PartialRepay;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* An action repaying entire obligation debt.
|
|
35
|
+
*/
|
|
36
|
+
export type FullRepay = {
|
|
37
|
+
type: OrderActionType.FullRepay;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* An action repaying the given amount of the debt.
|
|
42
|
+
*/
|
|
43
|
+
export type PartialRepay = {
|
|
44
|
+
type: OrderActionType.PartialRepay;
|
|
45
|
+
repayDebtAmountLamports: Decimal;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A high-level specification of an order.
|
|
50
|
+
* The trigger type `T` depends on the order's flavour (e.g. price-based or LTV-based).
|
|
51
|
+
*/
|
|
52
|
+
export type OrderSpecification<T> = {
|
|
53
|
+
/**
|
|
54
|
+
* The condition that makes the {@link action} available to be executed.
|
|
55
|
+
*/
|
|
56
|
+
trigger: T;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The action that may be executed.
|
|
60
|
+
*/
|
|
61
|
+
action: OrderAction;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The minimum and maximum bonus for an executor of the action, in bps.
|
|
65
|
+
*
|
|
66
|
+
* The minimum is paid when the order condition is "barely met", and then the bonus grows towards maximum as the
|
|
67
|
+
* condition gets exceeded more and more.
|
|
68
|
+
*/
|
|
69
|
+
executionBonusBpsRange: [number, number];
|
|
70
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import Decimal from 'decimal.js';
|
|
2
|
+
import {
|
|
3
|
+
DeleverageAllDebt,
|
|
4
|
+
DeleverageDebtAmount,
|
|
5
|
+
KaminoObligationOrder,
|
|
6
|
+
OrderCondition,
|
|
7
|
+
OrderOpportunity,
|
|
8
|
+
} from '../classes/obligationOrder';
|
|
9
|
+
import { checkThat, getSingleElement } from '../utils/validations';
|
|
10
|
+
import { ONE_HUNDRED_PCT_IN_BPS } from '../utils';
|
|
11
|
+
import { OrderAction, OrderActionType, OrderContext, OrderSpecification, OrderType } from './common';
|
|
12
|
+
|
|
13
|
+
// These methods are exported, buy only used internally within the obligation orders utils:
|
|
14
|
+
|
|
15
|
+
export function toOrderIndex(orderType: OrderType): number {
|
|
16
|
+
switch (orderType) {
|
|
17
|
+
case OrderType.StopLoss:
|
|
18
|
+
return 0;
|
|
19
|
+
case OrderType.TakeProfit:
|
|
20
|
+
return 1;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createConditionBasedOrder<C>(
|
|
25
|
+
context: OrderContext,
|
|
26
|
+
condition: OrderCondition,
|
|
27
|
+
specification: OrderSpecification<C>
|
|
28
|
+
): KaminoObligationOrder {
|
|
29
|
+
checkThat(condition.evaluate(context.kaminoObligation) === null, `cannot create an immediately-triggered order`);
|
|
30
|
+
const opportunity = toOrderOpportunity(context, specification.action);
|
|
31
|
+
const [minExecutionBonusRate, maxExecutionBonusRate] = toExecutionBonusRates(specification.executionBonusBpsRange);
|
|
32
|
+
return new KaminoObligationOrder(condition, opportunity, minExecutionBonusRate, maxExecutionBonusRate);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function readTriggerBasedOrder<T>(kaminoOrder: KaminoObligationOrder, trigger: T): OrderSpecification<T> {
|
|
36
|
+
return {
|
|
37
|
+
trigger,
|
|
38
|
+
action: toAction(kaminoOrder.opportunity),
|
|
39
|
+
executionBonusBpsRange: toExecutionBonusBps(kaminoOrder.minExecutionBonusRate, kaminoOrder.maxExecutionBonusRate),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Only internals below:
|
|
44
|
+
|
|
45
|
+
function toOrderOpportunity(context: OrderContext, action: OrderAction): OrderOpportunity {
|
|
46
|
+
switch (action.type) {
|
|
47
|
+
case OrderActionType.FullRepay:
|
|
48
|
+
return new DeleverageAllDebt();
|
|
49
|
+
case OrderActionType.PartialRepay:
|
|
50
|
+
const { repayDebtAmountLamports } = action;
|
|
51
|
+
checkThat(repayDebtAmountLamports.gt(0), `repay amount must be positive; got ${repayDebtAmountLamports}`);
|
|
52
|
+
const availableDebtAmountLamports = getSingleElement(context.kaminoObligation.getBorrows(), 'borrow').amount;
|
|
53
|
+
checkThat(
|
|
54
|
+
repayDebtAmountLamports.lte(availableDebtAmountLamports),
|
|
55
|
+
`partial repay amount ${repayDebtAmountLamports} cannot exceed the borrowed amount ${availableDebtAmountLamports}`
|
|
56
|
+
);
|
|
57
|
+
return new DeleverageDebtAmount(action.repayDebtAmountLamports);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function toExecutionBonusRates(executionBonusBpsRange: [number, number]): [Decimal, Decimal] {
|
|
62
|
+
const [minExecutionBonusRate, maxExecutionBonusRate] = executionBonusBpsRange.map((bps) =>
|
|
63
|
+
new Decimal(bps).div(ONE_HUNDRED_PCT_IN_BPS)
|
|
64
|
+
);
|
|
65
|
+
checkThat(minExecutionBonusRate.gte(0), `execution bonus rate cannot be negative: ${minExecutionBonusRate}`);
|
|
66
|
+
checkThat(
|
|
67
|
+
maxExecutionBonusRate.gte(minExecutionBonusRate),
|
|
68
|
+
`max execution bonus rate ${maxExecutionBonusRate} cannot be lower than min ${minExecutionBonusRate}`
|
|
69
|
+
);
|
|
70
|
+
return [minExecutionBonusRate, maxExecutionBonusRate];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function toAction(opportunity: OrderOpportunity): OrderAction {
|
|
74
|
+
if (opportunity instanceof DeleverageAllDebt) {
|
|
75
|
+
return {
|
|
76
|
+
type: OrderActionType.FullRepay,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (opportunity instanceof DeleverageDebtAmount) {
|
|
80
|
+
return {
|
|
81
|
+
type: OrderActionType.PartialRepay,
|
|
82
|
+
repayDebtAmountLamports: opportunity.amount,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
throw new Error(`incompatible on-chain opportunity ${opportunity.constructor.name}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function toExecutionBonusBps(minExecutionBonusRate: Decimal, maxExecutionBonusRate: Decimal): [number, number] {
|
|
89
|
+
return [minExecutionBonusRate, maxExecutionBonusRate].map((rate) =>
|
|
90
|
+
new Decimal(rate).mul(ONE_HUNDRED_PCT_IN_BPS).toNumber()
|
|
91
|
+
) as [number, number];
|
|
92
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import Decimal from 'decimal.js';
|
|
2
|
+
import { ObligationOrderAtIndex, OrderCondition, UserLtvAbove, UserLtvBelow } from '../classes/obligationOrder';
|
|
3
|
+
import { checkThat } from '../utils/validations';
|
|
4
|
+
import { OrderContext, OrderSpecification, OrderType } from './common';
|
|
5
|
+
import { createConditionBasedOrder, readTriggerBasedOrder, toOrderIndex } from './internal';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates an LTV-based {@link ObligationOrderAtIndex} based on the given stop-loss or take-profit specification.
|
|
9
|
+
*
|
|
10
|
+
* The returned object can then be passed directly to {@link KaminoAction.buildSetObligationOrderIxn()} to build an
|
|
11
|
+
* instruction which replaces (or cancels, if the specification is `null`) the given obligation's stop-loss or
|
|
12
|
+
* take-profit order on-chain.
|
|
13
|
+
*
|
|
14
|
+
* The given obligation cannot use 0-LTV collaterals (see {@link checkObligationCompatible()} for rationale).
|
|
15
|
+
*/
|
|
16
|
+
export function createLtvBasedOrder(
|
|
17
|
+
context: OrderContext,
|
|
18
|
+
orderType: OrderType,
|
|
19
|
+
specification: LtvBasedOrderSpecification | null
|
|
20
|
+
): ObligationOrderAtIndex {
|
|
21
|
+
checkObligationCompatible(context);
|
|
22
|
+
const index = toOrderIndex(orderType);
|
|
23
|
+
if (specification === null) {
|
|
24
|
+
return ObligationOrderAtIndex.empty(index);
|
|
25
|
+
}
|
|
26
|
+
const condition = toOrderCondition(orderType, specification.trigger);
|
|
27
|
+
checkThat(
|
|
28
|
+
condition.threshold().gte(MIN_LTV_THRESHOLD) && condition.threshold().lte(MAX_LTV_THRESHOLD),
|
|
29
|
+
`LTV-based trigger outside valid range [${MIN_LTV_THRESHOLD}%; ${MAX_LTV_THRESHOLD}%]: ${condition.threshold()}%`
|
|
30
|
+
);
|
|
31
|
+
return createConditionBasedOrder(context, condition, specification).atIndex(index);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parses an {@link OrderSpecification} from the selected stop-loss or take-profit order of the given obligation.
|
|
36
|
+
*
|
|
37
|
+
* The given obligation cannot use 0-LTV collaterals (see {@link checkObligationCompatible()} for rationale).
|
|
38
|
+
*
|
|
39
|
+
* The selected order is expected to be of matching type (i.e. as if it was created using the
|
|
40
|
+
* {@link createLtvBasedOrder()}).
|
|
41
|
+
*/
|
|
42
|
+
export function readLtvBasedOrder(context: OrderContext, orderType: OrderType): LtvBasedOrderSpecification | null {
|
|
43
|
+
checkObligationCompatible(context);
|
|
44
|
+
const kaminoOrder = context.kaminoObligation.getOrders()[toOrderIndex(orderType)];
|
|
45
|
+
if (kaminoOrder === null) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const trigger = toTrigger(kaminoOrder.condition, orderType);
|
|
49
|
+
return readTriggerBasedOrder(kaminoOrder, trigger);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A high-level specification of an LTV-based order.
|
|
54
|
+
*/
|
|
55
|
+
export type LtvBasedOrderSpecification = OrderSpecification<LtvBasedOrderTrigger>;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A discriminator enum for {@link LtvBasedOrderTrigger};
|
|
59
|
+
*/
|
|
60
|
+
export enum LtvBasedOrderTriggerType {
|
|
61
|
+
StopLoss = 'StopLoss',
|
|
62
|
+
TakeProfit = 'TakeProfit',
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* One of possible triggers depending on the obligation's type and the price bracket's side.
|
|
67
|
+
*/
|
|
68
|
+
export type LtvBasedOrderTrigger = StopLoss | TakeProfit;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A trigger for a stop-loss on LTV.
|
|
72
|
+
*/
|
|
73
|
+
export type StopLoss = {
|
|
74
|
+
type: LtvBasedOrderTriggerType.StopLoss;
|
|
75
|
+
whenLtvPctAbove: number;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A trigger for a take-profit on LTV.
|
|
80
|
+
*/
|
|
81
|
+
export type TakeProfit = {
|
|
82
|
+
type: LtvBasedOrderTriggerType.TakeProfit;
|
|
83
|
+
whenLtvPctBelow: number;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Only internals below:
|
|
87
|
+
|
|
88
|
+
const FULL_PCT = 100;
|
|
89
|
+
const MIN_LTV_THRESHOLD = 0.01;
|
|
90
|
+
const MAX_LTV_THRESHOLD = 0.99;
|
|
91
|
+
|
|
92
|
+
function checkObligationCompatible({ kaminoMarket, kaminoObligation }: OrderContext) {
|
|
93
|
+
for (const depositReserveAddress of kaminoObligation.deposits.keys()) {
|
|
94
|
+
const depositReserve = kaminoMarket.getExistingReserveByAddress(depositReserveAddress);
|
|
95
|
+
// Note: the seemingly over-cautious requirement below ensures that the user-facing LTV calculation gives the same
|
|
96
|
+
// result as on the Klend SC side (they differ in the handling of 0-LTV collaterals; see
|
|
97
|
+
// `KaminoObligation.loanToValue()` doc for details). We may unify the 0-LTV handling some day and remove this.
|
|
98
|
+
checkThat(
|
|
99
|
+
depositReserve.state.config.loanToValuePct !== 0,
|
|
100
|
+
`LTV-based orders cannot be used with a 0-LTV collateral: ${depositReserve.symbol}`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function toOrderCondition(orderType: OrderType, trigger: LtvBasedOrderTrigger): OrderCondition {
|
|
106
|
+
switch (orderType) {
|
|
107
|
+
case OrderType.StopLoss:
|
|
108
|
+
if (trigger.type === LtvBasedOrderTriggerType.StopLoss) {
|
|
109
|
+
return new UserLtvAbove(new Decimal(trigger.whenLtvPctAbove).div(FULL_PCT));
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
case OrderType.TakeProfit:
|
|
113
|
+
if (trigger.type === LtvBasedOrderTriggerType.TakeProfit) {
|
|
114
|
+
return new UserLtvBelow(new Decimal(trigger.whenLtvPctBelow).div(FULL_PCT));
|
|
115
|
+
}
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
throw new Error(`an LTV-based ${orderType} order cannot use ${trigger.type} condition`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function toTrigger(condition: OrderCondition, orderType: OrderType): LtvBasedOrderTrigger {
|
|
122
|
+
switch (orderType) {
|
|
123
|
+
case OrderType.StopLoss:
|
|
124
|
+
if (condition instanceof UserLtvAbove) {
|
|
125
|
+
return {
|
|
126
|
+
type: LtvBasedOrderTriggerType.StopLoss,
|
|
127
|
+
whenLtvPctAbove: condition.minUserLtvExclusive.mul(FULL_PCT).toNumber(),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
case OrderType.TakeProfit:
|
|
132
|
+
if (condition instanceof UserLtvBelow) {
|
|
133
|
+
return {
|
|
134
|
+
type: LtvBasedOrderTriggerType.TakeProfit,
|
|
135
|
+
whenLtvPctBelow: condition.maxUserLtvExclusive.mul(FULL_PCT).toNumber(),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
throw new Error(
|
|
141
|
+
`an LTV-based ${orderType} order has an incompatible on-chain condition ${condition.constructor.name}`
|
|
142
|
+
);
|
|
143
|
+
}
|