@morpho-dev/router 0.2.1 → 0.3.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 +1200 -559
- package/dist/drizzle/migrations/0015_add-lots-table.sql +12 -0
- package/dist/drizzle/migrations/0016_merkle-metadata.sql +26 -0
- package/dist/drizzle/migrations/meta/0015_snapshot.json +1365 -0
- package/dist/drizzle/migrations/meta/0016_snapshot.json +1531 -0
- package/dist/drizzle/migrations/meta/_journal.json +14 -0
- package/dist/index.browser.d.mts +443 -229
- package/dist/index.browser.d.mts.map +1 -1
- package/dist/index.browser.d.ts +439 -229
- package/dist/index.browser.d.ts.map +1 -1
- package/dist/index.browser.js +533 -202
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +534 -203
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.node.d.mts +737 -67
- package/dist/index.node.d.mts.map +1 -1
- package/dist/index.node.d.ts +737 -67
- package/dist/index.node.d.ts.map +1 -1
- package/dist/index.node.js +1240 -513
- package/dist/index.node.js.map +1 -1
- package/dist/index.node.mjs +1241 -514
- package/dist/index.node.mjs.map +1 -1
- package/package.json +1 -2
package/dist/cli.js
CHANGED
|
@@ -9,17 +9,15 @@ import { AWSXRayPropagator } from "@opentelemetry/propagator-aws-xray";
|
|
|
9
9
|
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
10
10
|
import { BatchSpanProcessor, NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
11
11
|
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
|
12
|
-
import {
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import { bytesToHex, createPublicClient, createTestClient, decodeAbiParameters, encodeAbiParameters, erc20Abi, getAddress, hashMessage, hashTypedData, hexToBytes, http, isAddress, isHex, keccak256, maxUint256, parseAbi, parseEventLogs, publicActions, recoverAddress, stringify, walletActions, zeroAddress } from "viem";
|
|
14
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
13
15
|
import { anvil, base, mainnet } from "viem/chains";
|
|
14
16
|
import { getBlock, getBlockNumber, getLogs, multicall } from "viem/actions";
|
|
15
|
-
import
|
|
16
|
-
import { fileURLToPath } from "node:url";
|
|
17
|
-
import { spawn } from "node:child_process";
|
|
18
|
-
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
|
|
17
|
+
import { readFile } from "node:fs/promises";
|
|
19
18
|
import * as z$1 from "zod";
|
|
20
19
|
import { StandardMerkleTree } from "@openzeppelin/merkle-tree";
|
|
21
|
-
import { ungzip } from "pako";
|
|
22
|
-
import { readFile } from "node:fs/promises";
|
|
20
|
+
import { gzip, ungzip } from "pako";
|
|
23
21
|
import { serve } from "@hono/node-server";
|
|
24
22
|
import { Hono } from "hono";
|
|
25
23
|
import { cors } from "hono/cors";
|
|
@@ -29,6 +27,8 @@ import "reflect-metadata";
|
|
|
29
27
|
import { generateDocument } from "openapi-metadata";
|
|
30
28
|
import { ApiBody, ApiOperation, ApiProperty, ApiQuery, ApiResponse, ApiTags } from "openapi-metadata/decorators";
|
|
31
29
|
import { Base64 } from "js-base64";
|
|
30
|
+
import path from "node:path";
|
|
31
|
+
import { fileURLToPath } from "node:url";
|
|
32
32
|
import dotenv from "dotenv";
|
|
33
33
|
import { PGlite } from "@electric-sql/pglite";
|
|
34
34
|
import { drizzle } from "drizzle-orm/node-postgres";
|
|
@@ -40,15 +40,7 @@ import { and, asc, desc, eq, gt, gte, inArray, lte, ne, sql } from "drizzle-orm"
|
|
|
40
40
|
import { bigint, boolean, foreignKey, index, integer, numeric, pgSchema, primaryKey, serial, text, timestamp, uniqueIndex, varchar } from "drizzle-orm/pg-core";
|
|
41
41
|
|
|
42
42
|
//#region rolldown:runtime
|
|
43
|
-
var __create = Object.create;
|
|
44
43
|
var __defProp = Object.defineProperty;
|
|
45
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
46
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
47
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
48
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
49
|
-
var __esm = (fn, res) => function() {
|
|
50
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
51
|
-
};
|
|
52
44
|
var __export = (all) => {
|
|
53
45
|
let target = {};
|
|
54
46
|
for (var name$1 in all) __defProp(target, name$1, {
|
|
@@ -57,21 +49,6 @@ var __export = (all) => {
|
|
|
57
49
|
});
|
|
58
50
|
return target;
|
|
59
51
|
};
|
|
60
|
-
var __copyProps = (to, from$15, except, desc$1) => {
|
|
61
|
-
if (from$15 && typeof from$15 === "object" || typeof from$15 === "function") for (var keys = __getOwnPropNames(from$15), i = 0, n = keys.length, key; i < n; i++) {
|
|
62
|
-
key = keys[i];
|
|
63
|
-
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
64
|
-
get: ((k) => from$15[k]).bind(null, key),
|
|
65
|
-
enumerable: !(desc$1 = __getOwnPropDesc(from$15, key)) || desc$1.enumerable
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
return to;
|
|
69
|
-
};
|
|
70
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
71
|
-
value: mod,
|
|
72
|
-
enumerable: true
|
|
73
|
-
}) : target, mod));
|
|
74
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
75
52
|
|
|
76
53
|
//#endregion
|
|
77
54
|
//#region src/tracer/Tracer.ts
|
|
@@ -164,7 +141,7 @@ function startActiveSpan(tracer, name$1, fn) {
|
|
|
164
141
|
//#endregion
|
|
165
142
|
//#region package.json
|
|
166
143
|
var name = "@morpho-dev/router";
|
|
167
|
-
var version = "0.
|
|
144
|
+
var version = "0.3.0";
|
|
168
145
|
var description = "Router package for Morpho protocol";
|
|
169
146
|
|
|
170
147
|
//#endregion
|
|
@@ -391,22 +368,22 @@ const DEFAULT_BATCH_SIZE$1 = 2500;
|
|
|
391
368
|
const MAX_BLOCK_WINDOW = 1e4;
|
|
392
369
|
const DEFAULT_BLOCK_WINDOW = 8e3;
|
|
393
370
|
async function* streamLogs(parameters) {
|
|
394
|
-
const { client, contractAddress, event, blockNumberGte, blockNumberLte, order
|
|
371
|
+
const { client, contractAddress, event, blockNumberGte, blockNumberLte, order = "desc", options: { maxBatchSize = DEFAULT_BATCH_SIZE$1, blockWindow = DEFAULT_BLOCK_WINDOW } = {} } = parameters;
|
|
395
372
|
if (maxBatchSize > MAX_BATCH_SIZE) throw new InvalidBatchSizeError(maxBatchSize);
|
|
396
373
|
if (blockWindow > MAX_BLOCK_WINDOW) throw new InvalidBlockWindowError(blockWindow);
|
|
397
|
-
if (order
|
|
374
|
+
if (order === "asc" && blockNumberGte === void 0) throw new MissingBlockNumberError();
|
|
398
375
|
const latestBlock = (await getBlock(client, {
|
|
399
376
|
blockTag: "latest",
|
|
400
377
|
includeTransactions: false
|
|
401
378
|
})).number;
|
|
402
379
|
let toBlock = 0n;
|
|
403
|
-
if (order
|
|
404
|
-
if (order
|
|
380
|
+
if (order === "asc") toBlock = min(BigInt(blockNumberGte) + BigInt(blockWindow), blockNumberLte ? BigInt(blockNumberLte) : latestBlock);
|
|
381
|
+
if (order === "desc") toBlock = blockNumberLte === void 0 ? latestBlock : min(BigInt(blockNumberLte), latestBlock);
|
|
405
382
|
let fromBlock = 0n;
|
|
406
|
-
if (order
|
|
407
|
-
if (order
|
|
408
|
-
if (order
|
|
409
|
-
if (order
|
|
383
|
+
if (order === "asc") fromBlock = min(BigInt(blockNumberGte), latestBlock);
|
|
384
|
+
if (order === "desc") fromBlock = max(BigInt(blockNumberGte || toBlock - BigInt(blockWindow)), 0n);
|
|
385
|
+
if (order === "asc") toBlock = min(toBlock, fromBlock + BigInt(blockWindow));
|
|
386
|
+
if (order === "desc") fromBlock = max(fromBlock, toBlock - BigInt(blockWindow));
|
|
410
387
|
if (fromBlock > toBlock) throw new InvalidBlockRangeError(fromBlock, toBlock);
|
|
411
388
|
let streaming = true;
|
|
412
389
|
while (streaming) {
|
|
@@ -416,29 +393,29 @@ async function* streamLogs(parameters) {
|
|
|
416
393
|
fromBlock,
|
|
417
394
|
toBlock
|
|
418
395
|
});
|
|
419
|
-
streaming = order
|
|
396
|
+
streaming = order === "asc" ? toBlock < (blockNumberLte || latestBlock) : fromBlock > (blockNumberGte || 0n);
|
|
420
397
|
if (logs.length === 0 && !streaming) break;
|
|
421
398
|
if (logs.length === 0 && streaming) yield {
|
|
422
399
|
logs: [],
|
|
423
|
-
blockNumber: order
|
|
400
|
+
blockNumber: order === "asc" ? Number(toBlock) : Number(fromBlock)
|
|
424
401
|
};
|
|
425
402
|
logs.sort((a, b) => {
|
|
426
|
-
if (a.blockNumber !== b.blockNumber) return order
|
|
427
|
-
if (a.transactionIndex !== b.transactionIndex) return order
|
|
428
|
-
return order
|
|
403
|
+
if (a.blockNumber !== b.blockNumber) return order === "asc" ? Number(a.blockNumber - b.blockNumber) : Number(b.blockNumber - a.blockNumber);
|
|
404
|
+
if (a.transactionIndex !== b.transactionIndex) return order === "asc" ? a.transactionIndex - b.transactionIndex : b.transactionIndex - a.transactionIndex;
|
|
405
|
+
return order === "asc" ? a.logIndex - b.logIndex : b.logIndex - a.logIndex;
|
|
429
406
|
});
|
|
430
407
|
for (const logBatch of batch$1(logs, maxBatchSize)) yield {
|
|
431
408
|
logs: logBatch,
|
|
432
|
-
blockNumber: logBatch.length === maxBatchSize ? Number(logBatch[logBatch.length - 1]?.blockNumber) : order
|
|
409
|
+
blockNumber: logBatch.length === maxBatchSize ? Number(logBatch[logBatch.length - 1]?.blockNumber) : order === "asc" ? Number(toBlock) : Number(fromBlock)
|
|
433
410
|
};
|
|
434
|
-
if (order
|
|
411
|
+
if (order === "asc") {
|
|
435
412
|
const upperBound = BigInt(blockNumberLte || latestBlock);
|
|
436
413
|
const nextFromBlock = min(BigInt(toBlock) + 1n, upperBound);
|
|
437
414
|
const nextToBlock = min(toBlock + BigInt(blockWindow) + 1n, upperBound);
|
|
438
415
|
fromBlock = nextFromBlock;
|
|
439
416
|
toBlock = nextToBlock;
|
|
440
417
|
}
|
|
441
|
-
if (order
|
|
418
|
+
if (order === "desc") {
|
|
442
419
|
const lowerBound = BigInt(blockNumberGte || 0);
|
|
443
420
|
const nextToBlock = max(fromBlock - 1n, lowerBound);
|
|
444
421
|
const nextFromBlock = max(fromBlock - BigInt(blockWindow) - 1n, lowerBound);
|
|
@@ -448,7 +425,7 @@ async function* streamLogs(parameters) {
|
|
|
448
425
|
}
|
|
449
426
|
yield {
|
|
450
427
|
logs: [],
|
|
451
|
-
blockNumber: order
|
|
428
|
+
blockNumber: order === "asc" ? Number(toBlock) : Number(fromBlock)
|
|
452
429
|
};
|
|
453
430
|
}
|
|
454
431
|
var InvalidBlockRangeError = class extends BaseError {
|
|
@@ -477,13 +454,77 @@ var MissingBlockNumberError = class extends BaseError {
|
|
|
477
454
|
};
|
|
478
455
|
|
|
479
456
|
//#endregion
|
|
480
|
-
//#region
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
457
|
+
//#region src/cli/commands/mempool.ts
|
|
458
|
+
/** Default account for test transactions (first anvil account) */
|
|
459
|
+
const testAccount = privateKeyToAccount("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80");
|
|
460
|
+
/**
|
|
461
|
+
* Start a local anvil chain.
|
|
462
|
+
*/
|
|
463
|
+
async function serve$2(parameters) {
|
|
464
|
+
const { port, forkUrl, blockNumber } = parameters;
|
|
465
|
+
let started = false;
|
|
466
|
+
const args = [
|
|
467
|
+
"--chain-id",
|
|
468
|
+
ChainId.ANVIL.toString(),
|
|
469
|
+
"--fork-url",
|
|
470
|
+
forkUrl,
|
|
471
|
+
"--port",
|
|
472
|
+
String(port)
|
|
473
|
+
];
|
|
474
|
+
if (blockNumber) args.push("--fork-block-number", String(blockNumber));
|
|
475
|
+
const stop = await new Promise((resolve, reject) => {
|
|
476
|
+
const subprocess = spawn("anvil", args, { env: {
|
|
477
|
+
...process.env,
|
|
478
|
+
FOUNDRY_DISABLE_NIGHTLY_WARNING: "1"
|
|
479
|
+
} });
|
|
480
|
+
subprocess.stdout.on("data", (data) => {
|
|
481
|
+
if (`[port ${port}] ${data.toString()}`.includes("Listening on")) {
|
|
482
|
+
started = true;
|
|
483
|
+
resolve(() => subprocess.kill("SIGINT"));
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
subprocess.stderr.on("data", (data) => {
|
|
487
|
+
const message = `[port ${port}] ${data.toString()}`;
|
|
488
|
+
console.warn(message);
|
|
489
|
+
if (!started) reject(message);
|
|
490
|
+
else console.warn(message);
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
return {
|
|
494
|
+
rpcUrl: `http://localhost:${port}`,
|
|
495
|
+
stop
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
const mempoolCmd = new Command("mempool");
|
|
499
|
+
mempoolCmd.description("Start a local chain with the mempool contract").addOption(new Option("--port <port>").env("MEMPOOL_PORT").default(8545)).addOption(new Option("--fork-url <url>").env("MEMPOOL_FORK_URL").default("https://ethereum-rpc.publicnode.com")).addOption(new Option("--block-number <number>").default(0)).action(async (opts) => {
|
|
500
|
+
const { rpcUrl } = await serve$2({
|
|
501
|
+
port: opts.port,
|
|
502
|
+
forkUrl: opts.forkUrl,
|
|
503
|
+
blockNumber: opts.blockNumber
|
|
504
|
+
});
|
|
505
|
+
const client = createTestClient({
|
|
506
|
+
chain: {
|
|
507
|
+
...anvil,
|
|
508
|
+
id: Number(ChainId.ANVIL)
|
|
509
|
+
},
|
|
510
|
+
transport: http(rpcUrl),
|
|
511
|
+
mode: "anvil"
|
|
512
|
+
}).extend(publicActions).extend(walletActions);
|
|
513
|
+
const mempoolHash = await client.sendTransaction({
|
|
514
|
+
account: testAccount,
|
|
515
|
+
data: "0x60808060405234601357607d908160188239f35b5f80fdfe7f758c7cf107fab3cfdc5aefab966cd9e69c3f368761f7a218a18283c1fbb5574c60808060405260208152366020820152365f60408301375f604036830101526040817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f3601168101030190a100fea164736f6c634300081e000a"
|
|
516
|
+
});
|
|
517
|
+
const { contractAddress: mempoolAddress } = await client.waitForTransactionReceipt({ hash: mempoolHash });
|
|
518
|
+
if (!mempoolAddress) throw new Error("Failed to deploy mempool contract");
|
|
519
|
+
console.log(`Mempool contract deployed at ${mempoolAddress}`);
|
|
520
|
+
const termsHash = await client.sendTransaction({
|
|
521
|
+
account: testAccount,
|
|
522
|
+
data: "0x6080604052348015600e575f5ffd5b506101dc8061001c5f395ff3fe608060405234801561000f575f5ffd5b5060043610610029575f3560e01c80637edab8a61461002d575b5f5ffd5b6100476004803603810190610042919061012e565b610049565b005b818373ffffffffffffffffffffffffffffffffffffffff167f1eed8a711368c40dda6427683883619012c34cf436e11e0d773b0d05169ccb8283604051610090919061018d565b60405180910390a3505050565b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6100ca826100a1565b9050919050565b6100da816100c0565b81146100e4575f5ffd5b50565b5f813590506100f5816100d1565b92915050565b5f819050919050565b61010d816100fb565b8114610117575f5ffd5b50565b5f8135905061012881610104565b92915050565b5f5f5f606084860312156101455761014461009d565b5b5f610152868287016100e7565b93505060206101638682870161011a565b92505060406101748682870161011a565b9150509250925092565b610187816100fb565b82525050565b5f6020820190506101a05f83018461017e565b9291505056fea2646970667358221220cc06296395b7aafdb892cd7bfe25a87dc6b35fe29b2d5cb2dcb3aad43050396a64736f6c634300081e0033"
|
|
523
|
+
});
|
|
524
|
+
const { contractAddress: termsAddress } = await client.waitForTransactionReceipt({ hash: termsHash });
|
|
525
|
+
if (!termsAddress) throw new Error("Failed to deploy terms contract");
|
|
526
|
+
console.log(`Terms contract deployed at ${termsAddress}`);
|
|
527
|
+
});
|
|
487
528
|
|
|
488
529
|
//#endregion
|
|
489
530
|
//#region src/core/Abi/MetaMorpho.ts
|
|
@@ -562,6 +603,49 @@ function encodeSellERC20Callback(parameters) {
|
|
|
562
603
|
return encodeAbiParameters([{ type: "address[]" }, { type: "uint256[]" }], [parameters.collaterals, parameters.amounts]);
|
|
563
604
|
}
|
|
564
605
|
|
|
606
|
+
//#endregion
|
|
607
|
+
//#region src/utils/Random.ts
|
|
608
|
+
let currentRng = Math.random;
|
|
609
|
+
/**
|
|
610
|
+
* Returns a deterministic random float in [0, 1).
|
|
611
|
+
*/
|
|
612
|
+
function float() {
|
|
613
|
+
return currentRng();
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Returns a deterministic random integer in [min, maxExclusive).
|
|
617
|
+
*/
|
|
618
|
+
function int(maxExclusive, min$1 = 0) {
|
|
619
|
+
return Math.floor(float() * (maxExclusive - min$1)) + min$1;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Returns a deterministic random boolean.
|
|
623
|
+
*/
|
|
624
|
+
function bool(probability = .5) {
|
|
625
|
+
return float() < probability;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Returns deterministic random bytes.
|
|
629
|
+
*/
|
|
630
|
+
function bytes(length) {
|
|
631
|
+
const output = new Uint8Array(length);
|
|
632
|
+
for (let i = 0; i < length; i += 1) output[i] = int(256);
|
|
633
|
+
return output;
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Returns a deterministic random hex string for the given byte length.
|
|
637
|
+
*/
|
|
638
|
+
function hex(byteLength) {
|
|
639
|
+
const output = bytes(byteLength);
|
|
640
|
+
return `0x${Array.from(output, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Returns a deterministic random address.
|
|
644
|
+
*/
|
|
645
|
+
function address() {
|
|
646
|
+
return hex(20);
|
|
647
|
+
}
|
|
648
|
+
|
|
565
649
|
//#endregion
|
|
566
650
|
//#region src/utils/zod.ts
|
|
567
651
|
const transformHex = (val, ctx) => {
|
|
@@ -670,8 +754,8 @@ const from$13 = (parameters) => {
|
|
|
670
754
|
*/
|
|
671
755
|
function random$1() {
|
|
672
756
|
return from$13({
|
|
673
|
-
asset:
|
|
674
|
-
oracle:
|
|
757
|
+
asset: address(),
|
|
758
|
+
oracle: address(),
|
|
675
759
|
lltv: .965
|
|
676
760
|
});
|
|
677
761
|
}
|
|
@@ -976,48 +1060,191 @@ var CollateralsAreNotSortedError = class extends BaseError {
|
|
|
976
1060
|
//#endregion
|
|
977
1061
|
//#region src/core/Tree.ts
|
|
978
1062
|
const VERSION$1 = 1;
|
|
1063
|
+
const normalizeHash = (hash$1) => hash$1.toLowerCase();
|
|
979
1064
|
/**
|
|
980
1065
|
* Builds a Merkle tree from a list of offers.
|
|
981
1066
|
*
|
|
982
1067
|
* Leaves are the offer `hash` values as `bytes32` and are deterministically
|
|
983
|
-
* ordered
|
|
984
|
-
* regardless of the input order.
|
|
1068
|
+
* ordered following the StandardMerkleTree leaf ordering so that the resulting
|
|
1069
|
+
* root is stable regardless of the input order.
|
|
985
1070
|
*
|
|
986
1071
|
* @param offers - Offers to include in the tree.
|
|
987
1072
|
* @returns A `StandardMerkleTree` of `bytes32` leaves representing the offers.
|
|
1073
|
+
* @throws {TreeError} If tree building fails due to offer inconsistencies.
|
|
988
1074
|
*/
|
|
989
1075
|
const from$10 = (offers$1) => {
|
|
990
|
-
const leaves =
|
|
991
|
-
return [offer.hash];
|
|
992
|
-
});
|
|
1076
|
+
const leaves = offers$1.map((offer) => [offer.hash]);
|
|
993
1077
|
const tree = StandardMerkleTree.of(leaves, ["bytes32"]);
|
|
994
|
-
|
|
1078
|
+
const orderedOffers = orderOffers(tree, offers$1);
|
|
1079
|
+
return Object.assign(tree, { offers: orderedOffers });
|
|
1080
|
+
};
|
|
1081
|
+
const orderOffers = (tree, offers$1) => {
|
|
1082
|
+
const offerByHash = /* @__PURE__ */ new Map();
|
|
1083
|
+
for (const offer of offers$1) offerByHash.set(normalizeHash(offer.hash), offer);
|
|
1084
|
+
const entries = tree.dump().values.map((value) => {
|
|
1085
|
+
const hash$1 = normalizeHash(value.value[0]);
|
|
1086
|
+
const offer = offerByHash.get(hash$1);
|
|
1087
|
+
if (!offer) throw new TreeError(`missing offer for leaf ${hash$1}`);
|
|
1088
|
+
return {
|
|
1089
|
+
offer,
|
|
1090
|
+
treeIndex: value.treeIndex
|
|
1091
|
+
};
|
|
1092
|
+
});
|
|
1093
|
+
entries.sort((a, b) => b.treeIndex - a.treeIndex);
|
|
1094
|
+
return entries.map((item) => item.offer);
|
|
1095
|
+
};
|
|
1096
|
+
/**
|
|
1097
|
+
* Generates merkle proofs for all offers in a tree.
|
|
1098
|
+
*
|
|
1099
|
+
* Each proof allows independent verification that an offer is included in the tree
|
|
1100
|
+
* without requiring the full tree. Proofs are ordered by StandardMerkleTree leaf ordering.
|
|
1101
|
+
*
|
|
1102
|
+
* @param tree - The {@link Tree} to generate proofs for.
|
|
1103
|
+
* @returns Array of proofs - {@link Proof}
|
|
1104
|
+
*/
|
|
1105
|
+
const proofs = (tree) => {
|
|
1106
|
+
return tree.offers.map((offer) => {
|
|
1107
|
+
return {
|
|
1108
|
+
offer,
|
|
1109
|
+
path: tree.getProof([offer.hash])
|
|
1110
|
+
};
|
|
1111
|
+
});
|
|
1112
|
+
};
|
|
1113
|
+
const assertHex = (value, expectedBytes, name$1) => {
|
|
1114
|
+
if (typeof value !== "string" || !isHex(value)) throw new DecodeError(`${name$1} is not a valid hex string`);
|
|
1115
|
+
if (hexToBytes(value).length !== expectedBytes) throw new DecodeError(`${name$1}: expected ${expectedBytes} bytes`);
|
|
1116
|
+
};
|
|
1117
|
+
const verifySignatureAndRecoverAddress = async (params) => {
|
|
1118
|
+
const { root, signature } = params;
|
|
1119
|
+
assertHex(signature, 65, "signature");
|
|
1120
|
+
const hash$1 = hashMessage({ raw: root });
|
|
1121
|
+
try {
|
|
1122
|
+
return await recoverAddress({
|
|
1123
|
+
hash: hash$1,
|
|
1124
|
+
signature
|
|
1125
|
+
});
|
|
1126
|
+
} catch {
|
|
1127
|
+
throw new DecodeError("signature recovery failed");
|
|
1128
|
+
}
|
|
995
1129
|
};
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1130
|
+
/**
|
|
1131
|
+
* Encodes a merkle tree without a signature into hex payload for client-side signing.
|
|
1132
|
+
*
|
|
1133
|
+
* Layout: `0x{vv}{gzip([...offers])}{root}` where:
|
|
1134
|
+
* - `{vv}`: 1-byte version (currently 0x01)
|
|
1135
|
+
* - `{gzip([...offers])}`: gzipped JSON array of serialized offers
|
|
1136
|
+
* - `{root}`: 32-byte merkle root
|
|
1137
|
+
*
|
|
1138
|
+
* Validates root integrity before encoding.
|
|
1139
|
+
*
|
|
1140
|
+
* @param tree - Merkle tree of offers
|
|
1141
|
+
* @returns Hex-encoded unsigned payload
|
|
1142
|
+
* @throws {EncodeError} If root mismatch
|
|
1143
|
+
*/
|
|
1144
|
+
const encodeUnsigned = (tree) => {
|
|
1145
|
+
validateTreeForEncoding(tree);
|
|
1146
|
+
return bytesToHex(encodeUnsignedBytes(tree));
|
|
1147
|
+
};
|
|
1148
|
+
const validateTreeForEncoding = (tree) => {
|
|
1149
|
+
if (VERSION$1 > 255) throw new EncodeError(`version overflow: ${VERSION$1} exceeds 255`);
|
|
1150
|
+
const computed = from$10(tree.offers);
|
|
1151
|
+
if (tree.root !== computed.root) throw new EncodeError(`root mismatch: expected ${computed.root}, got ${tree.root}`);
|
|
1152
|
+
};
|
|
1153
|
+
const encodeUnsignedBytes = (tree) => {
|
|
1154
|
+
const offersPayload = tree.offers.map(serialize);
|
|
1155
|
+
const compressed = gzip(JSON.stringify(offersPayload));
|
|
1156
|
+
const rootBytes = hexToBytes(tree.root);
|
|
1157
|
+
const encoded = new Uint8Array(1 + compressed.length + 32);
|
|
1158
|
+
encoded[0] = VERSION$1;
|
|
1159
|
+
encoded.set(compressed, 1);
|
|
1160
|
+
encoded.set(rootBytes, 1 + compressed.length);
|
|
1161
|
+
return encoded;
|
|
999
1162
|
};
|
|
1000
1163
|
/**
|
|
1001
|
-
* Decodes
|
|
1164
|
+
* Decodes hex calldata into a validated merkle tree.
|
|
1165
|
+
*
|
|
1166
|
+
* Validates signature before decompression for fail-fast rejection of invalid payloads.
|
|
1167
|
+
* Returns the tree with separately validated signature and recovered signer address.
|
|
1002
1168
|
*
|
|
1003
|
-
*
|
|
1004
|
-
*
|
|
1169
|
+
* Validation order:
|
|
1170
|
+
* 1. Version check
|
|
1171
|
+
* 2. Signature verification (fail-fast, before decompression)
|
|
1172
|
+
* 3. Decompression (only if signature valid)
|
|
1173
|
+
* 4. Root verification (computed from offers vs embedded root)
|
|
1005
1174
|
*
|
|
1006
|
-
* @
|
|
1007
|
-
*
|
|
1008
|
-
*
|
|
1175
|
+
* @example
|
|
1176
|
+
* ```typescript
|
|
1177
|
+
* const { tree, signature, signer } = await Tree.decode(calldata);
|
|
1178
|
+
* console.log(`Tree signed by ${signer} with ${tree.offers.length} offers`);
|
|
1179
|
+
* ```
|
|
1180
|
+
*
|
|
1181
|
+
* @param encoded - Hex calldata in format `0x{vv}{gzip}{root}{signature}`
|
|
1182
|
+
* @returns Validated tree, signature, and recovered signer address
|
|
1183
|
+
* @throws {DecodeError} If version invalid, signature invalid, or root mismatch
|
|
1184
|
+
*/
|
|
1185
|
+
const decode$1 = async (encoded) => {
|
|
1186
|
+
const bytes$1 = hexToBytes(encoded);
|
|
1187
|
+
if (bytes$1.length < 98) throw new DecodeError("payload too short");
|
|
1188
|
+
const version$1 = bytes$1[0];
|
|
1189
|
+
if (version$1 !== (VERSION$1 & 255)) throw new DecodeError(`invalid version: expected ${VERSION$1}, got ${version$1 ?? 0}`);
|
|
1190
|
+
const signature = bytesToHex(bytes$1.slice(-65));
|
|
1191
|
+
const root = bytesToHex(bytes$1.slice(-97, -65));
|
|
1192
|
+
assertHex(root, 32, "root");
|
|
1193
|
+
assertHex(signature, 65, "signature");
|
|
1194
|
+
const signer = await verifySignatureAndRecoverAddress({
|
|
1195
|
+
root,
|
|
1196
|
+
signature
|
|
1197
|
+
});
|
|
1198
|
+
const compressed = bytes$1.slice(1, -97);
|
|
1199
|
+
let decoded;
|
|
1200
|
+
try {
|
|
1201
|
+
decoded = ungzip(compressed, { to: "string" });
|
|
1202
|
+
} catch {
|
|
1203
|
+
throw new DecodeError("decompression failed");
|
|
1204
|
+
}
|
|
1205
|
+
let rawOffers;
|
|
1206
|
+
try {
|
|
1207
|
+
rawOffers = JSON.parse(decoded);
|
|
1208
|
+
} catch {
|
|
1209
|
+
throw new DecodeError("JSON parse failed");
|
|
1210
|
+
}
|
|
1211
|
+
const tree = from$10(rawOffers.map((o) => OfferSchema().parse(o)));
|
|
1212
|
+
if (root !== tree.root) throw new DecodeError(`root mismatch: expected ${tree.root}, got ${root}`);
|
|
1213
|
+
return {
|
|
1214
|
+
tree,
|
|
1215
|
+
signature,
|
|
1216
|
+
signer
|
|
1217
|
+
};
|
|
1218
|
+
};
|
|
1219
|
+
/**
|
|
1220
|
+
* Error thrown during tree building operations.
|
|
1221
|
+
* Indicates structural issues with the tree (missing offers, inconsistent state).
|
|
1009
1222
|
*/
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1223
|
+
var TreeError = class extends BaseError {
|
|
1224
|
+
name = "Tree.TreeError";
|
|
1225
|
+
constructor(reason) {
|
|
1226
|
+
super(`Tree error: ${reason}`);
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
1229
|
+
/**
|
|
1230
|
+
* Error thrown during tree encoding.
|
|
1231
|
+
* Indicates validation failures (signature, root mismatch, mixed makers).
|
|
1232
|
+
*/
|
|
1233
|
+
var EncodeError = class extends BaseError {
|
|
1234
|
+
name = "Tree.EncodeError";
|
|
1235
|
+
constructor(reason) {
|
|
1236
|
+
super(`Failed to encode tree: ${reason}`);
|
|
1237
|
+
}
|
|
1238
|
+
};
|
|
1239
|
+
/**
|
|
1240
|
+
* Error thrown during tree decoding.
|
|
1241
|
+
* Indicates payload corruption, version mismatch, or validation failures.
|
|
1242
|
+
*/
|
|
1243
|
+
var DecodeError = class extends BaseError {
|
|
1244
|
+
name = "Tree.DecodeError";
|
|
1245
|
+
constructor(reason) {
|
|
1246
|
+
super(`Failed to decode tree: ${reason}`);
|
|
1247
|
+
}
|
|
1021
1248
|
};
|
|
1022
1249
|
|
|
1023
1250
|
//#endregion
|
|
@@ -1089,16 +1316,47 @@ function fromSnakeCase(input) {
|
|
|
1089
1316
|
return from$9(fromSnakeCase$1(input));
|
|
1090
1317
|
}
|
|
1091
1318
|
/**
|
|
1319
|
+
* Serializes an offer for merkle tree encoding.
|
|
1320
|
+
* Converts BigInt fields to strings for JSON compatibility.
|
|
1321
|
+
*
|
|
1322
|
+
* @param offer - Offer to serialize
|
|
1323
|
+
* @returns JSON-serializable offer object
|
|
1324
|
+
*/
|
|
1325
|
+
const serialize = (offer) => ({
|
|
1326
|
+
offering: offer.offering,
|
|
1327
|
+
assets: offer.assets.toString(),
|
|
1328
|
+
rate: offer.rate.toString(),
|
|
1329
|
+
maturity: Number(offer.maturity),
|
|
1330
|
+
expiry: Number(offer.expiry),
|
|
1331
|
+
start: Number(offer.start),
|
|
1332
|
+
nonce: offer.nonce.toString(),
|
|
1333
|
+
buy: offer.buy,
|
|
1334
|
+
chainId: offer.chainId,
|
|
1335
|
+
loanToken: offer.loanToken,
|
|
1336
|
+
collaterals: offer.collaterals.map((c) => ({
|
|
1337
|
+
asset: c.asset,
|
|
1338
|
+
oracle: c.oracle,
|
|
1339
|
+
lltv: c.lltv.toString()
|
|
1340
|
+
})),
|
|
1341
|
+
callback: {
|
|
1342
|
+
address: offer.callback.address,
|
|
1343
|
+
data: offer.callback.data,
|
|
1344
|
+
gasLimit: offer.callback.gasLimit.toString()
|
|
1345
|
+
},
|
|
1346
|
+
signature: offer.signature,
|
|
1347
|
+
hash: offer.hash
|
|
1348
|
+
});
|
|
1349
|
+
/**
|
|
1092
1350
|
* Generates a random Offer.
|
|
1093
1351
|
* The returned Offer contains randomly generated values.
|
|
1094
1352
|
* @warning The generated Offer should not be used for production usage.
|
|
1095
1353
|
* @returns {Offer} A randomly generated Offer object.
|
|
1096
1354
|
*/
|
|
1097
1355
|
function random(config) {
|
|
1098
|
-
const chain = config?.chains ? config.chains[
|
|
1099
|
-
const loanToken = config?.loanTokens ? config.loanTokens[
|
|
1100
|
-
const collateralCandidates = config?.collateralTokens ? config.collateralTokens.filter((a) => a !== loanToken) : [
|
|
1101
|
-
const collateralAsset = collateralCandidates[
|
|
1356
|
+
const chain = config?.chains ? config.chains[int(config.chains.length)] : chains$2.ethereum;
|
|
1357
|
+
const loanToken = config?.loanTokens ? config.loanTokens[int(config.loanTokens.length)] : address();
|
|
1358
|
+
const collateralCandidates = config?.collateralTokens ? config.collateralTokens.filter((a) => a !== loanToken) : [address()];
|
|
1359
|
+
const collateralAsset = collateralCandidates[int(collateralCandidates.length)];
|
|
1102
1360
|
const maturityOption = weightedChoice([["end_of_month", 1], ["end_of_next_month", 1]]);
|
|
1103
1361
|
const maturity$1 = config?.maturity ?? from$12(maturityOption);
|
|
1104
1362
|
const lltv = from$14(weightedChoice([
|
|
@@ -1112,7 +1370,7 @@ function random(config) {
|
|
|
1112
1370
|
[.965, 4],
|
|
1113
1371
|
[.98, 2]
|
|
1114
1372
|
]));
|
|
1115
|
-
const buy = config?.buy !== void 0 ? config.buy :
|
|
1373
|
+
const buy = config?.buy !== void 0 ? config.buy : bool();
|
|
1116
1374
|
const ONE = 1000000000000000000n;
|
|
1117
1375
|
const qMin = buy ? 16 : 4;
|
|
1118
1376
|
const len = (buy ? 32 : 16) - qMin + 1;
|
|
@@ -1123,9 +1381,9 @@ function random(config) {
|
|
|
1123
1381
|
const rate = config?.rate ?? weightedChoice(ratePairs);
|
|
1124
1382
|
const loanTokenDecimals = config?.assetsDecimals?.[loanToken] ?? 18;
|
|
1125
1383
|
const unit = BigInt(10) ** BigInt(loanTokenDecimals);
|
|
1126
|
-
const amountBase = BigInt(100 +
|
|
1384
|
+
const amountBase = BigInt(100 + int(999901));
|
|
1127
1385
|
const assetsScaled = config?.assets ?? amountBase * unit;
|
|
1128
|
-
const consumed = config?.consumed !== void 0 ? config.consumed :
|
|
1386
|
+
const consumed = config?.consumed !== void 0 ? config.consumed : float() < .8 ? 0n : assetsScaled * BigInt(1 + int(900)) / 1000n;
|
|
1129
1387
|
const callbackBySide = (() => {
|
|
1130
1388
|
if (buy) return {
|
|
1131
1389
|
address: zeroAddress,
|
|
@@ -1144,29 +1402,29 @@ function random(config) {
|
|
|
1144
1402
|
};
|
|
1145
1403
|
})();
|
|
1146
1404
|
return from$9({
|
|
1147
|
-
offering: config?.offering ??
|
|
1405
|
+
offering: config?.offering ?? address(),
|
|
1148
1406
|
assets: assetsScaled,
|
|
1149
1407
|
rate,
|
|
1150
1408
|
maturity: maturity$1,
|
|
1151
1409
|
expiry: config?.expiry ?? maturity$1 - 1,
|
|
1152
1410
|
start: config?.start ?? maturity$1 - 10,
|
|
1153
|
-
nonce: BigInt(
|
|
1411
|
+
nonce: BigInt(int(1e6)),
|
|
1154
1412
|
buy,
|
|
1155
1413
|
chainId: chain.id,
|
|
1156
1414
|
loanToken,
|
|
1157
|
-
collaterals: config?.collaterals ?? Array.from({ length:
|
|
1415
|
+
collaterals: config?.collaterals ?? Array.from({ length: int(3) + 1 }, () => ({
|
|
1158
1416
|
...random$1(),
|
|
1159
1417
|
lltv
|
|
1160
1418
|
})).sort((a, b) => a.asset.localeCompare(b.asset)),
|
|
1161
1419
|
callback: config?.callback ?? callbackBySide,
|
|
1162
1420
|
consumed,
|
|
1163
1421
|
takeable: config?.takeable ?? assetsScaled - consumed,
|
|
1164
|
-
blockNumber: config?.blockNumber ??
|
|
1422
|
+
blockNumber: config?.blockNumber ?? int(Number.MAX_SAFE_INTEGER)
|
|
1165
1423
|
});
|
|
1166
1424
|
}
|
|
1167
1425
|
const weightedChoice = (pairs) => {
|
|
1168
1426
|
const total = pairs.reduce((sum, [, weight]) => sum + weight, 0);
|
|
1169
|
-
let roll =
|
|
1427
|
+
let roll = float() * total;
|
|
1170
1428
|
for (const [value, weight] of pairs) {
|
|
1171
1429
|
roll -= weight;
|
|
1172
1430
|
if (roll < 0) return value;
|
|
@@ -1460,89 +1718,6 @@ function from$6(parameters) {
|
|
|
1460
1718
|
//#region src/core/types.ts
|
|
1461
1719
|
const BrandTypeId = Symbol.for("mempool/Brand");
|
|
1462
1720
|
|
|
1463
|
-
//#endregion
|
|
1464
|
-
//#region src/evm/EVM.ts
|
|
1465
|
-
const users = [privateKeyToAccount("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80")];
|
|
1466
|
-
const mockOracleAddress = privateKeyToAccount(keccak256(toHex("ORACLE"))).address;
|
|
1467
|
-
const mockMorphoAddress = privateKeyToAccount(keccak256(toHex("MORPHO"))).address;
|
|
1468
|
-
const mockFactoryAddress = privateKeyToAccount(keccak256(toHex("FACTORY"))).address;
|
|
1469
|
-
/**
|
|
1470
|
-
* Start a local anvil chain.
|
|
1471
|
-
* @example
|
|
1472
|
-
* ```ts
|
|
1473
|
-
* import { EVM } from "@morpho-dev/router";
|
|
1474
|
-
* EVM.serve(); // local chain rpc url running on http://localhost:8545
|
|
1475
|
-
* ```
|
|
1476
|
-
*/
|
|
1477
|
-
async function serve$2(parameters) {
|
|
1478
|
-
const { port, forkUrl, blockNumber } = parameters;
|
|
1479
|
-
let started = false;
|
|
1480
|
-
const args = [
|
|
1481
|
-
"--chain-id",
|
|
1482
|
-
ChainId.ANVIL.toString(),
|
|
1483
|
-
"--fork-url",
|
|
1484
|
-
forkUrl,
|
|
1485
|
-
"--port",
|
|
1486
|
-
String(port)
|
|
1487
|
-
];
|
|
1488
|
-
if (blockNumber) args.push("--fork-block-number", String(blockNumber));
|
|
1489
|
-
const stop = await new Promise((resolve, reject) => {
|
|
1490
|
-
const subprocess = spawn("anvil", args, { env: {
|
|
1491
|
-
...process.env,
|
|
1492
|
-
FOUNDRY_DISABLE_NIGHTLY_WARNING: "1"
|
|
1493
|
-
} });
|
|
1494
|
-
subprocess.stdout.on("data", (data) => {
|
|
1495
|
-
if (`[port ${port}] ${data.toString()}`.includes("Listening on")) {
|
|
1496
|
-
started = true;
|
|
1497
|
-
resolve(() => subprocess.kill("SIGINT"));
|
|
1498
|
-
}
|
|
1499
|
-
});
|
|
1500
|
-
subprocess.stderr.on("data", (data) => {
|
|
1501
|
-
const message = `[port ${port}] ${data.toString()}`;
|
|
1502
|
-
console.warn(message);
|
|
1503
|
-
if (!started) reject(message);
|
|
1504
|
-
else console.warn(message);
|
|
1505
|
-
});
|
|
1506
|
-
});
|
|
1507
|
-
return {
|
|
1508
|
-
rpcUrl: `http://localhost:${port}`,
|
|
1509
|
-
stop
|
|
1510
|
-
};
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
//#endregion
|
|
1514
|
-
//#region src/cli/commands/mempool.ts
|
|
1515
|
-
const mempoolCmd = new Command("mempool");
|
|
1516
|
-
mempoolCmd.description("Start a local chain with the mempool contract").addOption(new Option("--port <port>").env("MEMPOOL_PORT").default(8545)).addOption(new Option("--fork-url <url>").env("MEMPOOL_FORK_URL").default("https://ethereum-rpc.publicnode.com")).addOption(new Option("--block-number <number>").default(0)).action(async (opts) => {
|
|
1517
|
-
const { rpcUrl } = await serve$2({
|
|
1518
|
-
port: opts.port,
|
|
1519
|
-
forkUrl: opts.forkUrl,
|
|
1520
|
-
blockNumber: opts.blockNumber
|
|
1521
|
-
});
|
|
1522
|
-
const client = createTestClient({
|
|
1523
|
-
chain: {
|
|
1524
|
-
...anvil,
|
|
1525
|
-
id: Number(ChainId.ANVIL)
|
|
1526
|
-
},
|
|
1527
|
-
transport: http(rpcUrl),
|
|
1528
|
-
mode: "anvil"
|
|
1529
|
-
}).extend(publicActions).extend(walletActions);
|
|
1530
|
-
const mempoolHash = await client.sendTransaction({
|
|
1531
|
-
account: users[0],
|
|
1532
|
-
data: "0x60808060405234601357607d908160188239f35b5f80fdfe7f758c7cf107fab3cfdc5aefab966cd9e69c3f368761f7a218a18283c1fbb5574c60808060405260208152366020820152365f60408301375f604036830101526040817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f3601168101030190a100fea164736f6c634300081e000a"
|
|
1533
|
-
});
|
|
1534
|
-
const { contractAddress: mempoolAddress } = await client.waitForTransactionReceipt({ hash: mempoolHash });
|
|
1535
|
-
if (!mempoolAddress) throw new Error("Failed to deploy mempool contract");
|
|
1536
|
-
console.log(`Mempool contract deployed at ${mempoolAddress}`);
|
|
1537
|
-
const termsHash = await client.sendTransaction({
|
|
1538
|
-
account: users[0],
|
|
1539
|
-
data: "0x6080604052348015600e575f5ffd5b506101dc8061001c5f395ff3fe608060405234801561000f575f5ffd5b5060043610610029575f3560e01c80637edab8a61461002d575b5f5ffd5b6100476004803603810190610042919061012e565b610049565b005b818373ffffffffffffffffffffffffffffffffffffffff167f1eed8a711368c40dda6427683883619012c34cf436e11e0d773b0d05169ccb8283604051610090919061018d565b60405180910390a3505050565b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6100ca826100a1565b9050919050565b6100da816100c0565b81146100e4575f5ffd5b50565b5f813590506100f5816100d1565b92915050565b5f819050919050565b61010d816100fb565b8114610117575f5ffd5b50565b5f8135905061012881610104565b92915050565b5f5f5f606084860312156101455761014461009d565b5b5f610152868287016100e7565b93505060206101638682870161011a565b92505060406101748682870161011a565b9150509250925092565b610187816100fb565b82525050565b5f6020820190506101a05f83018461017e565b9291505056fea2646970667358221220cc06296395b7aafdb892cd7bfe25a87dc6b35fe29b2d5cb2dcb3aad43050396a64736f6c634300081e0033"
|
|
1540
|
-
});
|
|
1541
|
-
const { contractAddress: termsAddress } = await client.waitForTransactionReceipt({ hash: termsHash });
|
|
1542
|
-
if (!termsAddress) throw new Error("Failed to deploy terms contract");
|
|
1543
|
-
console.log(`Terms contract deployed at ${termsAddress}`);
|
|
1544
|
-
});
|
|
1545
|
-
|
|
1546
1721
|
//#endregion
|
|
1547
1722
|
//#region src/logger/Logger.ts
|
|
1548
1723
|
const LogLevelValues = [
|
|
@@ -1670,13 +1845,16 @@ function from$4(obligation, quote) {
|
|
|
1670
1845
|
* Creates an `OfferResponse` from an `Offer`.
|
|
1671
1846
|
* @constructor
|
|
1672
1847
|
* @param offer - {@link Offer}
|
|
1848
|
+
* @param attestation - {@link Attestation}
|
|
1673
1849
|
* @returns The created `OfferResponse`. {@link OfferResponse}
|
|
1674
1850
|
*/
|
|
1675
|
-
function from$3(offer) {
|
|
1676
|
-
const
|
|
1851
|
+
function from$3(offer, attestation) {
|
|
1852
|
+
const { signature: _, ...rest } = toSnakeCase(offer);
|
|
1677
1853
|
return {
|
|
1678
|
-
...
|
|
1679
|
-
|
|
1854
|
+
...rest,
|
|
1855
|
+
root: attestation?.root.toLowerCase() ?? null,
|
|
1856
|
+
proof: attestation?.proof.map((p) => p.toLowerCase()) ?? null,
|
|
1857
|
+
signature: attestation?.signature.toLowerCase() ?? null
|
|
1680
1858
|
};
|
|
1681
1859
|
}
|
|
1682
1860
|
|
|
@@ -1814,10 +1992,12 @@ const offerExample = {
|
|
|
1814
1992
|
data: "0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000000000000034cf890db685fc536e05652fb41f02090c3fb751000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000108e644e3ab01184155270aa92a00000000000",
|
|
1815
1993
|
gas_limit: "500000"
|
|
1816
1994
|
},
|
|
1817
|
-
signature: "0x1234567890123456789012345678901234567890123456789012345678901234123456789012345678901234567890123456789012345678901234567890123400",
|
|
1818
1995
|
consumed: "0",
|
|
1819
1996
|
takeable: "369216000000000000000000",
|
|
1820
|
-
block_number: 0xa7495128adfb1
|
|
1997
|
+
block_number: 0xa7495128adfb1,
|
|
1998
|
+
root: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
|
1999
|
+
proof: ["0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", "0x9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba"],
|
|
2000
|
+
signature: "0x1234567890123456789012345678901234567890123456789012345678901234123456789012345678901234567890123456789012345678901234567890123400"
|
|
1821
2001
|
};
|
|
1822
2002
|
const collectorsHealthExample = {
|
|
1823
2003
|
name: "offers",
|
|
@@ -1962,6 +2142,16 @@ __decorate([ApiProperty({
|
|
|
1962
2142
|
type: "number",
|
|
1963
2143
|
example: offerExample.block_number
|
|
1964
2144
|
})], OfferListItemResponse.prototype, "block_number", void 0);
|
|
2145
|
+
__decorate([ApiProperty({
|
|
2146
|
+
type: "string",
|
|
2147
|
+
nullable: true,
|
|
2148
|
+
example: offerExample.root
|
|
2149
|
+
})], OfferListItemResponse.prototype, "root", void 0);
|
|
2150
|
+
__decorate([ApiProperty({
|
|
2151
|
+
type: [String],
|
|
2152
|
+
nullable: true,
|
|
2153
|
+
example: offerExample.proof
|
|
2154
|
+
})], OfferListItemResponse.prototype, "proof", void 0);
|
|
1965
2155
|
__decorate([ApiProperty({
|
|
1966
2156
|
type: "string",
|
|
1967
2157
|
nullable: true,
|
|
@@ -2145,44 +2335,61 @@ __decorate([ApiProperty({
|
|
|
2145
2335
|
var ValidateOffersRequest = class {};
|
|
2146
2336
|
__decorate([ApiProperty({
|
|
2147
2337
|
type: () => [ValidateOfferRequest],
|
|
2148
|
-
description: "Array of offers in snake_case format.
|
|
2149
|
-
required:
|
|
2338
|
+
description: "Array of offers in snake_case format. Required, non-empty.",
|
|
2339
|
+
required: true
|
|
2150
2340
|
})], ValidateOffersRequest.prototype, "offers", void 0);
|
|
2341
|
+
var ValidationSuccessDataResponse = class {};
|
|
2151
2342
|
__decorate([ApiProperty({
|
|
2152
2343
|
type: "string",
|
|
2153
|
-
description: "
|
|
2154
|
-
example: "
|
|
2155
|
-
|
|
2156
|
-
})], ValidateOffersRequest.prototype, "calldata", void 0);
|
|
2157
|
-
var ValidateOfferResultResponse = class {};
|
|
2344
|
+
description: "Unsigned payload: version (1B) + gzip(offers) + root (32B).",
|
|
2345
|
+
example: "0x01789c..."
|
|
2346
|
+
})], ValidationSuccessDataResponse.prototype, "payload", void 0);
|
|
2158
2347
|
__decorate([ApiProperty({
|
|
2159
2348
|
type: "string",
|
|
2160
|
-
|
|
2161
|
-
|
|
2349
|
+
description: "Merkle tree root to sign with EIP-191.",
|
|
2350
|
+
example: "0xac4bd8318ec914f89f8af913f162230575b0ac0696a19256bc12138c5cfe1427"
|
|
2351
|
+
})], ValidationSuccessDataResponse.prototype, "root", void 0);
|
|
2352
|
+
var ValidationSuccessResponse = class extends SuccessResponse {};
|
|
2162
2353
|
__decorate([ApiProperty({
|
|
2163
|
-
type: "
|
|
2164
|
-
|
|
2165
|
-
|
|
2354
|
+
type: "string",
|
|
2355
|
+
nullable: true,
|
|
2356
|
+
example: null
|
|
2357
|
+
})], ValidationSuccessResponse.prototype, "cursor", void 0);
|
|
2358
|
+
__decorate([ApiProperty({
|
|
2359
|
+
type: () => ValidationSuccessDataResponse,
|
|
2360
|
+
description: "Payload and root for client-side signing."
|
|
2361
|
+
})], ValidationSuccessResponse.prototype, "data", void 0);
|
|
2362
|
+
var ValidationIssueResponse = class {};
|
|
2363
|
+
__decorate([ApiProperty({
|
|
2364
|
+
type: "number",
|
|
2365
|
+
description: "0-indexed position of the failed offer in the request array.",
|
|
2366
|
+
example: 0
|
|
2367
|
+
})], ValidationIssueResponse.prototype, "index", void 0);
|
|
2166
2368
|
__decorate([ApiProperty({
|
|
2167
2369
|
type: "string",
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
})],
|
|
2370
|
+
description: "Gatekeeper rule name that rejected the offer.",
|
|
2371
|
+
example: "no_buy"
|
|
2372
|
+
})], ValidationIssueResponse.prototype, "rule", void 0);
|
|
2171
2373
|
__decorate([ApiProperty({
|
|
2172
2374
|
type: "string",
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
})],
|
|
2176
|
-
var
|
|
2375
|
+
description: "Human-readable rejection reason.",
|
|
2376
|
+
example: "Buy offers are not supported"
|
|
2377
|
+
})], ValidationIssueResponse.prototype, "message", void 0);
|
|
2378
|
+
var ValidationFailureDataResponse = class {};
|
|
2379
|
+
__decorate([ApiProperty({
|
|
2380
|
+
type: () => [ValidationIssueResponse],
|
|
2381
|
+
description: "List of validation issues. Returned when any offer fails validation."
|
|
2382
|
+
})], ValidationFailureDataResponse.prototype, "issues", void 0);
|
|
2383
|
+
var ValidationFailureResponse = class extends SuccessResponse {};
|
|
2177
2384
|
__decorate([ApiProperty({
|
|
2178
2385
|
type: "string",
|
|
2179
2386
|
nullable: true,
|
|
2180
2387
|
example: null
|
|
2181
|
-
})],
|
|
2388
|
+
})], ValidationFailureResponse.prototype, "cursor", void 0);
|
|
2182
2389
|
__decorate([ApiProperty({
|
|
2183
|
-
type: () =>
|
|
2184
|
-
description: "
|
|
2185
|
-
})],
|
|
2390
|
+
type: () => ValidationFailureDataResponse,
|
|
2391
|
+
description: "List of validation issues. Returned when any offer fails validation."
|
|
2392
|
+
})], ValidationFailureResponse.prototype, "data", void 0);
|
|
2186
2393
|
var BookLevelResponse = class {};
|
|
2187
2394
|
__decorate([ApiProperty({
|
|
2188
2395
|
type: "string",
|
|
@@ -2247,13 +2454,18 @@ __decorate([
|
|
|
2247
2454
|
methods: ["post"],
|
|
2248
2455
|
path: "/v1/validate",
|
|
2249
2456
|
summary: "Validate offers",
|
|
2250
|
-
description: "Validates offers against router validation rules. Returns
|
|
2457
|
+
description: "Validates offers against router validation rules. Returns unsigned payload + root on success, or issues only on validation failure."
|
|
2251
2458
|
}),
|
|
2252
2459
|
ApiBody({ type: ValidateOffersRequest }),
|
|
2253
2460
|
ApiResponse({
|
|
2254
2461
|
status: 200,
|
|
2255
2462
|
description: "Success",
|
|
2256
|
-
type:
|
|
2463
|
+
type: ValidationSuccessResponse
|
|
2464
|
+
}),
|
|
2465
|
+
ApiResponse({
|
|
2466
|
+
status: 200,
|
|
2467
|
+
description: "Validation issues",
|
|
2468
|
+
type: ValidationFailureResponse
|
|
2257
2469
|
})
|
|
2258
2470
|
], ValidateController.prototype, "validateOffers", null);
|
|
2259
2471
|
ValidateController = __decorate([ApiTags("Validate"), ApiResponse({
|
|
@@ -2422,27 +2634,33 @@ const OpenApi = async (options = {}) => {
|
|
|
2422
2634
|
if (options.rules && options.rules.length > 0) {
|
|
2423
2635
|
const rulesDescription = options.rules.map((rule) => `- **${rule.name}**: ${rule.description}`).join("\n");
|
|
2424
2636
|
const validatePath = document.paths?.["/v1/validate"];
|
|
2425
|
-
if (validatePath && "post" in validatePath && validatePath.post) validatePath.post.description = `Validates offers against router validation rules. Returns
|
|
2637
|
+
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}`;
|
|
2426
2638
|
}
|
|
2427
2639
|
return document;
|
|
2428
2640
|
};
|
|
2429
2641
|
|
|
2430
2642
|
//#endregion
|
|
2431
2643
|
//#region src/database/utils/Cursor.ts
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
const c = cursor;
|
|
2435
|
-
if (![
|
|
2644
|
+
const isSort = (value) => {
|
|
2645
|
+
return [
|
|
2436
2646
|
"rate",
|
|
2437
2647
|
"maturity",
|
|
2438
2648
|
"expiry",
|
|
2439
2649
|
"amount"
|
|
2440
|
-
].includes(
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2650
|
+
].includes(value);
|
|
2651
|
+
};
|
|
2652
|
+
function validate(cursor) {
|
|
2653
|
+
if (!cursor || typeof cursor !== "object") throw new Error("Cursor must be an object");
|
|
2654
|
+
const c = cursor;
|
|
2655
|
+
const sort = c.sort;
|
|
2656
|
+
const dir = c.dir;
|
|
2657
|
+
const hash$1 = c.hash;
|
|
2658
|
+
if (typeof sort !== "string" || !isSort(sort)) throw new Error(`Invalid sort field: ${String(sort)}. Must be one of: rate, maturity, expiry, amount`);
|
|
2659
|
+
if (typeof dir !== "string" || !["asc", "desc"].includes(dir)) throw new Error(`Invalid direction: ${String(dir)}. Must be one of: asc, desc`);
|
|
2660
|
+
if (typeof hash$1 !== "string" || !/^0x[a-fA-F0-9]{64}$/.test(hash$1)) throw new Error(`Invalid hash format: ${String(hash$1)}. Must be a 64-character hex string starting with 0x`);
|
|
2661
|
+
const validation = {
|
|
2662
|
+
rate: {
|
|
2663
|
+
field: "rate",
|
|
2446
2664
|
type: "string",
|
|
2447
2665
|
pattern: /^\d+$/,
|
|
2448
2666
|
error: "numeric string"
|
|
@@ -2456,24 +2674,30 @@ function validate(cursor) {
|
|
|
2456
2674
|
maturity: {
|
|
2457
2675
|
field: "maturity",
|
|
2458
2676
|
type: "number",
|
|
2459
|
-
|
|
2677
|
+
min: 1,
|
|
2460
2678
|
error: "positive number"
|
|
2461
2679
|
},
|
|
2462
2680
|
expiry: {
|
|
2463
2681
|
field: "expiry",
|
|
2464
2682
|
type: "number",
|
|
2465
|
-
|
|
2683
|
+
min: 1,
|
|
2466
2684
|
error: "positive number"
|
|
2467
2685
|
}
|
|
2468
|
-
}[
|
|
2469
|
-
if (!validation) throw new Error(`Invalid sort field: ${
|
|
2686
|
+
}[sort];
|
|
2687
|
+
if (!validation) throw new Error(`Invalid sort field: ${sort}`);
|
|
2470
2688
|
const fieldValue = c[validation.field];
|
|
2471
|
-
if (
|
|
2472
|
-
if (
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2689
|
+
if (fieldValue === void 0 || fieldValue === null) throw new Error(`${sort} sort requires '${validation.field}' field to be present`);
|
|
2690
|
+
if (validation.type === "string") {
|
|
2691
|
+
if (typeof fieldValue !== "string") throw new Error(`${sort} sort requires '${validation.field}' field of type ${validation.type}`);
|
|
2692
|
+
if (!validation.pattern.test(fieldValue)) throw new Error(`Invalid ${validation.field} format: ${fieldValue}. Must be a ${validation.error}`);
|
|
2693
|
+
}
|
|
2694
|
+
if (validation.type === "number") {
|
|
2695
|
+
if (typeof fieldValue !== "number") throw new Error(`${sort} sort requires '${validation.field}' field of type ${validation.type}`);
|
|
2696
|
+
if (fieldValue < validation.min) throw new Error(`Invalid ${validation.field} value: ${fieldValue}. Must be a ${validation.error}`);
|
|
2697
|
+
}
|
|
2698
|
+
const page = c.page;
|
|
2699
|
+
if (page !== void 0) {
|
|
2700
|
+
if (typeof page !== "number" || !Number.isInteger(page) || page < 1) throw new Error("Invalid page: must be a positive integer");
|
|
2477
2701
|
}
|
|
2478
2702
|
return true;
|
|
2479
2703
|
}
|
|
@@ -2586,21 +2810,7 @@ const schemas = {
|
|
|
2586
2810
|
get_obligations: GetObligationsQueryParams,
|
|
2587
2811
|
get_obligation: GetObligationParams,
|
|
2588
2812
|
get_book: GetBookParams,
|
|
2589
|
-
validate_offers: z$1.object({
|
|
2590
|
-
offers: z$1.any().refine((val) => val === void 0 || Array.isArray(val), { message: "'offers' must be an array" }),
|
|
2591
|
-
calldata: z$1.string().regex(/^0x[a-fA-F0-9]*$/, { message: "'calldata' must be a hex string starting with '0x'" }).optional()
|
|
2592
|
-
}).superRefine((val, ctx) => {
|
|
2593
|
-
const hasOffers = val.offers !== void 0;
|
|
2594
|
-
const hasCalldata = val.calldata !== void 0;
|
|
2595
|
-
if (hasOffers && hasCalldata) ctx.addIssue({
|
|
2596
|
-
code: "custom",
|
|
2597
|
-
message: "Request body must contain either 'offers' or 'calldata', not both"
|
|
2598
|
-
});
|
|
2599
|
-
if (!hasOffers && !hasCalldata) ctx.addIssue({
|
|
2600
|
-
code: "custom",
|
|
2601
|
-
message: "Request body must contain either 'offers' array or 'calldata' hex string"
|
|
2602
|
-
});
|
|
2603
|
-
})
|
|
2813
|
+
validate_offers: z$1.object({ offers: z$1.array(z$1.unknown()).min(1, { message: "'offers' must contain at least 1 offer" }) }).strict()
|
|
2604
2814
|
};
|
|
2605
2815
|
function safeParse(action, query, error) {
|
|
2606
2816
|
return schemas[action].safeParse(query, { error });
|
|
@@ -2725,7 +2935,7 @@ function now() {
|
|
|
2725
2935
|
|
|
2726
2936
|
//#endregion
|
|
2727
2937
|
//#region src/indexer/collectors/Admin.ts
|
|
2728
|
-
function create$
|
|
2938
|
+
function create$17(parameters) {
|
|
2729
2939
|
const collector = "admin";
|
|
2730
2940
|
const { client, db, options: { maxBatchSize = 25, maxBlockNumber } = {} } = parameters;
|
|
2731
2941
|
const maxBlockNumberBI = maxBlockNumber !== void 0 ? BigInt(maxBlockNumber) : void 0;
|
|
@@ -2974,8 +3184,8 @@ const names = [
|
|
|
2974
3184
|
"positions",
|
|
2975
3185
|
"prices"
|
|
2976
3186
|
];
|
|
2977
|
-
function create$
|
|
2978
|
-
const admin = create$
|
|
3187
|
+
function create$16({ name: name$1, collect, client, db, options }) {
|
|
3188
|
+
const admin = create$17({
|
|
2979
3189
|
client,
|
|
2980
3190
|
db,
|
|
2981
3191
|
options
|
|
@@ -3025,7 +3235,10 @@ function create$13({ name: name$1, collect, client, db, options }) {
|
|
|
3025
3235
|
};
|
|
3026
3236
|
});
|
|
3027
3237
|
if (done) iterator = null;
|
|
3028
|
-
else
|
|
3238
|
+
else {
|
|
3239
|
+
lastBlockNumber = blockNumber;
|
|
3240
|
+
yield blockNumber;
|
|
3241
|
+
}
|
|
3029
3242
|
} catch (err) {
|
|
3030
3243
|
const isError = err instanceof Error;
|
|
3031
3244
|
logger.error({
|
|
@@ -3183,30 +3396,79 @@ async function* collectOffersV2(parameters) {
|
|
|
3183
3396
|
});
|
|
3184
3397
|
for await (const { logs, blockNumber: lastStreamBlockNumber } of stream) {
|
|
3185
3398
|
blockNumber = lastStreamBlockNumber;
|
|
3186
|
-
const
|
|
3399
|
+
const decodedTrees = [];
|
|
3187
3400
|
for (const log of logs) {
|
|
3188
3401
|
if (!log) continue;
|
|
3189
3402
|
const [payload] = decodeAbiParameters([{ type: "bytes" }], log.data);
|
|
3190
3403
|
try {
|
|
3191
|
-
const tree = decode$1(payload);
|
|
3192
|
-
|
|
3404
|
+
const { tree, signature, signer } = await decode$1(payload);
|
|
3405
|
+
const signerMismatch = tree.offers.find((offer) => offer.offering.toLowerCase() !== signer.toLowerCase());
|
|
3406
|
+
if (signerMismatch) {
|
|
3407
|
+
logger.debug({
|
|
3408
|
+
msg: "Tree rejected: signer mismatch",
|
|
3409
|
+
reason: "signer_mismatch",
|
|
3410
|
+
signer,
|
|
3411
|
+
offering: signerMismatch.offering,
|
|
3412
|
+
chain_id: client.chain.id
|
|
3413
|
+
});
|
|
3414
|
+
continue;
|
|
3415
|
+
}
|
|
3416
|
+
const offersWithBlock = tree.offers.map((offer) => ({
|
|
3193
3417
|
...offer,
|
|
3194
3418
|
blockNumber: Number(log.blockNumber)
|
|
3419
|
+
}));
|
|
3420
|
+
const treeWithBlock = Object.assign(Object.create(Object.getPrototypeOf(tree)), tree, { offers: offersWithBlock });
|
|
3421
|
+
decodedTrees.push({
|
|
3422
|
+
tree: treeWithBlock,
|
|
3423
|
+
signature,
|
|
3424
|
+
signer,
|
|
3425
|
+
blockNumber: Number(log.blockNumber)
|
|
3426
|
+
});
|
|
3427
|
+
} catch (err) {
|
|
3428
|
+
const reason = err instanceof DecodeError && err.message.includes("signature") ? "invalid_signature" : "decode_failed";
|
|
3429
|
+
logger.debug({
|
|
3430
|
+
msg: "Tree decode failed",
|
|
3431
|
+
reason,
|
|
3432
|
+
chain_id: client.chain.id,
|
|
3433
|
+
err: err instanceof Error ? err.message : String(err)
|
|
3195
3434
|
});
|
|
3196
|
-
}
|
|
3435
|
+
}
|
|
3197
3436
|
}
|
|
3198
3437
|
await db.transaction(async (dbTx) => {
|
|
3199
3438
|
const { epoch, blockNumber: latestBlockNumber } = await dbTx.chains.getBlockNumber(client.chain.id);
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3439
|
+
const treesToInsert = [];
|
|
3440
|
+
let totalValidOffers = 0;
|
|
3441
|
+
for (const { tree, signature } of decodedTrees) try {
|
|
3442
|
+
const allowedResults = await gatekeeper.isAllowed(tree.offers);
|
|
3443
|
+
const hasBlockWindowViolation = tree.offers.some((offer) => offer.blockNumber > latestBlockNumber);
|
|
3444
|
+
if (!(allowedResults.issues.length === 0 && allowedResults.valid.length === tree.offers.length) || hasBlockWindowViolation) {
|
|
3445
|
+
if (allowedResults.issues.length > 0) {
|
|
3446
|
+
const hasMixedMaker = allowedResults.issues.some((i) => i.ruleName === "mixed_maker");
|
|
3447
|
+
logger.debug({
|
|
3448
|
+
msg: "Tree offers rejected by gatekeeper",
|
|
3449
|
+
reason: hasMixedMaker ? "mixed_maker" : "gatekeeper_rejected",
|
|
3450
|
+
chain_id: client.chain.id,
|
|
3451
|
+
issues_count: allowedResults.issues.length
|
|
3452
|
+
});
|
|
3453
|
+
} else if (hasBlockWindowViolation) logger.debug({
|
|
3454
|
+
msg: "Tree rejected: offers outside block window",
|
|
3455
|
+
reason: "block_window",
|
|
3456
|
+
chain_id: client.chain.id
|
|
3457
|
+
});
|
|
3458
|
+
continue;
|
|
3459
|
+
}
|
|
3460
|
+
treesToInsert.push({
|
|
3461
|
+
tree,
|
|
3462
|
+
signature
|
|
3463
|
+
});
|
|
3464
|
+
totalValidOffers += tree.offers.length;
|
|
3203
3465
|
} catch (err) {
|
|
3204
3466
|
logger.error({
|
|
3205
3467
|
err,
|
|
3206
|
-
msg: "Failed to validate offers"
|
|
3468
|
+
msg: "Failed to validate offers for tree"
|
|
3207
3469
|
});
|
|
3208
3470
|
}
|
|
3209
|
-
await dbTx.
|
|
3471
|
+
if (treesToInsert.length > 0) await dbTx.trees.create(treesToInsert);
|
|
3210
3472
|
try {
|
|
3211
3473
|
await dbTx.collectors.saveBlockNumber({
|
|
3212
3474
|
collectorName: collector,
|
|
@@ -3214,10 +3476,11 @@ async function* collectOffersV2(parameters) {
|
|
|
3214
3476
|
blockNumber,
|
|
3215
3477
|
epoch
|
|
3216
3478
|
});
|
|
3217
|
-
if (
|
|
3479
|
+
if (totalValidOffers > 0) logger.info({
|
|
3218
3480
|
msg: `New offers`,
|
|
3219
3481
|
collector,
|
|
3220
|
-
count:
|
|
3482
|
+
count: totalValidOffers,
|
|
3483
|
+
trees_count: treesToInsert.length,
|
|
3221
3484
|
chain_id: client.chain.id,
|
|
3222
3485
|
block_range: [startBlock, lastStreamBlockNumber]
|
|
3223
3486
|
});
|
|
@@ -3860,7 +4123,7 @@ async function* collectPrices(parameters) {
|
|
|
3860
4123
|
//#region src/indexer/collectors/CollectorBuilder.ts
|
|
3861
4124
|
function createBuilder(parameters) {
|
|
3862
4125
|
const { client, db, gatekeeper, options: { maxBlockNumber, blockWindow } = {} } = parameters;
|
|
3863
|
-
const createCollector = (name$1, collect) => create$
|
|
4126
|
+
const createCollector = (name$1, collect) => create$16({
|
|
3864
4127
|
name: name$1,
|
|
3865
4128
|
collect,
|
|
3866
4129
|
client,
|
|
@@ -3959,7 +4222,7 @@ function from$1(config) {
|
|
|
3959
4222
|
retryAttempts,
|
|
3960
4223
|
retryDelayMs
|
|
3961
4224
|
});
|
|
3962
|
-
return create$
|
|
4225
|
+
return create$15({
|
|
3963
4226
|
db,
|
|
3964
4227
|
client,
|
|
3965
4228
|
collectors: [
|
|
@@ -3971,7 +4234,7 @@ function from$1(config) {
|
|
|
3971
4234
|
interval
|
|
3972
4235
|
});
|
|
3973
4236
|
}
|
|
3974
|
-
function create$
|
|
4237
|
+
function create$15(params) {
|
|
3975
4238
|
const { db, collectors: collectors$1, interval, client } = params;
|
|
3976
4239
|
const logger = getLogger();
|
|
3977
4240
|
const indexerId = `${client.chain.id.toString()}.indexer`;
|
|
@@ -4032,7 +4295,7 @@ const DEFAULT_MAX_ALLOWED_LAG = 5;
|
|
|
4032
4295
|
/**
|
|
4033
4296
|
* Create a health service that exposes collector and chain block numbers.
|
|
4034
4297
|
*/
|
|
4035
|
-
function create$
|
|
4298
|
+
function create$14(parameters) {
|
|
4036
4299
|
const { db, maxAllowedLag = DEFAULT_MAX_ALLOWED_LAG, healthClients } = parameters;
|
|
4037
4300
|
const loadSnapshot = async () => {
|
|
4038
4301
|
const [collectorRows, chainRows, remoteBlockByChainId] = await Promise.all([
|
|
@@ -4124,7 +4387,7 @@ async function getRemoteBlockNumbers(healthClients) {
|
|
|
4124
4387
|
async function getHealth(db) {
|
|
4125
4388
|
const logger = getLogger();
|
|
4126
4389
|
try {
|
|
4127
|
-
const status$1 = await create$
|
|
4390
|
+
const status$1 = await create$14({ db }).getStatus();
|
|
4128
4391
|
return success({ data: toSnakeCase({ status: status$1 }) });
|
|
4129
4392
|
} catch (err) {
|
|
4130
4393
|
logger.error({
|
|
@@ -4139,7 +4402,7 @@ async function getHealth(db) {
|
|
|
4139
4402
|
async function getHealthChains(db, healthClients) {
|
|
4140
4403
|
const logger = getLogger();
|
|
4141
4404
|
try {
|
|
4142
|
-
const chains$3 = await create$
|
|
4405
|
+
const chains$3 = await create$14({
|
|
4143
4406
|
db,
|
|
4144
4407
|
healthClients
|
|
4145
4408
|
}).getChains();
|
|
@@ -4162,7 +4425,7 @@ async function getHealthChains(db, healthClients) {
|
|
|
4162
4425
|
async function getHealthCollectors(db) {
|
|
4163
4426
|
const logger = getLogger();
|
|
4164
4427
|
try {
|
|
4165
|
-
const collectors$1 = await create$
|
|
4428
|
+
const collectors$1 = await create$14({ db }).getCollectors();
|
|
4166
4429
|
return success({ data: collectors$1.map(({ name: name$1, chainId, blockNumber, updatedAt, lag, status: status$1 }) => toSnakeCase({
|
|
4167
4430
|
name: name$1,
|
|
4168
4431
|
chainId,
|
|
@@ -4263,8 +4526,10 @@ async function getOffers(queryParameters, db) {
|
|
|
4263
4526
|
cursor: query.cursor,
|
|
4264
4527
|
limit: query.limit
|
|
4265
4528
|
});
|
|
4529
|
+
const hashes = offers$1.map((offer) => offer.hash);
|
|
4530
|
+
const attestationMap = await db.trees.getAttestations(hashes);
|
|
4266
4531
|
return success({
|
|
4267
|
-
data: offers$1.map(from$3),
|
|
4532
|
+
data: offers$1.map((offer) => from$3(offer, attestationMap.get(offer.hash.toLowerCase()))),
|
|
4268
4533
|
cursor: nextCursor ?? null
|
|
4269
4534
|
});
|
|
4270
4535
|
} catch (err) {
|
|
@@ -4284,56 +4549,49 @@ async function validateOffers(body, gatekeeper) {
|
|
|
4284
4549
|
const logger = getLogger();
|
|
4285
4550
|
const result = safeParse("validate_offers", body, (issue) => issue.message);
|
|
4286
4551
|
if (!result.success) return failure(new BadRequestError(result.error.issues[0]?.message ?? "Invalid request body"));
|
|
4287
|
-
const { offers: rawOffers
|
|
4288
|
-
const results = [];
|
|
4552
|
+
const { offers: rawOffers } = result.data;
|
|
4289
4553
|
const parsedOffers = [];
|
|
4290
|
-
const
|
|
4291
|
-
|
|
4292
|
-
if (calldata !== void 0) try {
|
|
4293
|
-
const tree = decode$1(calldata);
|
|
4294
|
-
for (const [i, offer] of tree.offers.entries()) {
|
|
4295
|
-
parsedOffers.push(offer);
|
|
4296
|
-
parsedOfferIndices.push(i);
|
|
4297
|
-
}
|
|
4298
|
-
} catch (err) {
|
|
4299
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
4300
|
-
return failure(new BadRequestError(`Failed to decode calldata: ${message}`));
|
|
4301
|
-
}
|
|
4302
|
-
if (hasOffers) for (let i = 0; i < rawOffers.length; i++) {
|
|
4554
|
+
const offerIndexByHash = /* @__PURE__ */ new Map();
|
|
4555
|
+
for (let i = 0; i < rawOffers.length; i++) {
|
|
4303
4556
|
const rawOffer = rawOffers[i];
|
|
4304
4557
|
try {
|
|
4305
4558
|
const offer = fromSnakeCase(rawOffer);
|
|
4306
|
-
|
|
4307
|
-
|
|
4559
|
+
if (!offerIndexByHash.has(offer.hash)) {
|
|
4560
|
+
offerIndexByHash.set(offer.hash, i);
|
|
4561
|
+
parsedOffers.push(offer);
|
|
4562
|
+
}
|
|
4308
4563
|
} catch (err) {
|
|
4309
4564
|
let message = err instanceof Error ? err.message : String(err);
|
|
4310
4565
|
if (err instanceof InvalidOfferError) message = err.formattedMessage;
|
|
4311
|
-
|
|
4312
|
-
offer_hash: rawOffer?.hash ?? "unknown",
|
|
4313
|
-
valid: false,
|
|
4314
|
-
rule: "parse_error",
|
|
4315
|
-
message
|
|
4316
|
-
};
|
|
4566
|
+
return failure(new BadRequestError(`Offer at index ${i} failed to parse: ${message}`));
|
|
4317
4567
|
}
|
|
4318
4568
|
}
|
|
4319
|
-
|
|
4320
|
-
const {
|
|
4321
|
-
|
|
4322
|
-
const
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
};
|
|
4569
|
+
try {
|
|
4570
|
+
const { issues } = await gatekeeper.isAllowed(parsedOffers);
|
|
4571
|
+
if (issues.length > 0) {
|
|
4572
|
+
const mappedIssues = issues.map((issue) => {
|
|
4573
|
+
const index$1 = offerIndexByHash.get(issue.item.hash);
|
|
4574
|
+
if (index$1 === void 0) return null;
|
|
4575
|
+
return {
|
|
4576
|
+
index: index$1,
|
|
4577
|
+
rule: issue.ruleName,
|
|
4578
|
+
message: issue.message
|
|
4579
|
+
};
|
|
4580
|
+
}).filter((issue) => issue !== null);
|
|
4581
|
+
return success({
|
|
4582
|
+
data: { issues: mappedIssues },
|
|
4583
|
+
cursor: null
|
|
4584
|
+
});
|
|
4336
4585
|
}
|
|
4586
|
+
const tree = from$10(parsedOffers);
|
|
4587
|
+
const payload = encodeUnsigned(tree);
|
|
4588
|
+
return success({
|
|
4589
|
+
data: {
|
|
4590
|
+
payload,
|
|
4591
|
+
root: tree.root
|
|
4592
|
+
},
|
|
4593
|
+
cursor: null
|
|
4594
|
+
});
|
|
4337
4595
|
} catch (err) {
|
|
4338
4596
|
logger.error({
|
|
4339
4597
|
err,
|
|
@@ -4343,24 +4601,19 @@ async function validateOffers(body, gatekeeper) {
|
|
|
4343
4601
|
});
|
|
4344
4602
|
return failure(err);
|
|
4345
4603
|
}
|
|
4346
|
-
const orderedResults = results.filter((r) => r !== void 0);
|
|
4347
|
-
return success({
|
|
4348
|
-
data: orderedResults,
|
|
4349
|
-
cursor: null
|
|
4350
|
-
});
|
|
4351
4604
|
}
|
|
4352
4605
|
|
|
4353
4606
|
//#endregion
|
|
4354
4607
|
//#region src/api/Api.ts
|
|
4355
4608
|
function from(config) {
|
|
4356
4609
|
const { db, gatekeeper, port } = config;
|
|
4357
|
-
return create$
|
|
4610
|
+
return create$13({
|
|
4358
4611
|
port,
|
|
4359
4612
|
db,
|
|
4360
4613
|
gatekeeper
|
|
4361
4614
|
});
|
|
4362
4615
|
}
|
|
4363
|
-
function create$
|
|
4616
|
+
function create$13(params) {
|
|
4364
4617
|
return { serve: () => serve$1(params) };
|
|
4365
4618
|
}
|
|
4366
4619
|
/**
|
|
@@ -4454,6 +4707,21 @@ function single(name$1, description$1, run$1) {
|
|
|
4454
4707
|
run: run$1
|
|
4455
4708
|
};
|
|
4456
4709
|
}
|
|
4710
|
+
/**
|
|
4711
|
+
* Create a validation rule iterating over a batch of items at a time.
|
|
4712
|
+
* @param name - The name of the rule.
|
|
4713
|
+
* @param description - A human-readable description of the rule.
|
|
4714
|
+
* @param run - The function that validates the rule.
|
|
4715
|
+
* @returns The created rule.
|
|
4716
|
+
*/
|
|
4717
|
+
function batch(name$1, description$1, run$1) {
|
|
4718
|
+
return {
|
|
4719
|
+
kind: "batch",
|
|
4720
|
+
name: name$1,
|
|
4721
|
+
description: description$1,
|
|
4722
|
+
run: run$1
|
|
4723
|
+
};
|
|
4724
|
+
}
|
|
4457
4725
|
async function run(parameters) {
|
|
4458
4726
|
const { items, rules, chunkSize } = parameters;
|
|
4459
4727
|
const issues = [];
|
|
@@ -4504,7 +4772,7 @@ async function run(parameters) {
|
|
|
4504
4772
|
|
|
4505
4773
|
//#endregion
|
|
4506
4774
|
//#region src/gatekeeper/Gatekeeper.ts
|
|
4507
|
-
function create$
|
|
4775
|
+
function create$12(parameters) {
|
|
4508
4776
|
return {
|
|
4509
4777
|
rules: parameters.rules,
|
|
4510
4778
|
isAllowed: async (offers$1) => {
|
|
@@ -4526,8 +4794,8 @@ function create$9(parameters) {
|
|
|
4526
4794
|
* @param address - Callback contract address
|
|
4527
4795
|
* @returns The callback type when found, otherwise undefined
|
|
4528
4796
|
*/
|
|
4529
|
-
function getCallbackType(chain, address) {
|
|
4530
|
-
return configs[chain].callbacks?.find((c) => c.type !== CallbackType.BuyWithEmptyCallback && c.addresses.includes(address?.toLowerCase()))?.type;
|
|
4797
|
+
function getCallbackType(chain, address$1) {
|
|
4798
|
+
return configs[chain].callbacks?.find((c) => c.type !== CallbackType.BuyWithEmptyCallback && c.addresses.includes(address$1?.toLowerCase()))?.type;
|
|
4531
4799
|
}
|
|
4532
4800
|
/**
|
|
4533
4801
|
* Returns the list of allowed non-empty callback addresses for a chain.
|
|
@@ -4656,10 +4924,29 @@ const token = ({ assets: assets$1 }) => single("token", "Validates that offer lo
|
|
|
4656
4924
|
if (!allowedAssets.includes(offer.loanToken.toLowerCase())) return { message: "Loan token is not allowed" };
|
|
4657
4925
|
if (offer.collaterals.some((collateral) => !allowedAssets.includes(collateral.asset.toLowerCase()))) return { message: "Collateral is not allowed" };
|
|
4658
4926
|
});
|
|
4927
|
+
/**
|
|
4928
|
+
* A batch validation rule that ensures all offers in a tree have the same maker (offering address).
|
|
4929
|
+
* Returns an issue only for the first non-conforming offer.
|
|
4930
|
+
* This rule is signing-agnostic; signer verification is handled at the collector level.
|
|
4931
|
+
*/
|
|
4932
|
+
const sameMaker = () => batch("mixed_maker", "Validates that all offers in a batch have the same maker (offering address)", (offers$1) => {
|
|
4933
|
+
const issues = /* @__PURE__ */ new Map();
|
|
4934
|
+
if (offers$1.length === 0) return issues;
|
|
4935
|
+
const firstMaker = offers$1[0].offering.toLowerCase();
|
|
4936
|
+
for (let i = 1; i < offers$1.length; i++) {
|
|
4937
|
+
const offer = offers$1[i];
|
|
4938
|
+
if (offer.offering.toLowerCase() !== firstMaker) {
|
|
4939
|
+
issues.set(i, { message: `Offer has different maker ${offer.offering} than first offer ${offers$1[0].offering}` });
|
|
4940
|
+
return issues;
|
|
4941
|
+
}
|
|
4942
|
+
}
|
|
4943
|
+
return issues;
|
|
4944
|
+
});
|
|
4659
4945
|
|
|
4660
4946
|
//#endregion
|
|
4661
4947
|
//#region src/gatekeeper/morphoRules.ts
|
|
4662
4948
|
const morphoRules = (chains$3) => [
|
|
4949
|
+
sameMaker(),
|
|
4663
4950
|
chains$1({ chains: chains$3 }),
|
|
4664
4951
|
maturity({ maturities: [MaturityType.EndOfMonth, MaturityType.EndOfNextMonth] }),
|
|
4665
4952
|
callback({
|
|
@@ -4673,6 +4960,12 @@ const morphoRules = (chains$3) => [
|
|
|
4673
4960
|
token({ assets: chains$3.flatMap((c) => assets[c.id.toString()] ?? []) })
|
|
4674
4961
|
];
|
|
4675
4962
|
|
|
4963
|
+
//#endregion
|
|
4964
|
+
//#region ../../node_modules/.pnpm/tsdown@0.16.5_ms@2.1.3_typescript@5.8.3/node_modules/tsdown/esm-shims.js
|
|
4965
|
+
const getFilename = () => fileURLToPath(import.meta.url);
|
|
4966
|
+
const getDirname = () => path.dirname(getFilename());
|
|
4967
|
+
const __dirname = /* @__PURE__ */ getDirname();
|
|
4968
|
+
|
|
4676
4969
|
//#endregion
|
|
4677
4970
|
//#region src/database/drizzle/VERSION.ts
|
|
4678
4971
|
const VERSION = "router_v1.6";
|
|
@@ -4689,15 +4982,19 @@ var schema_exports = /* @__PURE__ */ __export({
|
|
|
4689
4982
|
collectors: () => collectors,
|
|
4690
4983
|
consumedEvents: () => consumedEvents,
|
|
4691
4984
|
groups: () => groups,
|
|
4985
|
+
lots: () => lots,
|
|
4986
|
+
merklePaths: () => merklePaths,
|
|
4692
4987
|
obligationCollateralsV2: () => obligationCollateralsV2,
|
|
4693
4988
|
obligations: () => obligations,
|
|
4694
4989
|
offers: () => offers,
|
|
4695
4990
|
offersCallbacks: () => offersCallbacks,
|
|
4991
|
+
offsets: () => offsets,
|
|
4696
4992
|
oracles: () => oracles,
|
|
4697
4993
|
positionTypes: () => positionTypes,
|
|
4698
4994
|
positions: () => positions,
|
|
4699
4995
|
status: () => status,
|
|
4700
4996
|
transfers: () => transfers,
|
|
4997
|
+
trees: () => trees,
|
|
4701
4998
|
validations: () => validations
|
|
4702
4999
|
});
|
|
4703
5000
|
const s = pgSchema(VERSION);
|
|
@@ -4715,6 +5012,10 @@ var EnumTableName = /* @__PURE__ */ function(EnumTableName$1) {
|
|
|
4715
5012
|
EnumTableName$1["VALIDATIONS"] = "validations";
|
|
4716
5013
|
EnumTableName$1["COLLECTORS"] = "collectors";
|
|
4717
5014
|
EnumTableName$1["CHAINS"] = "chains";
|
|
5015
|
+
EnumTableName$1["LOTS"] = "lots";
|
|
5016
|
+
EnumTableName$1["OFFSETS"] = "offsets";
|
|
5017
|
+
EnumTableName$1["TREES"] = "trees";
|
|
5018
|
+
EnumTableName$1["MERKLE_PATHS"] = "merkle_paths";
|
|
4718
5019
|
return EnumTableName$1;
|
|
4719
5020
|
}(EnumTableName || {});
|
|
4720
5021
|
const TABLE_NAMES = Object.values(EnumTableName);
|
|
@@ -4877,6 +5178,86 @@ const callbacks = s.table(EnumTableName.CALLBACKS, {
|
|
|
4877
5178
|
],
|
|
4878
5179
|
name: "callbacks_positions_fk"
|
|
4879
5180
|
}).onDelete("cascade")]);
|
|
5181
|
+
const lots = s.table(EnumTableName.LOTS, {
|
|
5182
|
+
chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
|
|
5183
|
+
user: varchar("user", { length: 42 }).notNull(),
|
|
5184
|
+
contract: varchar("contract", { length: 42 }).notNull(),
|
|
5185
|
+
group: varchar("group", { length: 66 }).notNull(),
|
|
5186
|
+
lower: numeric("lower", {
|
|
5187
|
+
precision: 78,
|
|
5188
|
+
scale: 0
|
|
5189
|
+
}).notNull(),
|
|
5190
|
+
upper: numeric("upper", {
|
|
5191
|
+
precision: 78,
|
|
5192
|
+
scale: 0
|
|
5193
|
+
}).notNull()
|
|
5194
|
+
}, (table) => [
|
|
5195
|
+
primaryKey({
|
|
5196
|
+
columns: [
|
|
5197
|
+
table.chainId,
|
|
5198
|
+
table.user,
|
|
5199
|
+
table.contract,
|
|
5200
|
+
table.group
|
|
5201
|
+
],
|
|
5202
|
+
name: "lots_pk"
|
|
5203
|
+
}),
|
|
5204
|
+
foreignKey({
|
|
5205
|
+
columns: [
|
|
5206
|
+
table.chainId,
|
|
5207
|
+
table.contract,
|
|
5208
|
+
table.user
|
|
5209
|
+
],
|
|
5210
|
+
foreignColumns: [
|
|
5211
|
+
positions.chainId,
|
|
5212
|
+
positions.contract,
|
|
5213
|
+
positions.user
|
|
5214
|
+
],
|
|
5215
|
+
name: "lots_positions_fk"
|
|
5216
|
+
}).onDelete("cascade"),
|
|
5217
|
+
foreignKey({
|
|
5218
|
+
columns: [
|
|
5219
|
+
table.chainId,
|
|
5220
|
+
table.user,
|
|
5221
|
+
table.group
|
|
5222
|
+
],
|
|
5223
|
+
foreignColumns: [
|
|
5224
|
+
groups.chainId,
|
|
5225
|
+
groups.maker,
|
|
5226
|
+
groups.group
|
|
5227
|
+
],
|
|
5228
|
+
name: "lots_groups_fk"
|
|
5229
|
+
}).onDelete("cascade")
|
|
5230
|
+
]);
|
|
5231
|
+
const offsets = s.table(EnumTableName.OFFSETS, {
|
|
5232
|
+
chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
|
|
5233
|
+
user: varchar("user", { length: 42 }).notNull(),
|
|
5234
|
+
contract: varchar("contract", { length: 42 }).notNull(),
|
|
5235
|
+
group: varchar("group", { length: 66 }).notNull(),
|
|
5236
|
+
value: numeric("value", {
|
|
5237
|
+
precision: 78,
|
|
5238
|
+
scale: 0
|
|
5239
|
+
}).notNull()
|
|
5240
|
+
}, (table) => [primaryKey({
|
|
5241
|
+
columns: [
|
|
5242
|
+
table.chainId,
|
|
5243
|
+
table.user,
|
|
5244
|
+
table.contract,
|
|
5245
|
+
table.group
|
|
5246
|
+
],
|
|
5247
|
+
name: "offsets_pk"
|
|
5248
|
+
}), foreignKey({
|
|
5249
|
+
columns: [
|
|
5250
|
+
table.chainId,
|
|
5251
|
+
table.contract,
|
|
5252
|
+
table.user
|
|
5253
|
+
],
|
|
5254
|
+
foreignColumns: [
|
|
5255
|
+
positions.chainId,
|
|
5256
|
+
positions.contract,
|
|
5257
|
+
positions.user
|
|
5258
|
+
],
|
|
5259
|
+
name: "offsets_positions_fk"
|
|
5260
|
+
}).onDelete("cascade")]);
|
|
4880
5261
|
const PositionTypes = s.enum("position_type", Object.values(Type));
|
|
4881
5262
|
const positionTypes = s.table("position_types", {
|
|
4882
5263
|
id: serial("id").primaryKey(),
|
|
@@ -4972,12 +5353,23 @@ const chains = s.table(EnumTableName.CHAINS, {
|
|
|
4972
5353
|
}).default("0").notNull(),
|
|
4973
5354
|
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
4974
5355
|
}, (table) => [uniqueIndex("chains_id_epoch_idx").on(table.chainId, table.epoch)]);
|
|
5356
|
+
const trees = s.table(EnumTableName.TREES, {
|
|
5357
|
+
root: varchar("root", { length: 66 }).primaryKey(),
|
|
5358
|
+
rootSignature: varchar("root_signature", { length: 132 }).notNull(),
|
|
5359
|
+
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
5360
|
+
});
|
|
5361
|
+
const merklePaths = s.table(EnumTableName.MERKLE_PATHS, {
|
|
5362
|
+
offerHash: varchar("offer_hash", { length: 66 }).primaryKey().references(() => offers.hash, { onDelete: "cascade" }),
|
|
5363
|
+
treeRoot: varchar("tree_root", { length: 66 }).notNull().references(() => trees.root, { onDelete: "cascade" }),
|
|
5364
|
+
proofNodes: text("proof_nodes").notNull(),
|
|
5365
|
+
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
5366
|
+
}, (table) => [index("merkle_paths_tree_root_idx").on(table.treeRoot)]);
|
|
4975
5367
|
|
|
4976
5368
|
//#endregion
|
|
4977
5369
|
//#region src/database/domains/Book.ts
|
|
4978
5370
|
const DEFAULT_LIMIT$3 = 100;
|
|
4979
5371
|
const MAX_TOTAL_OFFERS = 500;
|
|
4980
|
-
function create$
|
|
5372
|
+
function create$11(config) {
|
|
4981
5373
|
const db = config.db;
|
|
4982
5374
|
const logger = getLogger();
|
|
4983
5375
|
const getOffers$1 = async (parameters) => {
|
|
@@ -4996,52 +5388,20 @@ function create$8(config) {
|
|
|
4996
5388
|
offers: [],
|
|
4997
5389
|
nextCursor: null
|
|
4998
5390
|
};
|
|
4999
|
-
const
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
obligationId: obligationId$1,
|
|
5010
|
-
side,
|
|
5011
|
-
now: now$1,
|
|
5012
|
-
rateSortDirection,
|
|
5013
|
-
cursor: offerCursor,
|
|
5014
|
-
limit: batchSize
|
|
5015
|
-
});
|
|
5016
|
-
if (rawOffers.length === 0) break;
|
|
5017
|
-
const newCallbackIds = rawOffers.flatMap((o) => o.callbackIds).filter((id$1) => !callbackState.has(id$1));
|
|
5018
|
-
await _updateCallbacksByIds(callbackState, db, newCallbackIds);
|
|
5019
|
-
await _updatePositionsByKeys(positionState, db, [...new Set(newCallbackIds.map((id$1) => callbackState.get(id$1)?.positionKey).filter((k) => k !== void 0 && !positionState.has(k)))]);
|
|
5020
|
-
await _updatePrices(prices, db, _collectNewOracleAddresses(rawOffers, callbackState, positionState, prices));
|
|
5021
|
-
const validOffers = _computeCrossInvalidation(rawOffers, callbackState, positionState, prices);
|
|
5022
|
-
let isOfferInPreviousPages = inputCursor === null;
|
|
5023
|
-
const cursorRate = inputCursor ? BigInt(inputCursor.rate) : 0n;
|
|
5024
|
-
for (const offer of validOffers) {
|
|
5025
|
-
if (!isOfferInPreviousPages) if (rateSortDirection === "asc" ? offer.rate > cursorRate : offer.rate < cursorRate) isOfferInPreviousPages = true;
|
|
5026
|
-
else if (offer.hash === inputCursor.hash) {
|
|
5027
|
-
isOfferInPreviousPages = true;
|
|
5028
|
-
continue;
|
|
5029
|
-
} else continue;
|
|
5030
|
-
book.push(offer);
|
|
5031
|
-
if (book.length >= effectiveLimit) {
|
|
5032
|
-
hasMoreOffers = true;
|
|
5033
|
-
break;
|
|
5034
|
-
}
|
|
5035
|
-
}
|
|
5036
|
-
offerCursor = rawNextCursor;
|
|
5037
|
-
if (!offerCursor) break;
|
|
5038
|
-
}
|
|
5039
|
-
const lastReturnedOffer = book[book.length - 1];
|
|
5040
|
-
const newTotalReturned = previouslyReturned + book.length;
|
|
5391
|
+
const { offers: offers$1, hasMore } = await _getOffers(db, {
|
|
5392
|
+
obligationId: obligationId$1,
|
|
5393
|
+
side,
|
|
5394
|
+
now: now$1,
|
|
5395
|
+
rateSortDirection,
|
|
5396
|
+
cursor: inputCursor,
|
|
5397
|
+
limit: Math.min(requestedLimit, MAX_TOTAL_OFFERS - previouslyReturned)
|
|
5398
|
+
});
|
|
5399
|
+
const lastReturnedOffer = offers$1[offers$1.length - 1];
|
|
5400
|
+
const newTotalReturned = previouslyReturned + offers$1.length;
|
|
5041
5401
|
const hasHitHardLimit = newTotalReturned >= MAX_TOTAL_OFFERS;
|
|
5042
5402
|
return {
|
|
5043
|
-
offers:
|
|
5044
|
-
nextCursor:
|
|
5403
|
+
offers: offers$1,
|
|
5404
|
+
nextCursor: offers$1.length > 0 && lastReturnedOffer && !hasHitHardLimit && hasMore ? Cursor.encode(lastReturnedOffer, newTotalReturned, now$1, side) : null
|
|
5045
5405
|
};
|
|
5046
5406
|
};
|
|
5047
5407
|
return {
|
|
@@ -5088,8 +5448,8 @@ function create$8(config) {
|
|
|
5088
5448
|
getOffers: getOffers$1
|
|
5089
5449
|
};
|
|
5090
5450
|
}
|
|
5091
|
-
/** Get offers with
|
|
5092
|
-
async function
|
|
5451
|
+
/** Get offers with computed takeable based on lot balance. */
|
|
5452
|
+
async function _getOffers(db, params) {
|
|
5093
5453
|
const { obligationId: obligationId$1, side, now: now$1, rateSortDirection, cursor, limit } = params;
|
|
5094
5454
|
const raw = await db.execute(sql`
|
|
5095
5455
|
WITH collats AS MATERIALIZED (
|
|
@@ -5157,32 +5517,202 @@ async function _getOffersWithCallbackIds(db, params) {
|
|
|
5157
5517
|
ORDER BY e.rate ${rateSortDirection === "asc" ? sql`ASC` : sql`DESC`}, e.block_number ASC, e.assets DESC, e.hash ASC
|
|
5158
5518
|
LIMIT ${limit}
|
|
5159
5519
|
),
|
|
5160
|
-
|
|
5520
|
+
-- Compute sum of offsets per position
|
|
5521
|
+
position_offsets AS (
|
|
5161
5522
|
SELECT
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5523
|
+
chain_id,
|
|
5524
|
+
"user",
|
|
5525
|
+
contract,
|
|
5526
|
+
SUM(value::numeric) AS total_offset
|
|
5527
|
+
FROM ${offsets}
|
|
5528
|
+
GROUP BY chain_id, "user", contract
|
|
5529
|
+
),
|
|
5530
|
+
-- Compute position_consumed: sum of consumed from all groups with lots on each position (converted to lot terms)
|
|
5531
|
+
position_consumed AS (
|
|
5532
|
+
SELECT
|
|
5533
|
+
l.chain_id,
|
|
5534
|
+
l.contract,
|
|
5535
|
+
l."user",
|
|
5536
|
+
SUM(
|
|
5537
|
+
CASE
|
|
5538
|
+
WHEN wo.assets::numeric > 0
|
|
5539
|
+
THEN COALESCE(g.consumed::numeric, 0) * (l.upper::numeric - l.lower::numeric) / wo.assets::numeric
|
|
5540
|
+
ELSE 0
|
|
5541
|
+
END
|
|
5542
|
+
) AS consumed
|
|
5543
|
+
FROM ${lots} l
|
|
5544
|
+
JOIN ${groups} g
|
|
5545
|
+
ON g.chain_id = l.chain_id
|
|
5546
|
+
AND LOWER(g.maker) = LOWER(l."user")
|
|
5547
|
+
AND g."group" = l."group"
|
|
5548
|
+
JOIN winners wo
|
|
5549
|
+
ON wo.group_chain_id = g.chain_id
|
|
5550
|
+
AND LOWER(wo.group_maker) = LOWER(g.maker)
|
|
5551
|
+
AND wo.group_group = g."group"
|
|
5552
|
+
GROUP BY l.chain_id, l.contract, l."user"
|
|
5553
|
+
),
|
|
5554
|
+
-- Compute callback contributions with lot balance
|
|
5555
|
+
callback_contributions AS (
|
|
5556
|
+
SELECT
|
|
5557
|
+
p.hash,
|
|
5558
|
+
p.obligation_id,
|
|
5559
|
+
p.assets,
|
|
5560
|
+
p.rate,
|
|
5561
|
+
p.maturity,
|
|
5562
|
+
p.expiry,
|
|
5563
|
+
p.start,
|
|
5564
|
+
p.nonce,
|
|
5565
|
+
p.buy,
|
|
5566
|
+
p.callback_address,
|
|
5567
|
+
p.callback_data,
|
|
5568
|
+
p.block_number,
|
|
5569
|
+
p.group_chain_id,
|
|
5570
|
+
p.group_maker,
|
|
5571
|
+
p.group_group,
|
|
5572
|
+
p.consumed,
|
|
5573
|
+
p.chain_id,
|
|
5574
|
+
p.loan_token,
|
|
5575
|
+
c.id AS callback_id,
|
|
5576
|
+
c.position_chain_id,
|
|
5577
|
+
c.position_contract,
|
|
5578
|
+
c.position_user,
|
|
5579
|
+
c.amount AS callback_amount,
|
|
5580
|
+
pos.balance AS position_balance,
|
|
5581
|
+
pos.asset AS position_asset,
|
|
5582
|
+
l.lower AS lot_lower,
|
|
5583
|
+
l.upper AS lot_upper,
|
|
5584
|
+
-- Compute lot_balance: min(position_balance + offset + position_consumed - lot.lower, lot.size - lot_consumed)
|
|
5585
|
+
-- lot_consumed is converted from loan token to lot terms: consumed * lot_size / assets
|
|
5586
|
+
GREATEST(0, LEAST(
|
|
5587
|
+
COALESCE(pos.balance::numeric, 0) + COALESCE(pos_offsets.total_offset, 0) + COALESCE(pc.consumed, 0) - COALESCE(l.lower::numeric, 0),
|
|
5588
|
+
(COALESCE(l.upper::numeric, 0) - COALESCE(l.lower::numeric, 0)) -
|
|
5589
|
+
CASE
|
|
5590
|
+
WHEN p.assets::numeric > 0
|
|
5591
|
+
THEN COALESCE(p.consumed::numeric, 0) * (COALESCE(l.upper::numeric, 0) - COALESCE(l.lower::numeric, 0)) / p.assets::numeric
|
|
5592
|
+
ELSE 0
|
|
5593
|
+
END
|
|
5594
|
+
)) AS lot_balance
|
|
5166
5595
|
FROM paged p
|
|
5167
5596
|
LEFT JOIN ${offersCallbacks} oc ON oc.offer_hash = p.hash
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5597
|
+
LEFT JOIN ${callbacks} c ON c.id = oc.callback_id
|
|
5598
|
+
LEFT JOIN ${lots} l
|
|
5599
|
+
ON l.chain_id = c.position_chain_id
|
|
5600
|
+
AND LOWER(l.contract) = LOWER(c.position_contract)
|
|
5601
|
+
AND LOWER(l."user") = LOWER(c.position_user)
|
|
5602
|
+
AND l."group" = p.group_group
|
|
5603
|
+
LEFT JOIN ${positions} pos
|
|
5604
|
+
ON pos.chain_id = c.position_chain_id
|
|
5605
|
+
AND LOWER(pos.contract) = LOWER(c.position_contract)
|
|
5606
|
+
AND LOWER(pos."user") = LOWER(c.position_user)
|
|
5607
|
+
LEFT JOIN position_offsets pos_offsets
|
|
5608
|
+
ON pos_offsets.chain_id = c.position_chain_id
|
|
5609
|
+
AND LOWER(pos_offsets.contract) = LOWER(c.position_contract)
|
|
5610
|
+
AND LOWER(pos_offsets."user") = LOWER(c.position_user)
|
|
5611
|
+
LEFT JOIN position_consumed pc
|
|
5612
|
+
ON pc.chain_id = c.position_chain_id
|
|
5613
|
+
AND LOWER(pc.contract) = LOWER(c.position_contract)
|
|
5614
|
+
AND LOWER(pc."user") = LOWER(c.position_user)
|
|
5615
|
+
),
|
|
5616
|
+
-- Compute contribution per callback in loan terms (with oracle price via LEFT JOIN)
|
|
5617
|
+
callback_loan_contribution AS (
|
|
5618
|
+
SELECT
|
|
5619
|
+
cc.*,
|
|
5620
|
+
CASE
|
|
5621
|
+
-- No lot exists: contribution is 0
|
|
5622
|
+
WHEN cc.lot_lower IS NULL THEN 0
|
|
5623
|
+
-- Loan token position: use lot_balance directly, apply callback limit
|
|
5624
|
+
WHEN LOWER(cc.position_asset) = LOWER(cc.loan_token) THEN
|
|
5625
|
+
LEAST(
|
|
5626
|
+
cc.lot_balance,
|
|
5627
|
+
COALESCE(cc.callback_amount::numeric, cc.lot_balance)
|
|
5628
|
+
)
|
|
5629
|
+
-- Collateral position: convert to loan using (amount * price / 10^36) * lltv / 10^18
|
|
5630
|
+
ELSE
|
|
5631
|
+
(
|
|
5632
|
+
LEAST(
|
|
5633
|
+
cc.lot_balance,
|
|
5634
|
+
COALESCE(cc.callback_amount::numeric, cc.lot_balance)
|
|
5635
|
+
) * COALESCE(collat_oracle.price::numeric, 0) / 1e36
|
|
5636
|
+
) * COALESCE(collat_info.lltv::numeric, 0) / 1e18
|
|
5637
|
+
END AS contribution_in_loan
|
|
5638
|
+
FROM callback_contributions cc
|
|
5639
|
+
LEFT JOIN ${obligationCollateralsV2} collat_info
|
|
5640
|
+
ON collat_info.obligation_id = cc.obligation_id
|
|
5641
|
+
AND LOWER(collat_info.asset) = LOWER(cc.position_asset)
|
|
5642
|
+
LEFT JOIN ${oracles} collat_oracle
|
|
5643
|
+
ON collat_oracle.chain_id = collat_info.oracle_chain_id
|
|
5644
|
+
AND LOWER(collat_oracle.address) = LOWER(collat_info.oracle_address)
|
|
5645
|
+
),
|
|
5646
|
+
-- Aggregate contributions per offer, deduplicating by position using DISTINCT ON
|
|
5647
|
+
offer_contributions AS (
|
|
5648
|
+
SELECT
|
|
5649
|
+
hash,
|
|
5650
|
+
obligation_id,
|
|
5651
|
+
assets,
|
|
5652
|
+
rate,
|
|
5653
|
+
maturity,
|
|
5654
|
+
expiry,
|
|
5655
|
+
start,
|
|
5656
|
+
nonce,
|
|
5657
|
+
buy,
|
|
5658
|
+
callback_address,
|
|
5659
|
+
callback_data,
|
|
5660
|
+
block_number,
|
|
5661
|
+
group_chain_id,
|
|
5662
|
+
group_maker,
|
|
5663
|
+
consumed,
|
|
5664
|
+
chain_id,
|
|
5665
|
+
loan_token,
|
|
5666
|
+
SUM(contribution_in_loan) AS total_available
|
|
5667
|
+
FROM (
|
|
5668
|
+
-- Take max contribution per position using DISTINCT ON (idiomatic PostgreSQL)
|
|
5669
|
+
SELECT DISTINCT ON (clc.hash, clc.position_chain_id, clc.position_contract, clc.position_user)
|
|
5670
|
+
clc.*
|
|
5671
|
+
FROM callback_loan_contribution clc
|
|
5672
|
+
WHERE clc.callback_id IS NOT NULL
|
|
5673
|
+
ORDER BY clc.hash, clc.position_chain_id, clc.position_contract, clc.position_user, clc.contribution_in_loan DESC
|
|
5674
|
+
) deduped
|
|
5675
|
+
GROUP BY hash, obligation_id, assets, rate, maturity, expiry, start, nonce, buy,
|
|
5676
|
+
callback_address, callback_data, block_number, group_chain_id, group_maker,
|
|
5677
|
+
consumed, chain_id, loan_token
|
|
5171
5678
|
)
|
|
5679
|
+
-- Final SELECT with inline takeable computation
|
|
5172
5680
|
SELECT
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5681
|
+
oc.hash,
|
|
5682
|
+
oc.group_maker,
|
|
5683
|
+
oc.assets,
|
|
5684
|
+
oc.consumed,
|
|
5685
|
+
oc.rate,
|
|
5686
|
+
oc.maturity,
|
|
5687
|
+
oc.expiry,
|
|
5688
|
+
oc.start,
|
|
5689
|
+
oc.nonce,
|
|
5690
|
+
oc.buy,
|
|
5691
|
+
oc.chain_id,
|
|
5692
|
+
oc.loan_token,
|
|
5693
|
+
oc.callback_address,
|
|
5694
|
+
oc.callback_data,
|
|
5695
|
+
oc.block_number,
|
|
5696
|
+
-- takeable = min(assets - consumed, total_available)
|
|
5697
|
+
GREATEST(0, LEAST(
|
|
5698
|
+
oc.assets::numeric - oc.consumed::numeric,
|
|
5699
|
+
COALESCE(oc.total_available, 0)
|
|
5700
|
+
)) AS takeable,
|
|
5701
|
+
c.collaterals
|
|
5702
|
+
FROM offer_contributions oc
|
|
5703
|
+
LEFT JOIN collats c ON c.obligation_id = oc.obligation_id
|
|
5704
|
+
WHERE GREATEST(0, LEAST(
|
|
5705
|
+
oc.assets::numeric - oc.consumed::numeric,
|
|
5706
|
+
COALESCE(oc.total_available, 0)
|
|
5707
|
+
)) > 0
|
|
5178
5708
|
ORDER BY
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5709
|
+
oc.rate ${rateSortDirection === "asc" ? sql`ASC` : sql`DESC`},
|
|
5710
|
+
oc.block_number ASC,
|
|
5711
|
+
oc.assets DESC,
|
|
5712
|
+
oc.hash ASC;
|
|
5183
5713
|
`);
|
|
5184
|
-
|
|
5185
|
-
|
|
5714
|
+
return {
|
|
5715
|
+
offers: raw.rows.map((row) => from$9({
|
|
5186
5716
|
offering: row.group_maker,
|
|
5187
5717
|
assets: BigInt(row.assets),
|
|
5188
5718
|
rate: BigInt(row.rate),
|
|
@@ -5204,165 +5734,12 @@ async function _getOffersWithCallbackIds(db, params) {
|
|
|
5204
5734
|
gasLimit: 0n
|
|
5205
5735
|
},
|
|
5206
5736
|
consumed: BigInt(row.consumed),
|
|
5207
|
-
takeable:
|
|
5737
|
+
takeable: BigInt(row.takeable.split(".")[0] ?? "0"),
|
|
5208
5738
|
blockNumber: row.block_number
|
|
5209
|
-
}),
|
|
5210
|
-
|
|
5211
|
-
}));
|
|
5212
|
-
let nextCursor = null;
|
|
5213
|
-
if (raw.rows.length === limit) {
|
|
5214
|
-
const last = raw.rows[raw.rows.length - 1];
|
|
5215
|
-
nextCursor = {
|
|
5216
|
-
rate: last.rate,
|
|
5217
|
-
blockNumber: last.block_number,
|
|
5218
|
-
assets: last.assets,
|
|
5219
|
-
hash: last.hash
|
|
5220
|
-
};
|
|
5221
|
-
}
|
|
5222
|
-
return {
|
|
5223
|
-
offers: offers$1,
|
|
5224
|
-
nextCursor
|
|
5739
|
+
})),
|
|
5740
|
+
hasMore: raw.rows.length === limit
|
|
5225
5741
|
};
|
|
5226
5742
|
}
|
|
5227
|
-
/** Get callbacks by their IDs. */
|
|
5228
|
-
async function _updateCallbacksByIds(state, db, ids) {
|
|
5229
|
-
if (ids.length === 0) return;
|
|
5230
|
-
const raw = await db.execute(sql`
|
|
5231
|
-
SELECT c.id, c.position_chain_id, c.position_contract, c.position_user, c.amount
|
|
5232
|
-
FROM ${callbacks} c
|
|
5233
|
-
WHERE c.id IN (${sql.join(ids.map((id$1) => sql`${id$1}`), sql`, `)})
|
|
5234
|
-
`);
|
|
5235
|
-
for (const row of raw.rows) if (!state.has(row.id)) state.set(row.id, {
|
|
5236
|
-
positionKey: _buildPositionKey(row.position_chain_id, row.position_contract, row.position_user),
|
|
5237
|
-
amount: row.amount != null ? BigInt(row.amount) : null
|
|
5238
|
-
});
|
|
5239
|
-
}
|
|
5240
|
-
/** Get positions by their composite keys. */
|
|
5241
|
-
async function _updatePositionsByKeys(state, db, keys) {
|
|
5242
|
-
if (keys.length === 0) return;
|
|
5243
|
-
const parsedKeys = keys.map((key) => {
|
|
5244
|
-
const parts = key.split(":");
|
|
5245
|
-
return {
|
|
5246
|
-
chainId: BigInt(parts[0]),
|
|
5247
|
-
contract: parts[1],
|
|
5248
|
-
user: parts[2]
|
|
5249
|
-
};
|
|
5250
|
-
});
|
|
5251
|
-
const raw = await db.execute(sql`
|
|
5252
|
-
SELECT p.chain_id, p.contract, p."user", p.balance, p.asset
|
|
5253
|
-
FROM ${positions} p
|
|
5254
|
-
WHERE (p.chain_id, LOWER(p.contract), LOWER(p."user")) IN (
|
|
5255
|
-
${sql.join(parsedKeys.map((k) => sql`(${k.chainId}, ${k.contract.toLowerCase()}, ${k.user.toLowerCase()})`), sql`, `)}
|
|
5256
|
-
)
|
|
5257
|
-
`);
|
|
5258
|
-
for (const row of raw.rows) {
|
|
5259
|
-
const key = _buildPositionKey(row.chain_id, row.contract, row.user);
|
|
5260
|
-
if (!state.has(key)) state.set(key, {
|
|
5261
|
-
balance: row.balance ? BigInt(row.balance) : 0n,
|
|
5262
|
-
remaining: row.balance ? BigInt(row.balance) : 0n,
|
|
5263
|
-
asset: row.asset ?? "0x0000000000000000000000000000000000000000"
|
|
5264
|
-
});
|
|
5265
|
-
}
|
|
5266
|
-
}
|
|
5267
|
-
/** Get oracle prices by chain_id and address. */
|
|
5268
|
-
async function _updatePrices(state, db, oracles$1) {
|
|
5269
|
-
if (oracles$1.length === 0) return;
|
|
5270
|
-
const raw = await db.execute(sql`
|
|
5271
|
-
SELECT o.chain_id, o.address, o.price
|
|
5272
|
-
FROM ${oracles} o
|
|
5273
|
-
WHERE (o.chain_id, LOWER(o.address)) IN (${sql.join(oracles$1.map((o) => sql`(${o.chainId}, ${o.address.toLowerCase()})`), sql`, `)})
|
|
5274
|
-
`);
|
|
5275
|
-
for (const row of raw.rows) {
|
|
5276
|
-
const key = `${row.chain_id}:${row.address.toLowerCase()}`;
|
|
5277
|
-
if (!state.has(key)) state.set(key, row.price ? BigInt(row.price) : 0n);
|
|
5278
|
-
}
|
|
5279
|
-
}
|
|
5280
|
-
/** Build a composite position key from its components. */
|
|
5281
|
-
function _buildPositionKey(chainId, contract, user) {
|
|
5282
|
-
return `${chainId}:${contract.toLowerCase()}:${user.toLowerCase()}`;
|
|
5283
|
-
}
|
|
5284
|
-
/** Collect oracle addresses that need to be Geted for collateral positions. */
|
|
5285
|
-
function _collectNewOracleAddresses(offers$1, callbackState, positionState, prices) {
|
|
5286
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5287
|
-
const result = [];
|
|
5288
|
-
for (const offer of offers$1) for (const callbackId of offer.callbackIds) {
|
|
5289
|
-
const callback$1 = callbackState.get(callbackId);
|
|
5290
|
-
if (!callback$1) continue;
|
|
5291
|
-
const position = positionState.get(callback$1.positionKey);
|
|
5292
|
-
if (!position) continue;
|
|
5293
|
-
if (position.asset.toLowerCase() === offer.loanToken.toLowerCase()) continue;
|
|
5294
|
-
const collateral = offer.collaterals.find((c) => c.asset.toLowerCase() === position.asset.toLowerCase());
|
|
5295
|
-
if (collateral) {
|
|
5296
|
-
const key = `${offer.chainId}:${collateral.oracle.toLowerCase()}`;
|
|
5297
|
-
if (!prices.has(key) && !seen.has(key)) {
|
|
5298
|
-
seen.add(key);
|
|
5299
|
-
result.push({
|
|
5300
|
-
chainId: offer.chainId,
|
|
5301
|
-
address: collateral.oracle.toLowerCase()
|
|
5302
|
-
});
|
|
5303
|
-
}
|
|
5304
|
-
}
|
|
5305
|
-
}
|
|
5306
|
-
return result;
|
|
5307
|
-
}
|
|
5308
|
-
/**
|
|
5309
|
-
* Compute cross-invalidation for a batch of offers.
|
|
5310
|
-
* Deducts consumed liquidity from shared positions and returns offers with takeable amounts.
|
|
5311
|
-
*/
|
|
5312
|
-
function _computeCrossInvalidation(offers$1, callbackState, positionState, prices) {
|
|
5313
|
-
const result = [];
|
|
5314
|
-
for (const offer of offers$1) {
|
|
5315
|
-
const contributions = /* @__PURE__ */ new Map();
|
|
5316
|
-
for (const callbackId of offer.callbackIds) {
|
|
5317
|
-
const callback$1 = callbackState.get(callbackId);
|
|
5318
|
-
if (!callback$1) continue;
|
|
5319
|
-
const position = positionState.get(callback$1.positionKey);
|
|
5320
|
-
if (!position) continue;
|
|
5321
|
-
let conversion;
|
|
5322
|
-
if (position.asset.toLowerCase() === offer.loanToken.toLowerCase()) conversion = null;
|
|
5323
|
-
else {
|
|
5324
|
-
const collateral = offer.collaterals.find((c) => c.asset.toLowerCase() === position.asset.toLowerCase());
|
|
5325
|
-
if (!collateral) conversion = {
|
|
5326
|
-
price: 0n,
|
|
5327
|
-
lltv: 0n
|
|
5328
|
-
};
|
|
5329
|
-
else {
|
|
5330
|
-
const key = `${offer.chainId}:${collateral.oracle.toLowerCase()}`;
|
|
5331
|
-
conversion = {
|
|
5332
|
-
price: prices.get(key) ?? 0n,
|
|
5333
|
-
lltv: collateral.lltv
|
|
5334
|
-
};
|
|
5335
|
-
}
|
|
5336
|
-
}
|
|
5337
|
-
const availableFromPosition = conversion === null ? position.remaining : Conversion.collateralToLoan(position.remaining, conversion);
|
|
5338
|
-
const callbackLimitInLoanTerms = conversion === null || callback$1.amount === null ? callback$1.amount : Conversion.collateralToLoan(callback$1.amount, conversion);
|
|
5339
|
-
const callbackAvailable = callbackLimitInLoanTerms === null ? availableFromPosition : min(availableFromPosition, callbackLimitInLoanTerms);
|
|
5340
|
-
const existing = contributions.get(callback$1.positionKey);
|
|
5341
|
-
if (existing) existing.available = min(availableFromPosition, max(existing.available, callbackAvailable));
|
|
5342
|
-
else contributions.set(callback$1.positionKey, {
|
|
5343
|
-
available: callbackAvailable,
|
|
5344
|
-
conversion
|
|
5345
|
-
});
|
|
5346
|
-
}
|
|
5347
|
-
let totalAvailable = 0n;
|
|
5348
|
-
for (const [, contrib] of contributions) totalAvailable += contrib.available;
|
|
5349
|
-
const takeable = min(offer.assets - offer.consumed, totalAvailable);
|
|
5350
|
-
if (takeable <= 0n) continue;
|
|
5351
|
-
for (const [key, contrib] of contributions) {
|
|
5352
|
-
const position = positionState.get(key);
|
|
5353
|
-
const proportionalTakeable = totalAvailable > 0n ? contrib.available * takeable / totalAvailable : 0n;
|
|
5354
|
-
const toDeduct = contrib.conversion === null ? proportionalTakeable : Conversion.loanToCollateral(proportionalTakeable, contrib.conversion);
|
|
5355
|
-
position.remaining = position.remaining - toDeduct;
|
|
5356
|
-
if (position.remaining < 0n) position.remaining = 0n;
|
|
5357
|
-
}
|
|
5358
|
-
const { callbackIds: _, ...cleanOffer } = offer;
|
|
5359
|
-
result.push(from$9({
|
|
5360
|
-
...cleanOffer,
|
|
5361
|
-
takeable
|
|
5362
|
-
}));
|
|
5363
|
-
}
|
|
5364
|
-
return result;
|
|
5365
|
-
}
|
|
5366
5743
|
let Cursor;
|
|
5367
5744
|
(function(_Cursor) {
|
|
5368
5745
|
function encode(offer, totalReturned, now$1, side) {
|
|
@@ -5427,7 +5804,7 @@ let LevelCursor;
|
|
|
5427
5804
|
//#endregion
|
|
5428
5805
|
//#region src/database/domains/Chains.ts
|
|
5429
5806
|
/** Postgres implementation. */
|
|
5430
|
-
const create$
|
|
5807
|
+
const create$10 = (config) => {
|
|
5431
5808
|
const db = config.db;
|
|
5432
5809
|
const logger = getLogger();
|
|
5433
5810
|
return {
|
|
@@ -5483,7 +5860,7 @@ const create$7 = (config) => {
|
|
|
5483
5860
|
//#endregion
|
|
5484
5861
|
//#region src/database/domains/Collectors.ts
|
|
5485
5862
|
/** Postgres implementation. */
|
|
5486
|
-
const create$
|
|
5863
|
+
const create$9 = (config) => {
|
|
5487
5864
|
const db = config.db;
|
|
5488
5865
|
const logger = getLogger();
|
|
5489
5866
|
return {
|
|
@@ -5576,7 +5953,7 @@ const DEFAULT_BATCH_SIZE = 4e3;
|
|
|
5576
5953
|
|
|
5577
5954
|
//#endregion
|
|
5578
5955
|
//#region src/database/domains/Consumed.ts
|
|
5579
|
-
function create$
|
|
5956
|
+
function create$8(db) {
|
|
5580
5957
|
return {
|
|
5581
5958
|
create: async (events) => {
|
|
5582
5959
|
if (events.length === 0) return;
|
|
@@ -5617,10 +5994,55 @@ function create$5(db) {
|
|
|
5617
5994
|
};
|
|
5618
5995
|
}
|
|
5619
5996
|
|
|
5997
|
+
//#endregion
|
|
5998
|
+
//#region src/database/domains/Lots.ts
|
|
5999
|
+
function create$7(db) {
|
|
6000
|
+
return {
|
|
6001
|
+
get: async (parameters) => {
|
|
6002
|
+
const { chainId, user, contract, group } = parameters ?? {};
|
|
6003
|
+
const conditions = [];
|
|
6004
|
+
if (chainId !== void 0) conditions.push(eq(lots.chainId, chainId));
|
|
6005
|
+
if (user !== void 0) conditions.push(eq(lots.user, user.toLowerCase()));
|
|
6006
|
+
if (contract !== void 0) conditions.push(eq(lots.contract, contract.toLowerCase()));
|
|
6007
|
+
if (group !== void 0) conditions.push(eq(lots.group, group));
|
|
6008
|
+
return (await db.select().from(lots).where(conditions.length > 0 ? and(...conditions) : void 0)).map((row) => ({
|
|
6009
|
+
chainId: row.chainId,
|
|
6010
|
+
user: row.user,
|
|
6011
|
+
contract: row.contract,
|
|
6012
|
+
group: row.group,
|
|
6013
|
+
lower: BigInt(row.lower),
|
|
6014
|
+
upper: BigInt(row.upper)
|
|
6015
|
+
}));
|
|
6016
|
+
},
|
|
6017
|
+
create: async (parameters) => {
|
|
6018
|
+
if (parameters.length === 0) return;
|
|
6019
|
+
const lotsByPositionGroup = /* @__PURE__ */ new Map();
|
|
6020
|
+
for (const offer of parameters) {
|
|
6021
|
+
const key = `${offer.positionChainId}-${offer.positionContract}-${offer.positionUser}-${offer.group}`.toLowerCase();
|
|
6022
|
+
const existing = lotsByPositionGroup.get(key);
|
|
6023
|
+
if (!existing || offer.size > existing.size) lotsByPositionGroup.set(key, offer);
|
|
6024
|
+
}
|
|
6025
|
+
for (const offer of lotsByPositionGroup.values()) if ((await db.select().from(lots).where(and(eq(lots.chainId, offer.positionChainId), eq(lots.contract, offer.positionContract.toLowerCase()), eq(lots.user, offer.positionUser.toLowerCase()), eq(lots.group, offer.group))).limit(1)).length === 0) {
|
|
6026
|
+
const maxUpperResult = await db.select({ maxUpper: sql`COALESCE(MAX(${lots.upper}::numeric), 0)` }).from(lots).where(and(eq(lots.chainId, offer.positionChainId), eq(lots.contract, offer.positionContract.toLowerCase()), eq(lots.user, offer.positionUser.toLowerCase())));
|
|
6027
|
+
const newLower = BigInt(maxUpperResult[0]?.maxUpper ?? "0");
|
|
6028
|
+
const newUpper = newLower + offer.size;
|
|
6029
|
+
await db.insert(lots).values({
|
|
6030
|
+
chainId: offer.positionChainId,
|
|
6031
|
+
user: offer.positionUser.toLowerCase(),
|
|
6032
|
+
contract: offer.positionContract.toLowerCase(),
|
|
6033
|
+
group: offer.group,
|
|
6034
|
+
lower: newLower.toString(),
|
|
6035
|
+
upper: newUpper.toString()
|
|
6036
|
+
});
|
|
6037
|
+
}
|
|
6038
|
+
}
|
|
6039
|
+
};
|
|
6040
|
+
}
|
|
6041
|
+
|
|
5620
6042
|
//#endregion
|
|
5621
6043
|
//#region src/database/domains/Offers.ts
|
|
5622
6044
|
const DEFAULT_LIMIT$2 = 100;
|
|
5623
|
-
function create$
|
|
6045
|
+
function create$6(config) {
|
|
5624
6046
|
const db = config.db;
|
|
5625
6047
|
return {
|
|
5626
6048
|
create: async (offers$1) => {
|
|
@@ -5779,6 +6201,22 @@ function create$4(config) {
|
|
|
5779
6201
|
amount: callback$1.amount
|
|
5780
6202
|
})));
|
|
5781
6203
|
for (const batch$2 of batch$1(callbacksRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(callbacks).values(batch$2).onConflictDoNothing();
|
|
6204
|
+
const lotInfos = [];
|
|
6205
|
+
for (const [offerHash, callbacks$1] of offersCallbacksMap.entries()) {
|
|
6206
|
+
const offer = inserted.find((o) => o.hash === offerHash);
|
|
6207
|
+
if (!offer) continue;
|
|
6208
|
+
for (const callback$1 of callbacks$1) {
|
|
6209
|
+
const isLoanPosition = obligationsMap.get(offer.obligationId)?.loanToken.toLowerCase() === callback$1.asset?.toLowerCase();
|
|
6210
|
+
lotInfos.push({
|
|
6211
|
+
positionChainId: callback$1.chainId,
|
|
6212
|
+
positionContract: callback$1.contract,
|
|
6213
|
+
positionUser: callback$1.user,
|
|
6214
|
+
group: offer.group,
|
|
6215
|
+
size: isLoanPosition ? BigInt(offer.assets) : BigInt(callback$1.amount)
|
|
6216
|
+
});
|
|
6217
|
+
}
|
|
6218
|
+
}
|
|
6219
|
+
if (lotInfos.length > 0) await dbTx.lots.create(lotInfos);
|
|
5782
6220
|
obligationsMap.clear();
|
|
5783
6221
|
collateralsMap.clear();
|
|
5784
6222
|
oraclesMap.clear();
|
|
@@ -5961,9 +6399,29 @@ function create$4(config) {
|
|
|
5961
6399
|
};
|
|
5962
6400
|
}
|
|
5963
6401
|
|
|
6402
|
+
//#endregion
|
|
6403
|
+
//#region src/database/domains/Offsets.ts
|
|
6404
|
+
function create$5(db) {
|
|
6405
|
+
return { get: async (parameters) => {
|
|
6406
|
+
const { chainId, user, contract, group } = parameters ?? {};
|
|
6407
|
+
const conditions = [];
|
|
6408
|
+
if (chainId !== void 0) conditions.push(eq(offsets.chainId, chainId));
|
|
6409
|
+
if (user !== void 0) conditions.push(eq(offsets.user, user.toLowerCase()));
|
|
6410
|
+
if (contract !== void 0) conditions.push(eq(offsets.contract, contract.toLowerCase()));
|
|
6411
|
+
if (group !== void 0) conditions.push(eq(offsets.group, group));
|
|
6412
|
+
return (await db.select().from(offsets).where(conditions.length > 0 ? and(...conditions) : void 0)).map((row) => ({
|
|
6413
|
+
chainId: row.chainId,
|
|
6414
|
+
user: row.user,
|
|
6415
|
+
contract: row.contract,
|
|
6416
|
+
group: row.group,
|
|
6417
|
+
value: BigInt(row.value)
|
|
6418
|
+
}));
|
|
6419
|
+
} };
|
|
6420
|
+
}
|
|
6421
|
+
|
|
5964
6422
|
//#endregion
|
|
5965
6423
|
//#region src/database/domains/Oracles.ts
|
|
5966
|
-
function create$
|
|
6424
|
+
function create$4(db) {
|
|
5967
6425
|
return {
|
|
5968
6426
|
get: async ({ chainId }) => {
|
|
5969
6427
|
return (await db.select({
|
|
@@ -6003,7 +6461,7 @@ function create$3(db) {
|
|
|
6003
6461
|
//#endregion
|
|
6004
6462
|
//#region src/database/domains/Positions.ts
|
|
6005
6463
|
const DEFAULT_LIMIT$1 = 100;
|
|
6006
|
-
const create$
|
|
6464
|
+
const create$3 = (db) => {
|
|
6007
6465
|
return {
|
|
6008
6466
|
upsert: async (positions$1) => {
|
|
6009
6467
|
const positionsMap = /* @__PURE__ */ new Map();
|
|
@@ -6119,7 +6577,7 @@ const create$2 = (db) => {
|
|
|
6119
6577
|
|
|
6120
6578
|
//#endregion
|
|
6121
6579
|
//#region src/database/domains/Transfers.ts
|
|
6122
|
-
const create$
|
|
6580
|
+
const create$2 = (db) => ({ create: async (transfers$1) => {
|
|
6123
6581
|
if (transfers$1.length === 0) return 0;
|
|
6124
6582
|
return await db.transaction(async (dbTx) => {
|
|
6125
6583
|
let totalInserted = 0;
|
|
@@ -6216,6 +6674,91 @@ const create$1 = (db) => ({ create: async (transfers$1) => {
|
|
|
6216
6674
|
});
|
|
6217
6675
|
} });
|
|
6218
6676
|
|
|
6677
|
+
//#endregion
|
|
6678
|
+
//#region src/database/domains/Trees.ts
|
|
6679
|
+
/**
|
|
6680
|
+
* Creates a Trees domain instance for managing merkle tree metadata.
|
|
6681
|
+
*
|
|
6682
|
+
* @param config - Configuration with database instance
|
|
6683
|
+
* @returns TreesDomain instance
|
|
6684
|
+
*/
|
|
6685
|
+
function create$1(config) {
|
|
6686
|
+
const db = config.db;
|
|
6687
|
+
return {
|
|
6688
|
+
create: async (trees$1) => {
|
|
6689
|
+
if (trees$1.length === 0) return [];
|
|
6690
|
+
return await db.transaction(async (dbTx) => {
|
|
6691
|
+
const roots = [];
|
|
6692
|
+
for (const { tree, signature } of trees$1) {
|
|
6693
|
+
const root = tree.root.toLowerCase();
|
|
6694
|
+
roots.push(root);
|
|
6695
|
+
await dbTx.insert(trees).values({
|
|
6696
|
+
root,
|
|
6697
|
+
rootSignature: signature.toLowerCase()
|
|
6698
|
+
}).onConflictDoUpdate({
|
|
6699
|
+
target: [trees.root],
|
|
6700
|
+
set: {
|
|
6701
|
+
rootSignature: signature.toLowerCase(),
|
|
6702
|
+
createdAt: sql`NOW()`
|
|
6703
|
+
}
|
|
6704
|
+
});
|
|
6705
|
+
await dbTx.offers.create(tree.offers);
|
|
6706
|
+
const pathRows = proofs(tree).map((proof) => ({
|
|
6707
|
+
offerHash: proof.offer.hash.toLowerCase(),
|
|
6708
|
+
treeRoot: root,
|
|
6709
|
+
proofNodes: concatenateProofs(proof.path)
|
|
6710
|
+
}));
|
|
6711
|
+
for (const batch$2 of batch$1(pathRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(merklePaths).values(batch$2).onConflictDoUpdate({
|
|
6712
|
+
target: [merklePaths.offerHash],
|
|
6713
|
+
set: {
|
|
6714
|
+
treeRoot: sql`excluded.tree_root`,
|
|
6715
|
+
proofNodes: sql`excluded.proof_nodes`,
|
|
6716
|
+
createdAt: sql`NOW()`
|
|
6717
|
+
}
|
|
6718
|
+
});
|
|
6719
|
+
}
|
|
6720
|
+
return roots;
|
|
6721
|
+
});
|
|
6722
|
+
},
|
|
6723
|
+
getAttestations: async (hashes) => {
|
|
6724
|
+
if (hashes.length === 0) return /* @__PURE__ */ new Map();
|
|
6725
|
+
const normalizedHashes = hashes.map((h) => h.toLowerCase());
|
|
6726
|
+
const results = await db.select({
|
|
6727
|
+
offerHash: merklePaths.offerHash,
|
|
6728
|
+
treeRoot: merklePaths.treeRoot,
|
|
6729
|
+
proofNodes: merklePaths.proofNodes,
|
|
6730
|
+
rootSignature: trees.rootSignature
|
|
6731
|
+
}).from(merklePaths).innerJoin(trees, eq(merklePaths.treeRoot, trees.root)).where(inArray(merklePaths.offerHash, normalizedHashes));
|
|
6732
|
+
const attestationMap = /* @__PURE__ */ new Map();
|
|
6733
|
+
for (const row of results) attestationMap.set(row.offerHash, {
|
|
6734
|
+
root: row.treeRoot,
|
|
6735
|
+
signature: row.rootSignature,
|
|
6736
|
+
proof: splitProofs(row.proofNodes)
|
|
6737
|
+
});
|
|
6738
|
+
return attestationMap;
|
|
6739
|
+
}
|
|
6740
|
+
};
|
|
6741
|
+
}
|
|
6742
|
+
/**
|
|
6743
|
+
* Concatenates an array of 32-byte hex hashes into a single hex string.
|
|
6744
|
+
* Empty arrays return "0x".
|
|
6745
|
+
*/
|
|
6746
|
+
function concatenateProofs(proofs$1) {
|
|
6747
|
+
if (proofs$1.length === 0) return "0x";
|
|
6748
|
+
return `0x${proofs$1.map((p) => p.slice(2)).join("")}`;
|
|
6749
|
+
}
|
|
6750
|
+
/**
|
|
6751
|
+
* Splits a concatenated hex string back into an array of 32-byte hex hashes.
|
|
6752
|
+
* Returns empty array for "0x" or empty string.
|
|
6753
|
+
*/
|
|
6754
|
+
function splitProofs(concatenated) {
|
|
6755
|
+
if (!concatenated || concatenated === "0x" || concatenated.length <= 2) return [];
|
|
6756
|
+
const hex$1 = concatenated.slice(2);
|
|
6757
|
+
const proofs$1 = [];
|
|
6758
|
+
for (let i = 0; i < hex$1.length; i += 64) proofs$1.push(`0x${hex$1.slice(i, i + 64)}`);
|
|
6759
|
+
return proofs$1;
|
|
6760
|
+
}
|
|
6761
|
+
|
|
6219
6762
|
//#endregion
|
|
6220
6763
|
//#region src/database/domains/Validations.ts
|
|
6221
6764
|
const DEFAULT_LIMIT = 100;
|
|
@@ -6280,15 +6823,18 @@ function create(db) {
|
|
|
6280
6823
|
//#region src/database/Database.ts
|
|
6281
6824
|
function createDomains(core) {
|
|
6282
6825
|
return {
|
|
6283
|
-
book: create$
|
|
6284
|
-
collectors: create$
|
|
6285
|
-
offers: create$
|
|
6286
|
-
chains: create$
|
|
6287
|
-
consumed: create$
|
|
6288
|
-
|
|
6826
|
+
book: create$11({ db: core }),
|
|
6827
|
+
collectors: create$9({ db: core }),
|
|
6828
|
+
offers: create$6({ db: core }),
|
|
6829
|
+
chains: create$10({ db: core }),
|
|
6830
|
+
consumed: create$8(core),
|
|
6831
|
+
lots: create$7(core),
|
|
6832
|
+
offsets: create$5(core),
|
|
6833
|
+
oracles: create$4(core),
|
|
6834
|
+
trees: create$1({ db: core }),
|
|
6289
6835
|
validations: create(core),
|
|
6290
|
-
positions: create$
|
|
6291
|
-
transfers: create$
|
|
6836
|
+
positions: create$3(core),
|
|
6837
|
+
transfers: create$2(core)
|
|
6292
6838
|
};
|
|
6293
6839
|
}
|
|
6294
6840
|
const AUGMENT_CACHE = /* @__PURE__ */ new WeakMap();
|
|
@@ -6323,10 +6869,22 @@ function augmentWithDomains(base$1) {
|
|
|
6323
6869
|
value: dms.consumed,
|
|
6324
6870
|
enumerable: true
|
|
6325
6871
|
},
|
|
6872
|
+
lots: {
|
|
6873
|
+
value: dms.lots,
|
|
6874
|
+
enumerable: true
|
|
6875
|
+
},
|
|
6876
|
+
offsets: {
|
|
6877
|
+
value: dms.offsets,
|
|
6878
|
+
enumerable: true
|
|
6879
|
+
},
|
|
6326
6880
|
oracles: {
|
|
6327
6881
|
value: dms.oracles,
|
|
6328
6882
|
enumerable: true
|
|
6329
6883
|
},
|
|
6884
|
+
trees: {
|
|
6885
|
+
value: dms.trees,
|
|
6886
|
+
enumerable: true
|
|
6887
|
+
},
|
|
6330
6888
|
validations: {
|
|
6331
6889
|
value: dms.validations,
|
|
6332
6890
|
enumerable: true
|
|
@@ -6343,6 +6901,7 @@ function augmentWithDomains(base$1) {
|
|
|
6343
6901
|
AUGMENT_CACHE.set(base$1, wrapped);
|
|
6344
6902
|
return wrapped;
|
|
6345
6903
|
}
|
|
6904
|
+
let cachedInMemoryDatabase;
|
|
6346
6905
|
/**
|
|
6347
6906
|
* Connect to the database.
|
|
6348
6907
|
* @notice If no connection string is provided, an in-process PGLite database is created.
|
|
@@ -6365,15 +6924,17 @@ function connect(connectionString) {
|
|
|
6365
6924
|
clean: async () => await clean(driver$1)
|
|
6366
6925
|
});
|
|
6367
6926
|
}
|
|
6927
|
+
if (cachedInMemoryDatabase) return cachedInMemoryDatabase;
|
|
6368
6928
|
const pool = new PGlite();
|
|
6369
6929
|
const driver = drizzle$1(pool, { schema: schema_exports });
|
|
6370
6930
|
const core = augmentWithDomains(driver);
|
|
6371
|
-
|
|
6931
|
+
cachedInMemoryDatabase = Object.assign(core, {
|
|
6372
6932
|
name: "pglite",
|
|
6373
6933
|
pool,
|
|
6374
6934
|
applyMigrations: applyMigrations("pglite", driver),
|
|
6375
6935
|
clean: async () => await clean(driver)
|
|
6376
6936
|
});
|
|
6937
|
+
return cachedInMemoryDatabase;
|
|
6377
6938
|
}
|
|
6378
6939
|
const MIGRATED_DRIVERS = /* @__PURE__ */ new WeakSet();
|
|
6379
6940
|
function applyMigrations(kind, driver) {
|
|
@@ -6577,6 +7138,35 @@ async function postMigrate(driver) {
|
|
|
6577
7138
|
REFERENCING OLD TABLE AS deleted_rows
|
|
6578
7139
|
FOR EACH STATEMENT
|
|
6579
7140
|
EXECUTE FUNCTION cleanup_orphan_positions();
|
|
7141
|
+
`);
|
|
7142
|
+
await driver.execute(`
|
|
7143
|
+
CREATE OR REPLACE FUNCTION cleanup_orphan_groups()
|
|
7144
|
+
RETURNS TRIGGER AS $$
|
|
7145
|
+
BEGIN
|
|
7146
|
+
DELETE FROM "${VERSION}"."groups" g
|
|
7147
|
+
USING (
|
|
7148
|
+
SELECT DISTINCT group_chain_id, group_maker, group_group
|
|
7149
|
+
FROM deleted_rows
|
|
7150
|
+
) AS affected
|
|
7151
|
+
WHERE g.chain_id = affected.group_chain_id
|
|
7152
|
+
AND g.maker = affected.group_maker
|
|
7153
|
+
AND g."group" = affected.group_group
|
|
7154
|
+
AND NOT EXISTS (
|
|
7155
|
+
SELECT 1 FROM "${VERSION}"."offers" o
|
|
7156
|
+
WHERE o.group_chain_id = g.chain_id
|
|
7157
|
+
AND o.group_maker = g.maker
|
|
7158
|
+
AND o.group_group = g."group"
|
|
7159
|
+
);
|
|
7160
|
+
RETURN NULL;
|
|
7161
|
+
END;
|
|
7162
|
+
$$ LANGUAGE plpgsql;
|
|
7163
|
+
`);
|
|
7164
|
+
await driver.execute(`
|
|
7165
|
+
CREATE OR REPLACE TRIGGER trg_cleanup_orphan_groups
|
|
7166
|
+
AFTER DELETE ON "${VERSION}"."offers"
|
|
7167
|
+
REFERENCING OLD TABLE AS deleted_rows
|
|
7168
|
+
FOR EACH STATEMENT
|
|
7169
|
+
EXECUTE FUNCTION cleanup_orphan_groups();
|
|
6580
7170
|
`);
|
|
6581
7171
|
await driver.execute(`
|
|
6582
7172
|
CREATE OR REPLACE FUNCTION cleanup_orphan_obligations_and_oracles()
|
|
@@ -6629,13 +7219,64 @@ async function postMigrate(driver) {
|
|
|
6629
7219
|
REFERENCING OLD TABLE AS deleted_rows
|
|
6630
7220
|
FOR EACH STATEMENT
|
|
6631
7221
|
EXECUTE FUNCTION cleanup_orphan_obligations_and_oracles();
|
|
7222
|
+
`);
|
|
7223
|
+
await driver.execute(`
|
|
7224
|
+
CREATE OR REPLACE FUNCTION create_offset_on_lot_delete()
|
|
7225
|
+
RETURNS trigger
|
|
7226
|
+
LANGUAGE plpgsql AS $$
|
|
7227
|
+
BEGIN
|
|
7228
|
+
INSERT INTO "${VERSION}"."offsets" (chain_id, "user", contract, "group", value)
|
|
7229
|
+
VALUES (
|
|
7230
|
+
OLD.chain_id,
|
|
7231
|
+
OLD."user",
|
|
7232
|
+
OLD.contract,
|
|
7233
|
+
OLD."group",
|
|
7234
|
+
OLD.upper::numeric - OLD.lower::numeric
|
|
7235
|
+
)
|
|
7236
|
+
ON CONFLICT (chain_id, "user", contract, "group") DO NOTHING;
|
|
7237
|
+
RETURN OLD;
|
|
7238
|
+
END;
|
|
7239
|
+
$$;
|
|
7240
|
+
`);
|
|
7241
|
+
await driver.execute(`
|
|
7242
|
+
CREATE OR REPLACE TRIGGER trg_lots_create_offset_before_delete
|
|
7243
|
+
BEFORE DELETE ON "${VERSION}"."lots"
|
|
7244
|
+
FOR EACH ROW
|
|
7245
|
+
EXECUTE FUNCTION create_offset_on_lot_delete();
|
|
7246
|
+
`);
|
|
7247
|
+
await driver.execute(`
|
|
7248
|
+
CREATE OR REPLACE FUNCTION delete_position_if_no_lots()
|
|
7249
|
+
RETURNS trigger
|
|
7250
|
+
LANGUAGE plpgsql AS $$
|
|
7251
|
+
BEGIN
|
|
7252
|
+
-- Check if any lots remain on this position
|
|
7253
|
+
IF NOT EXISTS (
|
|
7254
|
+
SELECT 1 FROM "${VERSION}"."lots" l
|
|
7255
|
+
WHERE l.chain_id = OLD.chain_id
|
|
7256
|
+
AND l.contract = OLD.contract
|
|
7257
|
+
AND l."user" = OLD."user"
|
|
7258
|
+
) THEN
|
|
7259
|
+
-- No lots remain, delete the position (cascades to offsets)
|
|
7260
|
+
DELETE FROM "${VERSION}"."positions" p
|
|
7261
|
+
WHERE p.chain_id = OLD.chain_id
|
|
7262
|
+
AND p.contract = OLD.contract
|
|
7263
|
+
AND p."user" = OLD."user";
|
|
7264
|
+
END IF;
|
|
7265
|
+
RETURN NULL;
|
|
7266
|
+
END;
|
|
7267
|
+
$$;
|
|
7268
|
+
`);
|
|
7269
|
+
await driver.execute(`
|
|
7270
|
+
CREATE OR REPLACE TRIGGER trg_lots_delete_position_if_empty
|
|
7271
|
+
AFTER DELETE ON "${VERSION}"."lots"
|
|
7272
|
+
FOR EACH ROW
|
|
7273
|
+
EXECUTE FUNCTION delete_position_if_no_lots();
|
|
6632
7274
|
`);
|
|
6633
7275
|
});
|
|
6634
7276
|
}
|
|
6635
7277
|
|
|
6636
7278
|
//#endregion
|
|
6637
7279
|
//#region src/cli/commands/RouterCmd.ts
|
|
6638
|
-
init_esm_shims();
|
|
6639
7280
|
dotenv.config();
|
|
6640
7281
|
var RouterCmd = class RouterCmd extends Command {
|
|
6641
7282
|
constructor(name$1) {
|
|
@@ -6721,7 +7362,7 @@ mockCmd.description("Start Router mock.").addOption(new Option("--seed <n>", "Se
|
|
|
6721
7362
|
file: opts.file
|
|
6722
7363
|
});
|
|
6723
7364
|
}
|
|
6724
|
-
const gatekeeper = create$
|
|
7365
|
+
const gatekeeper = create$12({ rules: morphoRules([client.chain]) });
|
|
6725
7366
|
from({
|
|
6726
7367
|
db,
|
|
6727
7368
|
gatekeeper,
|
|
@@ -6766,7 +7407,7 @@ startCmd.description("Start Router services.").addOption(new Option("--block-win
|
|
|
6766
7407
|
await runWithLogger(logger, async () => {
|
|
6767
7408
|
const stops = [];
|
|
6768
7409
|
const selectedServices = new Set(opts.services);
|
|
6769
|
-
const gatekeeper = create$
|
|
7410
|
+
const gatekeeper = create$12({ rules: morphoRules([client.chain]) });
|
|
6770
7411
|
if (selectedServices.has("indexer")) {
|
|
6771
7412
|
const indexer = from$1({
|
|
6772
7413
|
client,
|