@kamino-finance/klend-sdk 5.12.8 → 5.13.1

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 (88) hide show
  1. package/dist/classes/action.d.ts +46 -22
  2. package/dist/classes/action.d.ts.map +1 -1
  3. package/dist/classes/action.js +60 -37
  4. package/dist/classes/action.js.map +1 -1
  5. package/dist/classes/fraction.d.ts +2 -0
  6. package/dist/classes/fraction.d.ts.map +1 -1
  7. package/dist/classes/fraction.js +6 -0
  8. package/dist/classes/fraction.js.map +1 -1
  9. package/dist/classes/manager.d.ts.map +1 -1
  10. package/dist/classes/manager.js.map +1 -1
  11. package/dist/classes/market.d.ts +10 -0
  12. package/dist/classes/market.d.ts.map +1 -1
  13. package/dist/classes/market.js +17 -10
  14. package/dist/classes/market.js.map +1 -1
  15. package/dist/classes/obligation.d.ts +2 -2
  16. package/dist/classes/obligation.d.ts.map +1 -1
  17. package/dist/classes/obligation.js +2 -7
  18. package/dist/classes/obligation.js.map +1 -1
  19. package/dist/classes/obligationOrder.d.ts +30 -7
  20. package/dist/classes/obligationOrder.d.ts.map +1 -1
  21. package/dist/classes/obligationOrder.js +47 -12
  22. package/dist/classes/obligationOrder.js.map +1 -1
  23. package/dist/classes/reserve.d.ts.map +1 -1
  24. package/dist/classes/reserve.js +1 -4
  25. package/dist/classes/reserve.js.map +1 -1
  26. package/dist/classes/utils.d.ts +0 -1
  27. package/dist/classes/utils.d.ts.map +1 -1
  28. package/dist/classes/utils.js +0 -7
  29. package/dist/classes/utils.js.map +1 -1
  30. package/dist/classes/vault.d.ts.map +1 -1
  31. package/dist/classes/vault.js +3 -0
  32. package/dist/classes/vault.js.map +1 -1
  33. package/dist/lending_operations/repay_with_collateral_operations.d.ts.map +1 -1
  34. package/dist/lending_operations/repay_with_collateral_operations.js +2 -2
  35. package/dist/lending_operations/repay_with_collateral_operations.js.map +1 -1
  36. package/dist/lending_operations/swap_collateral_operations.js +2 -4
  37. package/dist/lending_operations/swap_collateral_operations.js.map +1 -1
  38. package/dist/leverage/operations.d.ts.map +1 -1
  39. package/dist/leverage/operations.js +7 -14
  40. package/dist/leverage/operations.js.map +1 -1
  41. package/dist/lib.d.ts +1 -0
  42. package/dist/lib.d.ts.map +1 -1
  43. package/dist/lib.js +1 -0
  44. package/dist/lib.js.map +1 -1
  45. package/dist/obligation_orders/common.d.ts +62 -0
  46. package/dist/obligation_orders/common.d.ts.map +1 -0
  47. package/dist/obligation_orders/common.js +20 -0
  48. package/dist/obligation_orders/common.js.map +1 -0
  49. package/dist/obligation_orders/index.d.ts +4 -0
  50. package/dist/obligation_orders/index.d.ts.map +1 -0
  51. package/dist/obligation_orders/index.js +20 -0
  52. package/dist/obligation_orders/index.js.map +1 -0
  53. package/dist/obligation_orders/internal.d.ts +6 -0
  54. package/dist/obligation_orders/internal.d.ts.map +1 -0
  55. package/dist/obligation_orders/internal.js +72 -0
  56. package/dist/obligation_orders/internal.js.map +1 -0
  57. package/dist/obligation_orders/ltv_based.d.ts +51 -0
  58. package/dist/obligation_orders/ltv_based.d.ts.map +1 -0
  59. package/dist/obligation_orders/ltv_based.js +107 -0
  60. package/dist/obligation_orders/ltv_based.js.map +1 -0
  61. package/dist/obligation_orders/price_based.d.ts +81 -0
  62. package/dist/obligation_orders/price_based.d.ts.map +1 -0
  63. package/dist/obligation_orders/price_based.js +167 -0
  64. package/dist/obligation_orders/price_based.js.map +1 -0
  65. package/dist/utils/validations.d.ts +5 -0
  66. package/dist/utils/validations.d.ts.map +1 -0
  67. package/dist/utils/validations.js +36 -0
  68. package/dist/utils/validations.js.map +1 -0
  69. package/package.json +1 -1
  70. package/src/classes/action.ts +61 -52
  71. package/src/classes/fraction.ts +7 -0
  72. package/src/classes/manager.ts +1 -4
  73. package/src/classes/market.ts +21 -11
  74. package/src/classes/obligation.ts +4 -9
  75. package/src/classes/obligationOrder.ts +57 -20
  76. package/src/classes/reserve.ts +2 -5
  77. package/src/classes/utils.ts +0 -7
  78. package/src/classes/vault.ts +3 -0
  79. package/src/lending_operations/repay_with_collateral_operations.ts +0 -2
  80. package/src/lending_operations/swap_collateral_operations.ts +2 -4
  81. package/src/leverage/operations.ts +7 -13
  82. package/src/lib.ts +1 -0
  83. package/src/obligation_orders/common.ts +70 -0
  84. package/src/obligation_orders/index.ts +3 -0
  85. package/src/obligation_orders/internal.ts +92 -0
  86. package/src/obligation_orders/ltv_based.ts +143 -0
  87. package/src/obligation_orders/price_based.ts +256 -0
  88. package/src/utils/validations.ts +31 -0
@@ -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
+ }
@@ -0,0 +1,256 @@
1
+ import { KaminoMarket } from '../classes';
2
+ import Decimal from 'decimal.js';
3
+ import {
4
+ DebtCollPriceRatioAbove,
5
+ DebtCollPriceRatioBelow,
6
+ ObligationOrderAtIndex,
7
+ OrderCondition,
8
+ } from '../classes/obligationOrder';
9
+ import { PublicKeySet } from '../utils';
10
+ import { PublicKey } from '@solana/web3.js';
11
+ import { checkThat, getSingleElement } from '../utils/validations';
12
+ import { OrderContext, OrderSpecification, OrderType } from './common';
13
+ import { createConditionBasedOrder, readTriggerBasedOrder, toOrderIndex } from './internal';
14
+
15
+ /**
16
+ * Creates a price-based {@link ObligationOrderAtIndex} based on the given stop-loss or take-profit specification.
17
+ *
18
+ * The returned object can then be passed directly to {@link KaminoAction.buildSetObligationOrderIxn()} to build an
19
+ * instruction which replaces (or cancels, if the specification is `null`) the given obligation's stop-loss or
20
+ * take-profit order on-chain.
21
+ *
22
+ * The given obligation is expected to be a "price-based position" - a single-debt, single-coll obligation which either
23
+ * deposits or borrows a stablecoin (i.e. a long or short position of some token against a stablecoin).
24
+ */
25
+ export function createPriceBasedOrder(
26
+ context: PriceBasedOrderContext,
27
+ orderType: OrderType,
28
+ specification: PriceBasedOrderSpecification | null
29
+ ): ObligationOrderAtIndex {
30
+ const positionType = resolvePositionType(context); // resolving this first has an intentional side effect of validating the obligation being compatible
31
+ const index = toOrderIndex(orderType);
32
+ if (specification === null) {
33
+ return ObligationOrderAtIndex.empty(index);
34
+ }
35
+ const condition = toOrderCondition(positionType, orderType, specification.trigger);
36
+ return createConditionBasedOrder(context, condition, specification).atIndex(index);
37
+ }
38
+
39
+ /**
40
+ * Parses an {@link PriceBasedOrderSpecification} from the selected stop-loss or take-profit order of the given obligation.
41
+ *
42
+ * The given obligation is expected to be a "price-based position" - a single-debt, single-coll obligation which either
43
+ * deposits or borrows a stablecoin (i.e. a long or short position of some token against a stablecoin).
44
+ *
45
+ * The selected order is expected to be of matching type (i.e. as if it was created using the
46
+ * {@link createPriceBasedOrder()}).
47
+ */
48
+ export function readPriceBasedOrder(
49
+ context: PriceBasedOrderContext,
50
+ orderType: OrderType
51
+ ): PriceBasedOrderSpecification | null {
52
+ const positionType = resolvePositionType(context); // resolving this first has an intentional side effect of validating the obligation being compatible
53
+ const kaminoOrder = context.kaminoObligation.getOrders()[toOrderIndex(orderType)];
54
+ if (kaminoOrder === null) {
55
+ return null;
56
+ }
57
+ const trigger = toTrigger(positionType, kaminoOrder.condition, orderType);
58
+ return readTriggerBasedOrder(kaminoOrder, trigger);
59
+ }
60
+
61
+ /**
62
+ * An extended {@link OrderContext} needed to interpret orders on "price-based position" obligations.
63
+ */
64
+ export type PriceBasedOrderContext = OrderContext & {
65
+ stablecoins: SymbolOrMintAddress[];
66
+ };
67
+
68
+ /**
69
+ * A convenient multi-way of specifying a token.
70
+ */
71
+ export type SymbolOrMintAddress = string | PublicKey;
72
+
73
+ /**
74
+ * A high-level specification of a price-based order.
75
+ */
76
+ export type PriceBasedOrderSpecification = OrderSpecification<PriceBasedOrderTrigger>;
77
+
78
+ /**
79
+ * A discriminator enum for {@link PriceBasedOrderTrigger};
80
+ */
81
+ export enum PriceBasedOrderTriggerType {
82
+ LongStopLoss = 'LongStopLoss',
83
+ LongTakeProfit = 'LongTakeProfit',
84
+ ShortStopLoss = 'ShortStopLoss',
85
+ ShortTakeProfit = 'ShortTakeProfit',
86
+ }
87
+
88
+ /**
89
+ * One of possible triggers depending on the obligation's type and the price bracket's side.
90
+ */
91
+ export type PriceBasedOrderTrigger = LongStopLoss | LongTakeProfit | ShortStopLoss | ShortTakeProfit;
92
+
93
+ /**
94
+ * A trigger for a stop-loss on a long position.
95
+ */
96
+ export type LongStopLoss = {
97
+ type: PriceBasedOrderTriggerType.LongStopLoss;
98
+ whenCollateralPriceBelow: Decimal;
99
+ };
100
+
101
+ /**
102
+ * A trigger for a take-profit on a long position.
103
+ */
104
+ export type LongTakeProfit = {
105
+ type: PriceBasedOrderTriggerType.LongTakeProfit;
106
+ whenCollateralPriceAbove: Decimal;
107
+ };
108
+
109
+ /**
110
+ * A trigger for a stop-loss on a short position.
111
+ */
112
+ export type ShortStopLoss = {
113
+ type: PriceBasedOrderTriggerType.ShortStopLoss;
114
+ whenDebtPriceAbove: Decimal;
115
+ };
116
+
117
+ /**
118
+ * A trigger for a take-profit on a short position.
119
+ */
120
+ export type ShortTakeProfit = {
121
+ type: PriceBasedOrderTriggerType.ShortTakeProfit;
122
+ whenDebtPriceBelow: Decimal;
123
+ };
124
+
125
+ // Only internals below:
126
+
127
+ function toOrderCondition(
128
+ positionType: PositionType,
129
+ orderType: OrderType,
130
+ trigger: PriceBasedOrderTrigger
131
+ ): OrderCondition {
132
+ switch (positionType) {
133
+ case PositionType.Long:
134
+ switch (orderType) {
135
+ case OrderType.StopLoss:
136
+ if (trigger.type === PriceBasedOrderTriggerType.LongStopLoss) {
137
+ return new DebtCollPriceRatioAbove(invertPriceRatio(trigger.whenCollateralPriceBelow));
138
+ }
139
+ break;
140
+ case OrderType.TakeProfit:
141
+ if (trigger.type === PriceBasedOrderTriggerType.LongTakeProfit) {
142
+ return new DebtCollPriceRatioBelow(invertPriceRatio(trigger.whenCollateralPriceAbove));
143
+ }
144
+ break;
145
+ }
146
+ break;
147
+ case PositionType.Short:
148
+ switch (orderType) {
149
+ case OrderType.StopLoss:
150
+ if (trigger.type === PriceBasedOrderTriggerType.ShortStopLoss) {
151
+ return new DebtCollPriceRatioAbove(trigger.whenDebtPriceAbove);
152
+ }
153
+ break;
154
+ case OrderType.TakeProfit:
155
+ if (trigger.type === PriceBasedOrderTriggerType.ShortTakeProfit) {
156
+ return new DebtCollPriceRatioBelow(trigger.whenDebtPriceBelow);
157
+ }
158
+ break;
159
+ }
160
+ break;
161
+ }
162
+ throw new Error(`a ${orderType} order on a ${positionType} position cannot use ${trigger.type} condition`);
163
+ }
164
+
165
+ function toTrigger(
166
+ positionType: PositionType,
167
+ condition: OrderCondition,
168
+ orderType: OrderType
169
+ ): PriceBasedOrderTrigger {
170
+ switch (positionType) {
171
+ case PositionType.Long:
172
+ switch (orderType) {
173
+ case OrderType.StopLoss:
174
+ if (condition instanceof DebtCollPriceRatioAbove) {
175
+ return {
176
+ type: PriceBasedOrderTriggerType.LongStopLoss,
177
+ whenCollateralPriceBelow: invertPriceRatio(condition.minDebtCollPriceRatioExclusive),
178
+ };
179
+ }
180
+ break;
181
+ case OrderType.TakeProfit:
182
+ if (condition instanceof DebtCollPriceRatioBelow) {
183
+ return {
184
+ type: PriceBasedOrderTriggerType.LongTakeProfit,
185
+ whenCollateralPriceAbove: invertPriceRatio(condition.maxDebtCollPriceRatioExclusive),
186
+ };
187
+ }
188
+ break;
189
+ }
190
+ break;
191
+ case PositionType.Short:
192
+ switch (orderType) {
193
+ case OrderType.StopLoss:
194
+ if (condition instanceof DebtCollPriceRatioAbove) {
195
+ return {
196
+ type: PriceBasedOrderTriggerType.ShortStopLoss,
197
+ whenDebtPriceAbove: condition.minDebtCollPriceRatioExclusive,
198
+ };
199
+ }
200
+ break;
201
+ case OrderType.TakeProfit:
202
+ if (condition instanceof DebtCollPriceRatioBelow) {
203
+ return {
204
+ type: PriceBasedOrderTriggerType.ShortTakeProfit,
205
+ whenDebtPriceBelow: condition.maxDebtCollPriceRatioExclusive,
206
+ };
207
+ }
208
+ break;
209
+ }
210
+ break;
211
+ }
212
+ throw new Error(
213
+ `a ${orderType} order on a ${positionType} position has an incompatible on-chain condition ${condition.constructor.name}`
214
+ );
215
+ }
216
+
217
+ function invertPriceRatio(priceRatio: Decimal): Decimal {
218
+ return new Decimal(1).div(priceRatio);
219
+ }
220
+
221
+ enum PositionType {
222
+ Long = 'Long',
223
+ Short = 'Short',
224
+ }
225
+
226
+ function resolvePositionType(context: PriceBasedOrderContext): PositionType {
227
+ const collateralReserveAddress = getSingleElement(context.kaminoObligation.deposits.keys(), 'deposit');
228
+ const debtReserveAddress = getSingleElement(context.kaminoObligation.borrows.keys(), 'borrow');
229
+ const stablecoinReserveAddresses = collectReserveAddresses(context.kaminoMarket, context.stablecoins);
230
+ if (stablecoinReserveAddresses.contains(collateralReserveAddress)) {
231
+ checkThat(
232
+ !stablecoinReserveAddresses.contains(debtReserveAddress),
233
+ 'cannot resolve long vs short position from all-stablecoins obligation'
234
+ );
235
+ return PositionType.Short;
236
+ } else {
237
+ checkThat(
238
+ stablecoinReserveAddresses.contains(debtReserveAddress),
239
+ 'cannot resolve long vs short position from no-stablecoins obligation'
240
+ );
241
+ return PositionType.Long;
242
+ }
243
+ }
244
+
245
+ function collectReserveAddresses(
246
+ kaminoMarket: KaminoMarket,
247
+ symbolOrMintAddresses: SymbolOrMintAddress[]
248
+ ): PublicKeySet<PublicKey> {
249
+ return new PublicKeySet(
250
+ symbolOrMintAddresses.map((symbolOrMintAddress) =>
251
+ typeof symbolOrMintAddress === 'string'
252
+ ? kaminoMarket.getExistingReserveBySymbol(symbolOrMintAddress).address
253
+ : kaminoMarket.getExistingReserveByMint(symbolOrMintAddress).address
254
+ )
255
+ );
256
+ }
@@ -0,0 +1,31 @@
1
+ export function checkThat(evaluationResult: boolean, message: string = 'precondition failed'): void {
2
+ if (!evaluationResult) {
3
+ throw new Error(message);
4
+ }
5
+ }
6
+
7
+ export function checkDefined<T>(value: T | undefined, message: string = 'value undefined'): T {
8
+ checkThat(value !== undefined, message);
9
+ return value as T;
10
+ }
11
+
12
+ export function checkNotNull<T>(value: T | null, message: string = 'value null'): T {
13
+ checkThat(value !== null, message);
14
+ return value as T;
15
+ }
16
+
17
+ export function getSingleElement<T>(iterable: Iterable<T>, nameWithinMessage: string = 'element'): T {
18
+ const nothingReturnedMarker = {};
19
+ let single: T | {} = nothingReturnedMarker;
20
+ for (const element of iterable) {
21
+ if (single === nothingReturnedMarker) {
22
+ single = element;
23
+ } else {
24
+ throw new Error(`exactly one ${nameWithinMessage} expected, but multiple found`);
25
+ }
26
+ }
27
+ if (single === nothingReturnedMarker) {
28
+ throw new Error(`exactly one ${nameWithinMessage} expected, but none found`);
29
+ }
30
+ return single as T;
31
+ }