@hyperbridge/simplex 0.16.6 → 0.16.7
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/bin/simplex.js +535 -182
- package/dist/bin/simplex.js.map +1 -1
- package/dist/index.cjs +324 -75
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +68 -50
- package/dist/index.d.ts +68 -50
- package/dist/index.js +324 -75
- package/dist/index.js.map +1 -1
- package/dist/ui/assets/index-Be446eYK.js +106 -0
- package/dist/ui/assets/index-C3T-gubp.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +2 -2
- package/dist/ui/assets/index-CsBo6xg3.js +0 -106
- package/dist/ui/assets/index-PIGVK8XG.css +0 -1
package/dist/index.js
CHANGED
|
@@ -2402,6 +2402,7 @@ function closedKey(chain, vault) {
|
|
|
2402
2402
|
// src/config/external-links.json
|
|
2403
2403
|
var external_links_default = {
|
|
2404
2404
|
hyperfxApp: "https://app.hyperfx.finance",
|
|
2405
|
+
alchemyDashboard: "https://dashboard.alchemy.com/",
|
|
2405
2406
|
hyperbridgeExplorers: {
|
|
2406
2407
|
mainnet: "https://nexus.statescan.io",
|
|
2407
2408
|
testnet: "https://gargantua.statescan.io"
|
|
@@ -2421,6 +2422,52 @@ var external_links_default = {
|
|
|
2421
2422
|
};
|
|
2422
2423
|
|
|
2423
2424
|
// src/cli/init/chains.ts
|
|
2425
|
+
var FREE_RPC_URLS = {
|
|
2426
|
+
1: [
|
|
2427
|
+
"https://mainnet.gateway.tenderly.co",
|
|
2428
|
+
"https://rpc.mevblocker.io",
|
|
2429
|
+
"https://ethereum-rpc.publicnode.com",
|
|
2430
|
+
"https://ethereum.public.blockpi.network/v1/rpc/public",
|
|
2431
|
+
"https://eth.blockrazor.xyz",
|
|
2432
|
+
"https://eth.api.pocket.network",
|
|
2433
|
+
"https://ethereum-public.nodies.app",
|
|
2434
|
+
"https://eth-mainnet.public.blastapi.io",
|
|
2435
|
+
"https://public-eth.nownodes.io"
|
|
2436
|
+
],
|
|
2437
|
+
56: [
|
|
2438
|
+
"https://bsc-rpc.publicnode.com",
|
|
2439
|
+
"https://public-bsc-mainnet.fastnode.io",
|
|
2440
|
+
"https://public-bsc.nownodes.io",
|
|
2441
|
+
"https://bsc.blockrazor.xyz",
|
|
2442
|
+
"https://rpc-bsc.48.club",
|
|
2443
|
+
"https://bsc.api.pocket.network"
|
|
2444
|
+
],
|
|
2445
|
+
137: [
|
|
2446
|
+
"https://polygon-bor-rpc.publicnode.com",
|
|
2447
|
+
"https://polygon.drpc.org",
|
|
2448
|
+
"https://polygon.gateway.tenderly.co",
|
|
2449
|
+
"https://rpc.private.mev-x.com/polygon",
|
|
2450
|
+
"https://poly.api.pocket.network"
|
|
2451
|
+
],
|
|
2452
|
+
8453: [
|
|
2453
|
+
"https://base-rpc.publicnode.com",
|
|
2454
|
+
"https://base.gateway.tenderly.co",
|
|
2455
|
+
"https://base.public.blockpi.network/v1/rpc/public",
|
|
2456
|
+
"https://base.rpc.blxrbdn.com",
|
|
2457
|
+
"https://rpc.baseazul.dev",
|
|
2458
|
+
"https://mainnet.base.org",
|
|
2459
|
+
"https://base-public.nodies.app"
|
|
2460
|
+
],
|
|
2461
|
+
42161: [
|
|
2462
|
+
"https://arbitrum-one-rpc.publicnode.com",
|
|
2463
|
+
"https://arbitrum.gateway.tenderly.co",
|
|
2464
|
+
"https://arbitrum.drpc.org",
|
|
2465
|
+
"https://public-arb-mainnet.fastnode.io",
|
|
2466
|
+
"https://arb-one.api.pocket.network",
|
|
2467
|
+
"https://arbitrum-one-public.nodies.app",
|
|
2468
|
+
"https://arb1.arbitrum.io/rpc"
|
|
2469
|
+
]
|
|
2470
|
+
};
|
|
2424
2471
|
var CHAIN_NATIVE_SYMBOLS = {
|
|
2425
2472
|
1: "ETH",
|
|
2426
2473
|
56: "BNB",
|
|
@@ -2448,7 +2495,8 @@ var INIT_CHAINS = [
|
|
|
2448
2495
|
label: "Ethereum",
|
|
2449
2496
|
network: "mainnet",
|
|
2450
2497
|
alchemySubdomain: "eth-mainnet",
|
|
2451
|
-
explorerUrl: external_links_default.chainExplorers.ethereum
|
|
2498
|
+
explorerUrl: external_links_default.chainExplorers.ethereum,
|
|
2499
|
+
defaultRpcUrls: FREE_RPC_URLS[1]
|
|
2452
2500
|
},
|
|
2453
2501
|
{
|
|
2454
2502
|
chainId: 42161,
|
|
@@ -2456,7 +2504,8 @@ var INIT_CHAINS = [
|
|
|
2456
2504
|
label: "Arbitrum",
|
|
2457
2505
|
network: "mainnet",
|
|
2458
2506
|
alchemySubdomain: "arb-mainnet",
|
|
2459
|
-
explorerUrl: external_links_default.chainExplorers.arbitrum
|
|
2507
|
+
explorerUrl: external_links_default.chainExplorers.arbitrum,
|
|
2508
|
+
defaultRpcUrls: FREE_RPC_URLS[42161]
|
|
2460
2509
|
},
|
|
2461
2510
|
{
|
|
2462
2511
|
chainId: 8453,
|
|
@@ -2464,7 +2513,8 @@ var INIT_CHAINS = [
|
|
|
2464
2513
|
label: "Base",
|
|
2465
2514
|
network: "mainnet",
|
|
2466
2515
|
alchemySubdomain: "base-mainnet",
|
|
2467
|
-
explorerUrl: external_links_default.chainExplorers.base
|
|
2516
|
+
explorerUrl: external_links_default.chainExplorers.base,
|
|
2517
|
+
defaultRpcUrls: FREE_RPC_URLS[8453]
|
|
2468
2518
|
},
|
|
2469
2519
|
{
|
|
2470
2520
|
chainId: 137,
|
|
@@ -2472,7 +2522,8 @@ var INIT_CHAINS = [
|
|
|
2472
2522
|
label: "Polygon",
|
|
2473
2523
|
network: "mainnet",
|
|
2474
2524
|
alchemySubdomain: "polygon-mainnet",
|
|
2475
|
-
explorerUrl: external_links_default.chainExplorers.polygon
|
|
2525
|
+
explorerUrl: external_links_default.chainExplorers.polygon,
|
|
2526
|
+
defaultRpcUrls: FREE_RPC_URLS[137]
|
|
2476
2527
|
},
|
|
2477
2528
|
{
|
|
2478
2529
|
chainId: 56,
|
|
@@ -2480,7 +2531,8 @@ var INIT_CHAINS = [
|
|
|
2480
2531
|
label: "BNB Chain",
|
|
2481
2532
|
network: "mainnet",
|
|
2482
2533
|
alchemySubdomain: "bnb-mainnet",
|
|
2483
|
-
explorerUrl: external_links_default.chainExplorers.bnb
|
|
2534
|
+
explorerUrl: external_links_default.chainExplorers.bnb,
|
|
2535
|
+
defaultRpcUrls: FREE_RPC_URLS[56]
|
|
2484
2536
|
},
|
|
2485
2537
|
{
|
|
2486
2538
|
chainId: 11155111,
|
|
@@ -2968,14 +3020,16 @@ var LimitOrderService = class {
|
|
|
2968
3020
|
}
|
|
2969
3021
|
}
|
|
2970
3022
|
/**
|
|
2971
|
-
* The pairs an order can be written against,
|
|
3023
|
+
* The pairs an order can be written against, the smallest payout each token may carry, and
|
|
3024
|
+
* the tokens the orderbook registers on each chain.
|
|
2972
3025
|
*
|
|
2973
3026
|
* The orderbook lists the books; `resolveBook` refuses a request naming anything else, so the
|
|
2974
|
-
* operator is offered exactly these and no combination of symbols they could not post.
|
|
3027
|
+
* operator is offered exactly these and no combination of symbols they could not post. The
|
|
3028
|
+
* chains do the same for the source chains an order can accept.
|
|
2975
3029
|
*/
|
|
2976
3030
|
async books() {
|
|
2977
3031
|
const limits = await this.limits();
|
|
2978
|
-
return { books: limits.books, minOrderSizes: limits.serverInfo.minOrderSizes };
|
|
3032
|
+
return { books: limits.books, minOrderSizes: limits.serverInfo.minOrderSizes, chains: limits.chains ?? [] };
|
|
2979
3033
|
}
|
|
2980
3034
|
/**
|
|
2981
3035
|
* Reads `serverInfo` and `books`, reusing the last read for {@link LIMITS_TTL_MS}.
|
|
@@ -3057,6 +3111,18 @@ var LimitOrderService = class {
|
|
|
3057
3111
|
);
|
|
3058
3112
|
}
|
|
3059
3113
|
}
|
|
3114
|
+
if (book.base !== book.quote) {
|
|
3115
|
+
const input = normalizeSymbol(request.tokenIn);
|
|
3116
|
+
const lacking = sources.filter((source) => {
|
|
3117
|
+
const listed = limits.chains?.find((chain) => chain.id === source);
|
|
3118
|
+
return listed && !listed.tokens.some((token) => normalizeSymbol(token.symbol) === input);
|
|
3119
|
+
});
|
|
3120
|
+
if (lacking.length > 0) {
|
|
3121
|
+
throw new LimitOrderValidationError(
|
|
3122
|
+
`The orderbook has no ${request.tokenIn} on ${lacking.join(", ")}, so swaps from there cannot pay this order`
|
|
3123
|
+
);
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3060
3126
|
if (ttlSecs < limits.serverInfo.minOrderTtlSecs) {
|
|
3061
3127
|
throw new LimitOrderValidationError(
|
|
3062
3128
|
`ttlSecs must be at least the orderbook's minimum of ${limits.serverInfo.minOrderTtlSecs}; got ${ttlSecs}`
|
|
@@ -12942,14 +13008,10 @@ var RATE_LIMIT_RPC_CODES = /* @__PURE__ */ new Set([-32005, -32097, -32016, 429]
|
|
|
12942
13008
|
var RATE_LIMIT_SUSPENSION_MS = 5 * 6e4;
|
|
12943
13009
|
var SCAN_BUDGET = {
|
|
12944
13010
|
timeoutMs: 5e3,
|
|
12945
|
-
retryCount: 1,
|
|
12946
|
-
retryDelayMs: 500,
|
|
12947
13011
|
deadlineMs: 12e3
|
|
12948
13012
|
};
|
|
12949
13013
|
var CONFIRMATION_BUDGET = {
|
|
12950
13014
|
timeoutMs: 1e4,
|
|
12951
|
-
retryCount: 1,
|
|
12952
|
-
retryDelayMs: 500,
|
|
12953
13015
|
deadlineMs: 3e4
|
|
12954
13016
|
};
|
|
12955
13017
|
var MALFORMED_RESPONSE_SUSPENSION_MS = 3e4;
|
|
@@ -13041,8 +13103,14 @@ var _QuorumPublicClient = class _QuorumPublicClient {
|
|
|
13041
13103
|
chain,
|
|
13042
13104
|
transport: http(url, {
|
|
13043
13105
|
timeout: budget.timeoutMs,
|
|
13044
|
-
|
|
13045
|
-
|
|
13106
|
+
// No transport-level retry. viem's retry sleeps for the provider's
|
|
13107
|
+
// `Retry-After` verbatim and uncapped, which reaches the socket
|
|
13108
|
+
// before {@link suspensionMsFor} can apply its clamp — and even
|
|
13109
|
+
// without that header, retrying inside the task is what makes a
|
|
13110
|
+
// task's worst case (timeout, backoff, timeout) approach the
|
|
13111
|
+
// deadline that is supposed to bound it. One attempt per endpoint
|
|
13112
|
+
// per call; `retryPromise` above the quorum owns the retry policy.
|
|
13113
|
+
retryCount: 0
|
|
13046
13114
|
})
|
|
13047
13115
|
})
|
|
13048
13116
|
);
|
|
@@ -13056,11 +13124,10 @@ var _QuorumPublicClient = class _QuorumPublicClient {
|
|
|
13056
13124
|
return this.rpcUrls.filter((url) => (_QuorumPublicClient.benchedUntil.get(url) ?? 0) > now);
|
|
13057
13125
|
}
|
|
13058
13126
|
/**
|
|
13059
|
-
* The endpoints this call queries
|
|
13060
|
-
*
|
|
13061
|
-
*
|
|
13062
|
-
*
|
|
13063
|
-
* queried again; there is nobody left to prefer.
|
|
13127
|
+
* The endpoints this call queries: everyone not benched. A benched endpoint is
|
|
13128
|
+
* dropped outright: it answers nothing while throttled, so asking it could only
|
|
13129
|
+
* cost a request. With every endpoint benched, everyone is queried again; there
|
|
13130
|
+
* is nobody left to prefer.
|
|
13064
13131
|
*/
|
|
13065
13132
|
participants() {
|
|
13066
13133
|
const now = Date.now();
|
|
@@ -13068,12 +13135,7 @@ var _QuorumPublicClient = class _QuorumPublicClient {
|
|
|
13068
13135
|
const active = all.filter(({ idx }) => (_QuorumPublicClient.benchedUntil.get(this.rpcUrls[idx]) ?? 0) <= now);
|
|
13069
13136
|
const allBenched = active.length === 0;
|
|
13070
13137
|
const queried = allBenched ? all : active;
|
|
13071
|
-
return {
|
|
13072
|
-
queried,
|
|
13073
|
-
skipped: all.length - queried.length,
|
|
13074
|
-
threshold: quorumThreshold(queried.length),
|
|
13075
|
-
allBenched
|
|
13076
|
-
};
|
|
13138
|
+
return { queried, skipped: all.length - queried.length, allBenched };
|
|
13077
13139
|
}
|
|
13078
13140
|
/**
|
|
13079
13141
|
* Records a task failure against its endpoint. Only the failures
|
|
@@ -13118,17 +13180,21 @@ var _QuorumPublicClient = class _QuorumPublicClient {
|
|
|
13118
13180
|
* retries would otherwise stall every confirmation poll). Stragglers settle
|
|
13119
13181
|
* out of band and are ignored. `tryDecide` returns undefined to keep
|
|
13120
13182
|
* waiting; once every task has settled, `finalize` must decide (it throws
|
|
13121
|
-
* the QuorumError when the full response set still has no quorum).
|
|
13122
|
-
*
|
|
13123
|
-
*
|
|
13183
|
+
* the QuorumError when the full response set still has no quorum).
|
|
13184
|
+
*
|
|
13185
|
+
* Both are handed the bar: the BFT threshold over the endpoints that have not
|
|
13186
|
+
* failed, answered or still out. A failure lowers it rather than counting
|
|
13187
|
+
* against it. Counting the pending endpoints keeps an early decision to a bar
|
|
13188
|
+
* no lower than the full response set would get, so a vote it did not wait for
|
|
13189
|
+
* could never have reversed it.
|
|
13124
13190
|
*
|
|
13125
13191
|
* `deadlineMs` bounds the other direction: when the bar is NOT reachable
|
|
13126
13192
|
* from the fast responders, the call used to wait out every straggler, and
|
|
13127
13193
|
* the caller's own retry loop multiplied that into minutes of a chain not
|
|
13128
13194
|
* being scanned at all, silently. At the deadline the outstanding endpoints
|
|
13129
13195
|
* are treated as failures — named in the QuorumError, so the operator sees
|
|
13130
|
-
* which ones held the call up — and `finalize` decides
|
|
13131
|
-
*
|
|
13196
|
+
* which ones held the call up — and `finalize` decides over the endpoints
|
|
13197
|
+
* that answered.
|
|
13132
13198
|
*/
|
|
13133
13199
|
settleUntilQuorum(tasks, tryDecide, finalize) {
|
|
13134
13200
|
return new Promise((resolve, reject) => {
|
|
@@ -13146,13 +13212,14 @@ var _QuorumPublicClient = class _QuorumPublicClient {
|
|
|
13146
13212
|
const evaluate = () => {
|
|
13147
13213
|
if (done) return;
|
|
13148
13214
|
try {
|
|
13149
|
-
const
|
|
13215
|
+
const threshold = quorumThreshold(tasks.length - failures.length);
|
|
13216
|
+
const early = tryDecide(fulfilled, threshold);
|
|
13150
13217
|
if (early !== void 0) {
|
|
13151
13218
|
finish(() => resolve(early));
|
|
13152
13219
|
return;
|
|
13153
13220
|
}
|
|
13154
13221
|
if (outstanding === 0) {
|
|
13155
|
-
const value = finalize(fulfilled, failures);
|
|
13222
|
+
const value = finalize(fulfilled, failures, threshold);
|
|
13156
13223
|
finish(() => resolve(value));
|
|
13157
13224
|
}
|
|
13158
13225
|
} catch (error) {
|
|
@@ -13170,7 +13237,7 @@ var _QuorumPublicClient = class _QuorumPublicClient {
|
|
|
13170
13237
|
error: new Error(`no answer within the ${this.deadlineMs}ms quorum deadline`)
|
|
13171
13238
|
}));
|
|
13172
13239
|
try {
|
|
13173
|
-
const value = finalize(fulfilled, [...failures, ...stragglers]);
|
|
13240
|
+
const value = finalize(fulfilled, [...failures, ...stragglers], quorumThreshold(fulfilled.length));
|
|
13174
13241
|
finish(() => resolve(value));
|
|
13175
13242
|
} catch (error) {
|
|
13176
13243
|
finish(() => reject(error));
|
|
@@ -13197,22 +13264,22 @@ var _QuorumPublicClient = class _QuorumPublicClient {
|
|
|
13197
13264
|
});
|
|
13198
13265
|
}
|
|
13199
13266
|
/**
|
|
13200
|
-
* Highest block head backed by a
|
|
13201
|
-
*
|
|
13267
|
+
* Highest block head backed by a quorum of the endpoints that answer. Throws
|
|
13268
|
+
* {@link QuorumError} when none do.
|
|
13202
13269
|
*/
|
|
13203
13270
|
async getBlockNumber() {
|
|
13204
|
-
const { queried, skipped,
|
|
13271
|
+
const { queried, skipped, allBenched } = this.participants();
|
|
13205
13272
|
return this.settleUntilQuorum(
|
|
13206
13273
|
queried.map(({ idx, client }) => ({ idx, task: client.getBlockNumber() })),
|
|
13207
13274
|
// Early exit as soon as a quorum is satisfiable. Late voters could
|
|
13208
13275
|
// only raise the reported head; the earlier (lower) head is
|
|
13209
13276
|
// conservative for every consumer (fewer confirmations counted,
|
|
13210
13277
|
// smaller scan windows).
|
|
13211
|
-
(fulfilled) => this.quorumHead(
|
|
13278
|
+
(fulfilled, threshold) => this.quorumHead(
|
|
13212
13279
|
fulfilled.map((f) => f.value),
|
|
13213
13280
|
threshold
|
|
13214
13281
|
) ?? void 0,
|
|
13215
|
-
(fulfilled, failures) => {
|
|
13282
|
+
(fulfilled, failures, threshold) => {
|
|
13216
13283
|
const head = this.quorumHead(
|
|
13217
13284
|
fulfilled.map((f) => f.value),
|
|
13218
13285
|
threshold
|
|
@@ -13253,7 +13320,7 @@ var _QuorumPublicClient = class _QuorumPublicClient {
|
|
|
13253
13320
|
}
|
|
13254
13321
|
return views;
|
|
13255
13322
|
};
|
|
13256
|
-
const { queried, skipped,
|
|
13323
|
+
const { queried, skipped, allBenched } = this.participants();
|
|
13257
13324
|
return this.settleUntilQuorum(
|
|
13258
13325
|
queried.map(({ idx, client }) => ({
|
|
13259
13326
|
idx,
|
|
@@ -13272,8 +13339,8 @@ var _QuorumPublicClient = class _QuorumPublicClient {
|
|
|
13272
13339
|
// Early exit only on a POSITIVE quorum: an agreeing inclusion group can
|
|
13273
13340
|
// only grow, so the first satisfiable quorum is final. Not-found votes
|
|
13274
13341
|
// never decide early — "not confirmed" needs the full response set.
|
|
13275
|
-
(fulfilled) => aggregateConfirmations(toViews(fulfilled), threshold) ?? void 0,
|
|
13276
|
-
(fulfilled, failures) => {
|
|
13342
|
+
(fulfilled, threshold) => aggregateConfirmations(toViews(fulfilled), threshold) ?? void 0,
|
|
13343
|
+
(fulfilled, failures, threshold) => {
|
|
13277
13344
|
const confirmations = aggregateConfirmations(toViews(fulfilled), threshold);
|
|
13278
13345
|
if (confirmations === null) {
|
|
13279
13346
|
throw new QuorumError(
|
|
@@ -13302,7 +13369,7 @@ var _QuorumPublicClient = class _QuorumPublicClient {
|
|
|
13302
13369
|
}
|
|
13303
13370
|
return groups;
|
|
13304
13371
|
};
|
|
13305
|
-
const { queried, skipped,
|
|
13372
|
+
const { queried, skipped, allBenched } = this.participants();
|
|
13306
13373
|
return this.settleUntilQuorum(
|
|
13307
13374
|
queried.map(({ idx, client }) => ({
|
|
13308
13375
|
idx,
|
|
@@ -13314,13 +13381,13 @@ var _QuorumPublicClient = class _QuorumPublicClient {
|
|
|
13314
13381
|
// two disjoint groups can't both hold one), so the first satisfiable
|
|
13315
13382
|
// agreement is final — no need to wait out stragglers that could
|
|
13316
13383
|
// only join or lose.
|
|
13317
|
-
(fulfilled) => {
|
|
13384
|
+
(fulfilled, threshold) => {
|
|
13318
13385
|
for (const group of groupOf(fulfilled).values()) {
|
|
13319
13386
|
if (group.providerIdxs.length >= threshold) return group.result;
|
|
13320
13387
|
}
|
|
13321
13388
|
return void 0;
|
|
13322
13389
|
},
|
|
13323
|
-
(fulfilled, failures) => {
|
|
13390
|
+
(fulfilled, failures, threshold) => {
|
|
13324
13391
|
const groups = groupOf(fulfilled);
|
|
13325
13392
|
for (const group of groups.values()) {
|
|
13326
13393
|
if (group.providerIdxs.length >= threshold) return group.result;
|
|
@@ -21276,8 +21343,15 @@ var IntentFiller = class {
|
|
|
21276
21343
|
const evalDurationSec = (Date.now() - evalStartMs) / 1e3;
|
|
21277
21344
|
this.monitor.emit("orderTiming", { orderId: order.id, phase: "evaluation", durationSec: evalDurationSec });
|
|
21278
21345
|
if (validStrategies.length === 0) {
|
|
21279
|
-
|
|
21280
|
-
this.
|
|
21346
|
+
const priced = eligibleStrategies.some((s) => s !== null);
|
|
21347
|
+
this.logger.warn(
|
|
21348
|
+
{ orderId: order.id },
|
|
21349
|
+
priced ? "No profitable strategy found for order" : "No strategy can fill this order"
|
|
21350
|
+
);
|
|
21351
|
+
this.monitor.emit("orderSkipped", {
|
|
21352
|
+
orderId: order.id,
|
|
21353
|
+
reason: priced ? "No profitable strategy" : "No strategy can fill this order"
|
|
21354
|
+
});
|
|
21281
21355
|
return null;
|
|
21282
21356
|
}
|
|
21283
21357
|
this.logger.info(
|
|
@@ -21724,16 +21798,63 @@ function byBestOfferFirst(a, b) {
|
|
|
21724
21798
|
if (a.available !== b.available) return a.available > b.available ? -1 : 1;
|
|
21725
21799
|
return a.order.id < b.order.id ? -1 : 1;
|
|
21726
21800
|
}
|
|
21801
|
+
function whyUnmatched(orders, incoming, resolve, now = /* @__PURE__ */ new Date()) {
|
|
21802
|
+
let furthest = -1;
|
|
21803
|
+
for (const order of orders) {
|
|
21804
|
+
const failed = firstFailed(order, incoming, resolve, now);
|
|
21805
|
+
furthest = Math.max(furthest, failed === null ? CHECKS.length : CHECKS.indexOf(failed));
|
|
21806
|
+
}
|
|
21807
|
+
const { source, destination, inputSymbol } = incoming;
|
|
21808
|
+
if (furthest < CHECKS.length) {
|
|
21809
|
+
switch (CHECKS[Math.max(furthest, 0)]) {
|
|
21810
|
+
case "open":
|
|
21811
|
+
return "no limit order is open";
|
|
21812
|
+
case "chain":
|
|
21813
|
+
return `no open limit order fills on ${destination}`;
|
|
21814
|
+
case "input":
|
|
21815
|
+
return `no limit order on ${destination} takes ${inputSymbol} in`;
|
|
21816
|
+
case "output":
|
|
21817
|
+
return `no limit order on ${destination} taking ${inputSymbol} in pays out the token this order asks for`;
|
|
21818
|
+
case "source":
|
|
21819
|
+
return `no limit order for this pair on ${destination} accepts swaps from ${source}`;
|
|
21820
|
+
}
|
|
21821
|
+
}
|
|
21822
|
+
const priced = orders.filter((order) => firstFailed(order, incoming, resolve, now) === null).map((order) => ({
|
|
21823
|
+
order,
|
|
21824
|
+
offer: offerFor({
|
|
21825
|
+
side: order.side,
|
|
21826
|
+
inputAmount: incoming.inputNet,
|
|
21827
|
+
price: BigInt(order.price),
|
|
21828
|
+
outputDecimals: incoming.outputDecimals
|
|
21829
|
+
})
|
|
21830
|
+
}));
|
|
21831
|
+
if (priced.some(({ offer }) => offer >= incoming.requestedOutput)) {
|
|
21832
|
+
return "the limit orders that meet its rate have nothing left to pay out";
|
|
21833
|
+
}
|
|
21834
|
+
const best = priced.reduce((a, b) => b.offer > a.offer ? b : a);
|
|
21835
|
+
const symbol = limitOrderLegs(best.order).output;
|
|
21836
|
+
return `it asks for ${readable(incoming.requestedOutput)} ${symbol}; the best limit order offers ${readable(best.offer)} ${symbol}`;
|
|
21837
|
+
}
|
|
21838
|
+
var CHECKS = ["open", "chain", "input", "output", "source"];
|
|
21727
21839
|
function serves(order, incoming, resolve, now) {
|
|
21728
|
-
|
|
21729
|
-
|
|
21730
|
-
|
|
21840
|
+
return firstFailed(order, incoming, resolve, now) === null;
|
|
21841
|
+
}
|
|
21842
|
+
function firstFailed(order, incoming, resolve, now) {
|
|
21843
|
+
if (order.status !== "open") return "open";
|
|
21844
|
+
if (order.expiresAt !== null && new Date(order.expiresAt) <= now) return "open";
|
|
21845
|
+
if (order.fillChain !== incoming.destination) return "chain";
|
|
21731
21846
|
const legs = limitOrderLegs(order);
|
|
21732
|
-
if (normalizeSymbol(legs.input) !== normalizeSymbol(incoming.inputSymbol)) return
|
|
21847
|
+
if (normalizeSymbol(legs.input) !== normalizeSymbol(incoming.inputSymbol)) return "input";
|
|
21733
21848
|
const outputToken = resolve(legs.output, order.fillChain);
|
|
21734
|
-
if (!outputToken || outputToken.toLowerCase() !== incoming.outputToken.toLowerCase()) return
|
|
21735
|
-
if (incoming.source === incoming.destination) return
|
|
21736
|
-
return order.acceptedSources.includes(incoming.source);
|
|
21849
|
+
if (!outputToken || outputToken.toLowerCase() !== incoming.outputToken.toLowerCase()) return "output";
|
|
21850
|
+
if (incoming.source === incoming.destination) return null;
|
|
21851
|
+
return order.acceptedSources.includes(incoming.source) ? null : "source";
|
|
21852
|
+
}
|
|
21853
|
+
function readable(amount) {
|
|
21854
|
+
const [whole, fraction] = toHuman(amount).split(".");
|
|
21855
|
+
const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
21856
|
+
const cut = fraction?.slice(0, 6).replace(/0+$/, "");
|
|
21857
|
+
return cut ? `${grouped}.${cut}` : grouped;
|
|
21737
21858
|
}
|
|
21738
21859
|
|
|
21739
21860
|
// src/orderbook/usd.ts
|
|
@@ -21875,15 +21996,20 @@ var FXFiller = class _FXFiller {
|
|
|
21875
21996
|
}
|
|
21876
21997
|
try {
|
|
21877
21998
|
if (order.inputs.length !== order.output.assets.length) {
|
|
21878
|
-
this.logger.
|
|
21879
|
-
{ inputs: order.inputs.length, outputs: order.output.assets.length },
|
|
21999
|
+
this.logger.info(
|
|
22000
|
+
{ orderId: order.id, inputs: order.inputs.length, outputs: order.output.assets.length },
|
|
21880
22001
|
"Order input/output length mismatch or empty"
|
|
21881
22002
|
);
|
|
21882
22003
|
return false;
|
|
21883
22004
|
}
|
|
21884
22005
|
if ((await this.matchOrder(order)).length === 0) {
|
|
21885
|
-
this.logger.
|
|
21886
|
-
{
|
|
22006
|
+
this.logger.info(
|
|
22007
|
+
{
|
|
22008
|
+
orderId: order.id,
|
|
22009
|
+
sourceChain: order.source,
|
|
22010
|
+
destChain: order.destination,
|
|
22011
|
+
reason: await this.explainUnmatched(order)
|
|
22012
|
+
},
|
|
21887
22013
|
"No limit order matches this order"
|
|
21888
22014
|
);
|
|
21889
22015
|
return false;
|
|
@@ -22425,30 +22551,53 @@ var FXFiller = class _FXFiller {
|
|
|
22425
22551
|
*/
|
|
22426
22552
|
async matchLeg(order, leg) {
|
|
22427
22553
|
if (!this.limitOrders) return [];
|
|
22554
|
+
const incoming = await this.incomingFor(order, leg);
|
|
22555
|
+
if (!incoming) return [];
|
|
22556
|
+
return matchLimitOrders(
|
|
22557
|
+
await this.limitOrders.open(),
|
|
22558
|
+
incoming,
|
|
22559
|
+
(symbol, chain) => this.registry.getAddress(symbol, chain)
|
|
22560
|
+
);
|
|
22561
|
+
}
|
|
22562
|
+
/** Why no limit order serves the order, leg by leg, for the line that says it was passed over. */
|
|
22563
|
+
async explainUnmatched(order) {
|
|
22564
|
+
if (!this.limitOrders) return "limit orders are not loaded";
|
|
22565
|
+
const open = await this.limitOrders.open();
|
|
22566
|
+
const reasons = [];
|
|
22567
|
+
for (let leg = 0; leg < order.inputs.length; leg++) {
|
|
22568
|
+
const incoming = await this.incomingFor(order, leg);
|
|
22569
|
+
const reason = incoming ? whyUnmatched(open, incoming, (symbol, chain) => this.registry.getAddress(symbol, chain)) : `its input token is not in the asset registry on ${order.source}`;
|
|
22570
|
+
reasons.push(order.inputs.length > 1 ? `leg ${leg}: ${reason}` : reason);
|
|
22571
|
+
}
|
|
22572
|
+
return reasons.join("; ");
|
|
22573
|
+
}
|
|
22574
|
+
/**
|
|
22575
|
+
* One leg as the matcher sees it, or null when its input token is not one the asset
|
|
22576
|
+
* registry knows on the source chain.
|
|
22577
|
+
*
|
|
22578
|
+
* Amounts cross into the matcher at 1e18, the unit limit orders are kept in.
|
|
22579
|
+
*/
|
|
22580
|
+
async incomingFor(order, leg) {
|
|
22428
22581
|
const input = order.inputs[leg];
|
|
22429
22582
|
const output = order.output.assets[leg];
|
|
22430
|
-
if (!input || !output) return
|
|
22583
|
+
if (!input || !output) return null;
|
|
22431
22584
|
const inputToken = bytes32ToBytes204(input.token);
|
|
22432
22585
|
const outputToken = bytes32ToBytes204(output.token);
|
|
22433
22586
|
const inputSymbol = this.registry.symbolFor(inputToken, order.source);
|
|
22434
|
-
if (!inputSymbol) return
|
|
22587
|
+
if (!inputSymbol) return null;
|
|
22435
22588
|
const [inputDecimals, outputDecimals] = await Promise.all([
|
|
22436
22589
|
this.contractService.getTokenDecimals(inputToken, order.source),
|
|
22437
22590
|
this.contractService.getTokenDecimals(outputToken, order.destination)
|
|
22438
22591
|
]);
|
|
22439
|
-
return
|
|
22440
|
-
|
|
22441
|
-
|
|
22442
|
-
|
|
22443
|
-
|
|
22444
|
-
|
|
22445
|
-
|
|
22446
|
-
|
|
22447
|
-
|
|
22448
|
-
outputDecimals
|
|
22449
|
-
},
|
|
22450
|
-
(symbol, chain) => this.registry.getAddress(symbol, chain)
|
|
22451
|
-
);
|
|
22592
|
+
return {
|
|
22593
|
+
source: order.source,
|
|
22594
|
+
destination: order.destination,
|
|
22595
|
+
inputSymbol,
|
|
22596
|
+
outputToken,
|
|
22597
|
+
inputNet: toScaled(input.amount, inputDecimals),
|
|
22598
|
+
requestedOutput: toScaled(output.amount, outputDecimals),
|
|
22599
|
+
outputDecimals
|
|
22600
|
+
};
|
|
22452
22601
|
}
|
|
22453
22602
|
/**
|
|
22454
22603
|
* The order's input in **USD**, or null when no limit order connects its input
|
|
@@ -24161,6 +24310,8 @@ var MAX_BLOCK_RANGE = 1000n;
|
|
|
24161
24310
|
var PUBLISH_CHUNK = 250;
|
|
24162
24311
|
var yieldToConsumers = () => new Promise((resolve) => setImmediate(resolve));
|
|
24163
24312
|
var STOP_DRAIN_TIMEOUT_MS = 5e3;
|
|
24313
|
+
var STALL_THRESHOLD_MS = 45e3;
|
|
24314
|
+
var STALL_REPORT_INTERVAL_MS = 6e4;
|
|
24164
24315
|
var ChainScanner = class {
|
|
24165
24316
|
constructor(target, loggers = defaultLoggerContext(), scanIntervalMs = DEFAULT_SCAN_INTERVAL_MS) {
|
|
24166
24317
|
this.target = target;
|
|
@@ -24174,6 +24325,18 @@ var ChainScanner = class {
|
|
|
24174
24325
|
this.errorHandler = () => {
|
|
24175
24326
|
};
|
|
24176
24327
|
this.stopped = false;
|
|
24328
|
+
/**
|
|
24329
|
+
* How far past the cursor the next pass reads, as `toBlock - fromBlock`:
|
|
24330
|
+
* halved after a failed read, doubled after a good one, between 0 (one block)
|
|
24331
|
+
* and {@link MAX_BLOCK_RANGE}.
|
|
24332
|
+
*
|
|
24333
|
+
* Free endpoints cap `eth_getLogs` ranges far below the full span — on
|
|
24334
|
+
* Arbitrum three of seven refused 100 blocks — and the cursor never moves
|
|
24335
|
+
* past a range it failed to read. At a fixed span a catch-up that outgrew
|
|
24336
|
+
* those caps was stuck for good: the range only grew, so the capped endpoints
|
|
24337
|
+
* never rejoined and the quorum never formed.
|
|
24338
|
+
*/
|
|
24339
|
+
this.span = MAX_BLOCK_RANGE;
|
|
24177
24340
|
this.logger = loggers.get("chain-scanner");
|
|
24178
24341
|
this.quorumClient = new QuorumPublicClient(target.chainId, target.rpcUrls, loggers);
|
|
24179
24342
|
if (this.quorumClient.size > 1) {
|
|
@@ -24216,8 +24379,9 @@ var ChainScanner = class {
|
|
|
24216
24379
|
if (this.timer || this.stopped) return;
|
|
24217
24380
|
this.logger.info({ chainId: this.target.chainId }, "Block scanner started");
|
|
24218
24381
|
this.timer = setInterval(() => {
|
|
24219
|
-
if (this.mutex.isLocked()) return;
|
|
24382
|
+
if (this.mutex.isLocked()) return this.reportStall();
|
|
24220
24383
|
void this.mutex.runExclusive(async () => {
|
|
24384
|
+
this.pass = { phase: "head", startedAt: Date.now(), reportedAt: 0 };
|
|
24221
24385
|
try {
|
|
24222
24386
|
await this.scan();
|
|
24223
24387
|
} catch (error) {
|
|
@@ -24231,10 +24395,66 @@ var ChainScanner = class {
|
|
|
24231
24395
|
"Error in block scanner"
|
|
24232
24396
|
);
|
|
24233
24397
|
this.errorHandler(error);
|
|
24398
|
+
} finally {
|
|
24399
|
+
this.notePassEnd();
|
|
24234
24400
|
}
|
|
24235
24401
|
});
|
|
24236
24402
|
}, this.scanIntervalMs);
|
|
24237
24403
|
}
|
|
24404
|
+
/**
|
|
24405
|
+
* Records which await the current pass is sitting on, for the watchdog.
|
|
24406
|
+
*
|
|
24407
|
+
* A no-op when no pass is registered — `scan()` is driven directly by tests
|
|
24408
|
+
* and by nothing else, so there is no watchdog to inform.
|
|
24409
|
+
*/
|
|
24410
|
+
enter(phase) {
|
|
24411
|
+
if (this.pass) this.pass.phase = phase;
|
|
24412
|
+
}
|
|
24413
|
+
/**
|
|
24414
|
+
* Says that a pass has outlived the quorum budgets and names the phase it is
|
|
24415
|
+
* stuck in, then repeats every {@link STALL_REPORT_INTERVAL_MS} until it ends.
|
|
24416
|
+
*
|
|
24417
|
+
* Observation only: the pass is left alone. Aborting it would need a cancel
|
|
24418
|
+
* path through the quorum client that does not exist, and the cursor is safe
|
|
24419
|
+
* either way — a pass that never completes never advances it.
|
|
24420
|
+
*/
|
|
24421
|
+
reportStall() {
|
|
24422
|
+
const pass = this.pass;
|
|
24423
|
+
if (!pass) return;
|
|
24424
|
+
const now = Date.now();
|
|
24425
|
+
const heldMs = now - pass.startedAt;
|
|
24426
|
+
if (heldMs < STALL_THRESHOLD_MS) return;
|
|
24427
|
+
if (pass.reportedAt !== 0 && now - pass.reportedAt < STALL_REPORT_INTERVAL_MS) return;
|
|
24428
|
+
pass.reportedAt = now;
|
|
24429
|
+
this.logger.warn(
|
|
24430
|
+
{
|
|
24431
|
+
chainId: this.target.chainId,
|
|
24432
|
+
phase: pass.phase,
|
|
24433
|
+
heldMs,
|
|
24434
|
+
cursor: this.cursor?.toString(),
|
|
24435
|
+
rpcUrls: this.target.rpcUrls
|
|
24436
|
+
},
|
|
24437
|
+
"Block scan pass has outlived its budget \u2014 this chain is not being scanned"
|
|
24438
|
+
);
|
|
24439
|
+
}
|
|
24440
|
+
/**
|
|
24441
|
+
* Closes out a pass, and says so if the watchdog complained about it — without
|
|
24442
|
+
* this the log shows a chain going quiet and never shows it coming back.
|
|
24443
|
+
*/
|
|
24444
|
+
notePassEnd() {
|
|
24445
|
+
const pass = this.pass;
|
|
24446
|
+
this.pass = void 0;
|
|
24447
|
+
if (!pass || pass.reportedAt === 0) return;
|
|
24448
|
+
this.logger.warn(
|
|
24449
|
+
{
|
|
24450
|
+
chainId: this.target.chainId,
|
|
24451
|
+
phase: pass.phase,
|
|
24452
|
+
heldMs: Date.now() - pass.startedAt,
|
|
24453
|
+
cursor: this.cursor?.toString()
|
|
24454
|
+
},
|
|
24455
|
+
"Block scan pass finished after outliving its budget \u2014 scanning has resumed"
|
|
24456
|
+
);
|
|
24457
|
+
}
|
|
24238
24458
|
/**
|
|
24239
24459
|
* Points this loop at different endpoints, keeping the cursor.
|
|
24240
24460
|
*
|
|
@@ -24276,11 +24496,12 @@ var ChainScanner = class {
|
|
|
24276
24496
|
}
|
|
24277
24497
|
if (currentBlock <= this.cursor) return;
|
|
24278
24498
|
const fromBlock = this.cursor + 1n;
|
|
24279
|
-
const toBlock = fromBlock +
|
|
24499
|
+
const toBlock = fromBlock + this.span > currentBlock ? currentBlock : fromBlock + this.span;
|
|
24280
24500
|
this.logger.debug(
|
|
24281
24501
|
{ chainId: this.target.chainId, fromBlock, toBlock, gap: Number(toBlock - fromBlock) },
|
|
24282
24502
|
"Scanning blocks"
|
|
24283
24503
|
);
|
|
24504
|
+
this.enter("logs");
|
|
24284
24505
|
let logs;
|
|
24285
24506
|
try {
|
|
24286
24507
|
logs = await retryPromise3(
|
|
@@ -24302,6 +24523,7 @@ var ChainScanner = class {
|
|
|
24302
24523
|
);
|
|
24303
24524
|
} catch (error) {
|
|
24304
24525
|
if (isBlockRangeError(error)) return;
|
|
24526
|
+
this.narrow(toBlock - fromBlock);
|
|
24305
24527
|
throw error;
|
|
24306
24528
|
}
|
|
24307
24529
|
if (this.stopped) return;
|
|
@@ -24312,6 +24534,7 @@ var ChainScanner = class {
|
|
|
24312
24534
|
{ chainId: this.target.chainId, fromBlock, toBlock, eventCount: placed.length },
|
|
24313
24535
|
"Found OrderPlaced events in block scan"
|
|
24314
24536
|
);
|
|
24537
|
+
this.enter("publish-orders");
|
|
24315
24538
|
await this.publishOrders(placed);
|
|
24316
24539
|
}
|
|
24317
24540
|
if (filled.length > 0) {
|
|
@@ -24319,10 +24542,36 @@ var ChainScanner = class {
|
|
|
24319
24542
|
{ chainId: this.target.chainId, fromBlock, toBlock, eventCount: filled.length },
|
|
24320
24543
|
"Found OrderFilled events in block scan"
|
|
24321
24544
|
);
|
|
24545
|
+
this.enter("publish-fills");
|
|
24322
24546
|
await this.publishFills(filled);
|
|
24323
24547
|
}
|
|
24324
24548
|
if (this.stopped) return;
|
|
24325
24549
|
this.cursor = toBlock;
|
|
24550
|
+
this.widen();
|
|
24551
|
+
}
|
|
24552
|
+
/**
|
|
24553
|
+
* Halves the span below the range that just failed, so the next pass asks
|
|
24554
|
+
* for something a range-capped endpoint will answer. Halving the attempted
|
|
24555
|
+
* range rather than the span matters near the head, where a pass reads fewer
|
|
24556
|
+
* blocks than the span allows.
|
|
24557
|
+
*/
|
|
24558
|
+
narrow(attempted) {
|
|
24559
|
+
const next = attempted / 2n;
|
|
24560
|
+
if (next >= this.span) return;
|
|
24561
|
+
this.span = next;
|
|
24562
|
+
this.logger.warn(
|
|
24563
|
+
{ chainId: this.target.chainId, span: Number(next) },
|
|
24564
|
+
"Narrowing the block scan range after a failed read"
|
|
24565
|
+
);
|
|
24566
|
+
}
|
|
24567
|
+
/** Doubles the span back toward {@link MAX_BLOCK_RANGE} after a good read. */
|
|
24568
|
+
widen() {
|
|
24569
|
+
if (this.span >= MAX_BLOCK_RANGE) return;
|
|
24570
|
+
const next = this.span === 0n ? 1n : this.span * 2n;
|
|
24571
|
+
this.span = next > MAX_BLOCK_RANGE ? MAX_BLOCK_RANGE : next;
|
|
24572
|
+
if (this.span === MAX_BLOCK_RANGE) {
|
|
24573
|
+
this.logger.info({ chainId: this.target.chainId }, "Block scan range back to full");
|
|
24574
|
+
}
|
|
24326
24575
|
}
|
|
24327
24576
|
async publishOrders(logs) {
|
|
24328
24577
|
const rebuilt = reconstructOrdersFromLogs(logs, {
|
|
@@ -24689,7 +24938,7 @@ var LimitOrderController = class {
|
|
|
24689
24938
|
]);
|
|
24690
24939
|
return { order, fills, bids };
|
|
24691
24940
|
}
|
|
24692
|
-
/** The pairs the orderbook lists,
|
|
24941
|
+
/** The pairs the orderbook lists, the smallest payout each token may carry, and its tokens per chain. */
|
|
24693
24942
|
books() {
|
|
24694
24943
|
return this.service.books();
|
|
24695
24944
|
}
|