@crewhaus/tool-defi 0.7.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/README.md +270 -0
- package/dist/fixtures.d.ts +111 -0
- package/dist/fixtures.js +261 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +918 -0
- package/dist/lib/abi.d.ts +147 -0
- package/dist/lib/abi.js +245 -0
- package/dist/lib/batch.d.ts +21 -0
- package/dist/lib/batch.js +83 -0
- package/dist/lib/decimal.d.ts +103 -0
- package/dist/lib/decimal.js +246 -0
- package/dist/lib/oracle.d.ts +147 -0
- package/dist/lib/oracle.js +270 -0
- package/dist/lib/positions.d.ts +51 -0
- package/dist/lib/positions.js +301 -0
- package/dist/lib/quotes.d.ts +85 -0
- package/dist/lib/quotes.js +370 -0
- package/dist/lib/rpc.d.ts +171 -0
- package/dist/lib/rpc.js +600 -0
- package/package.json +48 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type RpcOptions } from "./rpc";
|
|
2
|
+
/** The protocols this package reads. Everything else is refused by name. */
|
|
3
|
+
export declare const SUPPORTED_PROTOCOLS: readonly ["aave-v3", "compound-v3", "erc4626"];
|
|
4
|
+
export type Protocol = (typeof SUPPORTED_PROTOCOLS)[number];
|
|
5
|
+
/**
|
|
6
|
+
* Protocols a caller will reasonably ask for, each with what reading it would
|
|
7
|
+
* actually take. A named refusal is worth more than a missing case: it tells
|
|
8
|
+
* the caller what to go and do instead.
|
|
9
|
+
*/
|
|
10
|
+
export declare const REFUSED_PROTOCOLS: Readonly<Record<string, string>>;
|
|
11
|
+
export type PositionRequest = {
|
|
12
|
+
readonly protocol: Protocol;
|
|
13
|
+
readonly chainId: string;
|
|
14
|
+
readonly endpoint: string;
|
|
15
|
+
/** The protocol contract: an Aave Pool, a Comet, or an ERC-4626 vault. */
|
|
16
|
+
readonly contract: string;
|
|
17
|
+
readonly account: string;
|
|
18
|
+
readonly blockTag: string;
|
|
19
|
+
readonly multicall3?: string;
|
|
20
|
+
/** Aave only: the oracle base currency's decimals. Defaults to 8 with a flag. */
|
|
21
|
+
readonly baseCurrencyDecimals?: number;
|
|
22
|
+
/** Compound v3 only: collateral assets to read balances for. */
|
|
23
|
+
readonly collateralAssets?: ReadonlyArray<string>;
|
|
24
|
+
};
|
|
25
|
+
export type PositionProvenance = {
|
|
26
|
+
readonly protocol: Protocol;
|
|
27
|
+
readonly chainId: string;
|
|
28
|
+
readonly endpoint: string;
|
|
29
|
+
readonly contract: string;
|
|
30
|
+
readonly account: string;
|
|
31
|
+
readonly blockTag: string;
|
|
32
|
+
/** The view calls this row was assembled from, so the read is reproducible. */
|
|
33
|
+
readonly calls: ReadonlyArray<string>;
|
|
34
|
+
};
|
|
35
|
+
export type PositionRow = {
|
|
36
|
+
readonly protocol: Protocol;
|
|
37
|
+
readonly provenance: PositionProvenance;
|
|
38
|
+
/** Protocol-specific figures, each labelled with its own basis. */
|
|
39
|
+
readonly figures: Readonly<Record<string, string | number | boolean | null>>;
|
|
40
|
+
/** Health, when the protocol has one. Null with a reason when it does not. */
|
|
41
|
+
readonly health: {
|
|
42
|
+
readonly healthFactor: string | null;
|
|
43
|
+
readonly liquidatable: boolean | null;
|
|
44
|
+
readonly collateralDropToLiquidationBps: number | null;
|
|
45
|
+
readonly reason: string;
|
|
46
|
+
};
|
|
47
|
+
readonly notes: ReadonlyArray<string>;
|
|
48
|
+
};
|
|
49
|
+
/** Aave markets priced in USD use a 1e8 base unit; the rest say so explicitly. */
|
|
50
|
+
export declare const DEFAULT_AAVE_BASE_DECIMALS = 8;
|
|
51
|
+
export declare function readPosition(request: PositionRequest, options?: RpcOptions): Promise<PositionRow>;
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { SELECTORS, UINT256_MAX, addressFromWord, callNoArgs, callWithAddress, callWithAddresses, callWithUint256, decodeWords, normalizeAddress, } from "./abi";
|
|
2
|
+
/**
|
|
3
|
+
* One normalised row per lending or vault position, and a refusal for every
|
|
4
|
+
* protocol whose basis this package cannot state.
|
|
5
|
+
*
|
|
6
|
+
* Each protocol reports collateral and debt in its OWN basis, and the
|
|
7
|
+
* normalising is where the wrong number gets made:
|
|
8
|
+
*
|
|
9
|
+
* - **Aave v3** answers in the price oracle's base currency, scaled by
|
|
10
|
+
* `BASE_CURRENCY_UNIT`, and returns the health factor in wad. The base
|
|
11
|
+
* unit is a market parameter, not a constant, so it is an input here with
|
|
12
|
+
* a stated default and a flag saying whether the default was used —
|
|
13
|
+
* hard-coding 8 is right for the USD markets and a hundred-million-fold
|
|
14
|
+
* error anywhere else. Its "no debt" health factor is `type(uint256).max`,
|
|
15
|
+
* which read as a wad is 1.1e59 and prints as a spectacularly safe
|
|
16
|
+
* position; it is reported as null with a reason instead.
|
|
17
|
+
* - **Compound v3** is single-borrow-asset, so a supply balance and a borrow
|
|
18
|
+
* balance are mutually exclusive and both are in base-token units. Its
|
|
19
|
+
* liquidation verdict is read from the protocol's own `isLiquidatable`
|
|
20
|
+
* rather than derived, because Comet's collateral factors are per asset
|
|
21
|
+
* and a derived answer would disagree with the protocol UI.
|
|
22
|
+
* - **ERC-4626** share value is `convertToAssets(shares)`, NOT
|
|
23
|
+
* `totalAssets() / totalSupply()`. The two differ for any vault with entry
|
|
24
|
+
* or exit fees, and the second is the one that looks right.
|
|
25
|
+
*
|
|
26
|
+
* **A liquidation PRICE is not reported.** For an aggregate position it is not
|
|
27
|
+
* a fact: it depends on which collateral is assumed to move, and picking one
|
|
28
|
+
* produces a number that looks authoritative and disagrees with the protocol
|
|
29
|
+
* UI. What is reported instead is `collateralDropToLiquidationBps` — how far
|
|
30
|
+
* the whole collateral basket may fall before the health factor reaches 1 —
|
|
31
|
+
* which is derived from the same inputs, needs no assumption, and is what the
|
|
32
|
+
* liquidation price was being used to approximate.
|
|
33
|
+
*/
|
|
34
|
+
import { batchCalls } from "./batch";
|
|
35
|
+
import { compare, fixed, fromInteger, ratioBps, toDecimalString } from "./decimal";
|
|
36
|
+
import { DefiError, endpointLabel } from "./rpc";
|
|
37
|
+
/** The protocols this package reads. Everything else is refused by name. */
|
|
38
|
+
export const SUPPORTED_PROTOCOLS = Object.freeze(["aave-v3", "compound-v3", "erc4626"]);
|
|
39
|
+
/**
|
|
40
|
+
* Protocols a caller will reasonably ask for, each with what reading it would
|
|
41
|
+
* actually take. A named refusal is worth more than a missing case: it tells
|
|
42
|
+
* the caller what to go and do instead.
|
|
43
|
+
*/
|
|
44
|
+
export const REFUSED_PROTOCOLS = Object.freeze({
|
|
45
|
+
"morpho-blue": "Morpho Blue is per-market, and a market's collateral is valued by an oracle contract named in the market parameters — reading a position means reading that oracle too, at the same block, and this package will not guess which oracle a market id points at. Read the market's oracle with OraclePriceRead and value the position from it.",
|
|
46
|
+
"uniswap-v3": "A Uniswap v3 LP position is an amount of liquidity between two ticks, not a token balance: turning it into amounts needs the pool's current tick and the tick-math conversion, and the uncollected fees need the per-position fee growth checkpoints. That is a different job from reading a balance and it is not implemented here.",
|
|
47
|
+
"uniswap-v2": "A Uniswap v2 LP position is a share of the pool's reserves, which is readable — but a reserve-derived value is manipulable within a block, and pricing it needs the spot reserves and an independent price for at least one side. Price the underlying assets with PriceQuote or OraclePriceRead instead.",
|
|
48
|
+
lido: "stETH is a rebasing ERC-20: the balance IS the position, so read it as a token balance in PortfolioValuation and price it as an asset. wstETH needs the wrapper's exchange rate, which is an ERC-4626-shaped read this package does not pin an address for.",
|
|
49
|
+
maker: "A Maker vault is an urn in the CDP engine, addressed by ilk and urn rather than by owner, with the debt scaled by a per-ilk rate accumulator. Reading it needs the ilk registry, which is not pinned here.",
|
|
50
|
+
});
|
|
51
|
+
/** Aave's health factor and Compound's balances are wad-scaled. */
|
|
52
|
+
const WAD_DECIMALS = 18;
|
|
53
|
+
/** Aave markets priced in USD use a 1e8 base unit; the rest say so explicitly. */
|
|
54
|
+
export const DEFAULT_AAVE_BASE_DECIMALS = 8;
|
|
55
|
+
export async function readPosition(request, options = {}) {
|
|
56
|
+
switch (request.protocol) {
|
|
57
|
+
case "aave-v3":
|
|
58
|
+
return readAaveV3(request, options);
|
|
59
|
+
case "compound-v3":
|
|
60
|
+
return readCompoundV3(request, options);
|
|
61
|
+
default:
|
|
62
|
+
return readErc4626(request, options);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function readAaveV3(request, options) {
|
|
66
|
+
const contract = normalizeAddress(request.contract, "the Aave v3 Pool address");
|
|
67
|
+
const account = normalizeAddress(request.account, "account");
|
|
68
|
+
const results = await issue(request, [
|
|
69
|
+
{
|
|
70
|
+
to: contract,
|
|
71
|
+
data: callWithAddress(SELECTORS.getUserAccountData, account, "account"),
|
|
72
|
+
label: "getUserAccountData(address)",
|
|
73
|
+
},
|
|
74
|
+
], options);
|
|
75
|
+
const [collateralBase, debtBase, availableBorrowsBase, liquidationThresholdBps, ltvBps, healthFactorWad,] = decodeWords(required(results[0], "getUserAccountData(address)"), 6, "getUserAccountData(address)");
|
|
76
|
+
const baseDecimals = request.baseCurrencyDecimals ?? DEFAULT_AAVE_BASE_DECIMALS;
|
|
77
|
+
if (!Number.isSafeInteger(baseDecimals) || baseDecimals < 0 || baseDecimals > 36) {
|
|
78
|
+
throw new DefiError(`baseCurrencyDecimals must be an integer from 0 to 36, got ${baseDecimals}`);
|
|
79
|
+
}
|
|
80
|
+
const notes = [];
|
|
81
|
+
if (request.baseCurrencyDecimals === undefined) {
|
|
82
|
+
notes.push(`collateral and debt are reported in the market's base currency at the default ${DEFAULT_AAVE_BASE_DECIMALS} decimals, which is what Aave's USD markets use; pass baseCurrencyDecimals for a market with a different BASE_CURRENCY_UNIT`);
|
|
83
|
+
}
|
|
84
|
+
const collateral = fixed(collateralBase, baseDecimals);
|
|
85
|
+
const debt = fixed(debtBase, baseDecimals);
|
|
86
|
+
// type(uint256).max is Aave's "no debt" sentinel. As a wad it is about
|
|
87
|
+
// 1.15e59, which sorts, compares and prints as an extremely healthy
|
|
88
|
+
// position — which is exactly why it must not reach a caller as a number.
|
|
89
|
+
const noDebt = debtBase === 0n || healthFactorWad === UINT256_MAX;
|
|
90
|
+
const health = noDebt
|
|
91
|
+
? {
|
|
92
|
+
healthFactor: null,
|
|
93
|
+
liquidatable: false,
|
|
94
|
+
collateralDropToLiquidationBps: null,
|
|
95
|
+
reason: "no debt, so there is no health factor; Aave returns type(uint256).max here and reporting it as a number would read as an extraordinarily safe position",
|
|
96
|
+
}
|
|
97
|
+
: healthFrom(fixed(healthFactorWad, WAD_DECIMALS));
|
|
98
|
+
return {
|
|
99
|
+
protocol: "aave-v3",
|
|
100
|
+
provenance: provenanceOf(request, contract, account, ["getUserAccountData(address)"]),
|
|
101
|
+
figures: {
|
|
102
|
+
totalCollateralBase: toDecimalString(collateral),
|
|
103
|
+
totalDebtBase: toDecimalString(debt),
|
|
104
|
+
availableBorrowsBase: toDecimalString(fixed(availableBorrowsBase, baseDecimals)),
|
|
105
|
+
baseCurrencyDecimals: baseDecimals,
|
|
106
|
+
baseCurrencyDecimalsAssumed: request.baseCurrencyDecimals === undefined,
|
|
107
|
+
currentLiquidationThresholdBps: Number(liquidationThresholdBps),
|
|
108
|
+
ltvBps: Number(ltvBps),
|
|
109
|
+
healthFactorWad: healthFactorWad === UINT256_MAX ? null : healthFactorWad.toString(),
|
|
110
|
+
},
|
|
111
|
+
health,
|
|
112
|
+
notes,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
async function readCompoundV3(request, options) {
|
|
116
|
+
const contract = normalizeAddress(request.contract, "the Compound v3 Comet address");
|
|
117
|
+
const account = normalizeAddress(request.account, "account");
|
|
118
|
+
const collateralAssets = (request.collateralAssets ?? []).map((asset, i) => normalizeAddress(asset, `collateralAssets[${i}]`));
|
|
119
|
+
const calls = [
|
|
120
|
+
{ to: contract, data: callNoArgs(SELECTORS.baseToken), label: "baseToken()" },
|
|
121
|
+
{
|
|
122
|
+
to: contract,
|
|
123
|
+
data: callWithAddress(SELECTORS.balanceOf, account, "account"),
|
|
124
|
+
label: "balanceOf(address)",
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
to: contract,
|
|
128
|
+
data: callWithAddress(SELECTORS.borrowBalanceOf, account, "account"),
|
|
129
|
+
label: "borrowBalanceOf(address)",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
to: contract,
|
|
133
|
+
data: callWithAddress(SELECTORS.isLiquidatable, account, "account"),
|
|
134
|
+
label: "isLiquidatable(address)",
|
|
135
|
+
},
|
|
136
|
+
...collateralAssets.map((asset) => ({
|
|
137
|
+
to: contract,
|
|
138
|
+
data: callWithAddresses(SELECTORS.collateralBalanceOf, account, asset, "collateralBalanceOf"),
|
|
139
|
+
label: `collateralBalanceOf(address,${asset})`,
|
|
140
|
+
})),
|
|
141
|
+
];
|
|
142
|
+
const results = await issue(request, calls, options);
|
|
143
|
+
const baseTokenWord = decodeWords(required(results[0], "baseToken()"), 1, "baseToken()")[0];
|
|
144
|
+
const baseTokenAddress = addressFromWord(baseTokenWord);
|
|
145
|
+
const supplied = decodeWords(required(results[1], "balanceOf(address)"), 1, "balanceOf(address)")[0];
|
|
146
|
+
const borrowed = decodeWords(required(results[2], "borrowBalanceOf(address)"), 1, "borrowBalanceOf(address)")[0];
|
|
147
|
+
const liquidatableWord = decodeWords(required(results[3], "isLiquidatable(address)"), 1, "isLiquidatable(address)")[0];
|
|
148
|
+
const collateral = {};
|
|
149
|
+
const notes = [];
|
|
150
|
+
if (baseTokenAddress.paddingDirty) {
|
|
151
|
+
notes.push("baseToken() returned a word whose top 12 bytes are not zero; the low 20 are read as the address, as an ABI decoder does, but a conforming contract does not do that — check that this Comet address is the market you meant");
|
|
152
|
+
}
|
|
153
|
+
collateralAssets.forEach((asset, index) => {
|
|
154
|
+
const outcome = results[4 + index];
|
|
155
|
+
if (outcome === undefined || !outcome.ok) {
|
|
156
|
+
// One unreadable collateral balance does not invalidate the rest of the
|
|
157
|
+
// position; it is named so nobody reads its absence as a zero balance.
|
|
158
|
+
notes.push(`collateralBalanceOf for ${asset} could not be read: ${outcome?.reason ?? "no row returned"}`);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const amount = decodeWords(outcome.data, 1, `collateralBalanceOf(${asset})`)[0];
|
|
162
|
+
collateral[asset] = amount.toString();
|
|
163
|
+
});
|
|
164
|
+
notes.push("supplied and borrowed are in the base token's own units, not a common currency — read the base token's decimals before valuing them");
|
|
165
|
+
if (supplied > 0n && borrowed > 0n) {
|
|
166
|
+
notes.push("both a supply and a borrow balance are non-zero, which Comet's single-base-asset design does not produce; check that this Comet address is the market you meant");
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
protocol: "compound-v3",
|
|
170
|
+
provenance: provenanceOf(request, contract, account, calls.map((call) => call.label)),
|
|
171
|
+
figures: {
|
|
172
|
+
baseToken: baseTokenAddress.address,
|
|
173
|
+
suppliedBase: supplied.toString(),
|
|
174
|
+
borrowedBase: borrowed.toString(),
|
|
175
|
+
collateral: JSON.stringify(collateral),
|
|
176
|
+
},
|
|
177
|
+
health: {
|
|
178
|
+
healthFactor: null,
|
|
179
|
+
liquidatable: liquidatableWord !== 0n,
|
|
180
|
+
collateralDropToLiquidationBps: null,
|
|
181
|
+
reason: "Compound v3 publishes no health factor; `liquidatable` is the protocol's own isLiquidatable(address) verdict rather than one derived here, because Comet's collateral factors are per asset and a derived answer would disagree with the protocol UI",
|
|
182
|
+
},
|
|
183
|
+
notes,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
async function readErc4626(request, options) {
|
|
187
|
+
const contract = normalizeAddress(request.contract, "the ERC-4626 vault address");
|
|
188
|
+
const account = normalizeAddress(request.account, "account");
|
|
189
|
+
const shareCall = [
|
|
190
|
+
{
|
|
191
|
+
to: contract,
|
|
192
|
+
data: callWithAddress(SELECTORS.balanceOf, account, "account"),
|
|
193
|
+
label: "balanceOf(address)",
|
|
194
|
+
},
|
|
195
|
+
{ to: contract, data: callNoArgs(SELECTORS.asset), label: "asset()" },
|
|
196
|
+
{ to: contract, data: callNoArgs(SELECTORS.decimals), label: "decimals()" },
|
|
197
|
+
];
|
|
198
|
+
const first = await issue(request, shareCall, options);
|
|
199
|
+
const shares = decodeWords(required(first[0], "balanceOf(address)"), 1, "balanceOf(address)")[0];
|
|
200
|
+
const assetAddress = addressFromWord(decodeWords(required(first[1], "asset()"), 1, "asset()")[0]);
|
|
201
|
+
const shareDecimals = Number(decodeWords(required(first[2], "decimals()"), 1, "decimals()")[0]);
|
|
202
|
+
if (!Number.isSafeInteger(shareDecimals) || shareDecimals < 0 || shareDecimals > 36) {
|
|
203
|
+
throw new DefiError(`the vault answered decimals() with ${shareDecimals}, which is not a token decimalisation`);
|
|
204
|
+
}
|
|
205
|
+
// The second round trip is unavoidable: convertToAssets takes the share
|
|
206
|
+
// balance that the first read returned. Both are at the same pinned block
|
|
207
|
+
// tag, which is what makes them one answer rather than two.
|
|
208
|
+
const second = await issue(request, [
|
|
209
|
+
{
|
|
210
|
+
to: contract,
|
|
211
|
+
data: callWithUint256(SELECTORS.convertToAssets, shares, "shares"),
|
|
212
|
+
label: "convertToAssets(uint256)",
|
|
213
|
+
},
|
|
214
|
+
], options);
|
|
215
|
+
const assets = decodeWords(required(second[0], "convertToAssets(uint256)"), 1, "convertToAssets(uint256)")[0];
|
|
216
|
+
return {
|
|
217
|
+
protocol: "erc4626",
|
|
218
|
+
provenance: provenanceOf(request, contract, account, [
|
|
219
|
+
"balanceOf(address)",
|
|
220
|
+
"asset()",
|
|
221
|
+
"decimals()",
|
|
222
|
+
"convertToAssets(uint256)",
|
|
223
|
+
]),
|
|
224
|
+
figures: {
|
|
225
|
+
shares: shares.toString(),
|
|
226
|
+
shareDecimals,
|
|
227
|
+
asset: assetAddress.address,
|
|
228
|
+
assetsRedeemable: assets.toString(),
|
|
229
|
+
},
|
|
230
|
+
health: {
|
|
231
|
+
healthFactor: null,
|
|
232
|
+
liquidatable: null,
|
|
233
|
+
collateralDropToLiquidationBps: null,
|
|
234
|
+
reason: "an ERC-4626 share is not a borrow position, so it has no health factor",
|
|
235
|
+
},
|
|
236
|
+
notes: [
|
|
237
|
+
"assetsRedeemable is convertToAssets(shares), which is what the vault says these shares are worth — not totalAssets()/totalSupply(), which is the same figure only for a vault with no entry or exit fee",
|
|
238
|
+
"assetsRedeemable is in the UNDERLYING asset's units; read that token's decimals before valuing it",
|
|
239
|
+
...(assetAddress.paddingDirty
|
|
240
|
+
? [
|
|
241
|
+
"asset() returned a word whose top 12 bytes are not zero; the low 20 are read as the address, as an ABI decoder does, but a conforming vault does not do that",
|
|
242
|
+
]
|
|
243
|
+
: []),
|
|
244
|
+
],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Health from a wad health factor.
|
|
249
|
+
*
|
|
250
|
+
* `collateralDropToLiquidationBps` is `10000 - 10000/HF`: the fraction the
|
|
251
|
+
* collateral basket may lose before HF reaches 1. It needs no assumption about
|
|
252
|
+
* which asset moves, which is precisely why it is reported instead of a
|
|
253
|
+
* liquidation price. A position already below 1 has no room left, so it floors
|
|
254
|
+
* at zero rather than going negative.
|
|
255
|
+
*/
|
|
256
|
+
function healthFrom(healthFactor) {
|
|
257
|
+
const one = fromInteger(1n);
|
|
258
|
+
const liquidatable = compare(healthFactor, one) < 0;
|
|
259
|
+
return {
|
|
260
|
+
healthFactor: toDecimalString(healthFactor),
|
|
261
|
+
liquidatable,
|
|
262
|
+
// 10000/HF is what the basket would have to be worth, relative to now, for
|
|
263
|
+
// HF to reach 1; the room above that is the drop it can take. It is
|
|
264
|
+
// computed only for a position that HAS room: Aave answers a health factor
|
|
265
|
+
// of exactly ZERO for an account that still owes against collateral whose
|
|
266
|
+
// liquidation threshold has been set to nothing, and dividing by it threw
|
|
267
|
+
// DecimalError out of the tool — so the one position with no room left was
|
|
268
|
+
// the one position this could not report at all.
|
|
269
|
+
collateralDropToLiquidationBps: liquidatable
|
|
270
|
+
? 0
|
|
271
|
+
: Math.max(0, 10_000 - ratioBps(one, healthFactor)),
|
|
272
|
+
reason: liquidatable
|
|
273
|
+
? "the health factor is below 1, so this position is liquidatable now"
|
|
274
|
+
: "the collateral basket may fall by collateralDropToLiquidationBps before the health factor reaches 1; no liquidation price is given because that would require assuming which collateral moves",
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
async function issue(request, calls, options) {
|
|
278
|
+
return batchCalls(request.endpoint, calls, request.blockTag, {
|
|
279
|
+
...options,
|
|
280
|
+
chainId: request.chainId,
|
|
281
|
+
...(request.multicall3 === undefined ? {} : { multicall3: request.multicall3 }),
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
function provenanceOf(request, contract, account, calls) {
|
|
285
|
+
return {
|
|
286
|
+
protocol: request.protocol,
|
|
287
|
+
chainId: request.chainId,
|
|
288
|
+
endpoint: endpointLabel(request.endpoint),
|
|
289
|
+
contract,
|
|
290
|
+
account,
|
|
291
|
+
blockTag: request.blockTag,
|
|
292
|
+
calls,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function required(outcome, label) {
|
|
296
|
+
if (outcome === undefined)
|
|
297
|
+
throw new DefiError(`the batch returned no row for ${label}`);
|
|
298
|
+
if (!outcome.ok)
|
|
299
|
+
throw new DefiError(outcome.reason);
|
|
300
|
+
return outcome.data;
|
|
301
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { type Fixed, type PriceLiteralKind } from "./decimal";
|
|
2
|
+
import { type RpcOptions } from "./rpc";
|
|
3
|
+
/** The two origins this package reads prices from. There is no third, and no override. */
|
|
4
|
+
export declare const PROVIDER_ORIGINS: Readonly<{
|
|
5
|
+
frankfurter: "https://api.frankfurter.app";
|
|
6
|
+
coinbase: "https://api.coinbase.com";
|
|
7
|
+
}>;
|
|
8
|
+
export type ProviderName = keyof typeof PROVIDER_ORIGINS;
|
|
9
|
+
export declare const PROVIDER_NAMES: ReadonlyArray<ProviderName>;
|
|
10
|
+
/**
|
|
11
|
+
* Inversion and cross division are carried to this many places.
|
|
12
|
+
*
|
|
13
|
+
* 1/3 has no exact decimal expansion, so an inverted quote is rounded exactly
|
|
14
|
+
* once, here, with the mode named in the result. Eighteen places is more than
|
|
15
|
+
* any provider publishes and matches the widest token decimalisation, so the
|
|
16
|
+
* rounding is below the noise of the input rather than above it.
|
|
17
|
+
*/
|
|
18
|
+
export declare const INVERSION_SCALE = 18;
|
|
19
|
+
export type QuoteLeg = {
|
|
20
|
+
readonly source: ProviderName;
|
|
21
|
+
readonly endpoint: string;
|
|
22
|
+
readonly pair: string;
|
|
23
|
+
/** `direct` when the provider publishes this pair; `inverted` when it publishes the other one. */
|
|
24
|
+
readonly derivation: "direct" | "inverted";
|
|
25
|
+
readonly price: string;
|
|
26
|
+
/** The provider's OWN date for this price, or null when it publishes none. */
|
|
27
|
+
readonly asOf: string | null;
|
|
28
|
+
readonly asOfSource: "frankfurter.date" | "none";
|
|
29
|
+
/** What "this price, at this date" means for this provider. */
|
|
30
|
+
readonly convention: string;
|
|
31
|
+
/** Whether the number arrived as a JSON string (exact) or a JSON number (through a double). */
|
|
32
|
+
readonly literal: PriceLiteralKind;
|
|
33
|
+
readonly requestedAt: string | null;
|
|
34
|
+
};
|
|
35
|
+
export type Quote = {
|
|
36
|
+
readonly base: string;
|
|
37
|
+
readonly quote: string;
|
|
38
|
+
readonly price: string;
|
|
39
|
+
readonly derivation: "direct" | "inverted" | "cross";
|
|
40
|
+
/** The intermediate asset a cross went through. Null when it did not cross. */
|
|
41
|
+
readonly via: string | null;
|
|
42
|
+
/** Every leg, in the order they were composed. One leg unless this crossed. */
|
|
43
|
+
readonly legs: ReadonlyArray<QuoteLeg>;
|
|
44
|
+
/**
|
|
45
|
+
* The oldest `asOf` across the legs, or null when any leg publishes none. A
|
|
46
|
+
* composite price is exactly as old as its oldest input, and one leg without
|
|
47
|
+
* a timestamp makes the composite untimestamped rather than fresh.
|
|
48
|
+
*/
|
|
49
|
+
readonly asOf: string | null;
|
|
50
|
+
readonly notes: ReadonlyArray<string>;
|
|
51
|
+
};
|
|
52
|
+
export type QuoteValue = {
|
|
53
|
+
readonly quote: Quote;
|
|
54
|
+
readonly value: Fixed;
|
|
55
|
+
};
|
|
56
|
+
export type QuoteRequest = {
|
|
57
|
+
readonly base: string;
|
|
58
|
+
readonly quote: string;
|
|
59
|
+
/** An ISO date. Absent means the provider's current publication. */
|
|
60
|
+
readonly at?: string;
|
|
61
|
+
/** Provider order. Absent picks by pair: the ECB for fiat/fiat, Coinbase otherwise. */
|
|
62
|
+
readonly providers?: ReadonlyArray<ProviderName>;
|
|
63
|
+
/** The intermediate to cross through when no provider publishes the pair directly. */
|
|
64
|
+
readonly via?: string;
|
|
65
|
+
/** Set false to refuse rather than cross. A cross is two prices, not one. */
|
|
66
|
+
readonly allowCross?: boolean;
|
|
67
|
+
};
|
|
68
|
+
export declare function normalizeSymbol(raw: string, what: string): string;
|
|
69
|
+
/**
|
|
70
|
+
* Whether a code is a currency this monorepo knows.
|
|
71
|
+
*
|
|
72
|
+
* The table is `@crewhaus/tool-math`'s, through its public surface, rather than
|
|
73
|
+
* a second ISO 4217 list in this package that would drift from it.
|
|
74
|
+
*/
|
|
75
|
+
export declare function isFiat(code: string): boolean;
|
|
76
|
+
/** Which providers to try, and in what order, for a pair nobody named providers for. */
|
|
77
|
+
export declare function defaultProviders(base: string, quote: string): ReadonlyArray<ProviderName>;
|
|
78
|
+
/** The intermediates a cross tries when the caller names none. */
|
|
79
|
+
export declare const DEFAULT_CROSS_VIA: ReadonlyArray<string>;
|
|
80
|
+
/**
|
|
81
|
+
* Price one asset in another, direct if a provider publishes it, inverted if
|
|
82
|
+
* one publishes the other direction, crossed through an intermediate if
|
|
83
|
+
* neither — and say which of the three happened, every time.
|
|
84
|
+
*/
|
|
85
|
+
export declare function quotePrice(request: QuoteRequest, options?: RpcOptions): Promise<QuoteValue>;
|