@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.cjs
CHANGED
|
@@ -2459,6 +2459,7 @@ function closedKey(chain, vault) {
|
|
|
2459
2459
|
// src/config/external-links.json
|
|
2460
2460
|
var external_links_default = {
|
|
2461
2461
|
hyperfxApp: "https://app.hyperfx.finance",
|
|
2462
|
+
alchemyDashboard: "https://dashboard.alchemy.com/",
|
|
2462
2463
|
hyperbridgeExplorers: {
|
|
2463
2464
|
mainnet: "https://nexus.statescan.io",
|
|
2464
2465
|
testnet: "https://gargantua.statescan.io"
|
|
@@ -2478,6 +2479,52 @@ var external_links_default = {
|
|
|
2478
2479
|
};
|
|
2479
2480
|
|
|
2480
2481
|
// src/cli/init/chains.ts
|
|
2482
|
+
var FREE_RPC_URLS = {
|
|
2483
|
+
1: [
|
|
2484
|
+
"https://mainnet.gateway.tenderly.co",
|
|
2485
|
+
"https://rpc.mevblocker.io",
|
|
2486
|
+
"https://ethereum-rpc.publicnode.com",
|
|
2487
|
+
"https://ethereum.public.blockpi.network/v1/rpc/public",
|
|
2488
|
+
"https://eth.blockrazor.xyz",
|
|
2489
|
+
"https://eth.api.pocket.network",
|
|
2490
|
+
"https://ethereum-public.nodies.app",
|
|
2491
|
+
"https://eth-mainnet.public.blastapi.io",
|
|
2492
|
+
"https://public-eth.nownodes.io"
|
|
2493
|
+
],
|
|
2494
|
+
56: [
|
|
2495
|
+
"https://bsc-rpc.publicnode.com",
|
|
2496
|
+
"https://public-bsc-mainnet.fastnode.io",
|
|
2497
|
+
"https://public-bsc.nownodes.io",
|
|
2498
|
+
"https://bsc.blockrazor.xyz",
|
|
2499
|
+
"https://rpc-bsc.48.club",
|
|
2500
|
+
"https://bsc.api.pocket.network"
|
|
2501
|
+
],
|
|
2502
|
+
137: [
|
|
2503
|
+
"https://polygon-bor-rpc.publicnode.com",
|
|
2504
|
+
"https://polygon.drpc.org",
|
|
2505
|
+
"https://polygon.gateway.tenderly.co",
|
|
2506
|
+
"https://rpc.private.mev-x.com/polygon",
|
|
2507
|
+
"https://poly.api.pocket.network"
|
|
2508
|
+
],
|
|
2509
|
+
8453: [
|
|
2510
|
+
"https://base-rpc.publicnode.com",
|
|
2511
|
+
"https://base.gateway.tenderly.co",
|
|
2512
|
+
"https://base.public.blockpi.network/v1/rpc/public",
|
|
2513
|
+
"https://base.rpc.blxrbdn.com",
|
|
2514
|
+
"https://rpc.baseazul.dev",
|
|
2515
|
+
"https://mainnet.base.org",
|
|
2516
|
+
"https://base-public.nodies.app"
|
|
2517
|
+
],
|
|
2518
|
+
42161: [
|
|
2519
|
+
"https://arbitrum-one-rpc.publicnode.com",
|
|
2520
|
+
"https://arbitrum.gateway.tenderly.co",
|
|
2521
|
+
"https://arbitrum.drpc.org",
|
|
2522
|
+
"https://public-arb-mainnet.fastnode.io",
|
|
2523
|
+
"https://arb-one.api.pocket.network",
|
|
2524
|
+
"https://arbitrum-one-public.nodies.app",
|
|
2525
|
+
"https://arb1.arbitrum.io/rpc"
|
|
2526
|
+
]
|
|
2527
|
+
};
|
|
2481
2528
|
var CHAIN_NATIVE_SYMBOLS = {
|
|
2482
2529
|
1: "ETH",
|
|
2483
2530
|
56: "BNB",
|
|
@@ -2505,7 +2552,8 @@ var INIT_CHAINS = [
|
|
|
2505
2552
|
label: "Ethereum",
|
|
2506
2553
|
network: "mainnet",
|
|
2507
2554
|
alchemySubdomain: "eth-mainnet",
|
|
2508
|
-
explorerUrl: external_links_default.chainExplorers.ethereum
|
|
2555
|
+
explorerUrl: external_links_default.chainExplorers.ethereum,
|
|
2556
|
+
defaultRpcUrls: FREE_RPC_URLS[1]
|
|
2509
2557
|
},
|
|
2510
2558
|
{
|
|
2511
2559
|
chainId: 42161,
|
|
@@ -2513,7 +2561,8 @@ var INIT_CHAINS = [
|
|
|
2513
2561
|
label: "Arbitrum",
|
|
2514
2562
|
network: "mainnet",
|
|
2515
2563
|
alchemySubdomain: "arb-mainnet",
|
|
2516
|
-
explorerUrl: external_links_default.chainExplorers.arbitrum
|
|
2564
|
+
explorerUrl: external_links_default.chainExplorers.arbitrum,
|
|
2565
|
+
defaultRpcUrls: FREE_RPC_URLS[42161]
|
|
2517
2566
|
},
|
|
2518
2567
|
{
|
|
2519
2568
|
chainId: 8453,
|
|
@@ -2521,7 +2570,8 @@ var INIT_CHAINS = [
|
|
|
2521
2570
|
label: "Base",
|
|
2522
2571
|
network: "mainnet",
|
|
2523
2572
|
alchemySubdomain: "base-mainnet",
|
|
2524
|
-
explorerUrl: external_links_default.chainExplorers.base
|
|
2573
|
+
explorerUrl: external_links_default.chainExplorers.base,
|
|
2574
|
+
defaultRpcUrls: FREE_RPC_URLS[8453]
|
|
2525
2575
|
},
|
|
2526
2576
|
{
|
|
2527
2577
|
chainId: 137,
|
|
@@ -2529,7 +2579,8 @@ var INIT_CHAINS = [
|
|
|
2529
2579
|
label: "Polygon",
|
|
2530
2580
|
network: "mainnet",
|
|
2531
2581
|
alchemySubdomain: "polygon-mainnet",
|
|
2532
|
-
explorerUrl: external_links_default.chainExplorers.polygon
|
|
2582
|
+
explorerUrl: external_links_default.chainExplorers.polygon,
|
|
2583
|
+
defaultRpcUrls: FREE_RPC_URLS[137]
|
|
2533
2584
|
},
|
|
2534
2585
|
{
|
|
2535
2586
|
chainId: 56,
|
|
@@ -2537,7 +2588,8 @@ var INIT_CHAINS = [
|
|
|
2537
2588
|
label: "BNB Chain",
|
|
2538
2589
|
network: "mainnet",
|
|
2539
2590
|
alchemySubdomain: "bnb-mainnet",
|
|
2540
|
-
explorerUrl: external_links_default.chainExplorers.bnb
|
|
2591
|
+
explorerUrl: external_links_default.chainExplorers.bnb,
|
|
2592
|
+
defaultRpcUrls: FREE_RPC_URLS[56]
|
|
2541
2593
|
},
|
|
2542
2594
|
{
|
|
2543
2595
|
chainId: 11155111,
|
|
@@ -3025,14 +3077,16 @@ var LimitOrderService = class {
|
|
|
3025
3077
|
}
|
|
3026
3078
|
}
|
|
3027
3079
|
/**
|
|
3028
|
-
* The pairs an order can be written against,
|
|
3080
|
+
* The pairs an order can be written against, the smallest payout each token may carry, and
|
|
3081
|
+
* the tokens the orderbook registers on each chain.
|
|
3029
3082
|
*
|
|
3030
3083
|
* The orderbook lists the books; `resolveBook` refuses a request naming anything else, so the
|
|
3031
|
-
* operator is offered exactly these and no combination of symbols they could not post.
|
|
3084
|
+
* operator is offered exactly these and no combination of symbols they could not post. The
|
|
3085
|
+
* chains do the same for the source chains an order can accept.
|
|
3032
3086
|
*/
|
|
3033
3087
|
async books() {
|
|
3034
3088
|
const limits = await this.limits();
|
|
3035
|
-
return { books: limits.books, minOrderSizes: limits.serverInfo.minOrderSizes };
|
|
3089
|
+
return { books: limits.books, minOrderSizes: limits.serverInfo.minOrderSizes, chains: limits.chains ?? [] };
|
|
3036
3090
|
}
|
|
3037
3091
|
/**
|
|
3038
3092
|
* Reads `serverInfo` and `books`, reusing the last read for {@link LIMITS_TTL_MS}.
|
|
@@ -3114,6 +3168,18 @@ var LimitOrderService = class {
|
|
|
3114
3168
|
);
|
|
3115
3169
|
}
|
|
3116
3170
|
}
|
|
3171
|
+
if (book.base !== book.quote) {
|
|
3172
|
+
const input = normalizeSymbol(request.tokenIn);
|
|
3173
|
+
const lacking = sources.filter((source) => {
|
|
3174
|
+
const listed = limits.chains?.find((chain) => chain.id === source);
|
|
3175
|
+
return listed && !listed.tokens.some((token) => normalizeSymbol(token.symbol) === input);
|
|
3176
|
+
});
|
|
3177
|
+
if (lacking.length > 0) {
|
|
3178
|
+
throw new LimitOrderValidationError(
|
|
3179
|
+
`The orderbook has no ${request.tokenIn} on ${lacking.join(", ")}, so swaps from there cannot pay this order`
|
|
3180
|
+
);
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3117
3183
|
if (ttlSecs < limits.serverInfo.minOrderTtlSecs) {
|
|
3118
3184
|
throw new LimitOrderValidationError(
|
|
3119
3185
|
`ttlSecs must be at least the orderbook's minimum of ${limits.serverInfo.minOrderTtlSecs}; got ${ttlSecs}`
|
|
@@ -12984,14 +13050,10 @@ var RATE_LIMIT_RPC_CODES = /* @__PURE__ */ new Set([-32005, -32097, -32016, 429]
|
|
|
12984
13050
|
var RATE_LIMIT_SUSPENSION_MS = 5 * 6e4;
|
|
12985
13051
|
var SCAN_BUDGET = {
|
|
12986
13052
|
timeoutMs: 5e3,
|
|
12987
|
-
retryCount: 1,
|
|
12988
|
-
retryDelayMs: 500,
|
|
12989
13053
|
deadlineMs: 12e3
|
|
12990
13054
|
};
|
|
12991
13055
|
var CONFIRMATION_BUDGET = {
|
|
12992
13056
|
timeoutMs: 1e4,
|
|
12993
|
-
retryCount: 1,
|
|
12994
|
-
retryDelayMs: 500,
|
|
12995
13057
|
deadlineMs: 3e4
|
|
12996
13058
|
};
|
|
12997
13059
|
var MALFORMED_RESPONSE_SUSPENSION_MS = 3e4;
|
|
@@ -13083,8 +13145,14 @@ var _QuorumPublicClient = class _QuorumPublicClient {
|
|
|
13083
13145
|
chain,
|
|
13084
13146
|
transport: (0, import_viem14.http)(url, {
|
|
13085
13147
|
timeout: budget.timeoutMs,
|
|
13086
|
-
|
|
13087
|
-
|
|
13148
|
+
// No transport-level retry. viem's retry sleeps for the provider's
|
|
13149
|
+
// `Retry-After` verbatim and uncapped, which reaches the socket
|
|
13150
|
+
// before {@link suspensionMsFor} can apply its clamp — and even
|
|
13151
|
+
// without that header, retrying inside the task is what makes a
|
|
13152
|
+
// task's worst case (timeout, backoff, timeout) approach the
|
|
13153
|
+
// deadline that is supposed to bound it. One attempt per endpoint
|
|
13154
|
+
// per call; `retryPromise` above the quorum owns the retry policy.
|
|
13155
|
+
retryCount: 0
|
|
13088
13156
|
})
|
|
13089
13157
|
})
|
|
13090
13158
|
);
|
|
@@ -13098,11 +13166,10 @@ var _QuorumPublicClient = class _QuorumPublicClient {
|
|
|
13098
13166
|
return this.rpcUrls.filter((url) => (_QuorumPublicClient.benchedUntil.get(url) ?? 0) > now);
|
|
13099
13167
|
}
|
|
13100
13168
|
/**
|
|
13101
|
-
* The endpoints this call queries
|
|
13102
|
-
*
|
|
13103
|
-
*
|
|
13104
|
-
*
|
|
13105
|
-
* queried again; there is nobody left to prefer.
|
|
13169
|
+
* The endpoints this call queries: everyone not benched. A benched endpoint is
|
|
13170
|
+
* dropped outright: it answers nothing while throttled, so asking it could only
|
|
13171
|
+
* cost a request. With every endpoint benched, everyone is queried again; there
|
|
13172
|
+
* is nobody left to prefer.
|
|
13106
13173
|
*/
|
|
13107
13174
|
participants() {
|
|
13108
13175
|
const now = Date.now();
|
|
@@ -13110,12 +13177,7 @@ var _QuorumPublicClient = class _QuorumPublicClient {
|
|
|
13110
13177
|
const active = all.filter(({ idx }) => (_QuorumPublicClient.benchedUntil.get(this.rpcUrls[idx]) ?? 0) <= now);
|
|
13111
13178
|
const allBenched = active.length === 0;
|
|
13112
13179
|
const queried = allBenched ? all : active;
|
|
13113
|
-
return {
|
|
13114
|
-
queried,
|
|
13115
|
-
skipped: all.length - queried.length,
|
|
13116
|
-
threshold: quorumThreshold(queried.length),
|
|
13117
|
-
allBenched
|
|
13118
|
-
};
|
|
13180
|
+
return { queried, skipped: all.length - queried.length, allBenched };
|
|
13119
13181
|
}
|
|
13120
13182
|
/**
|
|
13121
13183
|
* Records a task failure against its endpoint. Only the failures
|
|
@@ -13160,17 +13222,21 @@ var _QuorumPublicClient = class _QuorumPublicClient {
|
|
|
13160
13222
|
* retries would otherwise stall every confirmation poll). Stragglers settle
|
|
13161
13223
|
* out of band and are ignored. `tryDecide` returns undefined to keep
|
|
13162
13224
|
* waiting; once every task has settled, `finalize` must decide (it throws
|
|
13163
|
-
* the QuorumError when the full response set still has no quorum).
|
|
13164
|
-
*
|
|
13165
|
-
*
|
|
13225
|
+
* the QuorumError when the full response set still has no quorum).
|
|
13226
|
+
*
|
|
13227
|
+
* Both are handed the bar: the BFT threshold over the endpoints that have not
|
|
13228
|
+
* failed, answered or still out. A failure lowers it rather than counting
|
|
13229
|
+
* against it. Counting the pending endpoints keeps an early decision to a bar
|
|
13230
|
+
* no lower than the full response set would get, so a vote it did not wait for
|
|
13231
|
+
* could never have reversed it.
|
|
13166
13232
|
*
|
|
13167
13233
|
* `deadlineMs` bounds the other direction: when the bar is NOT reachable
|
|
13168
13234
|
* from the fast responders, the call used to wait out every straggler, and
|
|
13169
13235
|
* the caller's own retry loop multiplied that into minutes of a chain not
|
|
13170
13236
|
* being scanned at all, silently. At the deadline the outstanding endpoints
|
|
13171
13237
|
* are treated as failures — named in the QuorumError, so the operator sees
|
|
13172
|
-
* which ones held the call up — and `finalize` decides
|
|
13173
|
-
*
|
|
13238
|
+
* which ones held the call up — and `finalize` decides over the endpoints
|
|
13239
|
+
* that answered.
|
|
13174
13240
|
*/
|
|
13175
13241
|
settleUntilQuorum(tasks, tryDecide, finalize) {
|
|
13176
13242
|
return new Promise((resolve, reject) => {
|
|
@@ -13188,13 +13254,14 @@ var _QuorumPublicClient = class _QuorumPublicClient {
|
|
|
13188
13254
|
const evaluate = () => {
|
|
13189
13255
|
if (done) return;
|
|
13190
13256
|
try {
|
|
13191
|
-
const
|
|
13257
|
+
const threshold = quorumThreshold(tasks.length - failures.length);
|
|
13258
|
+
const early = tryDecide(fulfilled, threshold);
|
|
13192
13259
|
if (early !== void 0) {
|
|
13193
13260
|
finish(() => resolve(early));
|
|
13194
13261
|
return;
|
|
13195
13262
|
}
|
|
13196
13263
|
if (outstanding === 0) {
|
|
13197
|
-
const value = finalize(fulfilled, failures);
|
|
13264
|
+
const value = finalize(fulfilled, failures, threshold);
|
|
13198
13265
|
finish(() => resolve(value));
|
|
13199
13266
|
}
|
|
13200
13267
|
} catch (error) {
|
|
@@ -13212,7 +13279,7 @@ var _QuorumPublicClient = class _QuorumPublicClient {
|
|
|
13212
13279
|
error: new Error(`no answer within the ${this.deadlineMs}ms quorum deadline`)
|
|
13213
13280
|
}));
|
|
13214
13281
|
try {
|
|
13215
|
-
const value = finalize(fulfilled, [...failures, ...stragglers]);
|
|
13282
|
+
const value = finalize(fulfilled, [...failures, ...stragglers], quorumThreshold(fulfilled.length));
|
|
13216
13283
|
finish(() => resolve(value));
|
|
13217
13284
|
} catch (error) {
|
|
13218
13285
|
finish(() => reject(error));
|
|
@@ -13239,22 +13306,22 @@ var _QuorumPublicClient = class _QuorumPublicClient {
|
|
|
13239
13306
|
});
|
|
13240
13307
|
}
|
|
13241
13308
|
/**
|
|
13242
|
-
* Highest block head backed by a
|
|
13243
|
-
*
|
|
13309
|
+
* Highest block head backed by a quorum of the endpoints that answer. Throws
|
|
13310
|
+
* {@link QuorumError} when none do.
|
|
13244
13311
|
*/
|
|
13245
13312
|
async getBlockNumber() {
|
|
13246
|
-
const { queried, skipped,
|
|
13313
|
+
const { queried, skipped, allBenched } = this.participants();
|
|
13247
13314
|
return this.settleUntilQuorum(
|
|
13248
13315
|
queried.map(({ idx, client }) => ({ idx, task: client.getBlockNumber() })),
|
|
13249
13316
|
// Early exit as soon as a quorum is satisfiable. Late voters could
|
|
13250
13317
|
// only raise the reported head; the earlier (lower) head is
|
|
13251
13318
|
// conservative for every consumer (fewer confirmations counted,
|
|
13252
13319
|
// smaller scan windows).
|
|
13253
|
-
(fulfilled) => this.quorumHead(
|
|
13320
|
+
(fulfilled, threshold) => this.quorumHead(
|
|
13254
13321
|
fulfilled.map((f) => f.value),
|
|
13255
13322
|
threshold
|
|
13256
13323
|
) ?? void 0,
|
|
13257
|
-
(fulfilled, failures) => {
|
|
13324
|
+
(fulfilled, failures, threshold) => {
|
|
13258
13325
|
const head = this.quorumHead(
|
|
13259
13326
|
fulfilled.map((f) => f.value),
|
|
13260
13327
|
threshold
|
|
@@ -13295,7 +13362,7 @@ var _QuorumPublicClient = class _QuorumPublicClient {
|
|
|
13295
13362
|
}
|
|
13296
13363
|
return views;
|
|
13297
13364
|
};
|
|
13298
|
-
const { queried, skipped,
|
|
13365
|
+
const { queried, skipped, allBenched } = this.participants();
|
|
13299
13366
|
return this.settleUntilQuorum(
|
|
13300
13367
|
queried.map(({ idx, client }) => ({
|
|
13301
13368
|
idx,
|
|
@@ -13314,8 +13381,8 @@ var _QuorumPublicClient = class _QuorumPublicClient {
|
|
|
13314
13381
|
// Early exit only on a POSITIVE quorum: an agreeing inclusion group can
|
|
13315
13382
|
// only grow, so the first satisfiable quorum is final. Not-found votes
|
|
13316
13383
|
// never decide early — "not confirmed" needs the full response set.
|
|
13317
|
-
(fulfilled) => aggregateConfirmations(toViews(fulfilled), threshold) ?? void 0,
|
|
13318
|
-
(fulfilled, failures) => {
|
|
13384
|
+
(fulfilled, threshold) => aggregateConfirmations(toViews(fulfilled), threshold) ?? void 0,
|
|
13385
|
+
(fulfilled, failures, threshold) => {
|
|
13319
13386
|
const confirmations = aggregateConfirmations(toViews(fulfilled), threshold);
|
|
13320
13387
|
if (confirmations === null) {
|
|
13321
13388
|
throw new QuorumError(
|
|
@@ -13344,7 +13411,7 @@ var _QuorumPublicClient = class _QuorumPublicClient {
|
|
|
13344
13411
|
}
|
|
13345
13412
|
return groups;
|
|
13346
13413
|
};
|
|
13347
|
-
const { queried, skipped,
|
|
13414
|
+
const { queried, skipped, allBenched } = this.participants();
|
|
13348
13415
|
return this.settleUntilQuorum(
|
|
13349
13416
|
queried.map(({ idx, client }) => ({
|
|
13350
13417
|
idx,
|
|
@@ -13356,13 +13423,13 @@ var _QuorumPublicClient = class _QuorumPublicClient {
|
|
|
13356
13423
|
// two disjoint groups can't both hold one), so the first satisfiable
|
|
13357
13424
|
// agreement is final — no need to wait out stragglers that could
|
|
13358
13425
|
// only join or lose.
|
|
13359
|
-
(fulfilled) => {
|
|
13426
|
+
(fulfilled, threshold) => {
|
|
13360
13427
|
for (const group of groupOf(fulfilled).values()) {
|
|
13361
13428
|
if (group.providerIdxs.length >= threshold) return group.result;
|
|
13362
13429
|
}
|
|
13363
13430
|
return void 0;
|
|
13364
13431
|
},
|
|
13365
|
-
(fulfilled, failures) => {
|
|
13432
|
+
(fulfilled, failures, threshold) => {
|
|
13366
13433
|
const groups = groupOf(fulfilled);
|
|
13367
13434
|
for (const group of groups.values()) {
|
|
13368
13435
|
if (group.providerIdxs.length >= threshold) return group.result;
|
|
@@ -21275,8 +21342,15 @@ var IntentFiller = class {
|
|
|
21275
21342
|
const evalDurationSec = (Date.now() - evalStartMs) / 1e3;
|
|
21276
21343
|
this.monitor.emit("orderTiming", { orderId: order.id, phase: "evaluation", durationSec: evalDurationSec });
|
|
21277
21344
|
if (validStrategies.length === 0) {
|
|
21278
|
-
|
|
21279
|
-
this.
|
|
21345
|
+
const priced = eligibleStrategies.some((s) => s !== null);
|
|
21346
|
+
this.logger.warn(
|
|
21347
|
+
{ orderId: order.id },
|
|
21348
|
+
priced ? "No profitable strategy found for order" : "No strategy can fill this order"
|
|
21349
|
+
);
|
|
21350
|
+
this.monitor.emit("orderSkipped", {
|
|
21351
|
+
orderId: order.id,
|
|
21352
|
+
reason: priced ? "No profitable strategy" : "No strategy can fill this order"
|
|
21353
|
+
});
|
|
21280
21354
|
return null;
|
|
21281
21355
|
}
|
|
21282
21356
|
this.logger.info(
|
|
@@ -21720,16 +21794,63 @@ function byBestOfferFirst(a, b) {
|
|
|
21720
21794
|
if (a.available !== b.available) return a.available > b.available ? -1 : 1;
|
|
21721
21795
|
return a.order.id < b.order.id ? -1 : 1;
|
|
21722
21796
|
}
|
|
21797
|
+
function whyUnmatched(orders, incoming, resolve, now = /* @__PURE__ */ new Date()) {
|
|
21798
|
+
let furthest = -1;
|
|
21799
|
+
for (const order of orders) {
|
|
21800
|
+
const failed = firstFailed(order, incoming, resolve, now);
|
|
21801
|
+
furthest = Math.max(furthest, failed === null ? CHECKS.length : CHECKS.indexOf(failed));
|
|
21802
|
+
}
|
|
21803
|
+
const { source, destination, inputSymbol } = incoming;
|
|
21804
|
+
if (furthest < CHECKS.length) {
|
|
21805
|
+
switch (CHECKS[Math.max(furthest, 0)]) {
|
|
21806
|
+
case "open":
|
|
21807
|
+
return "no limit order is open";
|
|
21808
|
+
case "chain":
|
|
21809
|
+
return `no open limit order fills on ${destination}`;
|
|
21810
|
+
case "input":
|
|
21811
|
+
return `no limit order on ${destination} takes ${inputSymbol} in`;
|
|
21812
|
+
case "output":
|
|
21813
|
+
return `no limit order on ${destination} taking ${inputSymbol} in pays out the token this order asks for`;
|
|
21814
|
+
case "source":
|
|
21815
|
+
return `no limit order for this pair on ${destination} accepts swaps from ${source}`;
|
|
21816
|
+
}
|
|
21817
|
+
}
|
|
21818
|
+
const priced = orders.filter((order) => firstFailed(order, incoming, resolve, now) === null).map((order) => ({
|
|
21819
|
+
order,
|
|
21820
|
+
offer: offerFor({
|
|
21821
|
+
side: order.side,
|
|
21822
|
+
inputAmount: incoming.inputNet,
|
|
21823
|
+
price: BigInt(order.price),
|
|
21824
|
+
outputDecimals: incoming.outputDecimals
|
|
21825
|
+
})
|
|
21826
|
+
}));
|
|
21827
|
+
if (priced.some(({ offer }) => offer >= incoming.requestedOutput)) {
|
|
21828
|
+
return "the limit orders that meet its rate have nothing left to pay out";
|
|
21829
|
+
}
|
|
21830
|
+
const best = priced.reduce((a, b) => b.offer > a.offer ? b : a);
|
|
21831
|
+
const symbol = limitOrderLegs(best.order).output;
|
|
21832
|
+
return `it asks for ${readable(incoming.requestedOutput)} ${symbol}; the best limit order offers ${readable(best.offer)} ${symbol}`;
|
|
21833
|
+
}
|
|
21834
|
+
var CHECKS = ["open", "chain", "input", "output", "source"];
|
|
21723
21835
|
function serves(order, incoming, resolve, now) {
|
|
21724
|
-
|
|
21725
|
-
|
|
21726
|
-
|
|
21836
|
+
return firstFailed(order, incoming, resolve, now) === null;
|
|
21837
|
+
}
|
|
21838
|
+
function firstFailed(order, incoming, resolve, now) {
|
|
21839
|
+
if (order.status !== "open") return "open";
|
|
21840
|
+
if (order.expiresAt !== null && new Date(order.expiresAt) <= now) return "open";
|
|
21841
|
+
if (order.fillChain !== incoming.destination) return "chain";
|
|
21727
21842
|
const legs = limitOrderLegs(order);
|
|
21728
|
-
if (normalizeSymbol(legs.input) !== normalizeSymbol(incoming.inputSymbol)) return
|
|
21843
|
+
if (normalizeSymbol(legs.input) !== normalizeSymbol(incoming.inputSymbol)) return "input";
|
|
21729
21844
|
const outputToken = resolve(legs.output, order.fillChain);
|
|
21730
|
-
if (!outputToken || outputToken.toLowerCase() !== incoming.outputToken.toLowerCase()) return
|
|
21731
|
-
if (incoming.source === incoming.destination) return
|
|
21732
|
-
return order.acceptedSources.includes(incoming.source);
|
|
21845
|
+
if (!outputToken || outputToken.toLowerCase() !== incoming.outputToken.toLowerCase()) return "output";
|
|
21846
|
+
if (incoming.source === incoming.destination) return null;
|
|
21847
|
+
return order.acceptedSources.includes(incoming.source) ? null : "source";
|
|
21848
|
+
}
|
|
21849
|
+
function readable(amount) {
|
|
21850
|
+
const [whole, fraction] = toHuman(amount).split(".");
|
|
21851
|
+
const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
21852
|
+
const cut = fraction?.slice(0, 6).replace(/0+$/, "");
|
|
21853
|
+
return cut ? `${grouped}.${cut}` : grouped;
|
|
21733
21854
|
}
|
|
21734
21855
|
|
|
21735
21856
|
// src/orderbook/usd.ts
|
|
@@ -21871,15 +21992,20 @@ var FXFiller = class _FXFiller {
|
|
|
21871
21992
|
}
|
|
21872
21993
|
try {
|
|
21873
21994
|
if (order.inputs.length !== order.output.assets.length) {
|
|
21874
|
-
this.logger.
|
|
21875
|
-
{ inputs: order.inputs.length, outputs: order.output.assets.length },
|
|
21995
|
+
this.logger.info(
|
|
21996
|
+
{ orderId: order.id, inputs: order.inputs.length, outputs: order.output.assets.length },
|
|
21876
21997
|
"Order input/output length mismatch or empty"
|
|
21877
21998
|
);
|
|
21878
21999
|
return false;
|
|
21879
22000
|
}
|
|
21880
22001
|
if ((await this.matchOrder(order)).length === 0) {
|
|
21881
|
-
this.logger.
|
|
21882
|
-
{
|
|
22002
|
+
this.logger.info(
|
|
22003
|
+
{
|
|
22004
|
+
orderId: order.id,
|
|
22005
|
+
sourceChain: order.source,
|
|
22006
|
+
destChain: order.destination,
|
|
22007
|
+
reason: await this.explainUnmatched(order)
|
|
22008
|
+
},
|
|
21883
22009
|
"No limit order matches this order"
|
|
21884
22010
|
);
|
|
21885
22011
|
return false;
|
|
@@ -22421,30 +22547,53 @@ var FXFiller = class _FXFiller {
|
|
|
22421
22547
|
*/
|
|
22422
22548
|
async matchLeg(order, leg) {
|
|
22423
22549
|
if (!this.limitOrders) return [];
|
|
22550
|
+
const incoming = await this.incomingFor(order, leg);
|
|
22551
|
+
if (!incoming) return [];
|
|
22552
|
+
return matchLimitOrders(
|
|
22553
|
+
await this.limitOrders.open(),
|
|
22554
|
+
incoming,
|
|
22555
|
+
(symbol, chain) => this.registry.getAddress(symbol, chain)
|
|
22556
|
+
);
|
|
22557
|
+
}
|
|
22558
|
+
/** Why no limit order serves the order, leg by leg, for the line that says it was passed over. */
|
|
22559
|
+
async explainUnmatched(order) {
|
|
22560
|
+
if (!this.limitOrders) return "limit orders are not loaded";
|
|
22561
|
+
const open = await this.limitOrders.open();
|
|
22562
|
+
const reasons = [];
|
|
22563
|
+
for (let leg = 0; leg < order.inputs.length; leg++) {
|
|
22564
|
+
const incoming = await this.incomingFor(order, leg);
|
|
22565
|
+
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}`;
|
|
22566
|
+
reasons.push(order.inputs.length > 1 ? `leg ${leg}: ${reason}` : reason);
|
|
22567
|
+
}
|
|
22568
|
+
return reasons.join("; ");
|
|
22569
|
+
}
|
|
22570
|
+
/**
|
|
22571
|
+
* One leg as the matcher sees it, or null when its input token is not one the asset
|
|
22572
|
+
* registry knows on the source chain.
|
|
22573
|
+
*
|
|
22574
|
+
* Amounts cross into the matcher at 1e18, the unit limit orders are kept in.
|
|
22575
|
+
*/
|
|
22576
|
+
async incomingFor(order, leg) {
|
|
22424
22577
|
const input = order.inputs[leg];
|
|
22425
22578
|
const output = order.output.assets[leg];
|
|
22426
|
-
if (!input || !output) return
|
|
22579
|
+
if (!input || !output) return null;
|
|
22427
22580
|
const inputToken = (0, import_sdk15.bytes32ToBytes20)(input.token);
|
|
22428
22581
|
const outputToken = (0, import_sdk15.bytes32ToBytes20)(output.token);
|
|
22429
22582
|
const inputSymbol = this.registry.symbolFor(inputToken, order.source);
|
|
22430
|
-
if (!inputSymbol) return
|
|
22583
|
+
if (!inputSymbol) return null;
|
|
22431
22584
|
const [inputDecimals, outputDecimals] = await Promise.all([
|
|
22432
22585
|
this.contractService.getTokenDecimals(inputToken, order.source),
|
|
22433
22586
|
this.contractService.getTokenDecimals(outputToken, order.destination)
|
|
22434
22587
|
]);
|
|
22435
|
-
return
|
|
22436
|
-
|
|
22437
|
-
|
|
22438
|
-
|
|
22439
|
-
|
|
22440
|
-
|
|
22441
|
-
|
|
22442
|
-
|
|
22443
|
-
|
|
22444
|
-
outputDecimals
|
|
22445
|
-
},
|
|
22446
|
-
(symbol, chain) => this.registry.getAddress(symbol, chain)
|
|
22447
|
-
);
|
|
22588
|
+
return {
|
|
22589
|
+
source: order.source,
|
|
22590
|
+
destination: order.destination,
|
|
22591
|
+
inputSymbol,
|
|
22592
|
+
outputToken,
|
|
22593
|
+
inputNet: toScaled(input.amount, inputDecimals),
|
|
22594
|
+
requestedOutput: toScaled(output.amount, outputDecimals),
|
|
22595
|
+
outputDecimals
|
|
22596
|
+
};
|
|
22448
22597
|
}
|
|
22449
22598
|
/**
|
|
22450
22599
|
* The order's input in **USD**, or null when no limit order connects its input
|
|
@@ -24157,6 +24306,8 @@ var MAX_BLOCK_RANGE = 1000n;
|
|
|
24157
24306
|
var PUBLISH_CHUNK = 250;
|
|
24158
24307
|
var yieldToConsumers = () => new Promise((resolve) => setImmediate(resolve));
|
|
24159
24308
|
var STOP_DRAIN_TIMEOUT_MS = 5e3;
|
|
24309
|
+
var STALL_THRESHOLD_MS = 45e3;
|
|
24310
|
+
var STALL_REPORT_INTERVAL_MS = 6e4;
|
|
24160
24311
|
var ChainScanner = class {
|
|
24161
24312
|
constructor(target, loggers = defaultLoggerContext(), scanIntervalMs = DEFAULT_SCAN_INTERVAL_MS) {
|
|
24162
24313
|
this.target = target;
|
|
@@ -24170,6 +24321,18 @@ var ChainScanner = class {
|
|
|
24170
24321
|
this.errorHandler = () => {
|
|
24171
24322
|
};
|
|
24172
24323
|
this.stopped = false;
|
|
24324
|
+
/**
|
|
24325
|
+
* How far past the cursor the next pass reads, as `toBlock - fromBlock`:
|
|
24326
|
+
* halved after a failed read, doubled after a good one, between 0 (one block)
|
|
24327
|
+
* and {@link MAX_BLOCK_RANGE}.
|
|
24328
|
+
*
|
|
24329
|
+
* Free endpoints cap `eth_getLogs` ranges far below the full span — on
|
|
24330
|
+
* Arbitrum three of seven refused 100 blocks — and the cursor never moves
|
|
24331
|
+
* past a range it failed to read. At a fixed span a catch-up that outgrew
|
|
24332
|
+
* those caps was stuck for good: the range only grew, so the capped endpoints
|
|
24333
|
+
* never rejoined and the quorum never formed.
|
|
24334
|
+
*/
|
|
24335
|
+
this.span = MAX_BLOCK_RANGE;
|
|
24173
24336
|
this.logger = loggers.get("chain-scanner");
|
|
24174
24337
|
this.quorumClient = new QuorumPublicClient(target.chainId, target.rpcUrls, loggers);
|
|
24175
24338
|
if (this.quorumClient.size > 1) {
|
|
@@ -24212,8 +24375,9 @@ var ChainScanner = class {
|
|
|
24212
24375
|
if (this.timer || this.stopped) return;
|
|
24213
24376
|
this.logger.info({ chainId: this.target.chainId }, "Block scanner started");
|
|
24214
24377
|
this.timer = setInterval(() => {
|
|
24215
|
-
if (this.mutex.isLocked()) return;
|
|
24378
|
+
if (this.mutex.isLocked()) return this.reportStall();
|
|
24216
24379
|
void this.mutex.runExclusive(async () => {
|
|
24380
|
+
this.pass = { phase: "head", startedAt: Date.now(), reportedAt: 0 };
|
|
24217
24381
|
try {
|
|
24218
24382
|
await this.scan();
|
|
24219
24383
|
} catch (error) {
|
|
@@ -24227,10 +24391,66 @@ var ChainScanner = class {
|
|
|
24227
24391
|
"Error in block scanner"
|
|
24228
24392
|
);
|
|
24229
24393
|
this.errorHandler(error);
|
|
24394
|
+
} finally {
|
|
24395
|
+
this.notePassEnd();
|
|
24230
24396
|
}
|
|
24231
24397
|
});
|
|
24232
24398
|
}, this.scanIntervalMs);
|
|
24233
24399
|
}
|
|
24400
|
+
/**
|
|
24401
|
+
* Records which await the current pass is sitting on, for the watchdog.
|
|
24402
|
+
*
|
|
24403
|
+
* A no-op when no pass is registered — `scan()` is driven directly by tests
|
|
24404
|
+
* and by nothing else, so there is no watchdog to inform.
|
|
24405
|
+
*/
|
|
24406
|
+
enter(phase) {
|
|
24407
|
+
if (this.pass) this.pass.phase = phase;
|
|
24408
|
+
}
|
|
24409
|
+
/**
|
|
24410
|
+
* Says that a pass has outlived the quorum budgets and names the phase it is
|
|
24411
|
+
* stuck in, then repeats every {@link STALL_REPORT_INTERVAL_MS} until it ends.
|
|
24412
|
+
*
|
|
24413
|
+
* Observation only: the pass is left alone. Aborting it would need a cancel
|
|
24414
|
+
* path through the quorum client that does not exist, and the cursor is safe
|
|
24415
|
+
* either way — a pass that never completes never advances it.
|
|
24416
|
+
*/
|
|
24417
|
+
reportStall() {
|
|
24418
|
+
const pass = this.pass;
|
|
24419
|
+
if (!pass) return;
|
|
24420
|
+
const now = Date.now();
|
|
24421
|
+
const heldMs = now - pass.startedAt;
|
|
24422
|
+
if (heldMs < STALL_THRESHOLD_MS) return;
|
|
24423
|
+
if (pass.reportedAt !== 0 && now - pass.reportedAt < STALL_REPORT_INTERVAL_MS) return;
|
|
24424
|
+
pass.reportedAt = now;
|
|
24425
|
+
this.logger.warn(
|
|
24426
|
+
{
|
|
24427
|
+
chainId: this.target.chainId,
|
|
24428
|
+
phase: pass.phase,
|
|
24429
|
+
heldMs,
|
|
24430
|
+
cursor: this.cursor?.toString(),
|
|
24431
|
+
rpcUrls: this.target.rpcUrls
|
|
24432
|
+
},
|
|
24433
|
+
"Block scan pass has outlived its budget \u2014 this chain is not being scanned"
|
|
24434
|
+
);
|
|
24435
|
+
}
|
|
24436
|
+
/**
|
|
24437
|
+
* Closes out a pass, and says so if the watchdog complained about it — without
|
|
24438
|
+
* this the log shows a chain going quiet and never shows it coming back.
|
|
24439
|
+
*/
|
|
24440
|
+
notePassEnd() {
|
|
24441
|
+
const pass = this.pass;
|
|
24442
|
+
this.pass = void 0;
|
|
24443
|
+
if (!pass || pass.reportedAt === 0) return;
|
|
24444
|
+
this.logger.warn(
|
|
24445
|
+
{
|
|
24446
|
+
chainId: this.target.chainId,
|
|
24447
|
+
phase: pass.phase,
|
|
24448
|
+
heldMs: Date.now() - pass.startedAt,
|
|
24449
|
+
cursor: this.cursor?.toString()
|
|
24450
|
+
},
|
|
24451
|
+
"Block scan pass finished after outliving its budget \u2014 scanning has resumed"
|
|
24452
|
+
);
|
|
24453
|
+
}
|
|
24234
24454
|
/**
|
|
24235
24455
|
* Points this loop at different endpoints, keeping the cursor.
|
|
24236
24456
|
*
|
|
@@ -24272,11 +24492,12 @@ var ChainScanner = class {
|
|
|
24272
24492
|
}
|
|
24273
24493
|
if (currentBlock <= this.cursor) return;
|
|
24274
24494
|
const fromBlock = this.cursor + 1n;
|
|
24275
|
-
const toBlock = fromBlock +
|
|
24495
|
+
const toBlock = fromBlock + this.span > currentBlock ? currentBlock : fromBlock + this.span;
|
|
24276
24496
|
this.logger.debug(
|
|
24277
24497
|
{ chainId: this.target.chainId, fromBlock, toBlock, gap: Number(toBlock - fromBlock) },
|
|
24278
24498
|
"Scanning blocks"
|
|
24279
24499
|
);
|
|
24500
|
+
this.enter("logs");
|
|
24280
24501
|
let logs;
|
|
24281
24502
|
try {
|
|
24282
24503
|
logs = await (0, import_sdk17.retryPromise)(
|
|
@@ -24298,6 +24519,7 @@ var ChainScanner = class {
|
|
|
24298
24519
|
);
|
|
24299
24520
|
} catch (error) {
|
|
24300
24521
|
if (isBlockRangeError(error)) return;
|
|
24522
|
+
this.narrow(toBlock - fromBlock);
|
|
24301
24523
|
throw error;
|
|
24302
24524
|
}
|
|
24303
24525
|
if (this.stopped) return;
|
|
@@ -24308,6 +24530,7 @@ var ChainScanner = class {
|
|
|
24308
24530
|
{ chainId: this.target.chainId, fromBlock, toBlock, eventCount: placed.length },
|
|
24309
24531
|
"Found OrderPlaced events in block scan"
|
|
24310
24532
|
);
|
|
24533
|
+
this.enter("publish-orders");
|
|
24311
24534
|
await this.publishOrders(placed);
|
|
24312
24535
|
}
|
|
24313
24536
|
if (filled.length > 0) {
|
|
@@ -24315,10 +24538,36 @@ var ChainScanner = class {
|
|
|
24315
24538
|
{ chainId: this.target.chainId, fromBlock, toBlock, eventCount: filled.length },
|
|
24316
24539
|
"Found OrderFilled events in block scan"
|
|
24317
24540
|
);
|
|
24541
|
+
this.enter("publish-fills");
|
|
24318
24542
|
await this.publishFills(filled);
|
|
24319
24543
|
}
|
|
24320
24544
|
if (this.stopped) return;
|
|
24321
24545
|
this.cursor = toBlock;
|
|
24546
|
+
this.widen();
|
|
24547
|
+
}
|
|
24548
|
+
/**
|
|
24549
|
+
* Halves the span below the range that just failed, so the next pass asks
|
|
24550
|
+
* for something a range-capped endpoint will answer. Halving the attempted
|
|
24551
|
+
* range rather than the span matters near the head, where a pass reads fewer
|
|
24552
|
+
* blocks than the span allows.
|
|
24553
|
+
*/
|
|
24554
|
+
narrow(attempted) {
|
|
24555
|
+
const next = attempted / 2n;
|
|
24556
|
+
if (next >= this.span) return;
|
|
24557
|
+
this.span = next;
|
|
24558
|
+
this.logger.warn(
|
|
24559
|
+
{ chainId: this.target.chainId, span: Number(next) },
|
|
24560
|
+
"Narrowing the block scan range after a failed read"
|
|
24561
|
+
);
|
|
24562
|
+
}
|
|
24563
|
+
/** Doubles the span back toward {@link MAX_BLOCK_RANGE} after a good read. */
|
|
24564
|
+
widen() {
|
|
24565
|
+
if (this.span >= MAX_BLOCK_RANGE) return;
|
|
24566
|
+
const next = this.span === 0n ? 1n : this.span * 2n;
|
|
24567
|
+
this.span = next > MAX_BLOCK_RANGE ? MAX_BLOCK_RANGE : next;
|
|
24568
|
+
if (this.span === MAX_BLOCK_RANGE) {
|
|
24569
|
+
this.logger.info({ chainId: this.target.chainId }, "Block scan range back to full");
|
|
24570
|
+
}
|
|
24322
24571
|
}
|
|
24323
24572
|
async publishOrders(logs) {
|
|
24324
24573
|
const rebuilt = reconstructOrdersFromLogs(logs, {
|
|
@@ -24685,7 +24934,7 @@ var LimitOrderController = class {
|
|
|
24685
24934
|
]);
|
|
24686
24935
|
return { order, fills, bids };
|
|
24687
24936
|
}
|
|
24688
|
-
/** The pairs the orderbook lists,
|
|
24937
|
+
/** The pairs the orderbook lists, the smallest payout each token may carry, and its tokens per chain. */
|
|
24689
24938
|
books() {
|
|
24690
24939
|
return this.service.books();
|
|
24691
24940
|
}
|