@le-space/rates 0.0.0-stage → 0.1.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 CHANGED
@@ -1,3 +1,26 @@
1
- # Temporary Holding Version
1
+ # @le-space/rates
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The euro rate of a day, one definition for Le Space's tools – [Belege](https://github.com/Le-Space/belege) and the [Eigenbeleg](https://github.com/Le-Space/eigenbeleg-agent) API and web UI (decided 2026-11-06, [eigenbeleg-agent#6](https://github.com/Le-Space/eigenbeleg-agent/issues/6)):
4
+
5
+ - **a crypto asset: its price at 00:00 UTC of the day** – CoinGecko's daily snapshot; failing that, Kraken's daily candle, its open; before Kraken's candles, Kraken's first trade at or after midnight. With `prefer: 'kraken'` the order turns.
6
+ - **a currency:** the ECB reference rate of the day or the last before it, inverted; RUB after 2022-03-01 from the Bank of Russia, KZT from the National Bank of Kazakhstan, other currencies the Bank of Russia lists as a cross rate through the ruble (marked `cbr-cross`).
7
+ - **a token nobody prices:** its deepest Uniswap pool (V2, V3, V4) at the booking's block, against ETH or USDC, through an Alchemy key (`dex-rate`).
8
+
9
+ Every answer says its source and moment (`{ rate, usdRate, source, at }`), so a document can name them. Rates are decimal strings, never floats.
10
+
11
+ ```js
12
+ import { createRateService, createDexRates } from '@le-space/rates';
13
+
14
+ const rates = createRateService({
15
+ fetch, // the caller's; a browser needs a proxy for CORS
16
+ coingeckoKey: async () => key ?? null, // optional demo key
17
+ dex: createDexRates({ fetch, alchemyKey: async () => alchemy ?? null }) // optional
18
+ });
19
+ await rates.rate('AKT', '2026-09-01'); // { rate: '3.5', source: 'coingecko', at: '2026-09-01T00:00:00Z', … }
20
+ ```
21
+
22
+ ```bash
23
+ pnpm --filter @le-space/rates test # Node, and a scenario in Chromium
24
+ ```
25
+
26
+ Moved with its history from Belege's `bridge/src/rates.js` and `dex-rate.js`. AGPL-3.0-or-later.
package/package.json CHANGED
@@ -1,6 +1,41 @@
1
1
  {
2
2
  "name": "@le-space/rates",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "The euro rate of a day for Le Space's tools: the price at 00:00 UTC (CoinGecko, Kraken), the ECB reference rate (Bank of Russia, National Bank of Kazakhstan where it has none), a token's DEX pool at a block. Every answer names its source. Browser and Node; fetch and keys handed in.",
5
+ "license": "AGPL-3.0-or-later",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Le-Space/belege.git",
10
+ "directory": "packages/rates"
11
+ },
12
+ "homepage": "https://github.com/Le-Space/belege/tree/main/packages/rates#readme",
13
+ "sideEffects": false,
14
+ "exports": {
15
+ ".": "./src/index.js",
16
+ "./rates": "./src/rates.js",
17
+ "./dex-rate": "./src/dex-rate.js"
18
+ },
19
+ "files": [
20
+ "src"
21
+ ],
22
+ "engines": {
23
+ "node": ">=22"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public",
27
+ "provenance": true
28
+ },
29
+ "dependencies": {
30
+ "@noble/hashes": "2.4.0",
31
+ "@le-space/ledger": "^0.1.0"
32
+ },
33
+ "devDependencies": {
34
+ "prettier": "3.6.2",
35
+ "@le-space/browser-test": "0.1.0"
36
+ },
37
+ "scripts": {
38
+ "test": "node --test --test-reporter=spec \"test/*.test.js\"",
39
+ "lint": "prettier --check src test"
40
+ }
6
41
  }
@@ -0,0 +1,386 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // A token's rate from a DEX pool at the block of its booking (issue #163), for
3
+ // tokens CoinGecko does not price on that day. Read on chain through the
4
+ // caller's Alchemy key (Belege: `pnpm setup:alchemy`), so it needs no further service; without
5
+ // a key there is no pool rate.
6
+ //
7
+ // How:
8
+ // 1. the token's pools against ETH and against USDC:
9
+ // - Uniswap V2 (`getPair`) and V3 (`getPool`, 0.05 %, 0.3 %, 1 %) against
10
+ // WETH and USDC, from the factories below;
11
+ // - Uniswap V4 (one PoolManager for all pools, read through its
12
+ // `StateView`): the pool ids of the token against native ETH
13
+ // (`address(0)`) and USDC at the usual fee tiers, without hooks – a pool
14
+ // with hooks has an id nobody can guess and is not found;
15
+ // 2. how deep each pool is on its quote side at the booking's block: V2 the
16
+ // quote token it holds (`balanceOf`); V3 and V4 the reserve their active
17
+ // liquidity amounts to at the current price (`liquidity`, the square
18
+ // root price) – what moving the price costs, and alike for both, where a
19
+ // V3 pool's balance would count liquidity far from the price and V4 has
20
+ // no balance of its own – in euros, by the quote's rate of the day; the
21
+ // deepest counts, and only when it holds at least `minEur` (a thin
22
+ // pool's price is easily bent);
23
+ // 3. its price at that block: V2 `getReserves`, V3 `slot0`, V4 `getSlot0`;
24
+ // 4. times the quote's euro rate of the day (rates.js).
25
+ // Every call is an `eth_call` at that block, so the result can be recomputed
26
+ // by anyone: pool (or V4 pool id) and block go with the rate.
27
+ //
28
+ // Addresses checked against the chains: V2/V3 on 2026-09-28 (`getPair` /
29
+ // `getPool` of USDC/WETH give the well-known pools), V4's StateView on
30
+ // 2026-09-29 (the ETH/USDC pools at 0.05 % and 0.3 % give ~2,700 USDC per ETH).
31
+
32
+ import { keccak_256 } from '@noble/hashes/sha3.js';
33
+ import { fromHex, toHex, utf8 } from '@le-space/ledger/bytes';
34
+
35
+ /**
36
+ * @typedef {object} DexChain
37
+ * @property {string} network Alchemy's network, the host's first label
38
+ * @property {string} weth
39
+ * @property {string} usdc
40
+ * @property {string} v2Factory Uniswap V2
41
+ * @property {string} v3Factory Uniswap V3
42
+ * @property {string} v4StateView Uniswap V4's read-only view of its PoolManager
43
+ */
44
+
45
+ /** @type {Readonly<Record<string, DexChain>>} */
46
+ export const DEX_CHAINS = Object.freeze({
47
+ ethereum: {
48
+ network: 'eth-mainnet',
49
+ weth: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
50
+ usdc: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
51
+ v2Factory: '0x5c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f',
52
+ v3Factory: '0x1f98431c8ad98523631ae4a59f267346ea31f984',
53
+ v4StateView: '0x7ffe42c4a5deea5b0fec41c94c136cf115597227'
54
+ },
55
+ base: {
56
+ network: 'base-mainnet',
57
+ weth: '0x4200000000000000000000000000000000000006',
58
+ usdc: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
59
+ v2Factory: '0x8909dc15e40173ff4699343b6eb8132c65e18ec6',
60
+ v3Factory: '0x33128a8fc17869897dce68ed026d694621f6fdfd',
61
+ v4StateView: '0xa3c0c9b65bad0b08107aa264b0f3db444b867a71'
62
+ }
63
+ });
64
+
65
+ /** Uniswap V3's fee tiers, in hundredths of a basis point. */
66
+ const V3_FEES = [500, 3000, 10000];
67
+ /** Uniswap V4's usual fee tiers with their tick spacing. */
68
+ const V4_TIERS = [
69
+ [100, 1],
70
+ [500, 10],
71
+ [3000, 60],
72
+ [10000, 200]
73
+ ];
74
+ /** The least a pool must hold on its quote side, in euros, for its price to count. */
75
+ export const MIN_POOL_EUR = 3000;
76
+
77
+ /** @param {string} signature */
78
+ const selectorOf = (signature) => `0x${toHex(keccak_256(utf8(signature))).slice(0, 8)}`;
79
+
80
+ const SELECTOR = {
81
+ getPair: '0xe6a43905',
82
+ getPool: '0x1698ee82',
83
+ balanceOf: '0x70a08231',
84
+ token0: '0x0dfe1681',
85
+ getReserves: '0x0902f1ac',
86
+ slot0: '0x3850c7bd',
87
+ liquidity: '0x1a686502',
88
+ getSlot0: selectorOf('getSlot0(bytes32)'),
89
+ getLiquidity: selectorOf('getLiquidity(bytes32)')
90
+ };
91
+
92
+ const ADDRESS = /^0x[0-9a-f]{40}$/;
93
+ const ZERO = `0x${'0'.repeat(40)}`;
94
+
95
+ /** @param {string} a */
96
+ const word = (a) => a.replace(/^0x/, '').padStart(64, '0');
97
+ /** @param {bigint} n */
98
+ const uint = (n) => n.toString(16).padStart(64, '0');
99
+ /** The n-th 32-byte word of an ABI answer. @param {string} hex @param {number} n */
100
+ const nth = (hex, n) => BigInt(`0x${hex.replace(/^0x/, '').slice(n * 64, (n + 1) * 64) || '0'}`);
101
+ /** An address in an ABI answer's first word. @param {string} hex */
102
+ const addressOf = (hex) => `0x${hex.replace(/^0x/, '').slice(24, 64)}`.toLowerCase();
103
+
104
+ /**
105
+ * A Uniswap V4 pool's id: keccak256 of its key (currency0, currency1, fee,
106
+ * tickSpacing, hooks), the currencies sorted by address.
107
+ *
108
+ * @param {string} a
109
+ * @param {string} b
110
+ * @param {number} fee
111
+ * @param {number} tickSpacing
112
+ * @param {string} [hooks]
113
+ */
114
+ export function v4PoolId(a, b, fee, tickSpacing, hooks = ZERO) {
115
+ const [c0, c1] = BigInt(a) < BigInt(b) ? [a, b] : [b, a];
116
+ const key = word(c0) + word(c1) + uint(BigInt(fee)) + uint(BigInt(tickSpacing)) + word(hooks);
117
+ return `0x${toHex(keccak_256(fromHex(key)))}`;
118
+ }
119
+
120
+ /**
121
+ * `value / 10^scale` as a decimal string without trailing zeros.
122
+ *
123
+ * @param {bigint} value
124
+ * @param {number} scale
125
+ */
126
+ export function decimalOf(value, scale) {
127
+ const s = value.toString().padStart(scale + 1, '0');
128
+ const out = `${s.slice(0, -scale)}.${s.slice(-scale)}`;
129
+ return out.replace(/0+$/, '').replace(/\.$/, '');
130
+ }
131
+
132
+ /** A decimal string as `[value, scale]`. @param {string} d */
133
+ function scaled(d) {
134
+ const [int, frac = ''] = d.split('.');
135
+ return [BigInt(int + frac), frac.length];
136
+ }
137
+
138
+ /** A decimal string scaled by 10^18, cut after 18 places. @param {string} d */
139
+ const wei = (d) => {
140
+ const [int, frac = ''] = d.split('.');
141
+ return BigInt(int + frac.slice(0, 18).padEnd(18, '0'));
142
+ };
143
+
144
+ /**
145
+ * @typedef {{ version: 2, tokenIsToken0: boolean, reserve0: bigint, reserve1: bigint } | { version: 3 | 4, tokenIsToken0: boolean, sqrtPriceX96: bigint }} PoolState
146
+ */
147
+
148
+ /**
149
+ * The quote per whole token at a pool's price, scaled by 10^18.
150
+ *
151
+ * @param {PoolState} pool
152
+ * @param {number} decimals the token's
153
+ * @param {number} [quoteDecimals] the quote's (WETH/ETH 18, USDC 6)
154
+ * @returns {bigint}
155
+ */
156
+ export function quotePerToken(pool, decimals, quoteDecimals = 18) {
157
+ const up = 10n ** BigInt(decimals) * 10n ** 18n;
158
+ const down = 10n ** BigInt(quoteDecimals);
159
+ if (pool.version === 2) {
160
+ const [token, quote] = pool.tokenIsToken0
161
+ ? [pool.reserve0, pool.reserve1]
162
+ : [pool.reserve1, pool.reserve0];
163
+ if (token === 0n) return 0n;
164
+ return (quote * up) / (token * down);
165
+ }
166
+ // V3/V4: price = token1 per token0 in raw units = sqrtPriceX96² / 2^192.
167
+ const p2 = pool.sqrtPriceX96 * pool.sqrtPriceX96;
168
+ const q192 = 1n << 192n;
169
+ if (p2 === 0n) return 0n;
170
+ return pool.tokenIsToken0 ? (p2 * up) / (q192 * down) : (q192 * up) / (p2 * down);
171
+ }
172
+
173
+ /** ETH per whole token, in wei (a WETH-quoted pool). @param {PoolState} pool @param {number} decimals */
174
+ export const ethPerToken = (pool, decimals) => quotePerToken(pool, decimals, 18);
175
+
176
+ /**
177
+ * A V3/V4 pool's virtual reserve of one side in raw units, from its active
178
+ * liquidity: token0 = L·2^96/√P, token1 = L·√P/2^96.
179
+ *
180
+ * @param {bigint} liquidity
181
+ * @param {bigint} sqrtPriceX96
182
+ * @param {boolean} ofToken0
183
+ */
184
+ export function virtualReserve(liquidity, sqrtPriceX96, ofToken0) {
185
+ if (sqrtPriceX96 === 0n) return 0n;
186
+ return ofToken0 ? (liquidity << 96n) / sqrtPriceX96 : (liquidity * sqrtPriceX96) >> 96n;
187
+ }
188
+
189
+ /**
190
+ * @param {object} options
191
+ * @param {typeof fetch} [options.fetch]
192
+ * @param {() => Promise<string | null>} options.alchemyKey
193
+ * @param {(network: string) => string} [options.alchemyBaseUrl]
194
+ * @param {number} [options.minEur]
195
+ */
196
+ export function createDexRates({
197
+ fetch: f = fetch,
198
+ alchemyKey,
199
+ alchemyBaseUrl = (network) => `https://${network}.g.alchemy.com/v2`,
200
+ minEur = MIN_POOL_EUR
201
+ }) {
202
+ /**
203
+ * The token's price in a quote at a block, from its deepest pool; null when
204
+ * there is no pool, no key, or none deep enough.
205
+ *
206
+ * @param {{ chain: string, contract: string, block: number, decimals: number, quoteEur: (quote: 'ETH' | 'USDC') => Promise<string | null> }} params
207
+ * `quoteEur`: the quote's euro rate of the day, to weigh the pools
208
+ * @returns {Promise<{ price: string, quote: 'ETH' | 'USDC', pool: string, version: 2 | 3 | 4, block: number, at: string, depth: string } | null>}
209
+ */
210
+ async function poolPrice({ chain, contract, block, decimals, quoteEur }) {
211
+ const dex = Object.hasOwn(DEX_CHAINS, chain) ? DEX_CHAINS[chain] : null;
212
+ const token = String(contract).toLowerCase();
213
+ if (!dex || !ADDRESS.test(token) || !Number.isSafeInteger(block) || block <= 0) return null;
214
+ if (!Number.isInteger(decimals) || decimals < 0 || decimals > 36) return null;
215
+ const key = await alchemyKey().catch(() => null);
216
+ if (typeof key !== 'string' || !/^[A-Za-z0-9_-]{8,128}$/.test(key)) return null;
217
+ const url = `${alchemyBaseUrl(dex.network)}/${key}`;
218
+ const tag = `0x${block.toString(16)}`;
219
+
220
+ /** @param {string} to @param {string} data @returns {Promise<string>} */
221
+ async function ethCall(to, data) {
222
+ let res;
223
+ try {
224
+ res = await f(url, {
225
+ method: 'POST',
226
+ headers: { 'content-type': 'application/json' },
227
+ body: JSON.stringify({
228
+ jsonrpc: '2.0',
229
+ id: 1,
230
+ method: 'eth_call',
231
+ params: [{ to, data }, tag]
232
+ })
233
+ });
234
+ } catch {
235
+ // The URL holds the key: the message names the host only.
236
+ throw new Error(`${dex.network}.g.alchemy.com is not reachable`);
237
+ }
238
+ const body = await res.json().catch(() => null);
239
+ if (!res.ok || body?.error || typeof body?.result !== 'string') {
240
+ throw new Error(`${dex.network}.g.alchemy.com refused eth_call`);
241
+ }
242
+ return body.result;
243
+ }
244
+
245
+ const quotes = /** @type {const} */ ([
246
+ { symbol: 'ETH', erc20: dex.weth, v4: ZERO, decimals: 18 },
247
+ { symbol: 'USDC', erc20: dex.usdc, v4: dex.usdc, decimals: 6 }
248
+ ]);
249
+ /** @type {Map<string, bigint | null>} quote → EUR per whole unit, scaled by 10^18 */
250
+ const eurOf = new Map();
251
+ /** @param {'ETH' | 'USDC'} q */
252
+ const eurRate = async (q) => {
253
+ if (!eurOf.has(q)) {
254
+ const r = await quoteEur(q).catch(() => null);
255
+ eurOf.set(q, typeof r === 'string' && /^\d+(\.\d+)?$/.test(r) ? wei(r) : null);
256
+ }
257
+ return eurOf.get(q) ?? null;
258
+ };
259
+
260
+ // Each pool's depth first; its price only for the one that counts.
261
+ /** @type {{ pool: string, version: 2 | 3 | 4, quote: (typeof quotes)[number], depthRaw: bigint, state: () => Promise<PoolState> }[]} */
262
+ const found = [];
263
+ for (const q of quotes) {
264
+ const v2 = addressOf(
265
+ await ethCall(dex.v2Factory, SELECTOR.getPair + word(token) + word(q.erc20))
266
+ );
267
+ if (v2 !== ZERO) {
268
+ found.push({
269
+ pool: v2,
270
+ version: 2,
271
+ quote: q,
272
+ depthRaw: nth(await ethCall(q.erc20, SELECTOR.balanceOf + word(v2)), 0),
273
+ state: async () => {
274
+ const tokenIsToken0 = addressOf(await ethCall(v2, SELECTOR.token0)) === token;
275
+ const r = await ethCall(v2, SELECTOR.getReserves);
276
+ return { version: 2, tokenIsToken0, reserve0: nth(r, 0), reserve1: nth(r, 1) };
277
+ }
278
+ });
279
+ }
280
+ for (const fee of V3_FEES) {
281
+ const v3 = addressOf(
282
+ await ethCall(
283
+ dex.v3Factory,
284
+ SELECTOR.getPool + word(token) + word(q.erc20) + uint(BigInt(fee))
285
+ )
286
+ );
287
+ if (v3 === ZERO) continue;
288
+ const tokenIsToken0 = addressOf(await ethCall(v3, SELECTOR.token0)) === token;
289
+ const sqrt = nth(await ethCall(v3, SELECTOR.slot0), 0);
290
+ const liquidity = nth(await ethCall(v3, SELECTOR.liquidity), 0);
291
+ found.push({
292
+ pool: v3,
293
+ version: 3,
294
+ quote: q,
295
+ depthRaw: virtualReserve(liquidity, sqrt, !tokenIsToken0),
296
+ state: async () => ({ version: 3, tokenIsToken0, sqrtPriceX96: sqrt })
297
+ });
298
+ }
299
+ for (const [fee, spacing] of V4_TIERS) {
300
+ const id = v4PoolId(token, q.v4, fee, spacing);
301
+ const sqrt = nth(await ethCall(dex.v4StateView, SELECTOR.getSlot0 + id.slice(2)), 0);
302
+ // An empty slot: no such pool.
303
+ if (sqrt === 0n) continue;
304
+ const liquidity = nth(
305
+ await ethCall(dex.v4StateView, SELECTOR.getLiquidity + id.slice(2)),
306
+ 0
307
+ );
308
+ const tokenIsToken0 = BigInt(token) < BigInt(q.v4);
309
+ found.push({
310
+ pool: id,
311
+ version: 4,
312
+ quote: q,
313
+ // The quote is the other currency.
314
+ depthRaw: virtualReserve(liquidity, sqrt, !tokenIsToken0),
315
+ state: async () => ({ version: 4, tokenIsToken0, sqrtPriceX96: sqrt })
316
+ });
317
+ }
318
+ }
319
+
320
+ let best = null;
321
+ let bestEur = 0n;
322
+ for (const c of found) {
323
+ const rate = await eurRate(c.quote.symbol);
324
+ if (rate === null) continue;
325
+ // Euros on the quote side, scaled by 10^18.
326
+ const eur = (c.depthRaw * rate) / 10n ** BigInt(c.quote.decimals);
327
+ if (eur > bestEur) {
328
+ best = c;
329
+ bestEur = eur;
330
+ }
331
+ }
332
+ if (!best || bestEur < BigInt(minEur) * 10n ** 18n) return null;
333
+ const price = quotePerToken(await best.state(), decimals, best.quote.decimals);
334
+ if (price <= 0n) return null;
335
+
336
+ // When: the block's own time.
337
+ let at = '';
338
+ try {
339
+ const res = await f(url, {
340
+ method: 'POST',
341
+ headers: { 'content-type': 'application/json' },
342
+ body: JSON.stringify({
343
+ jsonrpc: '2.0',
344
+ id: 1,
345
+ method: 'eth_getBlockByNumber',
346
+ params: [tag, false]
347
+ })
348
+ });
349
+ const ts = Number(BigInt((await res.json())?.result?.timestamp ?? '0x0'));
350
+ if (ts > 0) at = new Date(ts * 1000).toISOString().replace(/\.000Z$/, 'Z');
351
+ } catch {
352
+ at = '';
353
+ }
354
+ return {
355
+ price: decimalOf(price, 18),
356
+ quote: best.quote.symbol,
357
+ pool: best.pool,
358
+ version: best.version,
359
+ block,
360
+ at,
361
+ depth: decimalOf(best.depthRaw, best.quote.decimals)
362
+ };
363
+ }
364
+
365
+ return { poolPrice };
366
+ }
367
+
368
+ /**
369
+ * EUR per token: ETH per token × EUR per ETH, as a decimal string with at
370
+ * most 18 decimals (a token can be worth a millionth of a cent).
371
+ *
372
+ * @param {string} ethPerTokenDecimal
373
+ * @param {string} eurPerEth
374
+ */
375
+ export function multiplyRates(ethPerTokenDecimal, eurPerEth) {
376
+ const [a, as] = scaled(ethPerTokenDecimal);
377
+ const [b, bs] = scaled(eurPerEth);
378
+ const product = a * b; // scale as + bs
379
+ const scale = as + bs;
380
+ const keep = 18;
381
+ const value =
382
+ scale > keep
383
+ ? (product + 5n * 10n ** BigInt(scale - keep - 1)) / 10n ** BigInt(scale - keep)
384
+ : product * 10n ** BigInt(keep - scale);
385
+ return decimalOf(value, keep);
386
+ }
package/src/index.js ADDED
@@ -0,0 +1,28 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // @le-space/rates: the euro rate of a day – the price at 00:00 UTC
3
+ // (CoinGecko's daily snapshot, else Kraken's daily open, else Kraken's first
4
+ // trade at or after midnight), the ECB reference rate for currencies (Bank of
5
+ // Russia, National Bank of Kazakhstan where the ECB has none), and a token's
6
+ // DEX pool at the booking's block where nobody prices it. Every answer names
7
+ // its source and moment. `fetch` and the keys are handed in.
8
+ export {
9
+ CBR_CROSS_CURRENCIES,
10
+ COINGECKO_PLATFORMS,
11
+ ECB_CURRENCIES,
12
+ RATE_SOURCES,
13
+ RateError,
14
+ createRateService,
15
+ invert,
16
+ toDecimal
17
+ } from './rates.js';
18
+ export {
19
+ DEX_CHAINS,
20
+ MIN_POOL_EUR,
21
+ createDexRates,
22
+ decimalOf,
23
+ ethPerToken,
24
+ multiplyRates,
25
+ quotePerToken,
26
+ v4PoolId,
27
+ virtualReserve
28
+ } from './dex-rate.js';