@morpho-dev/router 0.5.0 → 0.6.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/cli.js +2147 -989
- package/dist/drizzle/migrations/0023_remove-block-number-for-collaterals.sql +1 -0
- package/dist/drizzle/migrations/meta/0023_snapshot.json +1436 -0
- package/dist/drizzle/migrations/meta/_journal.json +7 -0
- package/dist/index.browser.d.mts +497 -162
- package/dist/index.browser.d.mts.map +1 -1
- package/dist/index.browser.d.ts +497 -162
- package/dist/index.browser.d.ts.map +1 -1
- package/dist/index.browser.js +881 -426
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +888 -427
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.node.d.mts +579 -174
- package/dist/index.node.d.mts.map +1 -1
- package/dist/index.node.d.ts +578 -173
- package/dist/index.node.d.ts.map +1 -1
- package/dist/index.node.js +6671 -5605
- package/dist/index.node.js.map +1 -1
- package/dist/index.node.mjs +6659 -5599
- package/dist/index.node.mjs.map +1 -1
- package/docs/integrator.md +7 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -16,7 +16,7 @@ import os from "node:os";
|
|
|
16
16
|
import path, { dirname, resolve } from "node:path";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
18
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
19
|
-
import { bytesToHex, createPublicClient, createWalletClient, decodeAbiParameters, encodeAbiParameters, erc20Abi, getAddress,
|
|
19
|
+
import { bytesToHex, createPublicClient, createWalletClient, decodeAbiParameters, encodeAbiParameters, erc20Abi, getAddress, hashTypedData, hexToBytes, http, isAddress, isHex, keccak256, maxUint256, numberToHex, pad, parseAbi, parseEventLogs, publicActions, recoverAddress, stringify, toHex, zeroAddress } from "viem";
|
|
20
20
|
import { mnemonicToAccount, privateKeyToAccount } from "viem/accounts";
|
|
21
21
|
import { getBlock, getBlockNumber, getLogs, multicall } from "viem/actions";
|
|
22
22
|
import { anvil, base, mainnet } from "viem/chains";
|
|
@@ -26,13 +26,13 @@ import { StandardMerkleTree } from "@openzeppelin/merkle-tree";
|
|
|
26
26
|
import { gzip, ungzip } from "pako";
|
|
27
27
|
import { serve } from "@hono/node-server";
|
|
28
28
|
import { Hono } from "hono";
|
|
29
|
-
import {
|
|
29
|
+
import crypto, { createHash } from "node:crypto";
|
|
30
30
|
import { z as z$1 } from "zod/v4";
|
|
31
31
|
import "reflect-metadata";
|
|
32
32
|
import { generateDocument } from "openapi-metadata";
|
|
33
33
|
import { ApiBody, ApiOperation, ApiParam, ApiProperty, ApiQuery, ApiResponse, ApiTags } from "openapi-metadata/decorators";
|
|
34
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
34
35
|
import dotenv from "dotenv";
|
|
35
|
-
import crypto from "node:crypto";
|
|
36
36
|
import { PGlite } from "@electric-sql/pglite";
|
|
37
37
|
import { drizzle } from "drizzle-orm/node-postgres";
|
|
38
38
|
import { migrate } from "drizzle-orm/node-postgres/migrator";
|
|
@@ -152,7 +152,7 @@ function startActiveSpan(tracer, name, fn) {
|
|
|
152
152
|
//#endregion
|
|
153
153
|
//#region package.json
|
|
154
154
|
var name = "@morpho-dev/router";
|
|
155
|
-
var version = "0.
|
|
155
|
+
var version = "0.6.0";
|
|
156
156
|
var description = "Router package for Morpho protocol";
|
|
157
157
|
|
|
158
158
|
//#endregion
|
|
@@ -1469,20 +1469,14 @@ async function run(parameters) {
|
|
|
1469
1469
|
* @param parameters - Gatekeeper parameters. {@link GatekeeperParameters}
|
|
1470
1470
|
* @returns Gatekeeper instance. {@link Gatekeeper}
|
|
1471
1471
|
*/
|
|
1472
|
-
function create$
|
|
1472
|
+
function create$20(parameters) {
|
|
1473
1473
|
const { rules } = parameters;
|
|
1474
|
-
return {
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
},
|
|
1481
|
-
getRules: async () => rules.map((rule) => ({
|
|
1482
|
-
name: rule.name,
|
|
1483
|
-
description: rule.description
|
|
1484
|
-
}))
|
|
1485
|
-
};
|
|
1474
|
+
return { isAllowed: async (offers) => {
|
|
1475
|
+
return await run({
|
|
1476
|
+
items: offers,
|
|
1477
|
+
rules
|
|
1478
|
+
});
|
|
1479
|
+
} };
|
|
1486
1480
|
}
|
|
1487
1481
|
|
|
1488
1482
|
//#endregion
|
|
@@ -1689,17 +1683,6 @@ function getCallbackType(chain, address) {
|
|
|
1689
1683
|
return configs[chain].callbacks?.find((c) => c.type !== Type$1.BuyWithEmptyCallback && c.addresses.includes(address?.toLowerCase()))?.type;
|
|
1690
1684
|
}
|
|
1691
1685
|
/**
|
|
1692
|
-
* Returns the callback addresses for a given chain and callback type, if it exists.
|
|
1693
|
-
* @param chain - Chain name for which to read the validation configuration
|
|
1694
|
-
* @param type - Callback type to retrieve
|
|
1695
|
-
* @returns The matching callback addresses or an empty array if not configured
|
|
1696
|
-
*/
|
|
1697
|
-
function getCallbackTypeAddresses(chain, type) {
|
|
1698
|
-
if (type === Type$1.BuyWithEmptyCallback) return [];
|
|
1699
|
-
const match = configs[chain].callbacks?.find((c) => c.type === type);
|
|
1700
|
-
return match && "addresses" in match ? match.addresses : [];
|
|
1701
|
-
}
|
|
1702
|
-
/**
|
|
1703
1686
|
* Returns the list of allowed non-empty callback addresses for a chain.
|
|
1704
1687
|
*
|
|
1705
1688
|
* @param chain - Chain name
|
|
@@ -1836,7 +1819,7 @@ const Oracle = [{
|
|
|
1836
1819
|
* @param chains - Array of chain objects to register.
|
|
1837
1820
|
* @returns A registry for looking up chains by ID. {@link ChainRegistry}
|
|
1838
1821
|
*/
|
|
1839
|
-
function create$
|
|
1822
|
+
function create$19(chains) {
|
|
1840
1823
|
const byId = /* @__PURE__ */ new Map();
|
|
1841
1824
|
for (const chain of chains) byId.set(chain.id, chain);
|
|
1842
1825
|
return {
|
|
@@ -2158,196 +2141,6 @@ var CollateralsAreNotSortedError = class extends BaseError {
|
|
|
2158
2141
|
}
|
|
2159
2142
|
};
|
|
2160
2143
|
|
|
2161
|
-
//#endregion
|
|
2162
|
-
//#region src/core/Tree.ts
|
|
2163
|
-
const VERSION$1 = 1;
|
|
2164
|
-
const normalizeHash = (hash) => hash.toLowerCase();
|
|
2165
|
-
/**
|
|
2166
|
-
* Builds a Merkle tree from a list of offers.
|
|
2167
|
-
*
|
|
2168
|
-
* Leaves are the offer `hash` values as `bytes32` and are deterministically
|
|
2169
|
-
* ordered following the StandardMerkleTree leaf ordering so that the resulting
|
|
2170
|
-
* root is stable regardless of the input order.
|
|
2171
|
-
*
|
|
2172
|
-
* @param offers - Offers to include in the tree.
|
|
2173
|
-
* @returns A `StandardMerkleTree` of `bytes32` leaves representing the offers.
|
|
2174
|
-
* @throws {TreeError} If tree building fails due to offer inconsistencies.
|
|
2175
|
-
*/
|
|
2176
|
-
const from$12 = (offers) => {
|
|
2177
|
-
const leaves = offers.map((offer) => [hash(offer)]);
|
|
2178
|
-
const tree = StandardMerkleTree.of(leaves, ["bytes32"]);
|
|
2179
|
-
const orderedOffers = orderOffers(tree, offers);
|
|
2180
|
-
return Object.assign(tree, { offers: orderedOffers });
|
|
2181
|
-
};
|
|
2182
|
-
const orderOffers = (tree, offers) => {
|
|
2183
|
-
const offerByHash = /* @__PURE__ */ new Map();
|
|
2184
|
-
for (const offer of offers) offerByHash.set(normalizeHash(hash(offer)), offer);
|
|
2185
|
-
const entries = tree.dump().values.map((value) => {
|
|
2186
|
-
const hash = normalizeHash(value.value[0]);
|
|
2187
|
-
const offer = offerByHash.get(hash);
|
|
2188
|
-
if (!offer) throw new TreeError(`missing offer for leaf ${hash}`);
|
|
2189
|
-
return {
|
|
2190
|
-
offer,
|
|
2191
|
-
treeIndex: value.treeIndex
|
|
2192
|
-
};
|
|
2193
|
-
});
|
|
2194
|
-
entries.sort((a, b) => b.treeIndex - a.treeIndex);
|
|
2195
|
-
return entries.map((item) => item.offer);
|
|
2196
|
-
};
|
|
2197
|
-
/**
|
|
2198
|
-
* Generates merkle proofs for all offers in a tree.
|
|
2199
|
-
*
|
|
2200
|
-
* Each proof allows independent verification that an offer is included in the tree
|
|
2201
|
-
* without requiring the full tree. Proofs are ordered by StandardMerkleTree leaf ordering.
|
|
2202
|
-
*
|
|
2203
|
-
* @param tree - The {@link Tree} to generate proofs for.
|
|
2204
|
-
* @returns Array of proofs - {@link Proof}
|
|
2205
|
-
*/
|
|
2206
|
-
const proofs = (tree) => {
|
|
2207
|
-
return tree.offers.map((offer) => {
|
|
2208
|
-
return {
|
|
2209
|
-
offer,
|
|
2210
|
-
path: tree.getProof([hash(offer)])
|
|
2211
|
-
};
|
|
2212
|
-
});
|
|
2213
|
-
};
|
|
2214
|
-
const assertHex = (value, expectedBytes, name) => {
|
|
2215
|
-
if (typeof value !== "string" || !isHex(value)) throw new DecodeError(`${name} is not a valid hex string`);
|
|
2216
|
-
if (hexToBytes(value).length !== expectedBytes) throw new DecodeError(`${name}: expected ${expectedBytes} bytes`);
|
|
2217
|
-
};
|
|
2218
|
-
const verifySignatureAndRecoverAddress = async (params) => {
|
|
2219
|
-
const { root, signature } = params;
|
|
2220
|
-
assertHex(signature, 65, "signature");
|
|
2221
|
-
const hash = hashMessage({ raw: root });
|
|
2222
|
-
try {
|
|
2223
|
-
return await recoverAddress({
|
|
2224
|
-
hash,
|
|
2225
|
-
signature
|
|
2226
|
-
});
|
|
2227
|
-
} catch {
|
|
2228
|
-
throw new DecodeError("signature recovery failed");
|
|
2229
|
-
}
|
|
2230
|
-
};
|
|
2231
|
-
/**
|
|
2232
|
-
* Encodes a merkle tree without a signature into hex payload for client-side signing.
|
|
2233
|
-
*
|
|
2234
|
-
* Layout: `0x{vv}{gzip([...offers])}{root}` where:
|
|
2235
|
-
* - `{vv}`: 1-byte version (currently 0x01)
|
|
2236
|
-
* - `{gzip([...offers])}`: gzipped JSON array of serialized offers
|
|
2237
|
-
* - `{root}`: 32-byte merkle root
|
|
2238
|
-
*
|
|
2239
|
-
* Validates root integrity before encoding.
|
|
2240
|
-
*
|
|
2241
|
-
* @param tree - Merkle tree of offers
|
|
2242
|
-
* @returns Hex-encoded unsigned payload
|
|
2243
|
-
* @throws {EncodeError} If root mismatch
|
|
2244
|
-
*/
|
|
2245
|
-
const encodeUnsigned = (tree) => {
|
|
2246
|
-
validateTreeForEncoding(tree);
|
|
2247
|
-
return bytesToHex(encodeUnsignedBytes(tree));
|
|
2248
|
-
};
|
|
2249
|
-
const validateTreeForEncoding = (tree) => {
|
|
2250
|
-
if (VERSION$1 > 255) throw new EncodeError(`version overflow: ${VERSION$1} exceeds 255`);
|
|
2251
|
-
const computed = from$12(tree.offers);
|
|
2252
|
-
if (tree.root !== computed.root) throw new EncodeError(`root mismatch: expected ${computed.root}, got ${tree.root}`);
|
|
2253
|
-
};
|
|
2254
|
-
const encodeUnsignedBytes = (tree) => {
|
|
2255
|
-
const offersPayload = tree.offers.map(serialize);
|
|
2256
|
-
const compressed = gzip(JSON.stringify(offersPayload));
|
|
2257
|
-
const rootBytes = hexToBytes(tree.root);
|
|
2258
|
-
const encoded = new Uint8Array(1 + compressed.length + 32);
|
|
2259
|
-
encoded[0] = VERSION$1;
|
|
2260
|
-
encoded.set(compressed, 1);
|
|
2261
|
-
encoded.set(rootBytes, 1 + compressed.length);
|
|
2262
|
-
return encoded;
|
|
2263
|
-
};
|
|
2264
|
-
/**
|
|
2265
|
-
* Decodes hex calldata into a validated merkle tree.
|
|
2266
|
-
*
|
|
2267
|
-
* Validates signature before decompression for fail-fast rejection of invalid payloads.
|
|
2268
|
-
* Returns the tree with separately validated signature and recovered signer address.
|
|
2269
|
-
*
|
|
2270
|
-
* Validation order:
|
|
2271
|
-
* 1. Version check
|
|
2272
|
-
* 2. Signature verification (fail-fast, before decompression)
|
|
2273
|
-
* 3. Decompression (only if signature valid)
|
|
2274
|
-
* 4. Root verification (computed from offers vs embedded root)
|
|
2275
|
-
*
|
|
2276
|
-
* @example
|
|
2277
|
-
* ```typescript
|
|
2278
|
-
* const { tree, signature, signer } = await Tree.decode(calldata);
|
|
2279
|
-
* console.log(`Tree signed by ${signer} with ${tree.offers.length} offers`);
|
|
2280
|
-
* ```
|
|
2281
|
-
*
|
|
2282
|
-
* @param encoded - Hex calldata in format `0x{vv}{gzip}{root}{signature}`
|
|
2283
|
-
* @returns Validated tree, signature, and recovered signer address
|
|
2284
|
-
* @throws {DecodeError} If version invalid, signature invalid, or root mismatch
|
|
2285
|
-
*/
|
|
2286
|
-
const decode = async (encoded) => {
|
|
2287
|
-
const bytes = hexToBytes(encoded);
|
|
2288
|
-
if (bytes.length < 98) throw new DecodeError("payload too short");
|
|
2289
|
-
const version = bytes[0];
|
|
2290
|
-
if (version !== (VERSION$1 & 255)) throw new DecodeError(`invalid version: expected ${VERSION$1}, got ${version ?? 0}`);
|
|
2291
|
-
const signature = bytesToHex(bytes.slice(-65));
|
|
2292
|
-
const root = bytesToHex(bytes.slice(-97, -65));
|
|
2293
|
-
assertHex(root, 32, "root");
|
|
2294
|
-
assertHex(signature, 65, "signature");
|
|
2295
|
-
const signer = await verifySignatureAndRecoverAddress({
|
|
2296
|
-
root,
|
|
2297
|
-
signature
|
|
2298
|
-
});
|
|
2299
|
-
const compressed = bytes.slice(1, -97);
|
|
2300
|
-
let decoded;
|
|
2301
|
-
try {
|
|
2302
|
-
decoded = ungzip(compressed, { to: "string" });
|
|
2303
|
-
} catch {
|
|
2304
|
-
throw new DecodeError("decompression failed");
|
|
2305
|
-
}
|
|
2306
|
-
let rawOffers;
|
|
2307
|
-
try {
|
|
2308
|
-
rawOffers = JSON.parse(decoded);
|
|
2309
|
-
} catch {
|
|
2310
|
-
throw new DecodeError("JSON parse failed");
|
|
2311
|
-
}
|
|
2312
|
-
const tree = from$12(rawOffers.map((o) => OfferSchema().parse(o)));
|
|
2313
|
-
if (root !== tree.root) throw new DecodeError(`root mismatch: expected ${tree.root}, got ${root}`);
|
|
2314
|
-
return {
|
|
2315
|
-
tree,
|
|
2316
|
-
signature,
|
|
2317
|
-
signer
|
|
2318
|
-
};
|
|
2319
|
-
};
|
|
2320
|
-
/**
|
|
2321
|
-
* Error thrown during tree building operations.
|
|
2322
|
-
* Indicates structural issues with the tree (missing offers, inconsistent state).
|
|
2323
|
-
*/
|
|
2324
|
-
var TreeError = class extends BaseError {
|
|
2325
|
-
name = "Tree.TreeError";
|
|
2326
|
-
constructor(reason) {
|
|
2327
|
-
super(`Tree error: ${reason}`);
|
|
2328
|
-
}
|
|
2329
|
-
};
|
|
2330
|
-
/**
|
|
2331
|
-
* Error thrown during tree encoding.
|
|
2332
|
-
* Indicates validation failures (signature, root mismatch, mixed makers).
|
|
2333
|
-
*/
|
|
2334
|
-
var EncodeError = class extends BaseError {
|
|
2335
|
-
name = "Tree.EncodeError";
|
|
2336
|
-
constructor(reason) {
|
|
2337
|
-
super(`Failed to encode tree: ${reason}`);
|
|
2338
|
-
}
|
|
2339
|
-
};
|
|
2340
|
-
/**
|
|
2341
|
-
* Error thrown during tree decoding.
|
|
2342
|
-
* Indicates payload corruption, version mismatch, or validation failures.
|
|
2343
|
-
*/
|
|
2344
|
-
var DecodeError = class extends BaseError {
|
|
2345
|
-
name = "Tree.DecodeError";
|
|
2346
|
-
constructor(reason) {
|
|
2347
|
-
super(`Failed to decode tree: ${reason}`);
|
|
2348
|
-
}
|
|
2349
|
-
};
|
|
2350
|
-
|
|
2351
2144
|
//#endregion
|
|
2352
2145
|
//#region src/core/Offer.ts
|
|
2353
2146
|
/** Internal symbol for caching the computed hash. */
|
|
@@ -2399,7 +2192,7 @@ const OfferSchema = () => {
|
|
|
2399
2192
|
* @param input - The offer to create.
|
|
2400
2193
|
* @returns The created offer.
|
|
2401
2194
|
*/
|
|
2402
|
-
function from$
|
|
2195
|
+
function from$12(input) {
|
|
2403
2196
|
try {
|
|
2404
2197
|
return OfferSchema().parse(input);
|
|
2405
2198
|
} catch (error) {
|
|
@@ -2413,7 +2206,7 @@ function from$11(input) {
|
|
|
2413
2206
|
* @returns The created offer.
|
|
2414
2207
|
*/
|
|
2415
2208
|
function fromSnakeCase(input) {
|
|
2416
|
-
return from$
|
|
2209
|
+
return from$12(fromSnakeCase$1(input));
|
|
2417
2210
|
}
|
|
2418
2211
|
/**
|
|
2419
2212
|
* Converts an offer to a snake case object.
|
|
@@ -2507,7 +2300,7 @@ function random(config) {
|
|
|
2507
2300
|
})
|
|
2508
2301
|
};
|
|
2509
2302
|
})();
|
|
2510
|
-
return from$
|
|
2303
|
+
return from$12({
|
|
2511
2304
|
maker: config?.maker ?? address(),
|
|
2512
2305
|
assets: assetsScaled,
|
|
2513
2306
|
obligationUnits: config?.obligationUnits ?? 0n,
|
|
@@ -2738,7 +2531,7 @@ var InvalidOfferError = class InvalidOfferError extends BaseError {
|
|
|
2738
2531
|
* @param data - The data to create the oracle from.
|
|
2739
2532
|
* @returns The created oracle.
|
|
2740
2533
|
*/
|
|
2741
|
-
function from$
|
|
2534
|
+
function from$11(data) {
|
|
2742
2535
|
return {
|
|
2743
2536
|
chainId: data.chainId,
|
|
2744
2537
|
address: data.address.toLowerCase(),
|
|
@@ -2772,7 +2565,7 @@ let Type = /* @__PURE__ */ function(Type) {
|
|
|
2772
2565
|
* @param parameters - {@link from.Parameters}
|
|
2773
2566
|
* @returns The created Position. {@link from.ReturnType}
|
|
2774
2567
|
*/
|
|
2775
|
-
function from$
|
|
2568
|
+
function from$10(parameters) {
|
|
2776
2569
|
return {
|
|
2777
2570
|
chainId: parameters.chainId,
|
|
2778
2571
|
contract: parameters.contract.toLowerCase(),
|
|
@@ -2803,7 +2596,7 @@ const QuoteSchema = z$2.object({
|
|
|
2803
2596
|
* const quote = Quote.from({ obligationId: "0x123", ask: { price: 100n }, bid: { price: 100n } });
|
|
2804
2597
|
* ```
|
|
2805
2598
|
*/
|
|
2806
|
-
function from$
|
|
2599
|
+
function from$9(parameters) {
|
|
2807
2600
|
try {
|
|
2808
2601
|
const parsedQuote = QuoteSchema.parse(parameters);
|
|
2809
2602
|
return {
|
|
@@ -2841,7 +2634,7 @@ const WAD = 10n ** 18n;
|
|
|
2841
2634
|
* const transfer = Transfer.from({ id: "1", chainId: 1, contract: "0x123", from: "0x456", to: "0x789", value: 100n, blockNumber: 100n });
|
|
2842
2635
|
* ```
|
|
2843
2636
|
*/
|
|
2844
|
-
function from$
|
|
2637
|
+
function from$8(parameters) {
|
|
2845
2638
|
return {
|
|
2846
2639
|
id: parameters.id,
|
|
2847
2640
|
chainId: parameters.chainId,
|
|
@@ -2854,51 +2647,288 @@ function from$7(parameters) {
|
|
|
2854
2647
|
}
|
|
2855
2648
|
|
|
2856
2649
|
//#endregion
|
|
2857
|
-
//#region src/core/
|
|
2858
|
-
const
|
|
2859
|
-
|
|
2860
|
-
//#endregion
|
|
2861
|
-
//#region src/gatekeeper/Rules.ts
|
|
2862
|
-
const chains$1 = ({ chains }) => single("chain_ids", `Validates that offer chain is one of: [${chains.map((c) => c.id).join(", ")}]`, (offer) => {
|
|
2863
|
-
const allowedChainIds = chains.map((c) => c.id);
|
|
2864
|
-
if (!allowedChainIds.some((id) => id === offer.chainId)) return { message: `Chain ID ${offer.chainId} is not in the allowed chains (${allowedChainIds.join(", ")})` };
|
|
2865
|
-
});
|
|
2866
|
-
const maturity = ({ maturities }) => single("maturity", `Validates that offer maturity is one of: [${maturities.join(", ")}]`, (offer) => {
|
|
2867
|
-
const allowedMaturities = maturities.map((m) => from$16(m));
|
|
2868
|
-
if (!allowedMaturities.includes(offer.maturity)) return { message: `Maturity must be end of current month (${allowedMaturities[0]}) or end of next month (${allowedMaturities[1]}). Got: ${offer.maturity}` };
|
|
2869
|
-
});
|
|
2870
|
-
const callback = ({ callbacks, allowedAddresses }) => single("callback", `Validates callbacks: buy empty callback is ${callbacks.includes(Type$1.BuyWithEmptyCallback) ? "allowed" : "not allowed"}; sell offers must use a non-empty callback; non-empty callbacks must target one of [${allowedAddresses.map((a) => a.toLowerCase()).join(", ")}]`, (offer) => {
|
|
2871
|
-
if (isEmptyCallback(offer) && offer.buy && !callbacks?.find((c) => c === Type$1.BuyWithEmptyCallback)) return { message: "Buy offers with empty callback not allowed." };
|
|
2872
|
-
if (isEmptyCallback(offer) && !offer.buy) return { message: "Sell offers require a non-empty callback." };
|
|
2873
|
-
if (!isEmptyCallback(offer)) {
|
|
2874
|
-
if (!allowedAddresses.includes(offer.callback.address?.toLowerCase())) return { message: `Callback address ${offer.callback.address} is not allowed.` };
|
|
2875
|
-
}
|
|
2876
|
-
});
|
|
2650
|
+
//#region src/core/Tree.ts
|
|
2651
|
+
const VERSION$1 = 1;
|
|
2877
2652
|
/**
|
|
2878
|
-
*
|
|
2879
|
-
* @param assetsByChainId - Allowed assets indexed by chain id.
|
|
2880
|
-
* @returns The issue that was found. If the offer is valid, this will be undefined.
|
|
2653
|
+
* EIP-712 types for signing the tree root (Root(bytes32 root)).
|
|
2881
2654
|
*/
|
|
2882
|
-
const
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2655
|
+
const signatureTypes = {
|
|
2656
|
+
EIP712Domain: [{
|
|
2657
|
+
name: "chainId",
|
|
2658
|
+
type: "uint256"
|
|
2659
|
+
}, {
|
|
2660
|
+
name: "verifyingContract",
|
|
2661
|
+
type: "address"
|
|
2662
|
+
}],
|
|
2663
|
+
Root: [{
|
|
2664
|
+
name: "root",
|
|
2665
|
+
type: "bytes32"
|
|
2666
|
+
}]
|
|
2667
|
+
};
|
|
2668
|
+
const normalizeHash = (hash) => hash.toLowerCase();
|
|
2888
2669
|
/**
|
|
2889
|
-
*
|
|
2890
|
-
*
|
|
2891
|
-
*
|
|
2670
|
+
* Builds a Merkle tree from a list of offers.
|
|
2671
|
+
*
|
|
2672
|
+
* Leaves are the offer `hash` values as `bytes32` and are deterministically
|
|
2673
|
+
* ordered following the StandardMerkleTree leaf ordering so that the resulting
|
|
2674
|
+
* root is stable regardless of the input order.
|
|
2675
|
+
*
|
|
2676
|
+
* @param offers - Offers to include in the tree.
|
|
2677
|
+
* @returns A `StandardMerkleTree` of `bytes32` leaves representing the offers.
|
|
2678
|
+
* @throws {TreeError} If tree building fails due to offer inconsistencies.
|
|
2892
2679
|
*/
|
|
2893
|
-
const
|
|
2894
|
-
const
|
|
2895
|
-
|
|
2896
|
-
const
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2680
|
+
const from$7 = (offers) => {
|
|
2681
|
+
const leaves = offers.map((offer) => [hash(offer)]);
|
|
2682
|
+
const tree = StandardMerkleTree.of(leaves, ["bytes32"]);
|
|
2683
|
+
const orderedOffers = orderOffers(tree, offers);
|
|
2684
|
+
return Object.assign(tree, { offers: orderedOffers });
|
|
2685
|
+
};
|
|
2686
|
+
const orderOffers = (tree, offers) => {
|
|
2687
|
+
const offerByHash = /* @__PURE__ */ new Map();
|
|
2688
|
+
for (const offer of offers) offerByHash.set(normalizeHash(hash(offer)), offer);
|
|
2689
|
+
const entries = tree.dump().values.map((value) => {
|
|
2690
|
+
const hash = normalizeHash(value.value[0]);
|
|
2691
|
+
const offer = offerByHash.get(hash);
|
|
2692
|
+
if (!offer) throw new TreeError(`missing offer for leaf ${hash}`);
|
|
2693
|
+
return {
|
|
2694
|
+
offer,
|
|
2695
|
+
treeIndex: value.treeIndex
|
|
2696
|
+
};
|
|
2697
|
+
});
|
|
2698
|
+
entries.sort((a, b) => b.treeIndex - a.treeIndex);
|
|
2699
|
+
return entries.map((item) => item.offer);
|
|
2700
|
+
};
|
|
2701
|
+
/**
|
|
2702
|
+
* Generates merkle proofs for all offers in a tree.
|
|
2703
|
+
*
|
|
2704
|
+
* Each proof allows independent verification that an offer is included in the tree
|
|
2705
|
+
* without requiring the full tree. Proofs are ordered by StandardMerkleTree leaf ordering.
|
|
2706
|
+
*
|
|
2707
|
+
* @param tree - The {@link Tree} to generate proofs for.
|
|
2708
|
+
* @returns Array of proofs - {@link Proof}
|
|
2709
|
+
*/
|
|
2710
|
+
const proofs = (tree) => {
|
|
2711
|
+
return tree.offers.map((offer) => {
|
|
2712
|
+
return {
|
|
2713
|
+
offer,
|
|
2714
|
+
path: tree.getProof([hash(offer)])
|
|
2715
|
+
};
|
|
2716
|
+
});
|
|
2717
|
+
};
|
|
2718
|
+
const normalizeSignatureDomain = (domain, errorFactory) => {
|
|
2719
|
+
let chainId;
|
|
2720
|
+
try {
|
|
2721
|
+
chainId = typeof domain.chainId === "bigint" ? domain.chainId : BigInt(domain.chainId);
|
|
2722
|
+
} catch {
|
|
2723
|
+
throw errorFactory("invalid chainId");
|
|
2724
|
+
}
|
|
2725
|
+
if (chainId < 0n) throw errorFactory("invalid chainId");
|
|
2726
|
+
if (!isAddress(domain.verifyingContract)) throw errorFactory("invalid verifyingContract");
|
|
2727
|
+
return {
|
|
2728
|
+
chainId,
|
|
2729
|
+
verifyingContract: domain.verifyingContract.toLowerCase()
|
|
2730
|
+
};
|
|
2731
|
+
};
|
|
2732
|
+
const assertHex = (value, expectedBytes, name, errorFactory = (reason) => new DecodeError(reason)) => {
|
|
2733
|
+
if (typeof value !== "string" || !isHex(value)) throw errorFactory(`${name} is not a valid hex string`);
|
|
2734
|
+
if (hexToBytes(value).length !== expectedBytes) throw errorFactory(`${name}: expected ${expectedBytes} bytes`);
|
|
2735
|
+
};
|
|
2736
|
+
const verifySignatureAndRecoverAddress = async (params) => {
|
|
2737
|
+
const { root, signature, domain, errorFactory } = params;
|
|
2738
|
+
assertHex(root, 32, "root", errorFactory);
|
|
2739
|
+
assertHex(signature, 65, "signature", errorFactory);
|
|
2740
|
+
const hash = hashTypedData({
|
|
2741
|
+
domain,
|
|
2742
|
+
types: signatureTypes,
|
|
2743
|
+
primaryType: "Root",
|
|
2744
|
+
message: { root }
|
|
2745
|
+
});
|
|
2746
|
+
try {
|
|
2747
|
+
return await recoverAddress({
|
|
2748
|
+
hash,
|
|
2749
|
+
signature
|
|
2750
|
+
});
|
|
2751
|
+
} catch {
|
|
2752
|
+
throw errorFactory("signature recovery failed");
|
|
2753
|
+
}
|
|
2754
|
+
};
|
|
2755
|
+
/**
|
|
2756
|
+
* Encodes a merkle tree without a signature into hex payload for client-side signing.
|
|
2757
|
+
*
|
|
2758
|
+
* Layout: `0x{vv}{gzip([...offers])}{root}` where:
|
|
2759
|
+
* - `{vv}`: 1-byte version (currently 0x01)
|
|
2760
|
+
* - `{gzip([...offers])}`: gzipped JSON array of serialized offers
|
|
2761
|
+
* - `{root}`: 32-byte merkle root
|
|
2762
|
+
*
|
|
2763
|
+
* Validates root integrity before encoding.
|
|
2764
|
+
*
|
|
2765
|
+
* @param tree - Merkle tree of offers
|
|
2766
|
+
* @returns Hex-encoded unsigned payload
|
|
2767
|
+
* @throws {EncodeError} If root mismatch
|
|
2768
|
+
*/
|
|
2769
|
+
const encodeUnsigned = (tree) => {
|
|
2770
|
+
validateTreeForEncoding(tree);
|
|
2771
|
+
return bytesToHex(encodeUnsignedBytes(tree));
|
|
2772
|
+
};
|
|
2773
|
+
const validateTreeForEncoding = (tree, domain) => {
|
|
2774
|
+
if (VERSION$1 > 255) throw new EncodeError(`version overflow: ${VERSION$1} exceeds 255`);
|
|
2775
|
+
const computed = from$7(tree.offers);
|
|
2776
|
+
if (tree.root !== computed.root) throw new EncodeError(`root mismatch: expected ${computed.root}, got ${tree.root}`);
|
|
2777
|
+
if (domain) {
|
|
2778
|
+
const mismatched = tree.offers.find((offer) => BigInt(offer.chainId) !== domain.chainId);
|
|
2779
|
+
if (mismatched) throw new EncodeError(`chainId mismatch: expected ${domain.chainId}, got ${mismatched.chainId}`);
|
|
2780
|
+
}
|
|
2781
|
+
};
|
|
2782
|
+
const encodeUnsignedBytes = (tree) => {
|
|
2783
|
+
const offersPayload = tree.offers.map(serialize);
|
|
2784
|
+
const compressed = gzip(JSON.stringify(offersPayload));
|
|
2785
|
+
const rootBytes = hexToBytes(tree.root);
|
|
2786
|
+
const encoded = new Uint8Array(1 + compressed.length + 32);
|
|
2787
|
+
encoded[0] = VERSION$1;
|
|
2788
|
+
encoded.set(compressed, 1);
|
|
2789
|
+
encoded.set(rootBytes, 1 + compressed.length);
|
|
2790
|
+
return encoded;
|
|
2791
|
+
};
|
|
2792
|
+
/**
|
|
2793
|
+
* Decodes hex calldata into a validated merkle tree.
|
|
2794
|
+
*
|
|
2795
|
+
* Validates signature before decompression for fail-fast rejection of invalid payloads.
|
|
2796
|
+
* Returns the tree with separately validated signature and recovered signer address.
|
|
2797
|
+
*
|
|
2798
|
+
* Validation order:
|
|
2799
|
+
* 1. Version check
|
|
2800
|
+
* 2. Signature verification (fail-fast, before decompression)
|
|
2801
|
+
* 3. Decompression (only if signature valid)
|
|
2802
|
+
* 4. Root verification (computed from offers vs embedded root)
|
|
2803
|
+
*
|
|
2804
|
+
* @example
|
|
2805
|
+
* ```typescript
|
|
2806
|
+
* const { tree, signature, signer } = await Tree.decode(calldata, { chainId, verifyingContract });
|
|
2807
|
+
* console.log(`Tree signed by ${signer} with ${tree.offers.length} offers`);
|
|
2808
|
+
* ```
|
|
2809
|
+
*
|
|
2810
|
+
* @param encoded - Hex calldata in format `0x{vv}{gzip}{root}{signature}`
|
|
2811
|
+
* @param domain - EIP-712 domain with chain id and verifying contract
|
|
2812
|
+
* @returns Validated tree, signature, and recovered signer address
|
|
2813
|
+
* @throws {DecodeError} If version invalid, signature invalid, or root mismatch
|
|
2814
|
+
*/
|
|
2815
|
+
const decode = async (encoded, domain) => {
|
|
2816
|
+
const errorFactory = (reason) => new DecodeError(reason);
|
|
2817
|
+
const normalizedDomain = normalizeSignatureDomain(domain, errorFactory);
|
|
2818
|
+
const bytes = hexToBytes(encoded);
|
|
2819
|
+
if (bytes.length < 98) throw new DecodeError("payload too short");
|
|
2820
|
+
const version = bytes[0];
|
|
2821
|
+
if (version !== (VERSION$1 & 255)) throw new DecodeError(`invalid version: expected ${VERSION$1}, got ${version ?? 0}`);
|
|
2822
|
+
const signature = bytesToHex(bytes.slice(-65));
|
|
2823
|
+
const root = bytesToHex(bytes.slice(-97, -65));
|
|
2824
|
+
assertHex(root, 32, "root");
|
|
2825
|
+
assertHex(signature, 65, "signature");
|
|
2826
|
+
const signer = await verifySignatureAndRecoverAddress({
|
|
2827
|
+
root,
|
|
2828
|
+
signature,
|
|
2829
|
+
domain: normalizedDomain,
|
|
2830
|
+
errorFactory
|
|
2831
|
+
});
|
|
2832
|
+
const compressed = bytes.slice(1, -97);
|
|
2833
|
+
let decoded;
|
|
2834
|
+
try {
|
|
2835
|
+
decoded = ungzip(compressed, { to: "string" });
|
|
2836
|
+
} catch {
|
|
2837
|
+
throw new DecodeError("decompression failed");
|
|
2838
|
+
}
|
|
2839
|
+
let rawOffers;
|
|
2840
|
+
try {
|
|
2841
|
+
rawOffers = JSON.parse(decoded);
|
|
2842
|
+
} catch {
|
|
2843
|
+
throw new DecodeError("JSON parse failed");
|
|
2844
|
+
}
|
|
2845
|
+
const tree = from$7(rawOffers.map((o) => OfferSchema().parse(o)));
|
|
2846
|
+
if (root !== tree.root) throw new DecodeError(`root mismatch: expected ${tree.root}, got ${root}`);
|
|
2847
|
+
const chainIdMismatch = tree.offers.find((offer) => BigInt(offer.chainId) !== normalizedDomain.chainId);
|
|
2848
|
+
if (chainIdMismatch) throw new DecodeError(`chainId mismatch: expected ${normalizedDomain.chainId}, got ${chainIdMismatch.chainId}`);
|
|
2849
|
+
return {
|
|
2850
|
+
tree,
|
|
2851
|
+
signature,
|
|
2852
|
+
signer
|
|
2853
|
+
};
|
|
2854
|
+
};
|
|
2855
|
+
/**
|
|
2856
|
+
* Error thrown during tree building operations.
|
|
2857
|
+
* Indicates structural issues with the tree (missing offers, inconsistent state).
|
|
2858
|
+
*/
|
|
2859
|
+
var TreeError = class extends BaseError {
|
|
2860
|
+
name = "Tree.TreeError";
|
|
2861
|
+
constructor(reason) {
|
|
2862
|
+
super(`Tree error: ${reason}`);
|
|
2863
|
+
}
|
|
2864
|
+
};
|
|
2865
|
+
/**
|
|
2866
|
+
* Error thrown during tree encoding.
|
|
2867
|
+
* Indicates validation failures (signature, root mismatch, mixed makers).
|
|
2868
|
+
*/
|
|
2869
|
+
var EncodeError = class extends BaseError {
|
|
2870
|
+
name = "Tree.EncodeError";
|
|
2871
|
+
constructor(reason) {
|
|
2872
|
+
super(`Failed to encode tree: ${reason}`);
|
|
2873
|
+
}
|
|
2874
|
+
};
|
|
2875
|
+
/**
|
|
2876
|
+
* Error thrown during tree decoding.
|
|
2877
|
+
* Indicates payload corruption, version mismatch, or validation failures.
|
|
2878
|
+
*/
|
|
2879
|
+
var DecodeError = class extends BaseError {
|
|
2880
|
+
name = "Tree.DecodeError";
|
|
2881
|
+
constructor(reason) {
|
|
2882
|
+
super(`Failed to decode tree: ${reason}`);
|
|
2883
|
+
}
|
|
2884
|
+
};
|
|
2885
|
+
|
|
2886
|
+
//#endregion
|
|
2887
|
+
//#region src/core/types.ts
|
|
2888
|
+
const BrandTypeId = Symbol.for("mempool/Brand");
|
|
2889
|
+
|
|
2890
|
+
//#endregion
|
|
2891
|
+
//#region src/gatekeeper/Rules.ts
|
|
2892
|
+
const chains$1 = ({ chains }) => single("chain_ids", `Validates that offer chain is one of: [${chains.map((c) => c.id).join(", ")}]`, (offer) => {
|
|
2893
|
+
const allowedChainIds = chains.map((c) => c.id);
|
|
2894
|
+
if (!allowedChainIds.some((id) => id === offer.chainId)) return { message: `Chain ID ${offer.chainId} is not in the allowed chains (${allowedChainIds.join(", ")})` };
|
|
2895
|
+
});
|
|
2896
|
+
const maturity = ({ maturities }) => single("maturity", `Validates that offer maturity is one of: [${maturities.join(", ")}]`, (offer) => {
|
|
2897
|
+
const allowedMaturities = maturities.map((m) => from$16(m));
|
|
2898
|
+
if (!allowedMaturities.includes(offer.maturity)) return { message: `Maturity must be end of current month (${allowedMaturities[0]}) or end of next month (${allowedMaturities[1]}). Got: ${offer.maturity}` };
|
|
2899
|
+
});
|
|
2900
|
+
const callback = ({ callbacks, allowedAddresses }) => single("callback", `Validates callbacks: buy empty callback is ${callbacks.includes(Type$1.BuyWithEmptyCallback) ? "allowed" : "not allowed"}; sell offers must use a non-empty callback; non-empty callbacks must target one of [${allowedAddresses.map((a) => a.toLowerCase()).join(", ")}]`, (offer) => {
|
|
2901
|
+
if (isEmptyCallback(offer) && offer.buy && !callbacks?.find((c) => c === Type$1.BuyWithEmptyCallback)) return { message: "Buy offers with empty callback not allowed." };
|
|
2902
|
+
if (isEmptyCallback(offer) && !offer.buy) return { message: "Sell offers require a non-empty callback." };
|
|
2903
|
+
if (!isEmptyCallback(offer)) {
|
|
2904
|
+
if (!allowedAddresses.includes(offer.callback.address?.toLowerCase())) return { message: `Callback address ${offer.callback.address} is not allowed.` };
|
|
2905
|
+
}
|
|
2906
|
+
});
|
|
2907
|
+
/**
|
|
2908
|
+
* A validation rule that checks if the offer's tokens are allowed for its chain.
|
|
2909
|
+
* @param assetsByChainId - Allowed assets indexed by chain id.
|
|
2910
|
+
* @returns The issue that was found. If the offer is valid, this will be undefined.
|
|
2911
|
+
*/
|
|
2912
|
+
const token = ({ assetsByChainId }) => single("token", "Validates that offer loan token and collateral tokens are in the allowed assets list for the offer chain", (offer) => {
|
|
2913
|
+
const allowedAssets = assetsByChainId[offer.chainId]?.map((asset) => asset.toLowerCase());
|
|
2914
|
+
if (!allowedAssets || allowedAssets.length === 0) return { message: `No allowed assets for chain ${offer.chainId}` };
|
|
2915
|
+
if (!allowedAssets.includes(offer.loanToken.toLowerCase())) return { message: "Loan token is not allowed" };
|
|
2916
|
+
if (offer.collaterals.some((collateral) => !allowedAssets.includes(collateral.asset.toLowerCase()))) return { message: "Collateral is not allowed" };
|
|
2917
|
+
});
|
|
2918
|
+
/**
|
|
2919
|
+
* A batch validation rule that ensures all offers in a tree have the same maker address.
|
|
2920
|
+
* Returns an issue only for the first non-conforming offer.
|
|
2921
|
+
* This rule is signing-agnostic; signer verification is handled at the collector level.
|
|
2922
|
+
*/
|
|
2923
|
+
const sameMaker = () => batch("mixed_maker", "Validates that all offers in a batch have the same maker address", (offers) => {
|
|
2924
|
+
const issues = /* @__PURE__ */ new Map();
|
|
2925
|
+
if (offers.length === 0) return issues;
|
|
2926
|
+
const firstMaker = offers[0].maker.toLowerCase();
|
|
2927
|
+
for (let i = 1; i < offers.length; i++) {
|
|
2928
|
+
const offer = offers[i];
|
|
2929
|
+
if (offer.maker.toLowerCase() !== firstMaker) {
|
|
2930
|
+
issues.set(i, { message: `Offer has different maker ${offer.maker} than first offer ${offers[0].maker}` });
|
|
2931
|
+
return issues;
|
|
2902
2932
|
}
|
|
2903
2933
|
}
|
|
2904
2934
|
return issues;
|
|
@@ -2935,171 +2965,78 @@ const morphoRules = (chains) => {
|
|
|
2935
2965
|
};
|
|
2936
2966
|
|
|
2937
2967
|
//#endregion
|
|
2938
|
-
//#region src/
|
|
2939
|
-
const API_ERROR_CODES = [
|
|
2940
|
-
"VALIDATION_ERROR",
|
|
2941
|
-
"NOT_FOUND",
|
|
2942
|
-
"INTERNAL_SERVER_ERROR",
|
|
2943
|
-
"BAD_REQUEST"
|
|
2944
|
-
];
|
|
2945
|
-
let STATUS_CODE = /* @__PURE__ */ function(STATUS_CODE) {
|
|
2946
|
-
STATUS_CODE[STATUS_CODE["SUCCESS"] = 200] = "SUCCESS";
|
|
2947
|
-
STATUS_CODE[STATUS_CODE["BAD_REQUEST"] = 400] = "BAD_REQUEST";
|
|
2948
|
-
STATUS_CODE[STATUS_CODE["NOT_FOUND"] = 404] = "NOT_FOUND";
|
|
2949
|
-
STATUS_CODE[STATUS_CODE["INTERNAL_SERVER_ERROR"] = 500] = "INTERNAL_SERVER_ERROR";
|
|
2950
|
-
return STATUS_CODE;
|
|
2951
|
-
}({});
|
|
2952
|
-
var APIError = class extends Error {
|
|
2953
|
-
constructor(statusCode, message, code, details) {
|
|
2954
|
-
super(message);
|
|
2955
|
-
this.statusCode = statusCode;
|
|
2956
|
-
this.code = code;
|
|
2957
|
-
this.details = details;
|
|
2958
|
-
this.name = "APIError";
|
|
2959
|
-
}
|
|
2960
|
-
};
|
|
2961
|
-
var ValidationError = class extends APIError {
|
|
2962
|
-
constructor(message, details) {
|
|
2963
|
-
super(STATUS_CODE.BAD_REQUEST, message, "VALIDATION_ERROR", details);
|
|
2964
|
-
}
|
|
2965
|
-
};
|
|
2966
|
-
var NotFoundError = class extends APIError {
|
|
2967
|
-
constructor(message) {
|
|
2968
|
-
super(STATUS_CODE.NOT_FOUND, message, "NOT_FOUND");
|
|
2969
|
-
}
|
|
2970
|
-
};
|
|
2971
|
-
var InternalServerError = class extends APIError {
|
|
2972
|
-
constructor(message = "Internal server error") {
|
|
2973
|
-
super(STATUS_CODE.INTERNAL_SERVER_ERROR, message, "INTERNAL_SERVER_ERROR");
|
|
2974
|
-
}
|
|
2975
|
-
};
|
|
2976
|
-
var BadRequestError = class extends APIError {
|
|
2977
|
-
constructor(message = "Invalid JSON format", details) {
|
|
2978
|
-
super(STATUS_CODE.BAD_REQUEST, message, "BAD_REQUEST", details);
|
|
2979
|
-
}
|
|
2980
|
-
};
|
|
2981
|
-
function success(args) {
|
|
2982
|
-
const { data, cursor } = args;
|
|
2983
|
-
return {
|
|
2984
|
-
statusCode: STATUS_CODE.SUCCESS,
|
|
2985
|
-
body: {
|
|
2986
|
-
meta: { timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
2987
|
-
cursor: cursor ?? null,
|
|
2988
|
-
data
|
|
2989
|
-
}
|
|
2990
|
-
};
|
|
2991
|
-
}
|
|
2968
|
+
//#region src/gatekeeper/ConfigRules.ts
|
|
2992
2969
|
/**
|
|
2993
|
-
*
|
|
2994
|
-
*
|
|
2970
|
+
* Build the configured rules (maturities + callback addresses + loan tokens) for the provided chains.
|
|
2971
|
+
* @param chains - Chains to include in the configured rules.
|
|
2972
|
+
* @returns Sorted list of config rules.
|
|
2995
2973
|
*/
|
|
2996
|
-
function
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
const field = issue.path.join(".");
|
|
3018
|
-
let msg = issue.message;
|
|
3019
|
-
switch (issue.code) {
|
|
3020
|
-
case "invalid_type":
|
|
3021
|
-
if (issue.message.includes("received undefined")) msg = `${field} is required`;
|
|
3022
|
-
break;
|
|
3023
|
-
case "invalid_format":
|
|
3024
|
-
msg = issue.format === "regex" ? issue.message : `${field} has an invalid format`;
|
|
3025
|
-
break;
|
|
3026
|
-
default: break;
|
|
3027
|
-
}
|
|
3028
|
-
return {
|
|
3029
|
-
field,
|
|
3030
|
-
issue: msg
|
|
3031
|
-
};
|
|
3032
|
-
}));
|
|
3033
|
-
}
|
|
3034
|
-
|
|
3035
|
-
//#endregion
|
|
3036
|
-
//#region src/logger/Logger.ts
|
|
3037
|
-
const LogLevelValues = [
|
|
3038
|
-
"trace",
|
|
3039
|
-
"debug",
|
|
3040
|
-
"info",
|
|
3041
|
-
"warn",
|
|
3042
|
-
"error",
|
|
3043
|
-
"fatal",
|
|
3044
|
-
"silent"
|
|
3045
|
-
];
|
|
3046
|
-
function defaultLogger(minLevel, pretty) {
|
|
3047
|
-
const threshold = minLevel ?? process.env.ROUTER_LOG_LEVEL ?? "info";
|
|
3048
|
-
const prettyEnabled = typeof pretty === "boolean" ? pretty : String(process.env.ROUTER_LOG_PRETTY ?? "false").toLowerCase() === "true";
|
|
3049
|
-
const levelIndexByName = LogLevelValues.reduce((acc, lvl, idx) => {
|
|
3050
|
-
acc[lvl] = idx;
|
|
3051
|
-
return acc;
|
|
3052
|
-
}, {});
|
|
3053
|
-
const isEnabled = (methodLevel) => levelIndexByName[methodLevel] >= levelIndexByName[threshold];
|
|
3054
|
-
const wrap = (consoleMethod, methodLevel) => isEnabled(methodLevel) ? (entry) => {
|
|
3055
|
-
if (!prettyEnabled) {
|
|
3056
|
-
console[consoleMethod](stringify({
|
|
3057
|
-
level: methodLevel,
|
|
3058
|
-
...entry
|
|
3059
|
-
}));
|
|
3060
|
-
return;
|
|
2974
|
+
function buildConfigRules(chains) {
|
|
2975
|
+
const rules = [];
|
|
2976
|
+
for (const chain of chains) {
|
|
2977
|
+
const config = configs[chain.name];
|
|
2978
|
+
const maturities = config.maturities ?? [];
|
|
2979
|
+
for (const maturityName of maturities) rules.push({
|
|
2980
|
+
type: "maturity",
|
|
2981
|
+
chain_id: chain.id,
|
|
2982
|
+
name: maturityName,
|
|
2983
|
+
timestamp: from$16(maturityName)
|
|
2984
|
+
});
|
|
2985
|
+
const callbacks = config.callbacks ?? [];
|
|
2986
|
+
for (const callback of callbacks) {
|
|
2987
|
+
if (callback.type === Type$1.BuyWithEmptyCallback) continue;
|
|
2988
|
+
if (!("addresses" in callback)) continue;
|
|
2989
|
+
for (const address of callback.addresses) rules.push({
|
|
2990
|
+
type: "callback",
|
|
2991
|
+
chain_id: chain.id,
|
|
2992
|
+
address: normalizeAddress(address),
|
|
2993
|
+
callback_type: callback.type
|
|
2994
|
+
});
|
|
3061
2995
|
}
|
|
3062
|
-
const
|
|
3063
|
-
const
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
const line = extras.length > 0 ? `${timestamp} [${level}] ${msg} ${extras}` : `${timestamp} [${level}] ${msg}`;
|
|
3069
|
-
console[consoleMethod](line);
|
|
3070
|
-
if (stack) console[consoleMethod](stack);
|
|
3071
|
-
} : () => {};
|
|
3072
|
-
return {
|
|
3073
|
-
trace: wrap("trace", "trace"),
|
|
3074
|
-
debug: wrap("debug", "debug"),
|
|
3075
|
-
info: wrap("info", "info"),
|
|
3076
|
-
warn: wrap("warn", "warn"),
|
|
3077
|
-
error: wrap("error", "error"),
|
|
3078
|
-
fatal: wrap("error", "fatal")
|
|
3079
|
-
};
|
|
3080
|
-
}
|
|
3081
|
-
const loggerContext = new AsyncLocalStorage();
|
|
3082
|
-
function runWithLogger(logger, fn) {
|
|
3083
|
-
return loggerContext.run(logger, fn);
|
|
3084
|
-
}
|
|
3085
|
-
function getLogger() {
|
|
3086
|
-
return loggerContext.getStore() ?? defaultLogger();
|
|
3087
|
-
}
|
|
3088
|
-
function formatValue(value) {
|
|
3089
|
-
if (value === null || value === void 0 || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") return String(value);
|
|
3090
|
-
if (typeof value === "string") {
|
|
3091
|
-
if (value.includes(" ")) return JSON.stringify(value);
|
|
3092
|
-
return value;
|
|
2996
|
+
const loanTokens = assets[chain.id.toString()] ?? [];
|
|
2997
|
+
for (const address of loanTokens) rules.push({
|
|
2998
|
+
type: "loan_token",
|
|
2999
|
+
chain_id: chain.id,
|
|
3000
|
+
address: normalizeAddress(address)
|
|
3001
|
+
});
|
|
3093
3002
|
}
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3003
|
+
rules.sort(compareConfigRules);
|
|
3004
|
+
return rules;
|
|
3005
|
+
}
|
|
3006
|
+
/**
|
|
3007
|
+
* Compute a stable checksum for the provided configured rules.
|
|
3008
|
+
* @param rules - Configured rules to checksum.
|
|
3009
|
+
* @returns MD5 checksum.
|
|
3010
|
+
*/
|
|
3011
|
+
function buildConfigRulesChecksum(rules) {
|
|
3012
|
+
const hash = createHash("md5");
|
|
3013
|
+
const orderedRules = [...rules].sort(compareConfigRules);
|
|
3014
|
+
for (const rule of orderedRules) {
|
|
3015
|
+
if (rule.type === "maturity") {
|
|
3016
|
+
hash.update(`maturity:${rule.chain_id}:${rule.name}:${rule.timestamp}\n`);
|
|
3017
|
+
continue;
|
|
3018
|
+
}
|
|
3019
|
+
if (rule.type === "callback") {
|
|
3020
|
+
hash.update(`callback:${rule.chain_id}:${rule.callback_type}:${rule.address}\n`);
|
|
3021
|
+
continue;
|
|
3101
3022
|
}
|
|
3023
|
+
hash.update(`loan_token:${rule.chain_id}:${rule.address}\n`);
|
|
3024
|
+
}
|
|
3025
|
+
return hash.digest("hex");
|
|
3026
|
+
}
|
|
3027
|
+
function normalizeAddress(address) {
|
|
3028
|
+
return address.toLowerCase();
|
|
3029
|
+
}
|
|
3030
|
+
function compareConfigRules(left, right) {
|
|
3031
|
+
if (left.chain_id !== right.chain_id) return left.chain_id - right.chain_id;
|
|
3032
|
+
if (left.type !== right.type) return left.type.localeCompare(right.type);
|
|
3033
|
+
if (left.type === "maturity" && right.type === "maturity") return left.timestamp - right.timestamp;
|
|
3034
|
+
if (left.type === "callback" && right.type === "callback") {
|
|
3035
|
+
if (left.callback_type !== right.callback_type) return left.callback_type.localeCompare(right.callback_type);
|
|
3036
|
+
return left.address.localeCompare(right.address);
|
|
3102
3037
|
}
|
|
3038
|
+
if (left.type === "loan_token" && right.type === "loan_token") return left.address.localeCompare(right.address);
|
|
3039
|
+
return 0;
|
|
3103
3040
|
}
|
|
3104
3041
|
|
|
3105
3042
|
//#endregion
|
|
@@ -3229,6 +3166,104 @@ function from$4(input) {
|
|
|
3229
3166
|
};
|
|
3230
3167
|
}
|
|
3231
3168
|
|
|
3169
|
+
//#endregion
|
|
3170
|
+
//#region src/api/Controllers/Payload.ts
|
|
3171
|
+
const API_ERROR_CODES = [
|
|
3172
|
+
"VALIDATION_ERROR",
|
|
3173
|
+
"NOT_FOUND",
|
|
3174
|
+
"INTERNAL_SERVER_ERROR",
|
|
3175
|
+
"BAD_REQUEST"
|
|
3176
|
+
];
|
|
3177
|
+
let STATUS_CODE = /* @__PURE__ */ function(STATUS_CODE) {
|
|
3178
|
+
STATUS_CODE[STATUS_CODE["SUCCESS"] = 200] = "SUCCESS";
|
|
3179
|
+
STATUS_CODE[STATUS_CODE["BAD_REQUEST"] = 400] = "BAD_REQUEST";
|
|
3180
|
+
STATUS_CODE[STATUS_CODE["NOT_FOUND"] = 404] = "NOT_FOUND";
|
|
3181
|
+
STATUS_CODE[STATUS_CODE["INTERNAL_SERVER_ERROR"] = 500] = "INTERNAL_SERVER_ERROR";
|
|
3182
|
+
return STATUS_CODE;
|
|
3183
|
+
}({});
|
|
3184
|
+
var APIError = class extends Error {
|
|
3185
|
+
constructor(statusCode, message, code, details) {
|
|
3186
|
+
super(message);
|
|
3187
|
+
this.statusCode = statusCode;
|
|
3188
|
+
this.code = code;
|
|
3189
|
+
this.details = details;
|
|
3190
|
+
this.name = "APIError";
|
|
3191
|
+
}
|
|
3192
|
+
};
|
|
3193
|
+
var ValidationError = class extends APIError {
|
|
3194
|
+
constructor(message, details) {
|
|
3195
|
+
super(STATUS_CODE.BAD_REQUEST, message, "VALIDATION_ERROR", details);
|
|
3196
|
+
}
|
|
3197
|
+
};
|
|
3198
|
+
var NotFoundError = class extends APIError {
|
|
3199
|
+
constructor(message) {
|
|
3200
|
+
super(STATUS_CODE.NOT_FOUND, message, "NOT_FOUND");
|
|
3201
|
+
}
|
|
3202
|
+
};
|
|
3203
|
+
var InternalServerError = class extends APIError {
|
|
3204
|
+
constructor(message = "Internal server error") {
|
|
3205
|
+
super(STATUS_CODE.INTERNAL_SERVER_ERROR, message, "INTERNAL_SERVER_ERROR");
|
|
3206
|
+
}
|
|
3207
|
+
};
|
|
3208
|
+
var BadRequestError = class extends APIError {
|
|
3209
|
+
constructor(message = "Invalid JSON format", details) {
|
|
3210
|
+
super(STATUS_CODE.BAD_REQUEST, message, "BAD_REQUEST", details);
|
|
3211
|
+
}
|
|
3212
|
+
};
|
|
3213
|
+
function success(args) {
|
|
3214
|
+
const { data, cursor } = args;
|
|
3215
|
+
return {
|
|
3216
|
+
statusCode: STATUS_CODE.SUCCESS,
|
|
3217
|
+
body: {
|
|
3218
|
+
meta: { timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
3219
|
+
cursor: cursor ?? null,
|
|
3220
|
+
data
|
|
3221
|
+
}
|
|
3222
|
+
};
|
|
3223
|
+
}
|
|
3224
|
+
/**
|
|
3225
|
+
* Generic failure builder. Preserves the concrete status code when the input is an APIError.
|
|
3226
|
+
* If not an APIError, maps to INTERNAL_SERVER_ERROR. Zod & SyntaxError are mapped to BAD_REQUEST.
|
|
3227
|
+
*/
|
|
3228
|
+
function failure(err) {
|
|
3229
|
+
if (err instanceof APIError) return handleAPIError(err);
|
|
3230
|
+
if (err instanceof SyntaxError) return handleAPIError(new BadRequestError(err.message));
|
|
3231
|
+
if (err instanceof z$2.ZodError) return handleAPIError(handleZodError(err));
|
|
3232
|
+
return handleAPIError(new InternalServerError());
|
|
3233
|
+
}
|
|
3234
|
+
function handleAPIError(error) {
|
|
3235
|
+
return {
|
|
3236
|
+
statusCode: error.statusCode,
|
|
3237
|
+
body: {
|
|
3238
|
+
meta: { timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
3239
|
+
error: {
|
|
3240
|
+
code: error.code,
|
|
3241
|
+
message: error.message,
|
|
3242
|
+
...error.details && typeof error.details === "object" ? { details: error.details } : {}
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
};
|
|
3246
|
+
}
|
|
3247
|
+
function handleZodError(error) {
|
|
3248
|
+
return new ValidationError("Validation failed", error.issues.map((issue) => {
|
|
3249
|
+
const field = issue.path.join(".");
|
|
3250
|
+
let msg = issue.message;
|
|
3251
|
+
switch (issue.code) {
|
|
3252
|
+
case "invalid_type":
|
|
3253
|
+
if (issue.message.includes("received undefined")) msg = `${field} is required`;
|
|
3254
|
+
break;
|
|
3255
|
+
case "invalid_format":
|
|
3256
|
+
msg = issue.format === "regex" ? issue.message : `${field} has an invalid format`;
|
|
3257
|
+
break;
|
|
3258
|
+
default: break;
|
|
3259
|
+
}
|
|
3260
|
+
return {
|
|
3261
|
+
field,
|
|
3262
|
+
issue: msg
|
|
3263
|
+
};
|
|
3264
|
+
}));
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3232
3267
|
//#endregion
|
|
3233
3268
|
//#region \0@oxc-project+runtime@0.110.0/helpers/decorate.js
|
|
3234
3269
|
function __decorate(decorators, target, key, desc) {
|
|
@@ -3321,6 +3356,21 @@ const validateOfferExample = {
|
|
|
3321
3356
|
data: "0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000000000000034cf890db685fc536e05652fb41f02090c3fb751000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000108e644e3ab01184155270aa92a00000000000"
|
|
3322
3357
|
}
|
|
3323
3358
|
};
|
|
3359
|
+
const callbackTypesRequestExample = { callbacks: [{
|
|
3360
|
+
chain_id: 1,
|
|
3361
|
+
addresses: [
|
|
3362
|
+
"0x1111111111111111111111111111111111111111",
|
|
3363
|
+
"0x3333333333333333333333333333333333333333",
|
|
3364
|
+
"0x9999999999999999999999999999999999999999"
|
|
3365
|
+
]
|
|
3366
|
+
}] };
|
|
3367
|
+
const callbackTypesResponseExample = [{
|
|
3368
|
+
chain_id: 1,
|
|
3369
|
+
sell_erc20_callback: ["0x1111111111111111111111111111111111111111"],
|
|
3370
|
+
buy_erc20: ["0x5555555555555555555555555555555555555555"],
|
|
3371
|
+
buy_vault_v1_callback: ["0x3333333333333333333333333333333333333333"],
|
|
3372
|
+
not_supported: ["0x9999999999999999999999999999999999999999"]
|
|
3373
|
+
}];
|
|
3324
3374
|
const routerStatusExample = {
|
|
3325
3375
|
status: "live",
|
|
3326
3376
|
initialized: true,
|
|
@@ -3389,6 +3439,55 @@ __decorate([ApiProperty({
|
|
|
3389
3439
|
type: "string",
|
|
3390
3440
|
example: validateOfferExample.callback.data
|
|
3391
3441
|
})], ValidateCallbackRequest.prototype, "data", void 0);
|
|
3442
|
+
var CallbackTypesChainRequest = class {};
|
|
3443
|
+
__decorate([ApiProperty({
|
|
3444
|
+
type: "number",
|
|
3445
|
+
example: callbackTypesRequestExample.callbacks[0].chain_id
|
|
3446
|
+
})], CallbackTypesChainRequest.prototype, "chain_id", void 0);
|
|
3447
|
+
__decorate([ApiProperty({
|
|
3448
|
+
type: () => [String],
|
|
3449
|
+
example: callbackTypesRequestExample.callbacks[0].addresses
|
|
3450
|
+
})], CallbackTypesChainRequest.prototype, "addresses", void 0);
|
|
3451
|
+
var CallbackTypesRequest = class {};
|
|
3452
|
+
__decorate([ApiProperty({
|
|
3453
|
+
type: () => [CallbackTypesChainRequest],
|
|
3454
|
+
example: callbackTypesRequestExample.callbacks
|
|
3455
|
+
})], CallbackTypesRequest.prototype, "callbacks", void 0);
|
|
3456
|
+
var CallbackTypesChainResponse = class {};
|
|
3457
|
+
__decorate([ApiProperty({
|
|
3458
|
+
type: "number",
|
|
3459
|
+
example: callbackTypesResponseExample[0].chain_id
|
|
3460
|
+
})], CallbackTypesChainResponse.prototype, "chain_id", void 0);
|
|
3461
|
+
__decorate([ApiProperty({
|
|
3462
|
+
type: () => [String],
|
|
3463
|
+
required: false,
|
|
3464
|
+
example: callbackTypesResponseExample[0].buy_vault_v1_callback
|
|
3465
|
+
})], CallbackTypesChainResponse.prototype, "buy_vault_v1_callback", void 0);
|
|
3466
|
+
__decorate([ApiProperty({
|
|
3467
|
+
type: () => [String],
|
|
3468
|
+
required: false,
|
|
3469
|
+
example: callbackTypesResponseExample[0].sell_erc20_callback
|
|
3470
|
+
})], CallbackTypesChainResponse.prototype, "sell_erc20_callback", void 0);
|
|
3471
|
+
__decorate([ApiProperty({
|
|
3472
|
+
type: () => [String],
|
|
3473
|
+
required: false,
|
|
3474
|
+
example: callbackTypesResponseExample[0].buy_erc20
|
|
3475
|
+
})], CallbackTypesChainResponse.prototype, "buy_erc20", void 0);
|
|
3476
|
+
__decorate([ApiProperty({
|
|
3477
|
+
type: () => [String],
|
|
3478
|
+
example: callbackTypesResponseExample[0].not_supported
|
|
3479
|
+
})], CallbackTypesChainResponse.prototype, "not_supported", void 0);
|
|
3480
|
+
var CallbackTypesSuccessResponse = class extends SuccessResponse {};
|
|
3481
|
+
__decorate([ApiProperty({
|
|
3482
|
+
type: "string",
|
|
3483
|
+
nullable: true,
|
|
3484
|
+
example: "maturity:1:1730415600:end_of_next_month"
|
|
3485
|
+
})], CallbackTypesSuccessResponse.prototype, "cursor", void 0);
|
|
3486
|
+
__decorate([ApiProperty({
|
|
3487
|
+
type: () => [CallbackTypesChainResponse],
|
|
3488
|
+
description: "Callback types grouped by chain.",
|
|
3489
|
+
example: callbackTypesResponseExample
|
|
3490
|
+
})], CallbackTypesSuccessResponse.prototype, "data", void 0);
|
|
3392
3491
|
var AskResponse = class {};
|
|
3393
3492
|
__decorate([ApiProperty({
|
|
3394
3493
|
type: "string",
|
|
@@ -3924,6 +4023,28 @@ ValidateController = __decorate([ApiTags("Make"), ApiResponse({
|
|
|
3924
4023
|
description: "Bad Request",
|
|
3925
4024
|
type: BadRequestResponse
|
|
3926
4025
|
})], ValidateController);
|
|
4026
|
+
let CallbacksController = class CallbacksController {
|
|
4027
|
+
async resolveCallbackTypes() {}
|
|
4028
|
+
};
|
|
4029
|
+
__decorate([
|
|
4030
|
+
ApiOperation({
|
|
4031
|
+
methods: ["post"],
|
|
4032
|
+
path: "/v1/callbacks",
|
|
4033
|
+
summary: "Resolve callback types",
|
|
4034
|
+
description: "Returns callback types for callback addresses grouped by chain."
|
|
4035
|
+
}),
|
|
4036
|
+
ApiBody({ type: CallbackTypesRequest }),
|
|
4037
|
+
ApiResponse({
|
|
4038
|
+
status: 200,
|
|
4039
|
+
description: "Success",
|
|
4040
|
+
type: CallbackTypesSuccessResponse
|
|
4041
|
+
})
|
|
4042
|
+
], CallbacksController.prototype, "resolveCallbackTypes", null);
|
|
4043
|
+
CallbacksController = __decorate([ApiTags("Make"), ApiResponse({
|
|
4044
|
+
status: 400,
|
|
4045
|
+
description: "Bad Request",
|
|
4046
|
+
type: BadRequestResponse
|
|
4047
|
+
})], CallbacksController);
|
|
3927
4048
|
let OffersController = class OffersController {
|
|
3928
4049
|
async getOffers() {}
|
|
3929
4050
|
};
|
|
@@ -3992,107 +4113,274 @@ __decorate([
|
|
|
3992
4113
|
description: "Returns the aggregated status of the router."
|
|
3993
4114
|
}),
|
|
3994
4115
|
ApiQuery({
|
|
3995
|
-
name: "strict",
|
|
3996
|
-
type: "boolean",
|
|
4116
|
+
name: "strict",
|
|
4117
|
+
type: "boolean",
|
|
4118
|
+
required: false,
|
|
4119
|
+
example: true,
|
|
4120
|
+
description: "Fail the request if initialization is incomplete."
|
|
4121
|
+
}),
|
|
4122
|
+
ApiResponse({
|
|
4123
|
+
status: 200,
|
|
4124
|
+
description: "Success",
|
|
4125
|
+
type: RouterStatusSuccessResponse
|
|
4126
|
+
})
|
|
4127
|
+
], HealthController.prototype, "getRouterStatus", null);
|
|
4128
|
+
__decorate([
|
|
4129
|
+
ApiOperation({
|
|
4130
|
+
methods: ["get"],
|
|
4131
|
+
path: "/v1/health/collectors",
|
|
4132
|
+
summary: "Retrieve collectors health",
|
|
4133
|
+
description: "Returns the latest block numbers processed by collectors and their sync status."
|
|
4134
|
+
}),
|
|
4135
|
+
ApiQuery({
|
|
4136
|
+
name: "strict",
|
|
4137
|
+
type: "boolean",
|
|
4138
|
+
required: false,
|
|
4139
|
+
example: true,
|
|
4140
|
+
description: "Fail the request if initialization is incomplete."
|
|
4141
|
+
}),
|
|
4142
|
+
ApiResponse({
|
|
4143
|
+
status: 200,
|
|
4144
|
+
description: "Success",
|
|
4145
|
+
type: CollectorsHealthSuccessResponse
|
|
4146
|
+
})
|
|
4147
|
+
], HealthController.prototype, "getCollectorsHealth", null);
|
|
4148
|
+
__decorate([
|
|
4149
|
+
ApiOperation({
|
|
4150
|
+
methods: ["get"],
|
|
4151
|
+
path: "/v1/health/chains",
|
|
4152
|
+
summary: "Retrieve chains health",
|
|
4153
|
+
description: "Returns the latest block that can be processed by collectors for each chain."
|
|
4154
|
+
}),
|
|
4155
|
+
ApiQuery({
|
|
4156
|
+
name: "strict",
|
|
4157
|
+
type: "boolean",
|
|
4158
|
+
required: false,
|
|
4159
|
+
example: true,
|
|
4160
|
+
description: "Fail the request if initialization is incomplete."
|
|
4161
|
+
}),
|
|
4162
|
+
ApiResponse({
|
|
4163
|
+
status: 200,
|
|
4164
|
+
description: "Success",
|
|
4165
|
+
type: ChainsHealthSuccessResponse
|
|
4166
|
+
})
|
|
4167
|
+
], HealthController.prototype, "getChainsHealth", null);
|
|
4168
|
+
HealthController = __decorate([ApiTags("System")], HealthController);
|
|
4169
|
+
const configContractsExample = {
|
|
4170
|
+
chain_id: 505050505,
|
|
4171
|
+
address: "0xD946246695A9259F3B33a78629026F61B3Ab40aF",
|
|
4172
|
+
name: "mempool"
|
|
4173
|
+
};
|
|
4174
|
+
const configContractsPayloadExample = [
|
|
4175
|
+
{
|
|
4176
|
+
chain_id: 505050505,
|
|
4177
|
+
address: "0xD946246695A9259F3B33a78629026F61B3Ab40aF",
|
|
4178
|
+
name: "mempool"
|
|
4179
|
+
},
|
|
4180
|
+
{
|
|
4181
|
+
chain_id: 505050505,
|
|
4182
|
+
address: "0x8A409D5D6394fC197c596d4E6E2c35e5d13f8a4d",
|
|
4183
|
+
name: "multicall"
|
|
4184
|
+
},
|
|
4185
|
+
{
|
|
4186
|
+
chain_id: 505050505,
|
|
4187
|
+
address: "0x23DFBc4B8B80C14CC5e25011B8491f268395BAd6",
|
|
4188
|
+
name: "v2"
|
|
4189
|
+
}
|
|
4190
|
+
];
|
|
4191
|
+
const configRulesMaturityExample = {
|
|
4192
|
+
type: "maturity",
|
|
4193
|
+
chain_id: 1,
|
|
4194
|
+
name: "end_of_next_month",
|
|
4195
|
+
timestamp: 1730415600
|
|
4196
|
+
};
|
|
4197
|
+
const configRulesCallbackExample = {
|
|
4198
|
+
type: "callback",
|
|
4199
|
+
chain_id: 1,
|
|
4200
|
+
address: "0x1111111111111111111111111111111111111111",
|
|
4201
|
+
callback_type: "sell_erc20_callback"
|
|
4202
|
+
};
|
|
4203
|
+
const configRulesLoanTokenExample = {
|
|
4204
|
+
type: "loan_token",
|
|
4205
|
+
chain_id: 1,
|
|
4206
|
+
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
|
|
4207
|
+
};
|
|
4208
|
+
const configRulesChecksumExample = "f1d2d2f924e986ac86fdf7b36c94bcdf";
|
|
4209
|
+
const configRulesPayloadExample = [
|
|
4210
|
+
configRulesMaturityExample,
|
|
4211
|
+
configRulesCallbackExample,
|
|
4212
|
+
configRulesLoanTokenExample
|
|
4213
|
+
];
|
|
4214
|
+
const configContractNames = [
|
|
4215
|
+
"mempool",
|
|
4216
|
+
"multicall",
|
|
4217
|
+
"v2"
|
|
4218
|
+
];
|
|
4219
|
+
const configContractsCursorExample = "505050505:0xd946246695a9259f3b33a78629026f61b3ab40af";
|
|
4220
|
+
var ConfigContractResponse = class {};
|
|
4221
|
+
__decorate([ApiProperty({
|
|
4222
|
+
type: "number",
|
|
4223
|
+
example: configContractsExample.chain_id
|
|
4224
|
+
})], ConfigContractResponse.prototype, "chain_id", void 0);
|
|
4225
|
+
__decorate([ApiProperty({
|
|
4226
|
+
type: "string",
|
|
4227
|
+
example: configContractsExample.address
|
|
4228
|
+
})], ConfigContractResponse.prototype, "address", void 0);
|
|
4229
|
+
__decorate([ApiProperty({
|
|
4230
|
+
type: "string",
|
|
4231
|
+
enum: configContractNames,
|
|
4232
|
+
example: configContractsExample.name
|
|
4233
|
+
})], ConfigContractResponse.prototype, "name", void 0);
|
|
4234
|
+
var ConfigContractsSuccessResponse = class extends SuccessResponse {};
|
|
4235
|
+
__decorate([ApiProperty({
|
|
4236
|
+
type: "string",
|
|
4237
|
+
nullable: true,
|
|
4238
|
+
example: null
|
|
4239
|
+
})], ConfigContractsSuccessResponse.prototype, "cursor", void 0);
|
|
4240
|
+
__decorate([ApiProperty({
|
|
4241
|
+
type: () => [ConfigContractResponse],
|
|
4242
|
+
description: "Indexer contract configuration for all indexed chains.",
|
|
4243
|
+
example: configContractsPayloadExample
|
|
4244
|
+
})], ConfigContractsSuccessResponse.prototype, "data", void 0);
|
|
4245
|
+
var ConfigRulesMeta = class {};
|
|
4246
|
+
__decorate([ApiProperty({
|
|
4247
|
+
type: "string",
|
|
4248
|
+
example: timestampExample
|
|
4249
|
+
})], ConfigRulesMeta.prototype, "timestamp", void 0);
|
|
4250
|
+
__decorate([ApiProperty({
|
|
4251
|
+
type: "string",
|
|
4252
|
+
example: configRulesChecksumExample
|
|
4253
|
+
})], ConfigRulesMeta.prototype, "checksum", void 0);
|
|
4254
|
+
var ConfigRulesRuleResponse = class {};
|
|
4255
|
+
__decorate([ApiProperty({
|
|
4256
|
+
type: "string",
|
|
4257
|
+
example: configRulesMaturityExample.type
|
|
4258
|
+
})], ConfigRulesRuleResponse.prototype, "type", void 0);
|
|
4259
|
+
__decorate([ApiProperty({
|
|
4260
|
+
type: "number",
|
|
4261
|
+
example: configRulesMaturityExample.chain_id
|
|
4262
|
+
})], ConfigRulesRuleResponse.prototype, "chain_id", void 0);
|
|
4263
|
+
__decorate([ApiProperty({
|
|
4264
|
+
type: "string",
|
|
4265
|
+
example: configRulesMaturityExample.name,
|
|
4266
|
+
required: false
|
|
4267
|
+
})], ConfigRulesRuleResponse.prototype, "name", void 0);
|
|
4268
|
+
__decorate([ApiProperty({
|
|
4269
|
+
type: "number",
|
|
4270
|
+
example: configRulesMaturityExample.timestamp,
|
|
4271
|
+
required: false
|
|
4272
|
+
})], ConfigRulesRuleResponse.prototype, "timestamp", void 0);
|
|
4273
|
+
__decorate([ApiProperty({
|
|
4274
|
+
type: "string",
|
|
4275
|
+
example: configRulesCallbackExample.address,
|
|
4276
|
+
required: false
|
|
4277
|
+
})], ConfigRulesRuleResponse.prototype, "address", void 0);
|
|
4278
|
+
__decorate([ApiProperty({
|
|
4279
|
+
type: "string",
|
|
4280
|
+
example: configRulesCallbackExample.callback_type,
|
|
4281
|
+
required: false
|
|
4282
|
+
})], ConfigRulesRuleResponse.prototype, "callback_type", void 0);
|
|
4283
|
+
var ConfigRulesSuccessResponse = class {};
|
|
4284
|
+
__decorate([ApiProperty({ type: () => ConfigRulesMeta })], ConfigRulesSuccessResponse.prototype, "meta", void 0);
|
|
4285
|
+
__decorate([ApiProperty({
|
|
4286
|
+
type: "string",
|
|
4287
|
+
nullable: true,
|
|
4288
|
+
example: null
|
|
4289
|
+
})], ConfigRulesSuccessResponse.prototype, "cursor", void 0);
|
|
4290
|
+
__decorate([ApiProperty({
|
|
4291
|
+
type: () => [ConfigRulesRuleResponse],
|
|
4292
|
+
description: "Configured rules returned by the router API.",
|
|
4293
|
+
example: configRulesPayloadExample
|
|
4294
|
+
})], ConfigRulesSuccessResponse.prototype, "data", void 0);
|
|
4295
|
+
let ConfigContractsController = class ConfigContractsController {
|
|
4296
|
+
async getConfigContracts() {}
|
|
4297
|
+
};
|
|
4298
|
+
__decorate([
|
|
4299
|
+
ApiOperation({
|
|
4300
|
+
methods: ["get"],
|
|
4301
|
+
path: "/v1/config/contracts",
|
|
4302
|
+
summary: "Get indexer contract configuration",
|
|
4303
|
+
description: "Returns contract addresses used by indexers (mempool, v2) and multicall for indexed chains."
|
|
4304
|
+
}),
|
|
4305
|
+
ApiQuery({
|
|
4306
|
+
name: "cursor",
|
|
4307
|
+
type: "string",
|
|
4308
|
+
required: false,
|
|
4309
|
+
example: configContractsCursorExample,
|
|
4310
|
+
description: "Pagination cursor in chain_id:address format (lowercase address)."
|
|
4311
|
+
}),
|
|
4312
|
+
ApiQuery({
|
|
4313
|
+
name: "limit",
|
|
4314
|
+
type: "number",
|
|
4315
|
+
required: false,
|
|
4316
|
+
example: 1e3,
|
|
4317
|
+
description: "Maximum number of contracts to return (max 1000)."
|
|
4318
|
+
}),
|
|
4319
|
+
ApiQuery({
|
|
4320
|
+
name: "chains",
|
|
4321
|
+
type: ["number"],
|
|
3997
4322
|
required: false,
|
|
3998
|
-
example:
|
|
3999
|
-
description: "
|
|
4323
|
+
example: "1,8453",
|
|
4324
|
+
description: "Filter by chain IDs (comma-separated).",
|
|
4325
|
+
style: "form",
|
|
4326
|
+
explode: false
|
|
4000
4327
|
}),
|
|
4001
4328
|
ApiResponse({
|
|
4002
4329
|
status: 200,
|
|
4003
4330
|
description: "Success",
|
|
4004
|
-
type:
|
|
4331
|
+
type: ConfigContractsSuccessResponse
|
|
4005
4332
|
})
|
|
4006
|
-
],
|
|
4333
|
+
], ConfigContractsController.prototype, "getConfigContracts", null);
|
|
4334
|
+
ConfigContractsController = __decorate([ApiTags("System")], ConfigContractsController);
|
|
4335
|
+
let ConfigRulesController = class ConfigRulesController {
|
|
4336
|
+
async getConfigRules() {}
|
|
4337
|
+
};
|
|
4007
4338
|
__decorate([
|
|
4008
4339
|
ApiOperation({
|
|
4009
4340
|
methods: ["get"],
|
|
4010
|
-
path: "/v1/
|
|
4011
|
-
summary: "
|
|
4012
|
-
description: "Returns
|
|
4341
|
+
path: "/v1/config/rules",
|
|
4342
|
+
summary: "Get config rules",
|
|
4343
|
+
description: "Returns configured rules for supported chains."
|
|
4013
4344
|
}),
|
|
4014
4345
|
ApiQuery({
|
|
4015
|
-
name: "
|
|
4016
|
-
type: "
|
|
4346
|
+
name: "cursor",
|
|
4347
|
+
type: "string",
|
|
4017
4348
|
required: false,
|
|
4018
|
-
example:
|
|
4019
|
-
description: "
|
|
4349
|
+
example: "maturity:1:1730415600:end_of_next_month",
|
|
4350
|
+
description: "Pagination cursor in type:chain_id:<value> format."
|
|
4020
4351
|
}),
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
__decorate([
|
|
4028
|
-
ApiOperation({
|
|
4029
|
-
methods: ["get"],
|
|
4030
|
-
path: "/v1/health/chains",
|
|
4031
|
-
summary: "Retrieve chains health",
|
|
4032
|
-
description: "Returns the latest block that can be processed by collectors for each chain."
|
|
4352
|
+
ApiQuery({
|
|
4353
|
+
name: "limit",
|
|
4354
|
+
type: "number",
|
|
4355
|
+
required: false,
|
|
4356
|
+
example: 100,
|
|
4357
|
+
description: "Maximum number of rules to return (max 1000)."
|
|
4033
4358
|
}),
|
|
4034
4359
|
ApiQuery({
|
|
4035
|
-
name: "
|
|
4036
|
-
type: "
|
|
4360
|
+
name: "types",
|
|
4361
|
+
type: ["string"],
|
|
4037
4362
|
required: false,
|
|
4038
|
-
example:
|
|
4039
|
-
description: "
|
|
4363
|
+
example: "maturity,loan_token",
|
|
4364
|
+
description: "Filter by rule types (comma-separated).",
|
|
4365
|
+
style: "form",
|
|
4366
|
+
explode: false
|
|
4367
|
+
}),
|
|
4368
|
+
ApiQuery({
|
|
4369
|
+
name: "chains",
|
|
4370
|
+
type: ["number"],
|
|
4371
|
+
required: false,
|
|
4372
|
+
example: "1,8453",
|
|
4373
|
+
description: "Filter by chain IDs (comma-separated).",
|
|
4374
|
+
style: "form",
|
|
4375
|
+
explode: false
|
|
4040
4376
|
}),
|
|
4041
4377
|
ApiResponse({
|
|
4042
4378
|
status: 200,
|
|
4043
4379
|
description: "Success",
|
|
4044
|
-
type:
|
|
4380
|
+
type: ConfigRulesSuccessResponse
|
|
4045
4381
|
})
|
|
4046
|
-
],
|
|
4047
|
-
|
|
4048
|
-
const callbacksExample = [Type$1.BuyWithEmptyCallback];
|
|
4049
|
-
const chainConfigExample = {
|
|
4050
|
-
chain_id: 505050505,
|
|
4051
|
-
contracts: { mempool: "0xD946246695A9259F3B33a78629026F61B3Ab40aF" },
|
|
4052
|
-
callbacks: callbacksExample
|
|
4053
|
-
};
|
|
4054
|
-
var ConfigContractsResponse = class {};
|
|
4055
|
-
__decorate([ApiProperty({
|
|
4056
|
-
type: "string",
|
|
4057
|
-
example: chainConfigExample.contracts.mempool
|
|
4058
|
-
})], ConfigContractsResponse.prototype, "mempool", void 0);
|
|
4059
|
-
var ConfigDataResponse = class {};
|
|
4060
|
-
__decorate([ApiProperty({
|
|
4061
|
-
type: "number",
|
|
4062
|
-
example: chainConfigExample.chain_id
|
|
4063
|
-
})], ConfigDataResponse.prototype, "chain_id", void 0);
|
|
4064
|
-
__decorate([ApiProperty({ type: () => ConfigContractsResponse })], ConfigDataResponse.prototype, "contracts", void 0);
|
|
4065
|
-
__decorate([ApiProperty({
|
|
4066
|
-
type: () => [String],
|
|
4067
|
-
enum: Object.values(Type$1),
|
|
4068
|
-
description: "Supported callback types for this chain.",
|
|
4069
|
-
example: callbacksExample
|
|
4070
|
-
})], ConfigDataResponse.prototype, "callbacks", void 0);
|
|
4071
|
-
var ConfigSuccessResponse = class extends SuccessResponse {};
|
|
4072
|
-
__decorate([ApiProperty({
|
|
4073
|
-
type: "string",
|
|
4074
|
-
nullable: true,
|
|
4075
|
-
example: null
|
|
4076
|
-
})], ConfigSuccessResponse.prototype, "cursor", void 0);
|
|
4077
|
-
__decorate([ApiProperty({
|
|
4078
|
-
type: () => [ConfigDataResponse],
|
|
4079
|
-
description: "Array of chain configurations for all indexed chains.",
|
|
4080
|
-
example: [chainConfigExample]
|
|
4081
|
-
})], ConfigSuccessResponse.prototype, "data", void 0);
|
|
4082
|
-
let ConfigController = class ConfigController {
|
|
4083
|
-
async getConfig() {}
|
|
4084
|
-
};
|
|
4085
|
-
__decorate([ApiOperation({
|
|
4086
|
-
methods: ["get"],
|
|
4087
|
-
path: "/v1/config",
|
|
4088
|
-
summary: "Get router configuration",
|
|
4089
|
-
description: "Returns chain configurations including contract addresses and supported callback types."
|
|
4090
|
-
}), ApiResponse({
|
|
4091
|
-
status: 200,
|
|
4092
|
-
description: "Success",
|
|
4093
|
-
type: ConfigSuccessResponse
|
|
4094
|
-
})], ConfigController.prototype, "getConfig", null);
|
|
4095
|
-
ConfigController = __decorate([ApiTags("System")], ConfigController);
|
|
4382
|
+
], ConfigRulesController.prototype, "getConfigRules", null);
|
|
4383
|
+
ConfigRulesController = __decorate([ApiTags("System")], ConfigRulesController);
|
|
4096
4384
|
let ObligationsController = class ObligationsController {
|
|
4097
4385
|
async getObligations() {}
|
|
4098
4386
|
async getObligation() {}
|
|
@@ -4221,16 +4509,18 @@ UsersController = __decorate([ApiTags("Make"), ApiResponse({
|
|
|
4221
4509
|
description: "Bad Request",
|
|
4222
4510
|
type: BadRequestResponse
|
|
4223
4511
|
})], UsersController);
|
|
4224
|
-
const OpenApi = async (
|
|
4225
|
-
|
|
4512
|
+
const OpenApi = async () => {
|
|
4513
|
+
return await generateDocument({
|
|
4226
4514
|
controllers: [
|
|
4227
4515
|
BooksController,
|
|
4228
|
-
|
|
4516
|
+
ConfigContractsController,
|
|
4517
|
+
ConfigRulesController,
|
|
4229
4518
|
OffersController,
|
|
4230
4519
|
ObligationsController,
|
|
4231
4520
|
HealthController,
|
|
4232
4521
|
UsersController,
|
|
4233
|
-
ValidateController
|
|
4522
|
+
ValidateController,
|
|
4523
|
+
CallbacksController
|
|
4234
4524
|
],
|
|
4235
4525
|
document: {
|
|
4236
4526
|
openapi: "3.1.0",
|
|
@@ -4262,12 +4552,6 @@ const OpenApi = async (options = {}) => {
|
|
|
4262
4552
|
]
|
|
4263
4553
|
}
|
|
4264
4554
|
});
|
|
4265
|
-
if (options.rules && options.rules.length > 0) {
|
|
4266
|
-
const rulesDescription = options.rules.map((rule) => `- **${rule.name}**: ${rule.description}`).join("\n");
|
|
4267
|
-
const validatePath = document.paths?.["/v1/validate"];
|
|
4268
|
-
if (validatePath && "post" in validatePath && validatePath.post) validatePath.post.description = `Validates offers against router validation rules. Returns unsigned payload + root on success, or issues only on validation failure.\n\n**Available validation rules:**\n${rulesDescription}`;
|
|
4269
|
-
}
|
|
4270
|
-
return document;
|
|
4271
4555
|
};
|
|
4272
4556
|
|
|
4273
4557
|
//#endregion
|
|
@@ -4291,6 +4575,10 @@ function from$3(position) {
|
|
|
4291
4575
|
//#region src/api/Schema/requests.ts
|
|
4292
4576
|
const MAX_LIMIT = 100;
|
|
4293
4577
|
const DEFAULT_LIMIT$4 = 20;
|
|
4578
|
+
const CONFIG_RULES_MAX_LIMIT = 1e3;
|
|
4579
|
+
const CONFIG_RULES_DEFAULT_LIMIT = 100;
|
|
4580
|
+
const CONFIG_CONTRACTS_MAX_LIMIT = 1e3;
|
|
4581
|
+
const CONFIG_CONTRACTS_DEFAULT_LIMIT = 1e3;
|
|
4294
4582
|
/** Validate cursor is a valid base64url-encoded JSON object.
|
|
4295
4583
|
* Domain layer handles semantic validation of cursor fields. */
|
|
4296
4584
|
function isValidBase64urlJson(val) {
|
|
@@ -4324,6 +4612,43 @@ const PaginationQueryParams = z$2.object({
|
|
|
4324
4612
|
example: 10
|
|
4325
4613
|
})
|
|
4326
4614
|
});
|
|
4615
|
+
const ConfigRuleTypes = z$2.enum([
|
|
4616
|
+
"maturity",
|
|
4617
|
+
"callback",
|
|
4618
|
+
"loan_token"
|
|
4619
|
+
]);
|
|
4620
|
+
const GetConfigRulesQueryParams = z$2.object({
|
|
4621
|
+
cursor: z$2.string().regex(/^(maturity|callback|loan_token):[1-9]\d*:.+$/, { message: "Cursor must be in the format type:chain_id:<value>" }).optional().meta({
|
|
4622
|
+
description: "Pagination cursor in type:chain_id:<value> format",
|
|
4623
|
+
example: "maturity:1:1730415600:end_of_next_month"
|
|
4624
|
+
}),
|
|
4625
|
+
limit: z$2.string().regex(/^[1-9]\d*$/, { message: "Limit must be a positive integer" }).transform((val) => Number.parseInt(val, 10)).pipe(z$2.number().max(CONFIG_RULES_MAX_LIMIT, { message: `Limit cannot exceed ${CONFIG_RULES_MAX_LIMIT}` })).optional().default(CONFIG_RULES_DEFAULT_LIMIT).meta({
|
|
4626
|
+
description: `Limit maximum: ${CONFIG_RULES_MAX_LIMIT}. Default: ${CONFIG_RULES_DEFAULT_LIMIT}`,
|
|
4627
|
+
example: 100
|
|
4628
|
+
}),
|
|
4629
|
+
types: csvArray(ConfigRuleTypes).meta({
|
|
4630
|
+
description: "Filter by rule types (comma-separated).",
|
|
4631
|
+
example: "maturity,loan_token"
|
|
4632
|
+
}),
|
|
4633
|
+
chains: csvArray(z$2.string().regex(/^[1-9]\d*$/, { message: "Chain must be a positive integer" }).transform((val) => Number.parseInt(val, 10))).meta({
|
|
4634
|
+
description: "Filter by chain IDs (comma-separated).",
|
|
4635
|
+
example: "1,8453"
|
|
4636
|
+
})
|
|
4637
|
+
});
|
|
4638
|
+
const GetConfigContractsQueryParams = z$2.object({
|
|
4639
|
+
cursor: z$2.string().regex(/^[1-9]\d*:0x[a-fA-F0-9]{40}$/, { message: "Cursor must be in the format chain_id:0x..." }).optional().meta({
|
|
4640
|
+
description: "Pagination cursor in chain_id:address format (lowercase address).",
|
|
4641
|
+
example: "1:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
|
|
4642
|
+
}),
|
|
4643
|
+
limit: z$2.string().regex(/^[1-9]\d*$/, { message: "Limit must be a positive integer" }).transform((val) => Number.parseInt(val, 10)).pipe(z$2.number().max(CONFIG_CONTRACTS_MAX_LIMIT, { message: `Limit cannot exceed ${CONFIG_CONTRACTS_MAX_LIMIT}` })).optional().default(CONFIG_CONTRACTS_DEFAULT_LIMIT).meta({
|
|
4644
|
+
description: `Limit maximum: ${CONFIG_CONTRACTS_MAX_LIMIT}. Default: ${CONFIG_CONTRACTS_DEFAULT_LIMIT}`,
|
|
4645
|
+
example: 1e3
|
|
4646
|
+
}),
|
|
4647
|
+
chains: csvArray(z$2.string().regex(/^[1-9]\d*$/, { message: "Chain must be a positive integer" }).transform((val) => Number.parseInt(val, 10))).meta({
|
|
4648
|
+
description: "Filter by chain IDs (comma-separated).",
|
|
4649
|
+
example: "1,8453"
|
|
4650
|
+
})
|
|
4651
|
+
});
|
|
4327
4652
|
const GetOffersQueryParams = z$2.object({
|
|
4328
4653
|
...PaginationQueryParams.shape,
|
|
4329
4654
|
side: z$2.enum(["buy", "sell"]).optional().meta({
|
|
@@ -4426,6 +4751,16 @@ const GetBookParams = z$2.object({
|
|
|
4426
4751
|
})
|
|
4427
4752
|
});
|
|
4428
4753
|
const ValidateOffersBody = z$2.object({ offers: z$2.array(z$2.unknown()).min(1, { message: "'offers' must contain at least 1 offer" }) }).strict();
|
|
4754
|
+
const CallbackTypesBody = z$2.object({ callbacks: z$2.array(z$2.object({
|
|
4755
|
+
chain_id: z$2.number().int().positive().meta({
|
|
4756
|
+
description: "Chain id.",
|
|
4757
|
+
example: 1
|
|
4758
|
+
}),
|
|
4759
|
+
addresses: z$2.array(z$2.string().regex(/^0x[a-fA-F0-9]{40}$/, { error: "Callback address must be a valid 20-byte address" }).transform((val) => val.toLowerCase())).meta({
|
|
4760
|
+
description: "Callback contract addresses.",
|
|
4761
|
+
example: ["0x1111111111111111111111111111111111111111", "0x3333333333333333333333333333333333333333"]
|
|
4762
|
+
})
|
|
4763
|
+
}).strict()) }).strict();
|
|
4429
4764
|
const GetUserPositionsParams = z$2.object({
|
|
4430
4765
|
...PaginationQueryParams.shape,
|
|
4431
4766
|
user_address: z$2.string().regex(/^0x[a-fA-F0-9]{40}$/, { error: "User address must be a valid 20-byte address" }).transform((val) => val.toLowerCase()).meta({
|
|
@@ -4437,17 +4772,204 @@ const schemas = {
|
|
|
4437
4772
|
get_health: HealthQueryParams,
|
|
4438
4773
|
get_health_collectors: HealthQueryParams,
|
|
4439
4774
|
get_health_chains: HealthQueryParams,
|
|
4775
|
+
get_config_contracts: GetConfigContractsQueryParams,
|
|
4776
|
+
get_config_rules: GetConfigRulesQueryParams,
|
|
4440
4777
|
get_offers: GetOffersQueryParams,
|
|
4441
4778
|
get_obligations: GetObligationsQueryParams,
|
|
4442
4779
|
get_obligation: GetObligationParams,
|
|
4443
4780
|
get_book: GetBookParams,
|
|
4444
4781
|
validate_offers: ValidateOffersBody,
|
|
4782
|
+
callback_types: CallbackTypesBody,
|
|
4445
4783
|
get_user_positions: GetUserPositionsParams
|
|
4446
4784
|
};
|
|
4447
4785
|
function safeParse(action, query, error) {
|
|
4448
4786
|
return schemas[action].safeParse(query, { error });
|
|
4449
4787
|
}
|
|
4450
4788
|
|
|
4789
|
+
//#endregion
|
|
4790
|
+
//#region src/api/Controllers/getConfigRules.ts
|
|
4791
|
+
/**
|
|
4792
|
+
* Returns configured rules for the configured chains.
|
|
4793
|
+
* @param query - Raw query parameters containing filters/cursor/limit.
|
|
4794
|
+
* @param chains - Chains to include in the configured rules.
|
|
4795
|
+
* @returns Config rules response payload. {@link ApiPayload.Payload}
|
|
4796
|
+
*/
|
|
4797
|
+
async function getConfigRules(query, chains) {
|
|
4798
|
+
const parsed = safeParse("get_config_rules", query ?? {});
|
|
4799
|
+
if (!parsed.success) return failure(parsed.error);
|
|
4800
|
+
const { cursor, limit, types, chains: chainIds } = parsed.data;
|
|
4801
|
+
const typeFilter = types?.length ? new Set(types) : null;
|
|
4802
|
+
const chainFilter = chainIds?.length ? new Set(chainIds) : null;
|
|
4803
|
+
const filteredRules = buildConfigRules(chains).filter((rule) => {
|
|
4804
|
+
if (chainFilter && !chainFilter.has(rule.chain_id)) return false;
|
|
4805
|
+
if (typeFilter && !typeFilter.has(rule.type)) return false;
|
|
4806
|
+
return true;
|
|
4807
|
+
});
|
|
4808
|
+
const checksum = buildConfigRulesChecksum(filteredRules);
|
|
4809
|
+
let cursorRule = null;
|
|
4810
|
+
if (cursor) try {
|
|
4811
|
+
cursorRule = parseCursor$1(cursor);
|
|
4812
|
+
} catch (err) {
|
|
4813
|
+
return failure(err);
|
|
4814
|
+
}
|
|
4815
|
+
if (cursorRule && typeFilter && !typeFilter.has(cursorRule.type)) return failure(new BadRequestError("Cursor type must match requested rule types"));
|
|
4816
|
+
if (cursorRule && chainFilter && !chainFilter.has(cursorRule.chain_id)) return failure(new BadRequestError("Cursor chain_id must match requested chains"));
|
|
4817
|
+
const startIndex = cursorRule ? findStartIndex$1(filteredRules, cursorRule) : 0;
|
|
4818
|
+
const page = filteredRules.slice(startIndex, startIndex + limit);
|
|
4819
|
+
const nextCursor = startIndex + limit < filteredRules.length && page.length > 0 ? formatCursor$1(page.at(-1)) : null;
|
|
4820
|
+
const response = success({
|
|
4821
|
+
data: page,
|
|
4822
|
+
cursor: nextCursor
|
|
4823
|
+
});
|
|
4824
|
+
response.body.meta.checksum = checksum;
|
|
4825
|
+
return response;
|
|
4826
|
+
}
|
|
4827
|
+
function formatCursor$1(rule) {
|
|
4828
|
+
if (rule.type === "maturity") return `maturity:${rule.chain_id}:${rule.timestamp}:${rule.name}`;
|
|
4829
|
+
if (rule.type === "callback") return `callback:${rule.chain_id}:${rule.callback_type}:${rule.address.toLowerCase()}`;
|
|
4830
|
+
return `loan_token:${rule.chain_id}:${rule.address.toLowerCase()}`;
|
|
4831
|
+
}
|
|
4832
|
+
function parseCursor$1(cursor) {
|
|
4833
|
+
const [type, chain, ...rest] = cursor.split(":");
|
|
4834
|
+
if (!type || !chain || rest.length === 0) throw new BadRequestError("Cursor must be in the format type:chain_id:<value>");
|
|
4835
|
+
if (!isConfigRuleType(type)) throw new BadRequestError("Cursor has an invalid rule type");
|
|
4836
|
+
const chain_id = Number.parseInt(chain, 10);
|
|
4837
|
+
if (!Number.isFinite(chain_id)) throw new BadRequestError("Cursor has an invalid chain_id");
|
|
4838
|
+
if (type === "maturity") {
|
|
4839
|
+
const timestampValue = Number.parseInt(rest[0] ?? "", 10);
|
|
4840
|
+
const nameValue = rest.slice(1).join(":");
|
|
4841
|
+
if (!Number.isFinite(timestampValue) || nameValue.length === 0) throw new BadRequestError("Cursor must be in the format maturity:chain_id:timestamp:name");
|
|
4842
|
+
if (!isMaturityType(nameValue)) throw new BadRequestError("Cursor has an invalid maturity name");
|
|
4843
|
+
return {
|
|
4844
|
+
type,
|
|
4845
|
+
chain_id,
|
|
4846
|
+
timestamp: parseMaturity(timestampValue),
|
|
4847
|
+
name: nameValue
|
|
4848
|
+
};
|
|
4849
|
+
}
|
|
4850
|
+
if (type === "callback") {
|
|
4851
|
+
const callbackTypeValue = rest[0] ?? "";
|
|
4852
|
+
const addressValue = rest.slice(1).join(":");
|
|
4853
|
+
if (!callbackTypeValue || !addressValue) throw new BadRequestError("Cursor must be in the format callback:chain_id:callback_type:address");
|
|
4854
|
+
if (!isCallbackType(callbackTypeValue)) throw new BadRequestError("Cursor has an invalid callback type");
|
|
4855
|
+
return {
|
|
4856
|
+
type,
|
|
4857
|
+
chain_id,
|
|
4858
|
+
callback_type: callbackTypeValue,
|
|
4859
|
+
address: parseAddress(addressValue, "Cursor address")
|
|
4860
|
+
};
|
|
4861
|
+
}
|
|
4862
|
+
const addressValue = rest.join(":");
|
|
4863
|
+
if (!addressValue) throw new BadRequestError("Cursor must be in the format loan_token:chain_id:address");
|
|
4864
|
+
return {
|
|
4865
|
+
type,
|
|
4866
|
+
chain_id,
|
|
4867
|
+
address: parseAddress(addressValue, "Cursor address")
|
|
4868
|
+
};
|
|
4869
|
+
}
|
|
4870
|
+
function findStartIndex$1(rules, cursor) {
|
|
4871
|
+
let low = 0;
|
|
4872
|
+
let high = rules.length;
|
|
4873
|
+
while (low < high) {
|
|
4874
|
+
const mid = Math.floor((low + high) / 2);
|
|
4875
|
+
const current = rules[mid];
|
|
4876
|
+
if (compareConfigRules(current, cursor) <= 0) low = mid + 1;
|
|
4877
|
+
else high = mid;
|
|
4878
|
+
}
|
|
4879
|
+
return low;
|
|
4880
|
+
}
|
|
4881
|
+
function parseAddress(address, label) {
|
|
4882
|
+
if (!/^0x[a-fA-F0-9]{40}$/.test(address)) throw new BadRequestError(`${label} must be a valid 20-byte address`);
|
|
4883
|
+
return address.toLowerCase();
|
|
4884
|
+
}
|
|
4885
|
+
function isConfigRuleType(value) {
|
|
4886
|
+
return value === "maturity" || value === "callback" || value === "loan_token";
|
|
4887
|
+
}
|
|
4888
|
+
function isMaturityType(value) {
|
|
4889
|
+
return Object.values(MaturityType).includes(value);
|
|
4890
|
+
}
|
|
4891
|
+
function parseMaturity(value) {
|
|
4892
|
+
try {
|
|
4893
|
+
return from$16(value);
|
|
4894
|
+
} catch (err) {
|
|
4895
|
+
throw new BadRequestError(err instanceof Error ? err.message : "Invalid maturity timestamp");
|
|
4896
|
+
}
|
|
4897
|
+
}
|
|
4898
|
+
function isCallbackType(value) {
|
|
4899
|
+
if (value === Type$1.BuyWithEmptyCallback) return false;
|
|
4900
|
+
return Object.values(Type$1).includes(value);
|
|
4901
|
+
}
|
|
4902
|
+
|
|
4903
|
+
//#endregion
|
|
4904
|
+
//#region src/logger/Logger.ts
|
|
4905
|
+
const LogLevelValues = [
|
|
4906
|
+
"trace",
|
|
4907
|
+
"debug",
|
|
4908
|
+
"info",
|
|
4909
|
+
"warn",
|
|
4910
|
+
"error",
|
|
4911
|
+
"fatal",
|
|
4912
|
+
"silent"
|
|
4913
|
+
];
|
|
4914
|
+
function defaultLogger(minLevel, pretty) {
|
|
4915
|
+
const threshold = minLevel ?? process.env.ROUTER_LOG_LEVEL ?? "info";
|
|
4916
|
+
const prettyEnabled = typeof pretty === "boolean" ? pretty : String(process.env.ROUTER_LOG_PRETTY ?? "false").toLowerCase() === "true";
|
|
4917
|
+
const levelIndexByName = LogLevelValues.reduce((acc, lvl, idx) => {
|
|
4918
|
+
acc[lvl] = idx;
|
|
4919
|
+
return acc;
|
|
4920
|
+
}, {});
|
|
4921
|
+
const isEnabled = (methodLevel) => levelIndexByName[methodLevel] >= levelIndexByName[threshold];
|
|
4922
|
+
const wrap = (consoleMethod, methodLevel) => isEnabled(methodLevel) ? (entry) => {
|
|
4923
|
+
if (!prettyEnabled) {
|
|
4924
|
+
console[consoleMethod](stringify({
|
|
4925
|
+
level: methodLevel,
|
|
4926
|
+
...entry
|
|
4927
|
+
}));
|
|
4928
|
+
return;
|
|
4929
|
+
}
|
|
4930
|
+
const { msg, ...rest } = entry;
|
|
4931
|
+
const stack = typeof rest.stack === "string" ? rest.stack : void 0;
|
|
4932
|
+
if (stack) delete rest.stack;
|
|
4933
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
4934
|
+
const level = methodLevel.toUpperCase();
|
|
4935
|
+
const extras = Object.entries(rest).map(([k, v]) => `${k}=${formatValue(v)}`).join(" ");
|
|
4936
|
+
const line = extras.length > 0 ? `${timestamp} [${level}] ${msg} ${extras}` : `${timestamp} [${level}] ${msg}`;
|
|
4937
|
+
console[consoleMethod](line);
|
|
4938
|
+
if (stack) console[consoleMethod](stack);
|
|
4939
|
+
} : () => {};
|
|
4940
|
+
return {
|
|
4941
|
+
trace: wrap("trace", "trace"),
|
|
4942
|
+
debug: wrap("debug", "debug"),
|
|
4943
|
+
info: wrap("info", "info"),
|
|
4944
|
+
warn: wrap("warn", "warn"),
|
|
4945
|
+
error: wrap("error", "error"),
|
|
4946
|
+
fatal: wrap("error", "fatal")
|
|
4947
|
+
};
|
|
4948
|
+
}
|
|
4949
|
+
const loggerContext = new AsyncLocalStorage();
|
|
4950
|
+
function runWithLogger(logger, fn) {
|
|
4951
|
+
return loggerContext.run(logger, fn);
|
|
4952
|
+
}
|
|
4953
|
+
function getLogger() {
|
|
4954
|
+
return loggerContext.getStore() ?? defaultLogger();
|
|
4955
|
+
}
|
|
4956
|
+
function formatValue(value) {
|
|
4957
|
+
if (value === null || value === void 0 || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") return String(value);
|
|
4958
|
+
if (typeof value === "string") {
|
|
4959
|
+
if (value.includes(" ")) return JSON.stringify(value);
|
|
4960
|
+
return value;
|
|
4961
|
+
}
|
|
4962
|
+
try {
|
|
4963
|
+
return stringify(value);
|
|
4964
|
+
} catch {
|
|
4965
|
+
try {
|
|
4966
|
+
return JSON.stringify(value);
|
|
4967
|
+
} catch {
|
|
4968
|
+
return String(value);
|
|
4969
|
+
}
|
|
4970
|
+
}
|
|
4971
|
+
}
|
|
4972
|
+
|
|
4451
4973
|
//#endregion
|
|
4452
4974
|
//#region src/api/Controllers/validateOffers.ts
|
|
4453
4975
|
async function validateOffers(body, gatekeeper) {
|
|
@@ -4461,9 +4983,9 @@ async function validateOffers(body, gatekeeper) {
|
|
|
4461
4983
|
const rawOffer = rawOffers[i];
|
|
4462
4984
|
try {
|
|
4463
4985
|
const offer = fromSnakeCase(rawOffer);
|
|
4464
|
-
const hash$
|
|
4465
|
-
if (!offerIndexByHash.has(hash$
|
|
4466
|
-
offerIndexByHash.set(hash$
|
|
4986
|
+
const hash$3 = hash(offer);
|
|
4987
|
+
if (!offerIndexByHash.has(hash$3)) {
|
|
4988
|
+
offerIndexByHash.set(hash$3, i);
|
|
4467
4989
|
parsedOffers.push(offer);
|
|
4468
4990
|
}
|
|
4469
4991
|
} catch (err) {
|
|
@@ -4489,7 +5011,7 @@ async function validateOffers(body, gatekeeper) {
|
|
|
4489
5011
|
cursor: null
|
|
4490
5012
|
});
|
|
4491
5013
|
}
|
|
4492
|
-
const tree = from$
|
|
5014
|
+
const tree = from$7(parsedOffers);
|
|
4493
5015
|
const payload = encodeUnsigned(tree);
|
|
4494
5016
|
return success({
|
|
4495
5017
|
data: {
|
|
@@ -4509,6 +5031,35 @@ async function validateOffers(body, gatekeeper) {
|
|
|
4509
5031
|
}
|
|
4510
5032
|
}
|
|
4511
5033
|
|
|
5034
|
+
//#endregion
|
|
5035
|
+
//#region src/gatekeeper/CallbackTypes.ts
|
|
5036
|
+
/**
|
|
5037
|
+
* Resolve callback types for a list of callback addresses grouped by chain.
|
|
5038
|
+
* @param parameters - Resolve parameters. {@link resolveCallbackTypes.Parameters}
|
|
5039
|
+
* @returns Callback types grouped by chain. {@link resolveCallbackTypes.ReturnType}
|
|
5040
|
+
* @throws If a chain id is unknown.
|
|
5041
|
+
*/
|
|
5042
|
+
function resolveCallbackTypes$2(parameters) {
|
|
5043
|
+
const { chains, request } = parameters;
|
|
5044
|
+
const chainsById = new Map(chains.map((chain) => [chain.id, chain]));
|
|
5045
|
+
return request.callbacks.map(({ chain_id, addresses }) => {
|
|
5046
|
+
const chain = chainsById.get(chain_id);
|
|
5047
|
+
if (!chain) throw new Error(`Unknown chain id ${chain_id}`);
|
|
5048
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
5049
|
+
const uniqueAddresses = new Set(addresses.map((address) => address.toLowerCase()));
|
|
5050
|
+
for (const address of uniqueAddresses) {
|
|
5051
|
+
const bucketKey = getCallbackType(chain.name, address) ?? "not_supported";
|
|
5052
|
+
const list = buckets.get(bucketKey) ?? [];
|
|
5053
|
+
list.push(address);
|
|
5054
|
+
buckets.set(bucketKey, list);
|
|
5055
|
+
}
|
|
5056
|
+
const response = { chain_id };
|
|
5057
|
+
for (const [type, list] of buckets.entries()) response[type] = list;
|
|
5058
|
+
if (!response.not_supported) response.not_supported = [];
|
|
5059
|
+
return response;
|
|
5060
|
+
});
|
|
5061
|
+
}
|
|
5062
|
+
|
|
4512
5063
|
//#endregion
|
|
4513
5064
|
//#region src/gatekeeper/Service.ts
|
|
4514
5065
|
/**
|
|
@@ -4516,28 +5067,59 @@ async function validateOffers(body, gatekeeper) {
|
|
|
4516
5067
|
* @param parameters - App parameters including the {@link Gatekeeper} instance.
|
|
4517
5068
|
* @returns Hono app exposing gatekeeper endpoints.
|
|
4518
5069
|
*/
|
|
5070
|
+
const CallbackTypesRequestSchema = z$2.object({ callbacks: z$2.array(z$2.object({
|
|
5071
|
+
chain_id: z$2.number(),
|
|
5072
|
+
addresses: z$2.array(z$2.string().regex(/^0x[a-fA-F0-9]{40}$/))
|
|
5073
|
+
})) });
|
|
4519
5074
|
function createApp(parameters) {
|
|
4520
|
-
const { gatekeeper } = parameters;
|
|
5075
|
+
const { gatekeeper, chainRegistry } = parameters;
|
|
4521
5076
|
const app = new Hono();
|
|
4522
5077
|
app.post("/v1/validate", async (c) => {
|
|
4523
5078
|
let body;
|
|
4524
5079
|
try {
|
|
4525
5080
|
body = await c.req.json();
|
|
4526
5081
|
} catch (err) {
|
|
4527
|
-
const failure$
|
|
4528
|
-
return c.json(failure$
|
|
5082
|
+
const failure$5 = failure(err);
|
|
5083
|
+
return c.json(failure$5.body, failure$5.statusCode);
|
|
4529
5084
|
}
|
|
4530
5085
|
if (body === null || typeof body !== "object") {
|
|
4531
|
-
const failure$
|
|
4532
|
-
return c.json(failure$
|
|
5086
|
+
const failure$9 = failure(new BadRequestError("Request body must be a JSON object"));
|
|
5087
|
+
return c.json(failure$9.body, failure$9.statusCode);
|
|
4533
5088
|
}
|
|
4534
5089
|
const { statusCode, body: payload } = await validateOffers(body, gatekeeper);
|
|
4535
5090
|
return c.json(payload, statusCode);
|
|
4536
5091
|
});
|
|
4537
|
-
app.get("/v1/rules", async (c) => {
|
|
4538
|
-
const
|
|
4539
|
-
|
|
4540
|
-
|
|
5092
|
+
app.get("/v1/config/rules", async (c) => {
|
|
5093
|
+
const { statusCode, body } = await getConfigRules(c.req.query(), chainRegistry.list());
|
|
5094
|
+
return c.json(body, statusCode);
|
|
5095
|
+
});
|
|
5096
|
+
app.post("/v1/callbacks", async (c) => {
|
|
5097
|
+
let body;
|
|
5098
|
+
try {
|
|
5099
|
+
body = await c.req.json();
|
|
5100
|
+
} catch (err) {
|
|
5101
|
+
const failure$8 = failure(err);
|
|
5102
|
+
return c.json(failure$8.body, failure$8.statusCode);
|
|
5103
|
+
}
|
|
5104
|
+
if (body === null || typeof body !== "object") {
|
|
5105
|
+
const failure$6 = failure(new BadRequestError("Request body must be a JSON object"));
|
|
5106
|
+
return c.json(failure$6.body, failure$6.statusCode);
|
|
5107
|
+
}
|
|
5108
|
+
try {
|
|
5109
|
+
const request = CallbackTypesRequestSchema.parse(body);
|
|
5110
|
+
const chainIds = new Set(chainRegistry.list().map((chain) => chain.id));
|
|
5111
|
+
const unknown = request.callbacks.find((entry) => !chainIds.has(entry.chain_id));
|
|
5112
|
+
if (unknown) throw new BadRequestError(`Unknown chain id ${unknown.chain_id}`);
|
|
5113
|
+
const data = resolveCallbackTypes$2({
|
|
5114
|
+
chains: chainRegistry.list(),
|
|
5115
|
+
request
|
|
5116
|
+
});
|
|
5117
|
+
const response = success({ data });
|
|
5118
|
+
return c.json(response.body, response.statusCode);
|
|
5119
|
+
} catch (err) {
|
|
5120
|
+
const failure$7 = failure(err);
|
|
5121
|
+
return c.json(failure$7.body, failure$7.statusCode);
|
|
5122
|
+
}
|
|
4541
5123
|
});
|
|
4542
5124
|
return app;
|
|
4543
5125
|
}
|
|
@@ -4547,8 +5129,11 @@ function createApp(parameters) {
|
|
|
4547
5129
|
* @returns Service handle including base URL and shutdown method. {@link ServiceHandle}
|
|
4548
5130
|
*/
|
|
4549
5131
|
async function start$1(config) {
|
|
4550
|
-
const { gatekeeper, port, hostname } = config;
|
|
4551
|
-
const app = createApp({
|
|
5132
|
+
const { gatekeeper, chainRegistry, port, hostname } = config;
|
|
5133
|
+
const app = createApp({
|
|
5134
|
+
gatekeeper,
|
|
5135
|
+
chainRegistry
|
|
5136
|
+
});
|
|
4552
5137
|
let address = null;
|
|
4553
5138
|
let server;
|
|
4554
5139
|
await new Promise((resolve) => {
|
|
@@ -4748,6 +5333,18 @@ async function* collectOffersV2(parameters) {
|
|
|
4748
5333
|
const logger = getLogger();
|
|
4749
5334
|
let startBlock = blockNumber;
|
|
4750
5335
|
let reorgDetected = false;
|
|
5336
|
+
if (client.chain.custom.morpho.address.toLowerCase() === zeroAddress) {
|
|
5337
|
+
const msg = "Morpho V2 address is zero, signature verification will fail. Please set the Morpho V2 address in the chain configuration.";
|
|
5338
|
+
logger.error({
|
|
5339
|
+
msg,
|
|
5340
|
+
chain_id: client.chain.id
|
|
5341
|
+
});
|
|
5342
|
+
throw new Error(msg);
|
|
5343
|
+
}
|
|
5344
|
+
const signatureDomain = {
|
|
5345
|
+
chainId: client.chain.id,
|
|
5346
|
+
verifyingContract: client.chain.custom.morpho.address
|
|
5347
|
+
};
|
|
4751
5348
|
const { blockNumber: latestBlockNumberChain } = await db.blocks.getChain(client.chain.id);
|
|
4752
5349
|
const stream = streamLogs({
|
|
4753
5350
|
client,
|
|
@@ -4778,7 +5375,7 @@ async function* collectOffersV2(parameters) {
|
|
|
4778
5375
|
if (!log) continue;
|
|
4779
5376
|
const [payload] = decodeAbiParameters([{ type: "bytes" }], log.data);
|
|
4780
5377
|
try {
|
|
4781
|
-
const { tree, signature, signer } = await decode(payload);
|
|
5378
|
+
const { tree, signature, signer } = await decode(payload, signatureDomain);
|
|
4782
5379
|
const signerMismatch = tree.offers.find((offer) => offer.maker.toLowerCase() !== signer.toLowerCase());
|
|
4783
5380
|
if (signerMismatch) {
|
|
4784
5381
|
logger.debug({
|
|
@@ -4810,6 +5407,7 @@ async function* collectOffersV2(parameters) {
|
|
|
4810
5407
|
const { epoch, blockNumber: latestBlockNumber } = await dbTx.blocks.getChain(client.chain.id);
|
|
4811
5408
|
const treesToInsert = [];
|
|
4812
5409
|
let totalValidOffers = 0;
|
|
5410
|
+
const offersWithBlock = [];
|
|
4813
5411
|
for (const { tree, signature, blockNumber: treeBlockNumber } of decodedTrees) try {
|
|
4814
5412
|
const allowedResults = await gatekeeper.isAllowed(tree.offers);
|
|
4815
5413
|
const hasBlockWindowViolation = treeBlockNumber > latestBlockNumber;
|
|
@@ -4831,10 +5429,13 @@ async function* collectOffersV2(parameters) {
|
|
|
4831
5429
|
}
|
|
4832
5430
|
treesToInsert.push({
|
|
4833
5431
|
tree,
|
|
4834
|
-
signature
|
|
4835
|
-
blockNumber: treeBlockNumber
|
|
5432
|
+
signature
|
|
4836
5433
|
});
|
|
4837
5434
|
totalValidOffers += tree.offers.length;
|
|
5435
|
+
offersWithBlock.push(...tree.offers.map((offer) => ({
|
|
5436
|
+
offer,
|
|
5437
|
+
blockNumber: treeBlockNumber
|
|
5438
|
+
})));
|
|
4838
5439
|
} catch (err) {
|
|
4839
5440
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
4840
5441
|
logger.error({
|
|
@@ -4844,7 +5445,24 @@ async function* collectOffersV2(parameters) {
|
|
|
4844
5445
|
});
|
|
4845
5446
|
throw new Error("Gatekeeper validation failed", { cause: error });
|
|
4846
5447
|
}
|
|
5448
|
+
const dependencies = buildOfferDependencies$1(offersWithBlock);
|
|
5449
|
+
await dbTx.oracles.upsert(dependencies.oracles);
|
|
5450
|
+
await dbTx.obligations.create(dependencies.obligations);
|
|
5451
|
+
await dbTx.groups.create(dependencies.groups);
|
|
5452
|
+
const insertedHashes = await dbTx.offers.create(dependencies.offerBatches);
|
|
4847
5453
|
if (treesToInsert.length > 0) await dbTx.trees.create(treesToInsert);
|
|
5454
|
+
const insertedOffers = filterInsertedOffers({
|
|
5455
|
+
offers: offersWithBlock,
|
|
5456
|
+
hashes: insertedHashes
|
|
5457
|
+
});
|
|
5458
|
+
const { callbacks, positions, lots } = await decodeCallbacks({
|
|
5459
|
+
chainId: client.chain.id,
|
|
5460
|
+
gatekeeper,
|
|
5461
|
+
offers: insertedOffers
|
|
5462
|
+
});
|
|
5463
|
+
if (positions.length > 0) await dbTx.positions.upsert(positions);
|
|
5464
|
+
if (callbacks.length > 0) await dbTx.callbacks.upsert(callbacks);
|
|
5465
|
+
if (lots.length > 0) await dbTx.lots.create(lots);
|
|
4848
5466
|
try {
|
|
4849
5467
|
await dbTx.blocks.advanceCollector({
|
|
4850
5468
|
collectorName: collector,
|
|
@@ -4897,10 +5515,151 @@ async function* collectOffersV2(parameters) {
|
|
|
4897
5515
|
}
|
|
4898
5516
|
}
|
|
4899
5517
|
});
|
|
4900
|
-
if (reorgDetected) return;
|
|
4901
|
-
yield blockNumber;
|
|
4902
|
-
startBlock = blockNumber;
|
|
5518
|
+
if (reorgDetected) return;
|
|
5519
|
+
yield blockNumber;
|
|
5520
|
+
startBlock = blockNumber;
|
|
5521
|
+
}
|
|
5522
|
+
}
|
|
5523
|
+
async function decodeCallbacks(parameters) {
|
|
5524
|
+
const { chainId, gatekeeper, offers } = parameters;
|
|
5525
|
+
if (offers.length === 0) return {
|
|
5526
|
+
callbacks: [],
|
|
5527
|
+
positions: [],
|
|
5528
|
+
lots: []
|
|
5529
|
+
};
|
|
5530
|
+
const addresses = offers.filter((entry) => entry.offer.callback.data !== "0x").map((entry) => entry.offer.callback.address);
|
|
5531
|
+
if (addresses.length === 0) return {
|
|
5532
|
+
callbacks: [],
|
|
5533
|
+
positions: [],
|
|
5534
|
+
lots: []
|
|
5535
|
+
};
|
|
5536
|
+
let response;
|
|
5537
|
+
try {
|
|
5538
|
+
response = await gatekeeper.getCallbackTypes({ callbacks: [{
|
|
5539
|
+
chain_id: chainId,
|
|
5540
|
+
addresses
|
|
5541
|
+
}] });
|
|
5542
|
+
} catch (err) {
|
|
5543
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
5544
|
+
throw new Error("Failed to resolve callback types", { cause: error });
|
|
5545
|
+
}
|
|
5546
|
+
const entry = response.find((item) => item.chain_id === chainId);
|
|
5547
|
+
const typeByAddress = /* @__PURE__ */ new Map();
|
|
5548
|
+
if (entry) for (const [key, list] of Object.entries(entry)) {
|
|
5549
|
+
if (key === "chain_id" || key === "not_supported") continue;
|
|
5550
|
+
if (!Array.isArray(list)) continue;
|
|
5551
|
+
for (const address of list) typeByAddress.set(address.toLowerCase(), key);
|
|
5552
|
+
}
|
|
5553
|
+
const callbacks = [];
|
|
5554
|
+
const positions = [];
|
|
5555
|
+
const lots = [];
|
|
5556
|
+
for (const { offer, blockNumber: offerBlockNumber } of offers) {
|
|
5557
|
+
if (offer.callback.data === "0x") continue;
|
|
5558
|
+
const callbackType = typeByAddress.get(offer.callback.address.toLowerCase());
|
|
5559
|
+
if (!callbackType) continue;
|
|
5560
|
+
let decoded;
|
|
5561
|
+
try {
|
|
5562
|
+
decoded = decode$1(callbackType, offer.callback.data);
|
|
5563
|
+
} catch (err) {
|
|
5564
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
5565
|
+
throw new Error("Failed to decode callback data", { cause: error });
|
|
5566
|
+
}
|
|
5567
|
+
if (decoded.length === 0) continue;
|
|
5568
|
+
const offerHash = hash(offer);
|
|
5569
|
+
const callbackInputs = decoded.map((callback) => ({
|
|
5570
|
+
chainId: offer.chainId,
|
|
5571
|
+
contract: callback.contract,
|
|
5572
|
+
user: offer.maker,
|
|
5573
|
+
amount: callback.amount
|
|
5574
|
+
}));
|
|
5575
|
+
callbacks.push({
|
|
5576
|
+
offerHash,
|
|
5577
|
+
callbacks: callbackInputs
|
|
5578
|
+
});
|
|
5579
|
+
for (const callback of decoded) {
|
|
5580
|
+
const contract = callback.contract;
|
|
5581
|
+
const positionType = callbackType === Type$1.BuyVaultV1Callback ? Type.VAULT_V1 : Type.ERC20;
|
|
5582
|
+
const asset = callbackType === Type$1.BuyVaultV1Callback ? void 0 : contract;
|
|
5583
|
+
positions.push(from$10({
|
|
5584
|
+
chainId: offer.chainId,
|
|
5585
|
+
contract,
|
|
5586
|
+
user: offer.maker,
|
|
5587
|
+
type: positionType,
|
|
5588
|
+
asset,
|
|
5589
|
+
blockNumber: offerBlockNumber
|
|
5590
|
+
}));
|
|
5591
|
+
const isLoanPosition = offer.loanToken.toLowerCase() === asset?.toLowerCase();
|
|
5592
|
+
lots.push({
|
|
5593
|
+
positionChainId: offer.chainId,
|
|
5594
|
+
positionContract: contract,
|
|
5595
|
+
positionUser: offer.maker,
|
|
5596
|
+
group: offer.group,
|
|
5597
|
+
size: isLoanPosition ? offer.assets : callback.amount
|
|
5598
|
+
});
|
|
5599
|
+
}
|
|
5600
|
+
}
|
|
5601
|
+
return {
|
|
5602
|
+
callbacks,
|
|
5603
|
+
positions,
|
|
5604
|
+
lots
|
|
5605
|
+
};
|
|
5606
|
+
}
|
|
5607
|
+
function buildOfferDependencies$1(offers) {
|
|
5608
|
+
const obligationsById = /* @__PURE__ */ new Map();
|
|
5609
|
+
const oraclesByKey = /* @__PURE__ */ new Map();
|
|
5610
|
+
const groupsByKey = /* @__PURE__ */ new Map();
|
|
5611
|
+
const offersByBlock = /* @__PURE__ */ new Map();
|
|
5612
|
+
for (const { offer, blockNumber } of offers) {
|
|
5613
|
+
const list = offersByBlock.get(blockNumber) ?? [];
|
|
5614
|
+
list.push(offer);
|
|
5615
|
+
offersByBlock.set(blockNumber, list);
|
|
5616
|
+
const obligationId$2 = obligationId(offer);
|
|
5617
|
+
if (!obligationsById.get(obligationId$2)) obligationsById.set(obligationId$2, from$13({
|
|
5618
|
+
chainId: offer.chainId,
|
|
5619
|
+
loanToken: offer.loanToken,
|
|
5620
|
+
maturity: offer.maturity,
|
|
5621
|
+
collaterals: offer.collaterals
|
|
5622
|
+
}));
|
|
5623
|
+
for (const collateral of offer.collaterals) {
|
|
5624
|
+
const oracleKey = `${offer.chainId}-${collateral.oracle}`.toLowerCase();
|
|
5625
|
+
if (!oraclesByKey.has(oracleKey)) oraclesByKey.set(oracleKey, from$11({
|
|
5626
|
+
chainId: offer.chainId,
|
|
5627
|
+
address: collateral.oracle,
|
|
5628
|
+
price: null,
|
|
5629
|
+
blockNumber
|
|
5630
|
+
}));
|
|
5631
|
+
}
|
|
5632
|
+
const groupKey = `${offer.chainId}-${offer.maker}-${offer.group}`.toLowerCase();
|
|
5633
|
+
if (!groupsByKey.has(groupKey)) groupsByKey.set(groupKey, {
|
|
5634
|
+
chainId: offer.chainId,
|
|
5635
|
+
maker: offer.maker,
|
|
5636
|
+
group: offer.group,
|
|
5637
|
+
blockNumber
|
|
5638
|
+
});
|
|
5639
|
+
}
|
|
5640
|
+
return {
|
|
5641
|
+
obligations: Array.from(obligationsById.values()),
|
|
5642
|
+
oracles: Array.from(oraclesByKey.values()),
|
|
5643
|
+
groups: Array.from(groupsByKey.values()),
|
|
5644
|
+
offerBatches: Array.from(offersByBlock.entries()).map(([blockNumber, items]) => ({
|
|
5645
|
+
blockNumber,
|
|
5646
|
+
offers: items
|
|
5647
|
+
}))
|
|
5648
|
+
};
|
|
5649
|
+
}
|
|
5650
|
+
function filterInsertedOffers(parameters) {
|
|
5651
|
+
if (parameters.hashes.length === 0) return [];
|
|
5652
|
+
const inserted = new Set(parameters.hashes.map((hash) => hash.toLowerCase()));
|
|
5653
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5654
|
+
const filtered = [];
|
|
5655
|
+
for (const entry of parameters.offers) {
|
|
5656
|
+
const hash$2 = hash(entry.offer).toLowerCase();
|
|
5657
|
+
if (!inserted.has(hash$2)) continue;
|
|
5658
|
+
if (seen.has(hash$2)) continue;
|
|
5659
|
+
seen.add(hash$2);
|
|
5660
|
+
filtered.push(entry);
|
|
4903
5661
|
}
|
|
5662
|
+
return filtered;
|
|
4904
5663
|
}
|
|
4905
5664
|
|
|
4906
5665
|
//#endregion
|
|
@@ -5160,7 +5919,7 @@ async function* collectPositions(parameters) {
|
|
|
5160
5919
|
});
|
|
5161
5920
|
continue;
|
|
5162
5921
|
}
|
|
5163
|
-
transfers.push(from$
|
|
5922
|
+
transfers.push(from$8({
|
|
5164
5923
|
id: `${client.chain.id}-${log.blockNumber.toString()}-${log.transactionHash}-${log.logIndex.toString()}`,
|
|
5165
5924
|
chainId: client.chain.id,
|
|
5166
5925
|
contract: log.address,
|
|
@@ -5499,7 +6258,7 @@ async function* collectPrices(parameters) {
|
|
|
5499
6258
|
//#region src/indexer/collectors/CollectorBuilder.ts
|
|
5500
6259
|
function createBuilder(parameters) {
|
|
5501
6260
|
const { client, db, gatekeeper, options: { maxBlockNumber, blockWindow, interval } = {} } = parameters;
|
|
5502
|
-
const createCollector = (name, collect) => create$
|
|
6261
|
+
const createCollector = (name, collect) => create$16({
|
|
5503
6262
|
name,
|
|
5504
6263
|
collect,
|
|
5505
6264
|
client,
|
|
@@ -5603,7 +6362,7 @@ function from$1(config) {
|
|
|
5603
6362
|
retryAttempts,
|
|
5604
6363
|
retryDelayMs
|
|
5605
6364
|
});
|
|
5606
|
-
return create$
|
|
6365
|
+
return create$18({
|
|
5607
6366
|
client,
|
|
5608
6367
|
collectors: [
|
|
5609
6368
|
offersCollector,
|
|
@@ -5613,7 +6372,7 @@ function from$1(config) {
|
|
|
5613
6372
|
]
|
|
5614
6373
|
});
|
|
5615
6374
|
}
|
|
5616
|
-
function create$
|
|
6375
|
+
function create$18(params) {
|
|
5617
6376
|
const { collectors, client } = params;
|
|
5618
6377
|
const indexerId = `${client.chain.id.toString()}.indexer`;
|
|
5619
6378
|
const tracer = getTracer(`router.${indexerId}`);
|
|
@@ -5642,7 +6401,7 @@ function create$15(params) {
|
|
|
5642
6401
|
|
|
5643
6402
|
//#endregion
|
|
5644
6403
|
//#region src/indexer/collectors/Admin.ts
|
|
5645
|
-
function create$
|
|
6404
|
+
function create$17(parameters) {
|
|
5646
6405
|
const collector = "admin";
|
|
5647
6406
|
const { client, db, options: { maxBatchSize = 25, maxBlockNumber } = {} } = parameters;
|
|
5648
6407
|
const maxBlockNumberBI = maxBlockNumber !== void 0 ? BigInt(maxBlockNumber) : void 0;
|
|
@@ -5882,8 +6641,8 @@ const names = [
|
|
|
5882
6641
|
"positions",
|
|
5883
6642
|
"prices"
|
|
5884
6643
|
];
|
|
5885
|
-
function create$
|
|
5886
|
-
const admin = create$
|
|
6644
|
+
function create$16({ name, collect, client, db, options }) {
|
|
6645
|
+
const admin = create$17({
|
|
5887
6646
|
client,
|
|
5888
6647
|
db,
|
|
5889
6648
|
options
|
|
@@ -6119,7 +6878,6 @@ const obligationCollateralsV2 = s.table(EnumTableName.OBLIGATION_COLLATERALS_V2,
|
|
|
6119
6878
|
oracleChainId: bigint("oracle_chain_id", { mode: "number" }).$type().notNull(),
|
|
6120
6879
|
oracleAddress: varchar("oracle_address", { length: 42 }).notNull(),
|
|
6121
6880
|
lltv: bigint("lltv", { mode: "bigint" }).notNull(),
|
|
6122
|
-
blockNumber: bigint("block_number", { mode: "number" }).notNull(),
|
|
6123
6881
|
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
6124
6882
|
}, (table) => [
|
|
6125
6883
|
primaryKey({
|
|
@@ -6415,7 +7173,7 @@ const merklePaths = s.table(EnumTableName.MERKLE_PATHS, {
|
|
|
6415
7173
|
//#endregion
|
|
6416
7174
|
//#region src/database/domains/Blocks.ts
|
|
6417
7175
|
/** Postgres implementation. */
|
|
6418
|
-
const create$
|
|
7176
|
+
const create$15 = (config) => {
|
|
6419
7177
|
const { db, chainRegistry } = config;
|
|
6420
7178
|
const getChain = async (chainId) => {
|
|
6421
7179
|
const rows = await db.select({
|
|
@@ -6596,7 +7354,7 @@ const create$12 = (config) => {
|
|
|
6596
7354
|
//#region src/database/domains/Book.ts
|
|
6597
7355
|
const DEFAULT_LIMIT$3 = 100;
|
|
6598
7356
|
const MAX_TOTAL_OFFERS = 500;
|
|
6599
|
-
function create$
|
|
7357
|
+
function create$14(config) {
|
|
6600
7358
|
const db = config.db;
|
|
6601
7359
|
const logger = getLogger();
|
|
6602
7360
|
const getOffers = async (parameters) => {
|
|
@@ -7060,16 +7818,76 @@ let LevelCursor;
|
|
|
7060
7818
|
*/
|
|
7061
7819
|
const DEFAULT_BATCH_SIZE = 4e3;
|
|
7062
7820
|
|
|
7821
|
+
//#endregion
|
|
7822
|
+
//#region src/database/domains/Callbacks.ts
|
|
7823
|
+
/**
|
|
7824
|
+
* Create a callbacks domain instance.
|
|
7825
|
+
* @param db - Database core instance.
|
|
7826
|
+
* @returns Callbacks domain. {@link CallbacksDomain}
|
|
7827
|
+
*/
|
|
7828
|
+
function create$13(db) {
|
|
7829
|
+
return {
|
|
7830
|
+
upsert: async (inputs) => {
|
|
7831
|
+
if (inputs.length === 0) return;
|
|
7832
|
+
const idCache = /* @__PURE__ */ new Map();
|
|
7833
|
+
const seenCallbackIds = /* @__PURE__ */ new Set();
|
|
7834
|
+
const callbacksRows = [];
|
|
7835
|
+
const offersCallbacksRows = [];
|
|
7836
|
+
const callbackId = (input) => {
|
|
7837
|
+
const preimage = `0x${input.chainId}${input.contract}${input.user}${input.amount.toString()}`.toLowerCase();
|
|
7838
|
+
const id = idCache.get(preimage) ?? keccak256(preimage);
|
|
7839
|
+
idCache.set(preimage, id);
|
|
7840
|
+
return id;
|
|
7841
|
+
};
|
|
7842
|
+
for (const { offerHash, callbacks } of inputs) {
|
|
7843
|
+
const normalizedOfferHash = offerHash.toLowerCase();
|
|
7844
|
+
for (const callback of callbacks) {
|
|
7845
|
+
const normalized = {
|
|
7846
|
+
chainId: callback.chainId,
|
|
7847
|
+
contract: callback.contract.toLowerCase(),
|
|
7848
|
+
user: callback.user.toLowerCase(),
|
|
7849
|
+
amount: callback.amount
|
|
7850
|
+
};
|
|
7851
|
+
const id = callbackId(normalized);
|
|
7852
|
+
offersCallbacksRows.push({
|
|
7853
|
+
offerHash: normalizedOfferHash,
|
|
7854
|
+
callbackId: id
|
|
7855
|
+
});
|
|
7856
|
+
if (seenCallbackIds.has(id)) continue;
|
|
7857
|
+
seenCallbackIds.add(id);
|
|
7858
|
+
callbacksRows.push({
|
|
7859
|
+
id,
|
|
7860
|
+
positionChainId: normalized.chainId,
|
|
7861
|
+
positionContract: normalized.contract,
|
|
7862
|
+
positionUser: normalized.user,
|
|
7863
|
+
amount: normalized.amount.toString()
|
|
7864
|
+
});
|
|
7865
|
+
}
|
|
7866
|
+
}
|
|
7867
|
+
if (offersCallbacksRows.length === 0) return;
|
|
7868
|
+
await db.transaction(async (dbTx) => {
|
|
7869
|
+
for (const batch of batch$1(callbacksRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(callbacks).values(batch).onConflictDoNothing();
|
|
7870
|
+
for (const batch of batch$1(offersCallbacksRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(offersCallbacks).values(batch).onConflictDoNothing();
|
|
7871
|
+
});
|
|
7872
|
+
},
|
|
7873
|
+
delete: async ({ offers }) => {
|
|
7874
|
+
if (offers.length === 0) return 0;
|
|
7875
|
+
const normalized = offers.map((offer) => offer.toLowerCase());
|
|
7876
|
+
return (await db.delete(offersCallbacks).where(inArray(offersCallbacks.offerHash, normalized))).affectedRows;
|
|
7877
|
+
}
|
|
7878
|
+
};
|
|
7879
|
+
}
|
|
7880
|
+
|
|
7063
7881
|
//#endregion
|
|
7064
7882
|
//#region src/database/domains/Consumed.ts
|
|
7065
|
-
function create$
|
|
7883
|
+
function create$12(db) {
|
|
7066
7884
|
return {
|
|
7067
7885
|
create: async (events) => {
|
|
7068
7886
|
if (events.length === 0) return;
|
|
7069
|
-
const groups$
|
|
7887
|
+
const groups$2 = /* @__PURE__ */ new Map();
|
|
7070
7888
|
for (const event of events) {
|
|
7071
7889
|
const groupId = `${event.chainId}-${event.maker}-${event.group}`.toLowerCase();
|
|
7072
|
-
groups$
|
|
7890
|
+
groups$2.set(groupId, {
|
|
7073
7891
|
chainId: event.chainId,
|
|
7074
7892
|
maker: event.maker,
|
|
7075
7893
|
group: event.group,
|
|
@@ -7077,7 +7895,7 @@ function create$10(db) {
|
|
|
7077
7895
|
});
|
|
7078
7896
|
}
|
|
7079
7897
|
await db.transaction(async (dbTx) => {
|
|
7080
|
-
const groupsRows = Array.from(groups$
|
|
7898
|
+
const groupsRows = Array.from(groups$2.values()).map((group) => ({
|
|
7081
7899
|
chainId: group.chainId,
|
|
7082
7900
|
maker: group.maker.toLowerCase(),
|
|
7083
7901
|
group: group.group.toLowerCase(),
|
|
@@ -7103,9 +7921,30 @@ function create$10(db) {
|
|
|
7103
7921
|
};
|
|
7104
7922
|
}
|
|
7105
7923
|
|
|
7924
|
+
//#endregion
|
|
7925
|
+
//#region src/database/domains/Groups.ts
|
|
7926
|
+
/**
|
|
7927
|
+
* Create a groups domain instance.
|
|
7928
|
+
* @param db - Database core instance.
|
|
7929
|
+
* @returns Groups domain. {@link GroupsDomain}
|
|
7930
|
+
*/
|
|
7931
|
+
function create$11(db) {
|
|
7932
|
+
return { create: async (groups$1) => {
|
|
7933
|
+
if (groups$1.length === 0) return;
|
|
7934
|
+
const rows = groups$1.map((group) => ({
|
|
7935
|
+
chainId: group.chainId,
|
|
7936
|
+
maker: group.maker.toLowerCase(),
|
|
7937
|
+
group: group.group.toLowerCase(),
|
|
7938
|
+
consumed: (group.consumed ?? 0n).toString(),
|
|
7939
|
+
blockNumber: group.blockNumber
|
|
7940
|
+
}));
|
|
7941
|
+
for (const batch of batch$1(rows, DEFAULT_BATCH_SIZE)) await db.insert(groups).values(batch).onConflictDoNothing();
|
|
7942
|
+
} };
|
|
7943
|
+
}
|
|
7944
|
+
|
|
7106
7945
|
//#endregion
|
|
7107
7946
|
//#region src/database/domains/Lots.ts
|
|
7108
|
-
function create$
|
|
7947
|
+
function create$10(db) {
|
|
7109
7948
|
return {
|
|
7110
7949
|
get: async (parameters) => {
|
|
7111
7950
|
const { chainId, user, contract, group } = parameters ?? {};
|
|
@@ -7149,268 +7988,88 @@ function create$9(db) {
|
|
|
7149
7988
|
}
|
|
7150
7989
|
|
|
7151
7990
|
//#endregion
|
|
7152
|
-
//#region src/
|
|
7153
|
-
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
7991
|
+
//#region src/database/domains/Obligations.ts
|
|
7154
7992
|
/**
|
|
7155
|
-
* Create an
|
|
7156
|
-
* @param
|
|
7157
|
-
* @returns
|
|
7993
|
+
* Create an obligations domain instance.
|
|
7994
|
+
* @param db - Database core instance.
|
|
7995
|
+
* @returns Obligations domain. {@link ObligationsDomain}
|
|
7158
7996
|
*/
|
|
7159
|
-
function
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7997
|
+
function create$9(db) {
|
|
7998
|
+
return { create: async (obligations$1) => {
|
|
7999
|
+
if (obligations$1.length === 0) return;
|
|
8000
|
+
const obligationsById = /* @__PURE__ */ new Map();
|
|
8001
|
+
for (const obligation of obligations$1) {
|
|
8002
|
+
const id$1 = id(obligation).toLowerCase();
|
|
8003
|
+
if (!obligationsById.get(id$1)) obligationsById.set(id$1, obligation);
|
|
8004
|
+
}
|
|
7166
8005
|
try {
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
8006
|
+
await db.transaction(async (dbTx) => {
|
|
8007
|
+
const obligationRows = obligations$1.map((obligation) => ({
|
|
8008
|
+
obligationId: id(obligation),
|
|
8009
|
+
chainId: obligation.chainId,
|
|
8010
|
+
loanToken: obligation.loanToken.toLowerCase(),
|
|
8011
|
+
maturity: obligation.maturity
|
|
8012
|
+
}));
|
|
8013
|
+
for (const batch of batch$1(obligationRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(obligations).values(batch).onConflictDoNothing();
|
|
8014
|
+
const collateralRows = obligations$1.flatMap((obligation) => {
|
|
8015
|
+
return obligation.collaterals.map((collateral) => ({
|
|
8016
|
+
obligationId: id(obligation),
|
|
8017
|
+
asset: collateral.asset.toLowerCase(),
|
|
8018
|
+
oracleChainId: obligation.chainId,
|
|
8019
|
+
oracleAddress: collateral.oracle.toLowerCase(),
|
|
8020
|
+
lltv: collateral.lltv
|
|
8021
|
+
}));
|
|
8022
|
+
});
|
|
8023
|
+
for (const batch of batch$1(collateralRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(obligationCollateralsV2).values(batch).onConflictDoNothing();
|
|
7170
8024
|
});
|
|
7171
|
-
}
|
|
7172
|
-
|
|
7173
|
-
|
|
7174
|
-
};
|
|
7175
|
-
const validate = async (body) => {
|
|
7176
|
-
const response = await request("/v1/validate", {
|
|
7177
|
-
method: "POST",
|
|
7178
|
-
headers: { "content-type": "application/json" },
|
|
7179
|
-
body: JSON.stringify(body)
|
|
7180
|
-
});
|
|
7181
|
-
const json = await response.json();
|
|
7182
|
-
return {
|
|
7183
|
-
statusCode: response.status,
|
|
7184
|
-
body: json
|
|
7185
|
-
};
|
|
7186
|
-
};
|
|
7187
|
-
const getRules = async () => {
|
|
7188
|
-
const response = await request("/v1/rules", { method: "GET" });
|
|
7189
|
-
const json = await response.json();
|
|
7190
|
-
if (!response.ok) throw new Error(`Gatekeeper rules request failed: ${extractErrorMessage(json) ?? response.statusText}`);
|
|
7191
|
-
if (!("data" in json) || !Array.isArray(json.data)) throw new Error("Gatekeeper rules response is invalid.");
|
|
7192
|
-
return json.data;
|
|
7193
|
-
};
|
|
7194
|
-
const isAllowed = async (offers) => {
|
|
7195
|
-
const { statusCode, body } = await validate({ offers: offers.map((offer) => toSnakeCase(offer)) });
|
|
7196
|
-
if (statusCode !== 200) {
|
|
7197
|
-
const errorMessage = extractErrorMessage(body);
|
|
7198
|
-
throw new Error(`Gatekeeper validation failed: ${errorMessage ?? `status ${statusCode}`}`);
|
|
7199
|
-
}
|
|
7200
|
-
const data = body.data;
|
|
7201
|
-
if (!data || typeof data !== "object") throw new Error("Gatekeeper validation response is invalid.");
|
|
7202
|
-
if ("issues" in data) {
|
|
7203
|
-
const issues = data.issues.map((issue) => ({
|
|
7204
|
-
ruleName: issue.rule,
|
|
7205
|
-
message: issue.message,
|
|
7206
|
-
item: offers[issue.index]
|
|
7207
|
-
}));
|
|
7208
|
-
const invalidIndices = new Set(data.issues.map((issue) => issue.index));
|
|
7209
|
-
return {
|
|
7210
|
-
valid: offers.filter((_, index) => !invalidIndices.has(index)),
|
|
7211
|
-
issues
|
|
7212
|
-
};
|
|
8025
|
+
} catch (err) {
|
|
8026
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
8027
|
+
throw new Error("Obligations.create failed. Ensure oracles exist before inserting obligations.", { cause: error });
|
|
7213
8028
|
}
|
|
7214
|
-
|
|
7215
|
-
return {
|
|
7216
|
-
valid: offers.slice(),
|
|
7217
|
-
issues: []
|
|
7218
|
-
};
|
|
7219
|
-
};
|
|
7220
|
-
return {
|
|
7221
|
-
baseUrl,
|
|
7222
|
-
validate,
|
|
7223
|
-
isAllowed,
|
|
7224
|
-
getRules
|
|
7225
|
-
};
|
|
7226
|
-
}
|
|
7227
|
-
function normalizeBaseUrl(url) {
|
|
7228
|
-
return url.trim().replace(/\/+$/, "");
|
|
7229
|
-
}
|
|
7230
|
-
function extractErrorMessage(payload) {
|
|
7231
|
-
if (!payload || typeof payload !== "object") return void 0;
|
|
7232
|
-
const error = payload.error;
|
|
7233
|
-
if (!error || typeof error !== "object") return void 0;
|
|
7234
|
-
return typeof error.message === "string" ? error.message : void 0;
|
|
8029
|
+
} };
|
|
7235
8030
|
}
|
|
7236
8031
|
|
|
7237
8032
|
//#endregion
|
|
7238
8033
|
//#region src/database/domains/Offers.ts
|
|
7239
8034
|
const DEFAULT_LIMIT$2 = 100;
|
|
7240
8035
|
function create$8(config) {
|
|
7241
|
-
const { db
|
|
8036
|
+
const { db } = config;
|
|
7242
8037
|
return {
|
|
7243
8038
|
create: async (batches) => {
|
|
7244
8039
|
if (batches.length === 0) return [];
|
|
7245
|
-
const
|
|
7246
|
-
offer,
|
|
8040
|
+
const offersRows = batches.flatMap(({ blockNumber, offers }) => offers.map((offer) => ({
|
|
8041
|
+
...serialize(offer),
|
|
8042
|
+
obligationId: obligationId(offer),
|
|
8043
|
+
groupChainId: offer.chainId,
|
|
8044
|
+
groupMaker: offer.maker.toLowerCase(),
|
|
8045
|
+
callbackAddress: offer.callback.address.toLowerCase(),
|
|
8046
|
+
callbackData: offer.callback.data,
|
|
7247
8047
|
blockNumber
|
|
7248
8048
|
})));
|
|
7249
|
-
if (
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
blockNumber
|
|
7266
|
-
});
|
|
7267
|
-
for (const collateral of offer.collaterals) {
|
|
7268
|
-
const oracleId = `${offer.chainId}-${collateral.oracle.toLowerCase()}`.toLowerCase();
|
|
7269
|
-
if (!oraclesMap.has(oracleId)) oraclesMap.set(oracleId, {
|
|
7270
|
-
chainId: offer.chainId,
|
|
7271
|
-
address: collateral.oracle,
|
|
7272
|
-
blockNumber
|
|
7273
|
-
});
|
|
8049
|
+
if (offersRows.length === 0) return [];
|
|
8050
|
+
try {
|
|
8051
|
+
return await db.transaction(async (dbTx) => {
|
|
8052
|
+
const selectExisting = async (hashes) => {
|
|
8053
|
+
if (hashes.length === 0) return /* @__PURE__ */ new Set();
|
|
8054
|
+
const existing = /* @__PURE__ */ new Set();
|
|
8055
|
+
for (const batch of batch$1(hashes, DEFAULT_BATCH_SIZE)) {
|
|
8056
|
+
const rows = await dbTx.select({ hash: offers.hash }).from(offers).where(inArray(offers.hash, batch));
|
|
8057
|
+
for (const row of rows) existing.add(String(row.hash).toLowerCase());
|
|
8058
|
+
}
|
|
8059
|
+
return existing;
|
|
8060
|
+
};
|
|
8061
|
+
const inserted = [];
|
|
8062
|
+
for (const batch of batch$1(offersRows, DEFAULT_BATCH_SIZE)) {
|
|
8063
|
+
const rows = await dbTx.insert(offers).values(batch).onConflictDoNothing().returning();
|
|
8064
|
+
inserted.push(...rows.map((row) => row.hash));
|
|
7274
8065
|
}
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
if (!groupsMap.has(groupId)) groupsMap.set(groupId, {
|
|
7278
|
-
chainId: offer.chainId,
|
|
7279
|
-
maker: offer.maker,
|
|
7280
|
-
group: offer.group,
|
|
7281
|
-
blockNumber
|
|
8066
|
+
const existing = await selectExisting(inserted);
|
|
8067
|
+
return inserted.filter((hash) => existing.has(hash));
|
|
7282
8068
|
});
|
|
8069
|
+
} catch (err) {
|
|
8070
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
8071
|
+
throw new Error("Offers.create failed. Ensure obligations and groups exist before inserting offers.", { cause: error });
|
|
7283
8072
|
}
|
|
7284
|
-
return await db.transaction(async (dbTx) => {
|
|
7285
|
-
const obligationsRows = Array.from(obligationsMap.entries()).map(([obligationId, obligation]) => ({
|
|
7286
|
-
obligationId,
|
|
7287
|
-
chainId: obligation.chainId,
|
|
7288
|
-
loanToken: obligation.loanToken.toLowerCase(),
|
|
7289
|
-
maturity: obligation.maturity
|
|
7290
|
-
}));
|
|
7291
|
-
for (const batch of batch$1(obligationsRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(obligations).values(batch).onConflictDoNothing();
|
|
7292
|
-
const oraclesRows = Array.from(oraclesMap.values()).map((oracle) => ({
|
|
7293
|
-
chainId: oracle.chainId,
|
|
7294
|
-
address: oracle.address.toLowerCase(),
|
|
7295
|
-
blockNumber: oracle.blockNumber
|
|
7296
|
-
}));
|
|
7297
|
-
for (const batch of batch$1(oraclesRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(oracles).values(batch).onConflictDoNothing();
|
|
7298
|
-
const collateralsRows = Array.from(collateralsMap.entries()).flatMap(([obligationId, items]) => items.collaterals.map((collateral) => ({
|
|
7299
|
-
obligationId,
|
|
7300
|
-
asset: collateral.asset.toLowerCase(),
|
|
7301
|
-
oracleChainId: obligationsMap.get(obligationId).chainId,
|
|
7302
|
-
oracleAddress: collateral.oracle.toLowerCase(),
|
|
7303
|
-
lltv: collateral.lltv,
|
|
7304
|
-
blockNumber: items.blockNumber
|
|
7305
|
-
})));
|
|
7306
|
-
for (const batch of batch$1(collateralsRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(obligationCollateralsV2).values(batch).onConflictDoNothing();
|
|
7307
|
-
const groupsRows = Array.from(groupsMap.values()).map((group) => ({
|
|
7308
|
-
chainId: group.chainId,
|
|
7309
|
-
maker: group.maker.toLowerCase(),
|
|
7310
|
-
group: group.group.toLowerCase(),
|
|
7311
|
-
consumed: "0",
|
|
7312
|
-
blockNumber: group.blockNumber
|
|
7313
|
-
}));
|
|
7314
|
-
for (const batch of batch$1(groupsRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(groups).values(batch).onConflictDoNothing();
|
|
7315
|
-
const offersRows = offersWithBlock.map(({ offer, blockNumber }) => ({
|
|
7316
|
-
...serialize(offer),
|
|
7317
|
-
obligationId: obligationId(offer),
|
|
7318
|
-
groupChainId: offer.chainId,
|
|
7319
|
-
groupMaker: offer.maker.toLowerCase(),
|
|
7320
|
-
callbackAddress: offer.callback.address.toLowerCase(),
|
|
7321
|
-
callbackData: offer.callback.data,
|
|
7322
|
-
blockNumber
|
|
7323
|
-
}));
|
|
7324
|
-
const inserted = [];
|
|
7325
|
-
for (const batch of batch$1(offersRows, DEFAULT_BATCH_SIZE)) {
|
|
7326
|
-
const result = await dbTx.insert(offers).values(batch).onConflictDoNothing().returning();
|
|
7327
|
-
inserted.push(...result);
|
|
7328
|
-
}
|
|
7329
|
-
if (inserted.length === 0) return [];
|
|
7330
|
-
const idCached = /* @__PURE__ */ new Map();
|
|
7331
|
-
const id = (params) => {
|
|
7332
|
-
const preimage = `0x${params.chainId}${params.contract}${params.user}${params.amount}`.toLowerCase();
|
|
7333
|
-
const id = idCached.get(preimage) ?? keccak256(preimage);
|
|
7334
|
-
idCached.set(preimage, id);
|
|
7335
|
-
return id;
|
|
7336
|
-
};
|
|
7337
|
-
const offersCallbacksMap = /* @__PURE__ */ new Map();
|
|
7338
|
-
for (const offer of inserted) {
|
|
7339
|
-
if (offer.callbackData === "0x") continue;
|
|
7340
|
-
const user = offer.groupMaker.toLowerCase();
|
|
7341
|
-
if (!offersCallbacksMap.has(offer.hash)) offersCallbacksMap.set(offer.hash, []);
|
|
7342
|
-
const chain = chainRegistry.getById(offer.groupChainId);
|
|
7343
|
-
if (!chain) continue;
|
|
7344
|
-
const callbackType = getCallbackType(chain.name, offer.callbackAddress);
|
|
7345
|
-
if (!callbackType) continue;
|
|
7346
|
-
const callbacks = decode$1(callbackType, offer.callbackData).map((callback) => ({
|
|
7347
|
-
chainId: offer.groupChainId,
|
|
7348
|
-
contract: callback.contract.toLowerCase(),
|
|
7349
|
-
user: user.toLowerCase(),
|
|
7350
|
-
amount: callback.amount.toString(),
|
|
7351
|
-
type: callbackType === Type$1.BuyVaultV1Callback ? Type.VAULT_V1 : Type.ERC20,
|
|
7352
|
-
asset: callbackType === Type$1.BuyVaultV1Callback ? void 0 : callback.contract.toLowerCase(),
|
|
7353
|
-
blockNumber: offer.blockNumber
|
|
7354
|
-
}));
|
|
7355
|
-
try {
|
|
7356
|
-
await dbTx.insert(offersCallbacks).values(callbacks.map((callback) => ({
|
|
7357
|
-
offerHash: offer.hash,
|
|
7358
|
-
callbackId: id(callback)
|
|
7359
|
-
}))).onConflictDoNothing();
|
|
7360
|
-
offersCallbacksMap.get(offer.hash).push(...callbacks);
|
|
7361
|
-
} catch (_) {
|
|
7362
|
-
offersCallbacksMap.delete(offer.hash);
|
|
7363
|
-
}
|
|
7364
|
-
}
|
|
7365
|
-
if (offersCallbacksMap.size === 0) {
|
|
7366
|
-
obligationsMap.clear();
|
|
7367
|
-
collateralsMap.clear();
|
|
7368
|
-
oraclesMap.clear();
|
|
7369
|
-
groupsMap.clear();
|
|
7370
|
-
offersCallbacksMap.clear();
|
|
7371
|
-
idCached.clear();
|
|
7372
|
-
return inserted.map((offer) => offer.hash);
|
|
7373
|
-
}
|
|
7374
|
-
await dbTx.positions.upsert(Array.from(offersCallbacksMap.values()).flatMap((callbacks) => callbacks.map((callback) => ({
|
|
7375
|
-
chainId: callback.chainId,
|
|
7376
|
-
contract: callback.contract,
|
|
7377
|
-
user: callback.user,
|
|
7378
|
-
type: callback.type,
|
|
7379
|
-
asset: callback.asset,
|
|
7380
|
-
blockNumber: callback.blockNumber
|
|
7381
|
-
}))));
|
|
7382
|
-
const callbacksRows = Array.from(offersCallbacksMap.values()).flatMap((callbacks) => callbacks.map((callback) => ({
|
|
7383
|
-
id: id(callback),
|
|
7384
|
-
positionChainId: callback.chainId,
|
|
7385
|
-
positionContract: callback.contract,
|
|
7386
|
-
positionUser: callback.user,
|
|
7387
|
-
amount: callback.amount
|
|
7388
|
-
})));
|
|
7389
|
-
for (const batch of batch$1(callbacksRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(callbacks).values(batch).onConflictDoNothing();
|
|
7390
|
-
const lotInfos = [];
|
|
7391
|
-
for (const [offerHash, callbacks] of offersCallbacksMap.entries()) {
|
|
7392
|
-
const offer = inserted.find((o) => o.hash === offerHash);
|
|
7393
|
-
if (!offer) continue;
|
|
7394
|
-
for (const callback of callbacks) {
|
|
7395
|
-
const isLoanPosition = obligationsMap.get(offer.obligationId)?.loanToken.toLowerCase() === callback.asset?.toLowerCase();
|
|
7396
|
-
lotInfos.push({
|
|
7397
|
-
positionChainId: callback.chainId,
|
|
7398
|
-
positionContract: callback.contract,
|
|
7399
|
-
positionUser: callback.user,
|
|
7400
|
-
group: offer.group,
|
|
7401
|
-
size: isLoanPosition ? BigInt(offer.assets) : BigInt(callback.amount)
|
|
7402
|
-
});
|
|
7403
|
-
}
|
|
7404
|
-
}
|
|
7405
|
-
if (lotInfos.length > 0) await dbTx.lots.create(lotInfos);
|
|
7406
|
-
obligationsMap.clear();
|
|
7407
|
-
collateralsMap.clear();
|
|
7408
|
-
oraclesMap.clear();
|
|
7409
|
-
groupsMap.clear();
|
|
7410
|
-
offersCallbacksMap.clear();
|
|
7411
|
-
idCached.clear();
|
|
7412
|
-
return inserted.map((offer) => offer.hash);
|
|
7413
|
-
});
|
|
7414
8073
|
},
|
|
7415
8074
|
get: async (parameters) => {
|
|
7416
8075
|
const limit = parameters?.limit ?? DEFAULT_LIMIT$2;
|
|
@@ -7430,36 +8089,12 @@ function create$8(config) {
|
|
|
7430
8089
|
'[]'::jsonb
|
|
7431
8090
|
)`.as("collaterals") }).from(obligationCollateralsV2).innerJoin(oracles, sql`${obligationCollateralsV2.oracleChainId} = ${oracles.chainId}
|
|
7432
8091
|
AND ${obligationCollateralsV2.oracleAddress} = ${oracles.address}`).where(eq(obligationCollateralsV2.obligationId, offers.obligationId)).as("collaterals_lateral");
|
|
7433
|
-
const availableLateral = db.select({ available: sql`COALESCE(SUM(
|
|
7434
|
-
CASE
|
|
7435
|
-
-- If asset is null, position available is 0
|
|
7436
|
-
WHEN ${positions.asset} IS NULL THEN 0
|
|
7437
|
-
|
|
7438
|
-
-- Position asset matches loan token: no conversion needed
|
|
7439
|
-
WHEN ${positions.asset} = ${obligations.loanToken} THEN
|
|
7440
|
-
CASE
|
|
7441
|
-
WHEN ${callbacks.amount} IS NULL THEN COALESCE(${positions.balance}, 0)::numeric
|
|
7442
|
-
ELSE LEAST(${callbacks.amount}::numeric, COALESCE(${positions.balance}, 0)::numeric)
|
|
7443
|
-
END
|
|
7444
|
-
|
|
7445
|
-
-- Position asset is collateral: apply oracle price * lltv
|
|
7446
|
-
-- Formula: balance * price / 1e36 * lltv / 1e18
|
|
7447
|
-
ELSE
|
|
7448
|
-
(CASE
|
|
7449
|
-
WHEN ${callbacks.amount} IS NULL THEN COALESCE(${positions.balance}, 0)::numeric
|
|
7450
|
-
ELSE LEAST(${callbacks.amount}::numeric, COALESCE(${positions.balance}, 0)::numeric)
|
|
7451
|
-
END)
|
|
7452
|
-
* COALESCE(${oracles.price}, 0)::numeric / 1e36
|
|
7453
|
-
* COALESCE(${obligationCollateralsV2.lltv}, 0)::numeric / 1e18
|
|
7454
|
-
END
|
|
7455
|
-
), 0)`.as("available") }).from(offersCallbacks).innerJoin(callbacks, eq(offersCallbacks.callbackId, callbacks.id)).innerJoin(positions, and(eq(callbacks.positionChainId, positions.chainId), eq(callbacks.positionContract, positions.contract), eq(callbacks.positionUser, positions.user))).leftJoin(obligationCollateralsV2, and(eq(obligationCollateralsV2.obligationId, offers.obligationId), eq(obligationCollateralsV2.asset, positions.asset))).leftJoin(oracles, and(eq(oracles.chainId, obligationCollateralsV2.oracleChainId), eq(oracles.address, obligationCollateralsV2.oracleAddress))).where(eq(offersCallbacks.offerHash, offers.hash)).as("available_lateral");
|
|
7456
8092
|
const rows = (await db.select({
|
|
7457
8093
|
hash: offers.hash,
|
|
7458
8094
|
maker: offers.groupMaker,
|
|
7459
8095
|
assets: offers.assets,
|
|
7460
8096
|
obligationUnits: offers.obligationUnits,
|
|
7461
8097
|
obligationShares: offers.obligationShares,
|
|
7462
|
-
consumed: groups.consumed,
|
|
7463
8098
|
price: offers.price,
|
|
7464
8099
|
maturity: offers.maturity,
|
|
7465
8100
|
expiry: offers.expiry,
|
|
@@ -7472,19 +8107,8 @@ function create$8(config) {
|
|
|
7472
8107
|
callbackAddress: offers.callbackAddress,
|
|
7473
8108
|
callbackData: offers.callbackData,
|
|
7474
8109
|
collaterals: collateralsLateral.collaterals,
|
|
7475
|
-
blockNumber: offers.blockNumber
|
|
7476
|
-
|
|
7477
|
-
takeable: sql`FLOOR(GREATEST(
|
|
7478
|
-
0,
|
|
7479
|
-
LEAST(
|
|
7480
|
-
${offers.assets}::numeric - ${groups.consumed}::numeric,
|
|
7481
|
-
COALESCE(${availableLateral.available}::numeric, 0)
|
|
7482
|
-
)
|
|
7483
|
-
))`.as("takeable")
|
|
7484
|
-
}).from(offers).innerJoin(obligations, eq(offers.obligationId, obligations.obligationId)).innerJoin(groups, and(eq(offers.groupChainId, groups.chainId), eq(offers.groupMaker, groups.maker), eq(offers.group, groups.group))).innerJoinLateral(collateralsLateral, sql`true`).leftJoinLateral(availableLateral, sql`true`).where(and(cursor !== null && cursor !== void 0 ? gt(offers.hash, cursor) : void 0, maker !== void 0 ? eq(offers.groupMaker, maker.toLowerCase()) : void 0, maker === void 0 ? sql`GREATEST(0, LEAST(
|
|
7485
|
-
${offers.assets}::numeric - ${groups.consumed}::numeric,
|
|
7486
|
-
COALESCE(${availableLateral.available}::numeric, 0)
|
|
7487
|
-
)) > 0` : void 0)).orderBy(asc(offers.hash)).limit(limit)).map((row) => {
|
|
8110
|
+
blockNumber: offers.blockNumber
|
|
8111
|
+
}).from(offers).innerJoin(obligations, eq(offers.obligationId, obligations.obligationId)).innerJoinLateral(collateralsLateral, sql`true`).where(and(cursor !== null && cursor !== void 0 ? gt(offers.hash, cursor) : void 0, maker !== void 0 ? eq(offers.groupMaker, maker.toLowerCase()) : void 0)).orderBy(asc(offers.hash)).limit(limit)).map((row) => {
|
|
7488
8112
|
return {
|
|
7489
8113
|
hash: row.hash,
|
|
7490
8114
|
maker: row.maker,
|
|
@@ -7509,13 +8133,9 @@ function create$8(config) {
|
|
|
7509
8133
|
address: row.callbackAddress,
|
|
7510
8134
|
data: row.callbackData
|
|
7511
8135
|
},
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
},
|
|
7516
|
-
consumed: BigInt(row.consumed),
|
|
7517
|
-
available: BigInt(String(row.available ?? "0").split(".")[0] ?? "0"),
|
|
7518
|
-
takeable: BigInt(String(row.takeable ?? "0").split(".")[0] ?? "0"),
|
|
8136
|
+
consumed: 0n,
|
|
8137
|
+
available: 0n,
|
|
8138
|
+
takeable: 0n,
|
|
7519
8139
|
blockNumber: row.blockNumber
|
|
7520
8140
|
};
|
|
7521
8141
|
});
|
|
@@ -7597,7 +8217,7 @@ function create$8(config) {
|
|
|
7597
8217
|
quote.bid = { price: BigInt(row.price) };
|
|
7598
8218
|
}
|
|
7599
8219
|
return Array.from(quotes.entries()).map(([id, quote]) => {
|
|
7600
|
-
return from$
|
|
8220
|
+
return from$9({
|
|
7601
8221
|
obligationId: id,
|
|
7602
8222
|
ask: quote.ask,
|
|
7603
8223
|
bid: quote.bid
|
|
@@ -7639,7 +8259,7 @@ function create$6(db) {
|
|
|
7639
8259
|
price: oracles.price,
|
|
7640
8260
|
blockNumber: oracles.blockNumber,
|
|
7641
8261
|
chainId: oracles.chainId
|
|
7642
|
-
}).from(oracles).where(eq(oracles.chainId, chainId))).map((r) => from$
|
|
8262
|
+
}).from(oracles).where(eq(oracles.chainId, chainId))).map((r) => from$11({
|
|
7643
8263
|
chainId: r.chainId,
|
|
7644
8264
|
address: r.address,
|
|
7645
8265
|
price: r.price,
|
|
@@ -7654,12 +8274,15 @@ function create$6(db) {
|
|
|
7654
8274
|
price: o.price !== null ? o.price.toString() : null,
|
|
7655
8275
|
blockNumber: o.blockNumber
|
|
7656
8276
|
}));
|
|
7657
|
-
db.transaction(async (dbTx) => {
|
|
8277
|
+
await db.transaction(async (dbTx) => {
|
|
7658
8278
|
for (const batch of batch$1(rows, DEFAULT_BATCH_SIZE)) await dbTx.insert(oracles).values(batch).onConflictDoUpdate({
|
|
7659
8279
|
target: [oracles.chainId, oracles.address],
|
|
7660
8280
|
set: {
|
|
7661
|
-
price: sql`EXCLUDED.price`,
|
|
7662
|
-
blockNumber: sql`
|
|
8281
|
+
price: sql`COALESCE(EXCLUDED.price, ${oracles.price})`,
|
|
8282
|
+
blockNumber: sql`CASE
|
|
8283
|
+
WHEN EXCLUDED.price IS NULL THEN ${oracles.blockNumber}
|
|
8284
|
+
ELSE EXCLUDED.block_number
|
|
8285
|
+
END`,
|
|
7663
8286
|
updatedAt: sql`NOW()`
|
|
7664
8287
|
}
|
|
7665
8288
|
});
|
|
@@ -8005,41 +8628,42 @@ function create$3(config) {
|
|
|
8005
8628
|
return {
|
|
8006
8629
|
create: async (trees$1) => {
|
|
8007
8630
|
if (trees$1.length === 0) return [];
|
|
8008
|
-
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
const
|
|
8012
|
-
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8631
|
+
try {
|
|
8632
|
+
return await db.transaction(async (dbTx) => {
|
|
8633
|
+
const roots = [];
|
|
8634
|
+
for (const { tree, signature } of trees$1) {
|
|
8635
|
+
const root = tree.root.toLowerCase();
|
|
8636
|
+
roots.push(root);
|
|
8637
|
+
await dbTx.insert(trees).values({
|
|
8638
|
+
root,
|
|
8639
|
+
rootSignature: signature.toLowerCase()
|
|
8640
|
+
}).onConflictDoUpdate({
|
|
8641
|
+
target: [trees.root],
|
|
8642
|
+
set: {
|
|
8643
|
+
rootSignature: signature.toLowerCase(),
|
|
8644
|
+
createdAt: sql`NOW()`
|
|
8645
|
+
}
|
|
8646
|
+
});
|
|
8647
|
+
const pathRows = proofs(tree).map((proof) => ({
|
|
8648
|
+
offerHash: hash(proof.offer).toLowerCase(),
|
|
8649
|
+
treeRoot: root,
|
|
8650
|
+
proofNodes: concatenateProofs(proof.path)
|
|
8651
|
+
}));
|
|
8652
|
+
for (const batch of batch$1(pathRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(merklePaths).values(batch).onConflictDoUpdate({
|
|
8653
|
+
target: [merklePaths.offerHash],
|
|
8654
|
+
set: {
|
|
8655
|
+
treeRoot: sql`excluded.tree_root`,
|
|
8656
|
+
proofNodes: sql`excluded.proof_nodes`,
|
|
8657
|
+
createdAt: sql`NOW()`
|
|
8658
|
+
}
|
|
8659
|
+
});
|
|
8660
|
+
}
|
|
8661
|
+
return roots;
|
|
8662
|
+
});
|
|
8663
|
+
} catch (err) {
|
|
8664
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
8665
|
+
throw new Error("Trees.create failed. Ensure offers exist before inserting merkle paths.", { cause: error });
|
|
8666
|
+
}
|
|
8043
8667
|
},
|
|
8044
8668
|
getAttestations: async (hashes) => {
|
|
8045
8669
|
if (hashes.length === 0) return /* @__PURE__ */ new Map();
|
|
@@ -8144,17 +8768,17 @@ function create$2(db) {
|
|
|
8144
8768
|
//#region src/database/Database.ts
|
|
8145
8769
|
function createDomains(core, chainRegistry) {
|
|
8146
8770
|
return {
|
|
8147
|
-
book: create$
|
|
8148
|
-
blocks: create$
|
|
8149
|
-
db: core,
|
|
8150
|
-
chainRegistry
|
|
8151
|
-
}),
|
|
8152
|
-
offers: create$8({
|
|
8771
|
+
book: create$14({ db: core }),
|
|
8772
|
+
blocks: create$15({
|
|
8153
8773
|
db: core,
|
|
8154
8774
|
chainRegistry
|
|
8155
8775
|
}),
|
|
8156
|
-
|
|
8157
|
-
|
|
8776
|
+
callbacks: create$13(core),
|
|
8777
|
+
offers: create$8({ db: core }),
|
|
8778
|
+
consumed: create$12(core),
|
|
8779
|
+
groups: create$11(core),
|
|
8780
|
+
lots: create$10(core),
|
|
8781
|
+
obligations: create$9(core),
|
|
8158
8782
|
offsets: create$7(core),
|
|
8159
8783
|
oracles: create$6(core),
|
|
8160
8784
|
trees: create$3({ db: core }),
|
|
@@ -8183,6 +8807,10 @@ function augmentWithDomains(base, chainRegistry) {
|
|
|
8183
8807
|
value: dms.blocks,
|
|
8184
8808
|
enumerable: true
|
|
8185
8809
|
},
|
|
8810
|
+
callbacks: {
|
|
8811
|
+
value: dms.callbacks,
|
|
8812
|
+
enumerable: true
|
|
8813
|
+
},
|
|
8186
8814
|
offers: {
|
|
8187
8815
|
value: dms.offers,
|
|
8188
8816
|
enumerable: true
|
|
@@ -8191,10 +8819,18 @@ function augmentWithDomains(base, chainRegistry) {
|
|
|
8191
8819
|
value: dms.consumed,
|
|
8192
8820
|
enumerable: true
|
|
8193
8821
|
},
|
|
8822
|
+
groups: {
|
|
8823
|
+
value: dms.groups,
|
|
8824
|
+
enumerable: true
|
|
8825
|
+
},
|
|
8194
8826
|
lots: {
|
|
8195
8827
|
value: dms.lots,
|
|
8196
8828
|
enumerable: true
|
|
8197
8829
|
},
|
|
8830
|
+
obligations: {
|
|
8831
|
+
value: dms.obligations,
|
|
8832
|
+
enumerable: true
|
|
8833
|
+
},
|
|
8198
8834
|
offsets: {
|
|
8199
8835
|
value: dms.offsets,
|
|
8200
8836
|
enumerable: true
|
|
@@ -8636,6 +9272,8 @@ const ApiSchema = z.object({ port: z.number().int().positive() }).strict();
|
|
|
8636
9272
|
const GatekeeperSchema = z.object({
|
|
8637
9273
|
url_env: z.string().min(1).optional(),
|
|
8638
9274
|
url: z.string().min(1).optional(),
|
|
9275
|
+
origin_secret_env: z.string().min(1).optional(),
|
|
9276
|
+
origin_secret: z.string().min(1).optional(),
|
|
8639
9277
|
timeout_ms: z.number().int().positive().optional(),
|
|
8640
9278
|
port: z.number().int().positive().optional()
|
|
8641
9279
|
}).strict().optional();
|
|
@@ -8701,6 +9339,7 @@ function resolveRouterConfig(config, env) {
|
|
|
8701
9339
|
api: config.api ? { port: config.api.port } : void 0,
|
|
8702
9340
|
gatekeeper: {
|
|
8703
9341
|
url: resolveGatekeeperUrl(config.gatekeeper, env),
|
|
9342
|
+
originSecret: resolveGatekeeperOriginSecret(config.gatekeeper, env),
|
|
8704
9343
|
timeoutMs: config.gatekeeper?.timeout_ms ?? 1e4,
|
|
8705
9344
|
port: config.gatekeeper?.port ?? 8082
|
|
8706
9345
|
},
|
|
@@ -8811,6 +9450,7 @@ function createDefaultConfig() {
|
|
|
8811
9450
|
api: { port: 8081 },
|
|
8812
9451
|
gatekeeper: {
|
|
8813
9452
|
url: void 0,
|
|
9453
|
+
originSecret: void 0,
|
|
8814
9454
|
timeoutMs: 1e4,
|
|
8815
9455
|
port: 8082
|
|
8816
9456
|
},
|
|
@@ -8829,6 +9469,11 @@ function resolveGatekeeperUrl(gatekeeper, env) {
|
|
|
8829
9469
|
if (gatekeeper.url_env) return readEnvVar(env, gatekeeper.url_env, "Gatekeeper URL");
|
|
8830
9470
|
return gatekeeper.url;
|
|
8831
9471
|
}
|
|
9472
|
+
function resolveGatekeeperOriginSecret(gatekeeper, env) {
|
|
9473
|
+
if (!gatekeeper) return void 0;
|
|
9474
|
+
if (gatekeeper.origin_secret_env) return readEnvVar(env, gatekeeper.origin_secret_env, "Gatekeeper origin secret");
|
|
9475
|
+
return gatekeeper.origin_secret;
|
|
9476
|
+
}
|
|
8832
9477
|
|
|
8833
9478
|
//#endregion
|
|
8834
9479
|
//#region src/cli/commands/RouterCmd.ts
|
|
@@ -8851,7 +9496,7 @@ var RouterCmd = class RouterCmd extends Command {
|
|
|
8851
9496
|
const configPath = resolveConfigPath(options.configFile);
|
|
8852
9497
|
const config = configPath !== null ? loadRouterConfig(configPath) : createDefaultConfig();
|
|
8853
9498
|
const logger = defaultLogger(config.logging.level, config.logging.pretty);
|
|
8854
|
-
const chainRegistry = create$
|
|
9499
|
+
const chainRegistry = create$19(Object.values(config.chains).map((entry) => entry.chain));
|
|
8855
9500
|
const clients = (config.indexer?.chains ?? []).map((name) => {
|
|
8856
9501
|
const chainConfig = config.chains[name];
|
|
8857
9502
|
if (!chainConfig) throw new Error(`Indexer chain ${name} is not defined under [chains].`);
|
|
@@ -8899,9 +9544,10 @@ const gatekeeperCmd = new RouterCmd("gatekeeper");
|
|
|
8899
9544
|
gatekeeperCmd.description("Start Gatekeeper validation service.").action(async (opts) => {
|
|
8900
9545
|
const { gatekeeper: gatekeeperConfig, chainRegistry, logger } = opts;
|
|
8901
9546
|
await runWithLogger(logger, async () => {
|
|
8902
|
-
const gatekeeperCore = create$
|
|
9547
|
+
const gatekeeperCore = create$20({ rules: morphoRules(chainRegistry.list()) });
|
|
8903
9548
|
const handle = await start$1({
|
|
8904
9549
|
gatekeeper: gatekeeperCore,
|
|
9550
|
+
chainRegistry,
|
|
8905
9551
|
port: gatekeeperConfig?.port ?? 8082
|
|
8906
9552
|
});
|
|
8907
9553
|
logger.info({
|
|
@@ -8952,23 +9598,99 @@ async function getBook(params, db) {
|
|
|
8952
9598
|
return failure(err);
|
|
8953
9599
|
}
|
|
8954
9600
|
}
|
|
8955
|
-
|
|
8956
|
-
//#endregion
|
|
8957
|
-
//#region src/api/Controllers/
|
|
8958
|
-
/**
|
|
8959
|
-
* Returns
|
|
8960
|
-
* @param
|
|
8961
|
-
* @
|
|
8962
|
-
|
|
8963
|
-
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
8968
|
-
|
|
8969
|
-
|
|
8970
|
-
|
|
8971
|
-
|
|
9601
|
+
|
|
9602
|
+
//#endregion
|
|
9603
|
+
//#region src/api/Controllers/getConfigContracts.ts
|
|
9604
|
+
/**
|
|
9605
|
+
* Returns contract addresses used by indexers (mempool, v2) plus multicall per chain.
|
|
9606
|
+
* @param query - Raw query parameters containing optional chain filters.
|
|
9607
|
+
* @param chainRegistry - The chain registry instance. {@link ChainRegistry.ChainRegistry}
|
|
9608
|
+
* @returns The indexer contract configuration. {@link ApiPayload.Payload<ConfigContract[]>}
|
|
9609
|
+
*/
|
|
9610
|
+
async function getConfigContracts(query, chainRegistry) {
|
|
9611
|
+
const parsed = safeParse("get_config_contracts", query ?? {});
|
|
9612
|
+
if (!parsed.success) return failure(parsed.error);
|
|
9613
|
+
const { chains: chainsFilter, cursor, limit } = parsed.data;
|
|
9614
|
+
const chainFilter = chainsFilter?.length ? new Set(chainsFilter) : null;
|
|
9615
|
+
const contracts = [];
|
|
9616
|
+
const seenAddresses = /* @__PURE__ */ new Set();
|
|
9617
|
+
for (const chain of chainRegistry.list()) {
|
|
9618
|
+
if (chainFilter && !chainFilter.has(chain.id)) continue;
|
|
9619
|
+
const mempool = chain.custom?.mempool?.address;
|
|
9620
|
+
if (!mempool) return failure(new InternalServerError(`Missing mempool address for chain ${chain.id}.`));
|
|
9621
|
+
const multicall = chain.contracts?.multicall3?.address;
|
|
9622
|
+
if (!multicall) return failure(new InternalServerError(`Missing multicall3 address for chain ${chain.id}.`));
|
|
9623
|
+
const v2 = chain.custom?.morpho?.address;
|
|
9624
|
+
if (!v2) return failure(new InternalServerError(`Missing morpho address for chain ${chain.id}.`));
|
|
9625
|
+
const chainContracts = [
|
|
9626
|
+
{
|
|
9627
|
+
chain_id: chain.id,
|
|
9628
|
+
name: "mempool",
|
|
9629
|
+
address: mempool
|
|
9630
|
+
},
|
|
9631
|
+
{
|
|
9632
|
+
chain_id: chain.id,
|
|
9633
|
+
name: "multicall",
|
|
9634
|
+
address: multicall
|
|
9635
|
+
},
|
|
9636
|
+
{
|
|
9637
|
+
chain_id: chain.id,
|
|
9638
|
+
name: "v2",
|
|
9639
|
+
address: v2
|
|
9640
|
+
}
|
|
9641
|
+
];
|
|
9642
|
+
for (const contract of chainContracts) {
|
|
9643
|
+
const cursorKey = `${contract.chain_id}:${contract.address.toLowerCase()}`;
|
|
9644
|
+
if (seenAddresses.has(cursorKey)) return failure(new InternalServerError(`Duplicate contract address ${contract.address} for chain ${chain.id}.`));
|
|
9645
|
+
seenAddresses.add(cursorKey);
|
|
9646
|
+
contracts.push(contract);
|
|
9647
|
+
}
|
|
9648
|
+
}
|
|
9649
|
+
contracts.sort((a, b) => {
|
|
9650
|
+
if (a.chain_id !== b.chain_id) return a.chain_id - b.chain_id;
|
|
9651
|
+
const addressCompare = a.address.toLowerCase().localeCompare(b.address.toLowerCase());
|
|
9652
|
+
if (addressCompare !== 0) return addressCompare;
|
|
9653
|
+
return a.name.localeCompare(b.name);
|
|
9654
|
+
});
|
|
9655
|
+
let cursorContract = null;
|
|
9656
|
+
if (cursor) try {
|
|
9657
|
+
cursorContract = parseCursor(cursor);
|
|
9658
|
+
} catch (err) {
|
|
9659
|
+
return failure(err);
|
|
9660
|
+
}
|
|
9661
|
+
const startIndex = cursorContract ? findStartIndex(contracts, cursorContract) : 0;
|
|
9662
|
+
const page = contracts.slice(startIndex, startIndex + limit);
|
|
9663
|
+
const nextCursor = startIndex + limit < contracts.length && page.length > 0 ? formatCursor(page.at(-1)) : null;
|
|
9664
|
+
return success({
|
|
9665
|
+
data: page,
|
|
9666
|
+
cursor: nextCursor
|
|
9667
|
+
});
|
|
9668
|
+
}
|
|
9669
|
+
function parseCursor(cursor) {
|
|
9670
|
+
const [chain, address] = cursor.split(":", 2);
|
|
9671
|
+
if (!chain || !address) throw new BadRequestError("Cursor must be in the format chain_id:0x...");
|
|
9672
|
+
return {
|
|
9673
|
+
chain_id: Number.parseInt(chain, 10),
|
|
9674
|
+
address: address.toLowerCase()
|
|
9675
|
+
};
|
|
9676
|
+
}
|
|
9677
|
+
function formatCursor(contract) {
|
|
9678
|
+
return `${contract.chain_id}:${contract.address.toLowerCase()}`;
|
|
9679
|
+
}
|
|
9680
|
+
function findStartIndex(contracts, cursor) {
|
|
9681
|
+
let low = 0;
|
|
9682
|
+
let high = contracts.length;
|
|
9683
|
+
while (low < high) {
|
|
9684
|
+
const mid = Math.floor((low + high) / 2);
|
|
9685
|
+
const current = contracts[mid];
|
|
9686
|
+
if (compareContract(current, cursor) <= 0) low = mid + 1;
|
|
9687
|
+
else high = mid;
|
|
9688
|
+
}
|
|
9689
|
+
return low;
|
|
9690
|
+
}
|
|
9691
|
+
function compareContract(contract, cursor) {
|
|
9692
|
+
if (contract.chain_id !== cursor.chain_id) return contract.chain_id - cursor.chain_id;
|
|
9693
|
+
return contract.address.toLowerCase().localeCompare(cursor.address.toLowerCase());
|
|
8972
9694
|
}
|
|
8973
9695
|
|
|
8974
9696
|
//#endregion
|
|
@@ -8980,28 +9702,19 @@ const __dirname = (() => {
|
|
|
8980
9702
|
return process.cwd();
|
|
8981
9703
|
}
|
|
8982
9704
|
})();
|
|
8983
|
-
const parse_error_Description = {
|
|
8984
|
-
name: "parse_error",
|
|
8985
|
-
description: "Returns when an offer fails to parse due to invalid format or missing required fields"
|
|
8986
|
-
};
|
|
8987
|
-
const getGatekeeperRules = async (gatekeeper) => {
|
|
8988
|
-
return [parse_error_Description, ...await gatekeeper.getRules()];
|
|
8989
|
-
};
|
|
8990
9705
|
/**
|
|
8991
9706
|
* Build the OpenAPI document for the router.
|
|
8992
|
-
* @param parameters - Includes a {@link RulesProvider} to fetch gatekeeper rules.
|
|
8993
9707
|
* @returns OpenAPI document. {@link OpenAPIDocument}
|
|
8994
9708
|
*/
|
|
8995
|
-
async function getSwaggerJson(
|
|
8996
|
-
return OpenApi(
|
|
9709
|
+
async function getSwaggerJson() {
|
|
9710
|
+
return OpenApi();
|
|
8997
9711
|
}
|
|
8998
9712
|
/**
|
|
8999
9713
|
* Render the API documentation HTML page.
|
|
9000
|
-
* @param parameters - Includes a {@link RulesProvider} to fetch gatekeeper rules.
|
|
9001
9714
|
* @returns HTML page as string.
|
|
9002
9715
|
*/
|
|
9003
|
-
async function getDocsHtml(
|
|
9004
|
-
const spec = await OpenApi(
|
|
9716
|
+
async function getDocsHtml() {
|
|
9717
|
+
const spec = await OpenApi();
|
|
9005
9718
|
return `<!DOCTYPE html>
|
|
9006
9719
|
<html>
|
|
9007
9720
|
<head>
|
|
@@ -9347,13 +10060,127 @@ async function getObligations(queryParameters, db) {
|
|
|
9347
10060
|
|
|
9348
10061
|
//#endregion
|
|
9349
10062
|
//#region src/api/Controllers/getOffers.ts
|
|
10063
|
+
/**
|
|
10064
|
+
* Query offers with computed consumed/available/takeable values.
|
|
10065
|
+
* @param db - The database client. {@link Database.Core}
|
|
10066
|
+
* @param parameters - {@link GetOffersQueryParams}
|
|
10067
|
+
* @returns The offers with pagination cursor.
|
|
10068
|
+
*/
|
|
10069
|
+
async function getOffersQuery(db, parameters) {
|
|
10070
|
+
const limit = parameters?.limit ?? DEFAULT_LIMIT$2;
|
|
10071
|
+
const cursor = parameters?.cursor;
|
|
10072
|
+
const maker = parameters?.maker;
|
|
10073
|
+
if (cursor !== null && cursor !== void 0) {
|
|
10074
|
+
if (!cursor.startsWith("0x") || cursor.length !== 66) throw new Error("Invalid cursor format");
|
|
10075
|
+
}
|
|
10076
|
+
const collateralsLateral = db.select({ collaterals: sql`COALESCE(
|
|
10077
|
+
jsonb_agg(
|
|
10078
|
+
jsonb_build_object(
|
|
10079
|
+
'asset', ${obligationCollateralsV2.asset},
|
|
10080
|
+
'oracle', ${oracles.address},
|
|
10081
|
+
'lltv', ${obligationCollateralsV2.lltv}
|
|
10082
|
+
)
|
|
10083
|
+
),
|
|
10084
|
+
'[]'::jsonb
|
|
10085
|
+
)`.as("collaterals") }).from(obligationCollateralsV2).innerJoin(oracles, sql`${obligationCollateralsV2.oracleChainId} = ${oracles.chainId}
|
|
10086
|
+
AND ${obligationCollateralsV2.oracleAddress} = ${oracles.address}`).where(eq(obligationCollateralsV2.obligationId, offers.obligationId)).as("collaterals_lateral");
|
|
10087
|
+
const availableLateral = db.select({ available: sql`COALESCE(SUM(
|
|
10088
|
+
CASE
|
|
10089
|
+
-- If asset is null, position available is 0
|
|
10090
|
+
WHEN ${positions.asset} IS NULL THEN 0
|
|
10091
|
+
|
|
10092
|
+
-- Position asset matches loan token: no conversion needed
|
|
10093
|
+
WHEN ${positions.asset} = ${obligations.loanToken} THEN
|
|
10094
|
+
CASE
|
|
10095
|
+
WHEN ${callbacks.amount} IS NULL THEN COALESCE(${positions.balance}, 0)::numeric
|
|
10096
|
+
ELSE LEAST(${callbacks.amount}::numeric, COALESCE(${positions.balance}, 0)::numeric)
|
|
10097
|
+
END
|
|
10098
|
+
|
|
10099
|
+
-- Position asset is collateral: apply oracle price * lltv
|
|
10100
|
+
-- Formula: balance * price / 1e36 * lltv / 1e18
|
|
10101
|
+
ELSE
|
|
10102
|
+
(CASE
|
|
10103
|
+
WHEN ${callbacks.amount} IS NULL THEN COALESCE(${positions.balance}, 0)::numeric
|
|
10104
|
+
ELSE LEAST(${callbacks.amount}::numeric, COALESCE(${positions.balance}, 0)::numeric)
|
|
10105
|
+
END)
|
|
10106
|
+
* COALESCE(${oracles.price}, 0)::numeric / 1e36
|
|
10107
|
+
* COALESCE(${obligationCollateralsV2.lltv}, 0)::numeric / 1e18
|
|
10108
|
+
END
|
|
10109
|
+
), 0)`.as("available") }).from(offersCallbacks).innerJoin(callbacks, eq(offersCallbacks.callbackId, callbacks.id)).innerJoin(positions, and(eq(callbacks.positionChainId, positions.chainId), eq(callbacks.positionContract, positions.contract), eq(callbacks.positionUser, positions.user))).leftJoin(obligationCollateralsV2, and(eq(obligationCollateralsV2.obligationId, offers.obligationId), eq(obligationCollateralsV2.asset, positions.asset))).leftJoin(oracles, and(eq(oracles.chainId, obligationCollateralsV2.oracleChainId), eq(oracles.address, obligationCollateralsV2.oracleAddress))).where(eq(offersCallbacks.offerHash, offers.hash)).as("available_lateral");
|
|
10110
|
+
const rows = (await db.select({
|
|
10111
|
+
hash: offers.hash,
|
|
10112
|
+
maker: offers.groupMaker,
|
|
10113
|
+
assets: offers.assets,
|
|
10114
|
+
obligationUnits: offers.obligationUnits,
|
|
10115
|
+
obligationShares: offers.obligationShares,
|
|
10116
|
+
consumed: groups.consumed,
|
|
10117
|
+
price: offers.price,
|
|
10118
|
+
maturity: offers.maturity,
|
|
10119
|
+
expiry: offers.expiry,
|
|
10120
|
+
start: offers.start,
|
|
10121
|
+
group: offers.group,
|
|
10122
|
+
session: offers.session,
|
|
10123
|
+
buy: offers.buy,
|
|
10124
|
+
chainId: obligations.chainId,
|
|
10125
|
+
loanToken: obligations.loanToken,
|
|
10126
|
+
callbackAddress: offers.callbackAddress,
|
|
10127
|
+
callbackData: offers.callbackData,
|
|
10128
|
+
collaterals: collateralsLateral.collaterals,
|
|
10129
|
+
blockNumber: offers.blockNumber,
|
|
10130
|
+
available: sql`COALESCE(${availableLateral.available}::numeric, 0)`.as("available"),
|
|
10131
|
+
takeable: sql`FLOOR(GREATEST(
|
|
10132
|
+
0,
|
|
10133
|
+
LEAST(
|
|
10134
|
+
${offers.assets}::numeric - ${groups.consumed}::numeric,
|
|
10135
|
+
COALESCE(${availableLateral.available}::numeric, 0)
|
|
10136
|
+
)
|
|
10137
|
+
))`.as("takeable")
|
|
10138
|
+
}).from(offers).innerJoin(obligations, eq(offers.obligationId, obligations.obligationId)).innerJoin(groups, and(eq(offers.groupChainId, groups.chainId), eq(offers.groupMaker, groups.maker), eq(offers.group, groups.group))).innerJoinLateral(collateralsLateral, sql`true`).leftJoinLateral(availableLateral, sql`true`).where(and(cursor !== null && cursor !== void 0 ? gt(offers.hash, cursor) : void 0, maker !== void 0 ? eq(offers.groupMaker, maker.toLowerCase()) : void 0, maker === void 0 ? sql`GREATEST(0, LEAST(
|
|
10139
|
+
${offers.assets}::numeric - ${groups.consumed}::numeric,
|
|
10140
|
+
COALESCE(${availableLateral.available}::numeric, 0)
|
|
10141
|
+
)) > 0` : void 0)).orderBy(asc(offers.hash)).limit(limit)).map((row) => {
|
|
10142
|
+
return {
|
|
10143
|
+
hash: row.hash,
|
|
10144
|
+
maker: row.maker,
|
|
10145
|
+
assets: BigInt(row.assets),
|
|
10146
|
+
obligationUnits: BigInt(row.obligationUnits),
|
|
10147
|
+
obligationShares: BigInt(row.obligationShares),
|
|
10148
|
+
price: BigInt(row.price),
|
|
10149
|
+
maturity: from$16(row.maturity),
|
|
10150
|
+
expiry: row.expiry,
|
|
10151
|
+
start: row.start,
|
|
10152
|
+
group: row.group,
|
|
10153
|
+
session: row.session,
|
|
10154
|
+
buy: row.buy,
|
|
10155
|
+
chainId: row.chainId,
|
|
10156
|
+
loanToken: row.loanToken,
|
|
10157
|
+
collaterals: row.collaterals.map((c) => ({
|
|
10158
|
+
asset: c.asset,
|
|
10159
|
+
oracle: c.oracle,
|
|
10160
|
+
lltv: BigInt(c.lltv)
|
|
10161
|
+
})).sort((a, b) => a.asset.toLowerCase().localeCompare(b.asset.toLowerCase())),
|
|
10162
|
+
callback: {
|
|
10163
|
+
address: row.callbackAddress,
|
|
10164
|
+
data: row.callbackData
|
|
10165
|
+
},
|
|
10166
|
+
consumed: BigInt(row.consumed),
|
|
10167
|
+
available: BigInt(String(row.available ?? "0").split(".")[0] ?? "0"),
|
|
10168
|
+
takeable: BigInt(String(row.takeable ?? "0").split(".")[0] ?? "0"),
|
|
10169
|
+
blockNumber: row.blockNumber
|
|
10170
|
+
};
|
|
10171
|
+
});
|
|
10172
|
+
return {
|
|
10173
|
+
rows,
|
|
10174
|
+
nextCursor: rows.length === limit ? rows[rows.length - 1].hash : null
|
|
10175
|
+
};
|
|
10176
|
+
}
|
|
9350
10177
|
async function getOffers(queryParameters, db) {
|
|
9351
10178
|
const logger = getLogger();
|
|
9352
10179
|
const result = safeParse("get_offers", queryParameters, (issue) => issue.message);
|
|
9353
10180
|
if (!result.success) return failure(result.error);
|
|
9354
10181
|
const query = result.data;
|
|
9355
10182
|
try {
|
|
9356
|
-
const { rows, nextCursor } = query.maker ? await db
|
|
10183
|
+
const { rows, nextCursor } = query.maker ? await getOffersQuery(db, {
|
|
9357
10184
|
maker: query.maker,
|
|
9358
10185
|
cursor: query.cursor,
|
|
9359
10186
|
limit: query.limit
|
|
@@ -9421,6 +10248,35 @@ async function getUserPositions(queryParameters, db) {
|
|
|
9421
10248
|
}
|
|
9422
10249
|
}
|
|
9423
10250
|
|
|
10251
|
+
//#endregion
|
|
10252
|
+
//#region src/api/Controllers/resolveCallbackTypes.ts
|
|
10253
|
+
/**
|
|
10254
|
+
* Resolve callback types for a list of callback addresses grouped by chain.
|
|
10255
|
+
* @param body - Request body with callback addresses. {@link CallbackTypesRequest}
|
|
10256
|
+
* @param chains - Chains to resolve callback types against. {@link Chain.Chain}
|
|
10257
|
+
* @returns Callback types grouped by chain. {@link CallbackTypesPayload}
|
|
10258
|
+
*/
|
|
10259
|
+
async function resolveCallbackTypes$1(body, chains) {
|
|
10260
|
+
const result = safeParse("callback_types", body, (issue) => issue.message);
|
|
10261
|
+
if (!result.success) return failure(result.error);
|
|
10262
|
+
const request = result.data;
|
|
10263
|
+
const chainIds = new Set(chains.map((chain) => chain.id));
|
|
10264
|
+
const unknown = request.callbacks.find((entry) => !chainIds.has(entry.chain_id));
|
|
10265
|
+
if (unknown) return failure(new BadRequestError(`Unknown chain id ${unknown.chain_id}`));
|
|
10266
|
+
try {
|
|
10267
|
+
const data = resolveCallbackTypes$2({
|
|
10268
|
+
chains,
|
|
10269
|
+
request
|
|
10270
|
+
});
|
|
10271
|
+
return success({
|
|
10272
|
+
data,
|
|
10273
|
+
cursor: null
|
|
10274
|
+
});
|
|
10275
|
+
} catch (err) {
|
|
10276
|
+
return failure(err);
|
|
10277
|
+
}
|
|
10278
|
+
}
|
|
10279
|
+
|
|
9424
10280
|
//#endregion
|
|
9425
10281
|
//#region src/api/Api.ts
|
|
9426
10282
|
function from(config) {
|
|
@@ -9494,6 +10350,21 @@ function serve$1(parameters) {
|
|
|
9494
10350
|
return c.json(failure$1.body, failure$1.statusCode);
|
|
9495
10351
|
}
|
|
9496
10352
|
});
|
|
10353
|
+
app.post("/v1/callbacks", async (c) => {
|
|
10354
|
+
let body;
|
|
10355
|
+
try {
|
|
10356
|
+
body = await c.req.json();
|
|
10357
|
+
} catch (err) {
|
|
10358
|
+
const failure$3 = failure(err);
|
|
10359
|
+
return c.json(failure$3.body, failure$3.statusCode);
|
|
10360
|
+
}
|
|
10361
|
+
if (body === null || typeof body !== "object") {
|
|
10362
|
+
const failure$2 = failure(new BadRequestError("Request body must be a JSON object"));
|
|
10363
|
+
return c.json(failure$2.body, failure$2.statusCode);
|
|
10364
|
+
}
|
|
10365
|
+
const { statusCode, body: responseBody } = await resolveCallbackTypes$1(body, chainRegistry.list());
|
|
10366
|
+
return c.json(responseBody, statusCode);
|
|
10367
|
+
});
|
|
9497
10368
|
app.get("/v1/users/:userAddress/positions", async (c) => {
|
|
9498
10369
|
const query = c.req.query();
|
|
9499
10370
|
const { statusCode, body } = await getUserPositions({
|
|
@@ -9515,12 +10386,21 @@ function serve$1(parameters) {
|
|
|
9515
10386
|
const { statusCode, body } = await getHealthChains(c.req.query(), db, void 0, chainRegistry);
|
|
9516
10387
|
return c.json(body, statusCode);
|
|
9517
10388
|
});
|
|
9518
|
-
app.get("/v1/config", async (c) => {
|
|
9519
|
-
const { statusCode, body } = await
|
|
10389
|
+
app.get("/v1/config/contracts", async (c) => {
|
|
10390
|
+
const { statusCode, body } = await getConfigContracts(c.req.query(), chainRegistry);
|
|
9520
10391
|
return c.json(body, statusCode);
|
|
9521
10392
|
});
|
|
9522
|
-
app.get("/
|
|
9523
|
-
|
|
10393
|
+
app.get("/v1/config/rules", async (c) => {
|
|
10394
|
+
try {
|
|
10395
|
+
const { statusCode, body } = await gatekeeper.getConfigRules(c.req.query());
|
|
10396
|
+
return c.json(body, statusCode);
|
|
10397
|
+
} catch (err) {
|
|
10398
|
+
const failure$4 = failure(err);
|
|
10399
|
+
return c.json(failure$4.body, failure$4.statusCode);
|
|
10400
|
+
}
|
|
10401
|
+
});
|
|
10402
|
+
app.get("/docs/openapi", async (c) => c.text(JSON.stringify(await getSwaggerJson()), 200, { "Content-Type": "application/json; charset=utf-8" }));
|
|
10403
|
+
app.get("/docs/api", async (c) => c.html(await getDocsHtml(), 200));
|
|
9524
10404
|
app.get("/docs", async (c) => c.html(await getIntegratorDocsHtml(), 200));
|
|
9525
10405
|
serve({
|
|
9526
10406
|
fetch: app.fetch,
|
|
@@ -9528,6 +10408,120 @@ function serve$1(parameters) {
|
|
|
9528
10408
|
});
|
|
9529
10409
|
}
|
|
9530
10410
|
|
|
10411
|
+
//#endregion
|
|
10412
|
+
//#region src/gatekeeper/Client.ts
|
|
10413
|
+
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
10414
|
+
/**
|
|
10415
|
+
* Create an HTTP client for a gatekeeper service.
|
|
10416
|
+
* @param config - Gatekeeper client configuration. {@link ClientConfig}
|
|
10417
|
+
* @returns An HTTP-backed gatekeeper client. {@link GatekeeperClient}
|
|
10418
|
+
*/
|
|
10419
|
+
function createHttpClient(config) {
|
|
10420
|
+
const fetchFn = config.fetchFn ?? fetch;
|
|
10421
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
10422
|
+
const baseUrl = normalizeBaseUrl(config.baseUrl);
|
|
10423
|
+
const baseHeaders = config.originSecret ? { "x-origin-verify": config.originSecret } : void 0;
|
|
10424
|
+
const request = async (path, init) => {
|
|
10425
|
+
const controller = new AbortController();
|
|
10426
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
10427
|
+
try {
|
|
10428
|
+
return await fetchFn(`${baseUrl}${path}`, {
|
|
10429
|
+
...init,
|
|
10430
|
+
headers: mergeHeaders(baseHeaders, init.headers),
|
|
10431
|
+
signal: controller.signal
|
|
10432
|
+
});
|
|
10433
|
+
} finally {
|
|
10434
|
+
clearTimeout(timeout);
|
|
10435
|
+
}
|
|
10436
|
+
};
|
|
10437
|
+
const validate = async (body) => {
|
|
10438
|
+
const response = await request("/v1/validate", {
|
|
10439
|
+
method: "POST",
|
|
10440
|
+
headers: { "content-type": "application/json" },
|
|
10441
|
+
body: JSON.stringify(body)
|
|
10442
|
+
});
|
|
10443
|
+
const json = await response.json();
|
|
10444
|
+
return {
|
|
10445
|
+
statusCode: response.status,
|
|
10446
|
+
body: json
|
|
10447
|
+
};
|
|
10448
|
+
};
|
|
10449
|
+
const getConfigRules = async (query) => {
|
|
10450
|
+
const params = new URLSearchParams();
|
|
10451
|
+
if (query?.cursor) params.set("cursor", query.cursor);
|
|
10452
|
+
if (query?.limit !== void 0) params.set("limit", query.limit.toString());
|
|
10453
|
+
if (query?.types !== void 0) {
|
|
10454
|
+
const typesValue = Array.isArray(query.types) ? query.types.join(",") : query.types;
|
|
10455
|
+
if (typesValue.length > 0) params.set("types", typesValue);
|
|
10456
|
+
}
|
|
10457
|
+
const response = await request(params.size > 0 ? `/v1/config/rules?${params.toString()}` : "/v1/config/rules", { method: "GET" });
|
|
10458
|
+
const json = await response.json();
|
|
10459
|
+
return {
|
|
10460
|
+
statusCode: response.status,
|
|
10461
|
+
body: json
|
|
10462
|
+
};
|
|
10463
|
+
};
|
|
10464
|
+
const isAllowed = async (offers) => {
|
|
10465
|
+
const { statusCode, body } = await validate({ offers: offers.map((offer) => toSnakeCase(offer)) });
|
|
10466
|
+
if (statusCode !== 200) {
|
|
10467
|
+
const errorMessage = extractErrorMessage(body);
|
|
10468
|
+
throw new Error(`Gatekeeper validation failed: ${errorMessage ?? `status ${statusCode}`}`);
|
|
10469
|
+
}
|
|
10470
|
+
const data = body.data;
|
|
10471
|
+
if (!data || typeof data !== "object") throw new Error("Gatekeeper validation response is invalid.");
|
|
10472
|
+
if ("issues" in data) {
|
|
10473
|
+
const issues = data.issues.map((issue) => ({
|
|
10474
|
+
ruleName: issue.rule,
|
|
10475
|
+
message: issue.message,
|
|
10476
|
+
item: offers[issue.index]
|
|
10477
|
+
}));
|
|
10478
|
+
const invalidIndices = new Set(data.issues.map((issue) => issue.index));
|
|
10479
|
+
return {
|
|
10480
|
+
valid: offers.filter((_, index) => !invalidIndices.has(index)),
|
|
10481
|
+
issues
|
|
10482
|
+
};
|
|
10483
|
+
}
|
|
10484
|
+
if (!("payload" in data) || !("root" in data)) throw new Error("Gatekeeper validation response is missing payload data.");
|
|
10485
|
+
return {
|
|
10486
|
+
valid: offers.slice(),
|
|
10487
|
+
issues: []
|
|
10488
|
+
};
|
|
10489
|
+
};
|
|
10490
|
+
const getCallbackTypes = async (requestPayload) => {
|
|
10491
|
+
const response = await request("/v1/callbacks", {
|
|
10492
|
+
method: "POST",
|
|
10493
|
+
headers: { "content-type": "application/json" },
|
|
10494
|
+
body: JSON.stringify(requestPayload)
|
|
10495
|
+
});
|
|
10496
|
+
const json = await response.json();
|
|
10497
|
+
if (!response.ok) throw new Error(`Gatekeeper callbacks request failed: ${extractErrorMessage(json) ?? response.statusText}`);
|
|
10498
|
+
if (!("data" in json) || !Array.isArray(json.data)) throw new Error("Gatekeeper callbacks response is invalid.");
|
|
10499
|
+
return json.data;
|
|
10500
|
+
};
|
|
10501
|
+
return {
|
|
10502
|
+
baseUrl,
|
|
10503
|
+
validate,
|
|
10504
|
+
getConfigRules,
|
|
10505
|
+
isAllowed,
|
|
10506
|
+
getCallbackTypes
|
|
10507
|
+
};
|
|
10508
|
+
}
|
|
10509
|
+
function mergeHeaders(base, extra) {
|
|
10510
|
+
if (!base && !extra) return void 0;
|
|
10511
|
+
const merged = new Headers(base ?? void 0);
|
|
10512
|
+
if (extra) for (const [key, value] of new Headers(extra).entries()) merged.set(key, value);
|
|
10513
|
+
return merged;
|
|
10514
|
+
}
|
|
10515
|
+
function normalizeBaseUrl(url) {
|
|
10516
|
+
return url.trim().replace(/\/+$/, "");
|
|
10517
|
+
}
|
|
10518
|
+
function extractErrorMessage(payload) {
|
|
10519
|
+
if (!payload || typeof payload !== "object") return void 0;
|
|
10520
|
+
const error = payload.error;
|
|
10521
|
+
if (!error || typeof error !== "object") return void 0;
|
|
10522
|
+
return typeof error.message === "string" ? error.message : void 0;
|
|
10523
|
+
}
|
|
10524
|
+
|
|
9531
10525
|
//#endregion
|
|
9532
10526
|
//#region src/cli/commands/start.ts
|
|
9533
10527
|
const startCmd = new RouterCmd("start");
|
|
@@ -9538,9 +10532,10 @@ startCmd.description("Start Router services.").addOption(new Option("--mock <n>"
|
|
|
9538
10532
|
let gatekeeperUrl = gatekeeperConfig?.url;
|
|
9539
10533
|
let gatekeeperHandle = null;
|
|
9540
10534
|
if (!gatekeeperUrl) {
|
|
9541
|
-
const gatekeeperCore = create$
|
|
10535
|
+
const gatekeeperCore = create$20({ rules: morphoRules(chainRegistry.list()) });
|
|
9542
10536
|
gatekeeperHandle = await start$1({
|
|
9543
10537
|
gatekeeper: gatekeeperCore,
|
|
10538
|
+
chainRegistry,
|
|
9544
10539
|
port: gatekeeperConfig?.port ?? 8082
|
|
9545
10540
|
});
|
|
9546
10541
|
gatekeeperUrl = gatekeeperHandle.url;
|
|
@@ -9552,72 +10547,36 @@ startCmd.description("Start Router services.").addOption(new Option("--mock <n>"
|
|
|
9552
10547
|
}
|
|
9553
10548
|
const gatekeeper = createHttpClient({
|
|
9554
10549
|
baseUrl: gatekeeperUrl ?? "http://localhost:8082",
|
|
9555
|
-
timeoutMs: gatekeeperConfig?.timeoutMs
|
|
10550
|
+
timeoutMs: gatekeeperConfig?.timeoutMs,
|
|
10551
|
+
originSecret: gatekeeperConfig?.originSecret
|
|
9556
10552
|
});
|
|
9557
10553
|
if (mock !== void 0 && mock > 0) {
|
|
9558
|
-
const
|
|
9559
|
-
|
|
9560
|
-
|
|
9561
|
-
|
|
9562
|
-
|
|
9563
|
-
"0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599": 8,
|
|
9564
|
-
"0x4200000000000000000000000000000000000006": 18,
|
|
9565
|
-
"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913": 6,
|
|
9566
|
-
"0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb": 18
|
|
9567
|
-
};
|
|
9568
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
9569
|
-
const offerBlockNumber = 1;
|
|
9570
|
-
const positionBlockNumber = offerBlockNumber + 1;
|
|
9571
|
-
const start = Math.max(0, now - 60);
|
|
9572
|
-
const expiry = now + 3600;
|
|
9573
|
-
const maturity = from$16("end_of_next_month");
|
|
9574
|
-
const oraclePrice = 10n ** 36n;
|
|
9575
|
-
const offers = createMockOffers({
|
|
9576
|
-
count: mock,
|
|
9577
|
-
chains: configuredChains,
|
|
9578
|
-
loanTokens: Object.keys(tokens).map((key) => key),
|
|
9579
|
-
assetsDecimals: tokens,
|
|
9580
|
-
start,
|
|
9581
|
-
expiry,
|
|
9582
|
-
maturity,
|
|
9583
|
-
chainRegistry
|
|
10554
|
+
const offers = await seedMockOffers({
|
|
10555
|
+
db,
|
|
10556
|
+
gatekeeper,
|
|
10557
|
+
chainRegistry,
|
|
10558
|
+
count: mock
|
|
9584
10559
|
});
|
|
9585
|
-
await db.offers.create([{
|
|
9586
|
-
blockNumber: offerBlockNumber,
|
|
9587
|
-
offers
|
|
9588
|
-
}]);
|
|
9589
|
-
await db.oracles.upsert(seedMockOracles(offers, oraclePrice, positionBlockNumber));
|
|
9590
|
-
for (const offer of offers) {
|
|
9591
|
-
if (offer.callback.data === "0x") continue;
|
|
9592
|
-
const chain = chainRegistry.getById(offer.chainId);
|
|
9593
|
-
if (!chain) continue;
|
|
9594
|
-
const callbackType = getCallbackType(chain.name, offer.callback.address);
|
|
9595
|
-
if (!callbackType) continue;
|
|
9596
|
-
const callbacks = decode$1(callbackType, offer.callback.data);
|
|
9597
|
-
for (const cb of callbacks) {
|
|
9598
|
-
const positionAsset = callbackType === Type$1.SellERC20Callback ? cb.contract : offer.loanToken;
|
|
9599
|
-
await db.positions.upsert([from$9({
|
|
9600
|
-
chainId: offer.chainId,
|
|
9601
|
-
contract: cb.contract,
|
|
9602
|
-
user: offer.maker,
|
|
9603
|
-
type: callbackType === Type$1.BuyVaultV1Callback ? Type.VAULT_V1 : Type.ERC20,
|
|
9604
|
-
balance: cb.amount * 2n,
|
|
9605
|
-
asset: positionAsset,
|
|
9606
|
-
blockNumber: positionBlockNumber
|
|
9607
|
-
})]);
|
|
9608
|
-
}
|
|
9609
|
-
}
|
|
9610
10560
|
logger.info({
|
|
9611
10561
|
msg: `Offers seeded`,
|
|
9612
|
-
count:
|
|
10562
|
+
count: offers.length
|
|
9613
10563
|
});
|
|
9614
10564
|
}
|
|
9615
10565
|
if (file) {
|
|
9616
10566
|
const offers = await getOffersFromFile(file);
|
|
9617
|
-
|
|
9618
|
-
|
|
9619
|
-
|
|
9620
|
-
|
|
10567
|
+
const offerBlockNumber = 1;
|
|
10568
|
+
const positionBlockNumber = offerBlockNumber + 1;
|
|
10569
|
+
await seedOffers({
|
|
10570
|
+
db,
|
|
10571
|
+
offers,
|
|
10572
|
+
blockNumber: offerBlockNumber
|
|
10573
|
+
});
|
|
10574
|
+
await seedOfferAssociations({
|
|
10575
|
+
db,
|
|
10576
|
+
gatekeeper,
|
|
10577
|
+
offers,
|
|
10578
|
+
blockNumber: positionBlockNumber
|
|
10579
|
+
});
|
|
9621
10580
|
logger.info({
|
|
9622
10581
|
msg: `Offers seeded from file`,
|
|
9623
10582
|
count: offers.length,
|
|
@@ -9669,10 +10628,59 @@ startCmd.description("Start Router services.").addOption(new Option("--mock <n>"
|
|
|
9669
10628
|
process.stdin.resume();
|
|
9670
10629
|
});
|
|
9671
10630
|
});
|
|
10631
|
+
/**
|
|
10632
|
+
* Seed mock offers and their dependencies for the router API.
|
|
10633
|
+
* @param parameters - Seed parameters. {@link seedMockOffers.Parameters}
|
|
10634
|
+
* @returns The generated mock offers. {@link seedMockOffers.ReturnType}
|
|
10635
|
+
*/
|
|
10636
|
+
async function seedMockOffers(parameters) {
|
|
10637
|
+
const { db, gatekeeper, chainRegistry, count } = parameters;
|
|
10638
|
+
if (count <= 0) return [];
|
|
10639
|
+
const configuredChains = chainRegistry.list();
|
|
10640
|
+
const tokens = {
|
|
10641
|
+
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": 6,
|
|
10642
|
+
"0x6B175474E89094C44Da98b954EedeAC495271d0F": 18,
|
|
10643
|
+
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2": 18,
|
|
10644
|
+
"0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599": 8,
|
|
10645
|
+
"0x4200000000000000000000000000000000000006": 18,
|
|
10646
|
+
"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913": 6,
|
|
10647
|
+
"0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb": 18
|
|
10648
|
+
};
|
|
10649
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
10650
|
+
const offerBlockNumber = 1;
|
|
10651
|
+
const positionBlockNumber = offerBlockNumber + 1;
|
|
10652
|
+
const start = Math.max(0, now - 60);
|
|
10653
|
+
const expiry = now + 3600;
|
|
10654
|
+
const maturity = from$16("end_of_next_month");
|
|
10655
|
+
const oraclePrice = 10n ** 36n;
|
|
10656
|
+
const offers = createMockOffers({
|
|
10657
|
+
count,
|
|
10658
|
+
chains: configuredChains,
|
|
10659
|
+
loanTokens: Object.keys(tokens).map((key) => key),
|
|
10660
|
+
assetsDecimals: tokens,
|
|
10661
|
+
start,
|
|
10662
|
+
expiry,
|
|
10663
|
+
maturity,
|
|
10664
|
+
chainRegistry
|
|
10665
|
+
});
|
|
10666
|
+
await seedOffers({
|
|
10667
|
+
db,
|
|
10668
|
+
offers,
|
|
10669
|
+
blockNumber: offerBlockNumber
|
|
10670
|
+
});
|
|
10671
|
+
await db.oracles.upsert(seedMockOracles(offers, oraclePrice, positionBlockNumber));
|
|
10672
|
+
await seedOfferAssociations({
|
|
10673
|
+
db,
|
|
10674
|
+
gatekeeper,
|
|
10675
|
+
offers,
|
|
10676
|
+
blockNumber: positionBlockNumber
|
|
10677
|
+
});
|
|
10678
|
+
return offers;
|
|
10679
|
+
}
|
|
9672
10680
|
async function getOffersFromFile(filePath) {
|
|
9673
10681
|
const content = await readFile(filePath, "utf-8");
|
|
9674
10682
|
const data = JSON.parse(content);
|
|
9675
|
-
return Array.isArray(data) ? data.map(from$
|
|
10683
|
+
return Array.isArray(data) ? data.map(from$12) : [from$12(data)];
|
|
9676
10684
|
}
|
|
9677
10685
|
function createMockOffers(parameters) {
|
|
9678
10686
|
const { count, chains, loanTokens, assetsDecimals, start, expiry, maturity, chainRegistry } = parameters;
|
|
@@ -9687,13 +10695,11 @@ function createMockOffers(parameters) {
|
|
|
9687
10695
|
maturity,
|
|
9688
10696
|
buy
|
|
9689
10697
|
});
|
|
9690
|
-
|
|
9691
|
-
if (!chain) throw new Error(`Missing chain config for id ${offer.chainId}`);
|
|
10698
|
+
if (!chainRegistry.getById(offer.chainId)) throw new Error(`Missing chain config for id ${offer.chainId}`);
|
|
9692
10699
|
const callbackType = buy ? Type$1.BuyVaultV1Callback : Type$1.SellERC20Callback;
|
|
9693
|
-
const callbackAddress =
|
|
9694
|
-
if (!callbackAddress) throw new Error(`Missing callback addresses for ${chain.name} (${buy ? "buy" : "sell"} offers)`);
|
|
10700
|
+
const callbackAddress = buy ? BUY_CALLBACK_ADDRESS : SELL_CALLBACK_ADDRESS;
|
|
9695
10701
|
const callbackData = buildMockCallbackData(callbackType, offer);
|
|
9696
|
-
return from$
|
|
10702
|
+
return from$12({
|
|
9697
10703
|
...offer,
|
|
9698
10704
|
callback: {
|
|
9699
10705
|
address: callbackAddress,
|
|
@@ -9719,7 +10725,7 @@ function seedMockOracles(offers, price, blockNumber) {
|
|
|
9719
10725
|
for (const offer of offers) for (const collateral of offer.collaterals) {
|
|
9720
10726
|
const key = `${offer.chainId}-${collateral.oracle}`.toLowerCase();
|
|
9721
10727
|
if (oracleMap.has(key)) continue;
|
|
9722
|
-
oracleMap.set(key, from$
|
|
10728
|
+
oracleMap.set(key, from$11({
|
|
9723
10729
|
chainId: offer.chainId,
|
|
9724
10730
|
address: collateral.oracle,
|
|
9725
10731
|
price: price.toString(),
|
|
@@ -9728,6 +10734,158 @@ function seedMockOracles(offers, price, blockNumber) {
|
|
|
9728
10734
|
}
|
|
9729
10735
|
return Array.from(oracleMap.values());
|
|
9730
10736
|
}
|
|
10737
|
+
const BUY_CALLBACK_ADDRESS = "0x3333333333333333333333333333333333333333";
|
|
10738
|
+
const SELL_CALLBACK_ADDRESS = "0x1111111111111111111111111111111111111111";
|
|
10739
|
+
async function seedOffers(parameters) {
|
|
10740
|
+
const { db, offers, blockNumber } = parameters;
|
|
10741
|
+
if (offers.length === 0) return;
|
|
10742
|
+
const dependencies = buildOfferDependencies({
|
|
10743
|
+
offers,
|
|
10744
|
+
blockNumber
|
|
10745
|
+
});
|
|
10746
|
+
await db.oracles.upsert(dependencies.oracles);
|
|
10747
|
+
await db.obligations.create(dependencies.obligations);
|
|
10748
|
+
await db.groups.create(dependencies.groups);
|
|
10749
|
+
await db.offers.create([{
|
|
10750
|
+
blockNumber,
|
|
10751
|
+
offers
|
|
10752
|
+
}]);
|
|
10753
|
+
}
|
|
10754
|
+
async function seedOfferAssociations(parameters) {
|
|
10755
|
+
const { db, gatekeeper, offers, blockNumber } = parameters;
|
|
10756
|
+
if (offers.length === 0) return;
|
|
10757
|
+
const { callbacks, positions, lots } = buildOfferAssociationsFromOffers({
|
|
10758
|
+
offers,
|
|
10759
|
+
callbackTypes: await resolveCallbackTypes({
|
|
10760
|
+
gatekeeper,
|
|
10761
|
+
offers
|
|
10762
|
+
}),
|
|
10763
|
+
blockNumber
|
|
10764
|
+
});
|
|
10765
|
+
if (positions.length > 0) await db.positions.upsert(positions);
|
|
10766
|
+
if (callbacks.length > 0) await db.callbacks.upsert(callbacks);
|
|
10767
|
+
if (lots.length > 0) await db.lots.create(lots);
|
|
10768
|
+
}
|
|
10769
|
+
function buildOfferDependencies(parameters) {
|
|
10770
|
+
const { offers, blockNumber } = parameters;
|
|
10771
|
+
const obligationsById = /* @__PURE__ */ new Map();
|
|
10772
|
+
const oraclesByKey = /* @__PURE__ */ new Map();
|
|
10773
|
+
const groupsByKey = /* @__PURE__ */ new Map();
|
|
10774
|
+
for (const offer of offers) {
|
|
10775
|
+
const obligationId$1 = obligationId(offer);
|
|
10776
|
+
if (!obligationsById.get(obligationId$1)) obligationsById.set(obligationId$1, from$13({
|
|
10777
|
+
chainId: offer.chainId,
|
|
10778
|
+
loanToken: offer.loanToken,
|
|
10779
|
+
maturity: offer.maturity,
|
|
10780
|
+
collaterals: offer.collaterals
|
|
10781
|
+
}));
|
|
10782
|
+
for (const collateral of offer.collaterals) {
|
|
10783
|
+
const oracleKey = `${offer.chainId}-${collateral.oracle}`.toLowerCase();
|
|
10784
|
+
if (!oraclesByKey.has(oracleKey)) oraclesByKey.set(oracleKey, from$11({
|
|
10785
|
+
chainId: offer.chainId,
|
|
10786
|
+
address: collateral.oracle,
|
|
10787
|
+
price: null,
|
|
10788
|
+
blockNumber
|
|
10789
|
+
}));
|
|
10790
|
+
}
|
|
10791
|
+
const groupKey = `${offer.chainId}-${offer.maker}-${offer.group}`.toLowerCase();
|
|
10792
|
+
if (!groupsByKey.has(groupKey)) groupsByKey.set(groupKey, {
|
|
10793
|
+
chainId: offer.chainId,
|
|
10794
|
+
maker: offer.maker,
|
|
10795
|
+
group: offer.group,
|
|
10796
|
+
blockNumber
|
|
10797
|
+
});
|
|
10798
|
+
}
|
|
10799
|
+
return {
|
|
10800
|
+
obligations: Array.from(obligationsById.values()),
|
|
10801
|
+
oracles: Array.from(oraclesByKey.values()),
|
|
10802
|
+
groups: Array.from(groupsByKey.values())
|
|
10803
|
+
};
|
|
10804
|
+
}
|
|
10805
|
+
async function resolveCallbackTypes(parameters) {
|
|
10806
|
+
const { gatekeeper, offers } = parameters;
|
|
10807
|
+
const addressesByChain = /* @__PURE__ */ new Map();
|
|
10808
|
+
for (const offer of offers) {
|
|
10809
|
+
if (offer.callback.data === "0x") continue;
|
|
10810
|
+
const set = addressesByChain.get(offer.chainId) ?? /* @__PURE__ */ new Set();
|
|
10811
|
+
set.add(offer.callback.address.toLowerCase());
|
|
10812
|
+
addressesByChain.set(offer.chainId, set);
|
|
10813
|
+
}
|
|
10814
|
+
if (addressesByChain.size === 0) return /* @__PURE__ */ new Map();
|
|
10815
|
+
const request = { callbacks: Array.from(addressesByChain.entries()).map(([chainId, addresses]) => ({
|
|
10816
|
+
chain_id: chainId,
|
|
10817
|
+
addresses: Array.from(addresses)
|
|
10818
|
+
})) };
|
|
10819
|
+
const response = await gatekeeper.getCallbackTypes(request);
|
|
10820
|
+
const typeByAddress = /* @__PURE__ */ new Map();
|
|
10821
|
+
for (const entry of response) {
|
|
10822
|
+
const chainId = entry.chain_id;
|
|
10823
|
+
for (const [key, list] of Object.entries(entry)) {
|
|
10824
|
+
if (key === "chain_id" || key === "not_supported") continue;
|
|
10825
|
+
if (!Array.isArray(list)) continue;
|
|
10826
|
+
for (const address of list) {
|
|
10827
|
+
const mapKey = `${chainId}-${address.toLowerCase()}`;
|
|
10828
|
+
typeByAddress.set(mapKey, key);
|
|
10829
|
+
}
|
|
10830
|
+
}
|
|
10831
|
+
}
|
|
10832
|
+
return typeByAddress;
|
|
10833
|
+
}
|
|
10834
|
+
function buildOfferAssociationsFromOffers(parameters) {
|
|
10835
|
+
const { offers, callbackTypes, blockNumber } = parameters;
|
|
10836
|
+
const callbacks = [];
|
|
10837
|
+
const positions = [];
|
|
10838
|
+
const lots = [];
|
|
10839
|
+
for (const offer of offers) {
|
|
10840
|
+
if (offer.callback.data === "0x") continue;
|
|
10841
|
+
const key = `${offer.chainId}-${offer.callback.address.toLowerCase()}`;
|
|
10842
|
+
const callbackType = callbackTypes.get(key);
|
|
10843
|
+
if (!callbackType) continue;
|
|
10844
|
+
let decoded;
|
|
10845
|
+
try {
|
|
10846
|
+
decoded = decode$1(callbackType, offer.callback.data);
|
|
10847
|
+
} catch (err) {
|
|
10848
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
10849
|
+
throw new Error("Failed to decode callback data", { cause: error });
|
|
10850
|
+
}
|
|
10851
|
+
if (decoded.length === 0) continue;
|
|
10852
|
+
callbacks.push({
|
|
10853
|
+
offerHash: hash(offer),
|
|
10854
|
+
callbacks: decoded.map((callback) => ({
|
|
10855
|
+
chainId: offer.chainId,
|
|
10856
|
+
contract: callback.contract,
|
|
10857
|
+
user: offer.maker,
|
|
10858
|
+
amount: callback.amount
|
|
10859
|
+
}))
|
|
10860
|
+
});
|
|
10861
|
+
for (const callback of decoded) {
|
|
10862
|
+
const positionType = callbackType === Type$1.BuyVaultV1Callback ? Type.VAULT_V1 : Type.ERC20;
|
|
10863
|
+
const positionAsset = callbackType === Type$1.BuyVaultV1Callback ? offer.loanToken : callback.contract;
|
|
10864
|
+
positions.push(from$10({
|
|
10865
|
+
chainId: offer.chainId,
|
|
10866
|
+
contract: callback.contract,
|
|
10867
|
+
user: offer.maker,
|
|
10868
|
+
type: positionType,
|
|
10869
|
+
balance: callback.amount * 2n,
|
|
10870
|
+
asset: positionAsset,
|
|
10871
|
+
blockNumber
|
|
10872
|
+
}));
|
|
10873
|
+
const isLoanPosition = positionAsset.toLowerCase() === offer.loanToken.toLowerCase();
|
|
10874
|
+
lots.push({
|
|
10875
|
+
positionChainId: offer.chainId,
|
|
10876
|
+
positionContract: callback.contract,
|
|
10877
|
+
positionUser: offer.maker,
|
|
10878
|
+
group: offer.group,
|
|
10879
|
+
size: isLoanPosition ? offer.assets : callback.amount
|
|
10880
|
+
});
|
|
10881
|
+
}
|
|
10882
|
+
}
|
|
10883
|
+
return {
|
|
10884
|
+
callbacks,
|
|
10885
|
+
positions,
|
|
10886
|
+
lots
|
|
10887
|
+
};
|
|
10888
|
+
}
|
|
9731
10889
|
|
|
9732
10890
|
//#endregion
|
|
9733
10891
|
//#region src/cli/cli.ts
|