@hyperbridge/simplex 0.16.8 → 0.16.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -14
- package/dist/bin/simplex.js +23713 -2079
- package/dist/bin/simplex.js.map +1 -1
- package/dist/config-storage.cjs +186 -0
- package/dist/config-storage.cjs.map +1 -0
- package/dist/config-storage.d.cts +28 -0
- package/dist/config-storage.d.ts +28 -0
- package/dist/config-storage.js +163 -0
- package/dist/config-storage.js.map +1 -0
- package/dist/index.cjs +589 -284
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +201 -100
- package/dist/index.d.ts +201 -100
- package/dist/index.js +460 -160
- package/dist/index.js.map +1 -1
- package/dist/sqlite.cjs +12 -0
- package/dist/sqlite.cjs.map +1 -1
- package/dist/sqlite.d.cts +2 -1
- package/dist/sqlite.d.ts +2 -1
- package/dist/sqlite.js +12 -0
- package/dist/sqlite.js.map +1 -1
- package/dist/{types-C7rnnKWn.d.cts → types-B5zkFhxR.d.cts} +11 -1
- package/dist/{types-C7rnnKWn.d.ts → types-B5zkFhxR.d.ts} +11 -1
- package/dist/ui/assets/{index-C3T-gubp.css → index-B_p2oQkc.css} +1 -1
- package/dist/ui/assets/{index-BQu6BMxc.js → index-CD2NZXpG.js} +35 -35
- package/dist/ui/index.html +2 -2
- package/package.json +13 -2
package/dist/index.js
CHANGED
|
@@ -2781,11 +2781,21 @@ import { randomBytes, randomUUID } from "node:crypto";
|
|
|
2781
2781
|
import { getChainId, MAX_DECLARED_ENTRIES } from "@hyperbridge/sdk";
|
|
2782
2782
|
|
|
2783
2783
|
// src/orderbook/amounts.ts
|
|
2784
|
+
import { keccak256, stringToBytes } from "viem";
|
|
2784
2785
|
var ORDERBOOK_SCALE = 10n ** 18n;
|
|
2785
2786
|
function toRaw(amount, decimals) {
|
|
2786
2787
|
const shift = 18 - decimals;
|
|
2787
2788
|
return shift >= 0 ? amount / 10n ** BigInt(shift) : amount * 10n ** BigInt(-shift);
|
|
2788
2789
|
}
|
|
2790
|
+
function budgetIdFor(limitOrderId) {
|
|
2791
|
+
return keccak256(stringToBytes(limitOrderId));
|
|
2792
|
+
}
|
|
2793
|
+
function budgetFor(order, token, decimals) {
|
|
2794
|
+
return { budgetId: budgetIdFor(order.id), cap: toRaw(BigInt(order.size), decimals), token };
|
|
2795
|
+
}
|
|
2796
|
+
function budgetRoom(budget, spent, decimals) {
|
|
2797
|
+
return spent >= budget.cap ? 0n : toScaled(budget.cap - spent, decimals);
|
|
2798
|
+
}
|
|
2789
2799
|
function fromHuman(amount) {
|
|
2790
2800
|
const text = amount.trim();
|
|
2791
2801
|
if (!/^[0-9]+(\.[0-9]+)?$/.test(text)) {
|
|
@@ -2806,6 +2816,10 @@ function toScaled(amount, decimals) {
|
|
|
2806
2816
|
const shift = 18 - decimals;
|
|
2807
2817
|
return shift >= 0 ? amount * 10n ** BigInt(shift) : amount / 10n ** BigInt(-shift);
|
|
2808
2818
|
}
|
|
2819
|
+
function toScaledUp(amount, decimals) {
|
|
2820
|
+
const shift = 18 - decimals;
|
|
2821
|
+
return shift >= 0 ? amount * 10n ** BigInt(shift) : divCeil(amount, 10n ** BigInt(-shift));
|
|
2822
|
+
}
|
|
2809
2823
|
function divCeil(numerator, denominator) {
|
|
2810
2824
|
return (numerator + denominator - 1n) / denominator;
|
|
2811
2825
|
}
|
|
@@ -2847,6 +2861,87 @@ function inputFor(params) {
|
|
|
2847
2861
|
return divCeil(scaled, unit) * unit;
|
|
2848
2862
|
}
|
|
2849
2863
|
|
|
2864
|
+
// src/orderbook/matching.ts
|
|
2865
|
+
function limitOrderLegs(order) {
|
|
2866
|
+
return order.side === "BID" ? { input: order.base, output: order.quote } : { input: order.quote, output: order.base };
|
|
2867
|
+
}
|
|
2868
|
+
function matchLimitOrders(orders, incoming, resolve, now = /* @__PURE__ */ new Date()) {
|
|
2869
|
+
const qualifying = orders.filter((order) => serves(order, incoming, resolve, now)).map((order) => {
|
|
2870
|
+
const offer = offerFor({
|
|
2871
|
+
side: order.side,
|
|
2872
|
+
inputAmount: incoming.inputNet,
|
|
2873
|
+
price: BigInt(order.price),
|
|
2874
|
+
outputDecimals: incoming.outputDecimals
|
|
2875
|
+
});
|
|
2876
|
+
const available = BigInt(order.remaining);
|
|
2877
|
+
return { order, offer, available, payout: offer < available ? offer : available };
|
|
2878
|
+
}).filter((candidate) => candidate.offer >= incoming.requestedOutput && candidate.payout > 0n).sort(byBestOfferFirst);
|
|
2879
|
+
return qualifying;
|
|
2880
|
+
}
|
|
2881
|
+
function byBestOfferFirst(a, b) {
|
|
2882
|
+
if (a.offer !== b.offer) return a.offer > b.offer ? -1 : 1;
|
|
2883
|
+
if (a.available !== b.available) return a.available > b.available ? -1 : 1;
|
|
2884
|
+
return a.order.id < b.order.id ? -1 : 1;
|
|
2885
|
+
}
|
|
2886
|
+
function whyUnmatched(orders, incoming, resolve, now = /* @__PURE__ */ new Date()) {
|
|
2887
|
+
let furthest = -1;
|
|
2888
|
+
for (const order of orders) {
|
|
2889
|
+
const failed = firstFailed(order, incoming, resolve, now);
|
|
2890
|
+
furthest = Math.max(furthest, failed === null ? CHECKS.length : CHECKS.indexOf(failed));
|
|
2891
|
+
}
|
|
2892
|
+
const { source, destination, inputSymbol } = incoming;
|
|
2893
|
+
if (furthest < CHECKS.length) {
|
|
2894
|
+
switch (CHECKS[Math.max(furthest, 0)]) {
|
|
2895
|
+
case "open":
|
|
2896
|
+
return "no limit order is open";
|
|
2897
|
+
case "chain":
|
|
2898
|
+
return `no open limit order fills on ${destination}`;
|
|
2899
|
+
case "input":
|
|
2900
|
+
return `no limit order on ${destination} takes ${inputSymbol} in`;
|
|
2901
|
+
case "output":
|
|
2902
|
+
return `no limit order on ${destination} taking ${inputSymbol} in pays out the token this order asks for`;
|
|
2903
|
+
case "source":
|
|
2904
|
+
return `no limit order for this pair on ${destination} accepts swaps from ${source}`;
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
const priced = orders.filter((order) => firstFailed(order, incoming, resolve, now) === null).map((order) => ({
|
|
2908
|
+
order,
|
|
2909
|
+
offer: offerFor({
|
|
2910
|
+
side: order.side,
|
|
2911
|
+
inputAmount: incoming.inputNet,
|
|
2912
|
+
price: BigInt(order.price),
|
|
2913
|
+
outputDecimals: incoming.outputDecimals
|
|
2914
|
+
})
|
|
2915
|
+
}));
|
|
2916
|
+
if (priced.some(({ offer }) => offer >= incoming.requestedOutput)) {
|
|
2917
|
+
return "the limit orders that meet its rate have nothing left to pay out";
|
|
2918
|
+
}
|
|
2919
|
+
const best = priced.reduce((a, b) => b.offer > a.offer ? b : a);
|
|
2920
|
+
const symbol = limitOrderLegs(best.order).output;
|
|
2921
|
+
return `it asks for ${readable(incoming.requestedOutput)} ${symbol}; the best limit order offers ${readable(best.offer)} ${symbol}`;
|
|
2922
|
+
}
|
|
2923
|
+
var CHECKS = ["open", "chain", "input", "output", "source"];
|
|
2924
|
+
function serves(order, incoming, resolve, now) {
|
|
2925
|
+
return firstFailed(order, incoming, resolve, now) === null;
|
|
2926
|
+
}
|
|
2927
|
+
function firstFailed(order, incoming, resolve, now) {
|
|
2928
|
+
if (order.status !== "open") return "open";
|
|
2929
|
+
if (order.expiresAt !== null && new Date(order.expiresAt) <= now) return "open";
|
|
2930
|
+
if (order.fillChain !== incoming.destination) return "chain";
|
|
2931
|
+
const legs = limitOrderLegs(order);
|
|
2932
|
+
if (normalizeSymbol(legs.input) !== normalizeSymbol(incoming.inputSymbol)) return "input";
|
|
2933
|
+
const outputToken = resolve(legs.output, order.fillChain);
|
|
2934
|
+
if (!outputToken || outputToken.toLowerCase() !== incoming.outputToken.toLowerCase()) return "output";
|
|
2935
|
+
if (incoming.source === incoming.destination) return null;
|
|
2936
|
+
return order.acceptedSources.includes(incoming.source) ? null : "source";
|
|
2937
|
+
}
|
|
2938
|
+
function readable(amount) {
|
|
2939
|
+
const [whole, fraction] = toHuman(amount).split(".");
|
|
2940
|
+
const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
2941
|
+
const cut = fraction?.slice(0, 6).replace(/0+$/, "");
|
|
2942
|
+
return cut ? `${grouped}.${cut}` : grouped;
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2850
2945
|
// src/orderbook/limit-orders.ts
|
|
2851
2946
|
var LIMITS_TTL_MS = 5 * 60 * 1e3;
|
|
2852
2947
|
var POSTABLE = ["open", "resizing"];
|
|
@@ -3361,6 +3456,58 @@ var LimitOrderService = class {
|
|
|
3361
3456
|
}
|
|
3362
3457
|
return report;
|
|
3363
3458
|
}
|
|
3459
|
+
/**
|
|
3460
|
+
* Lowers every open order's `remaining` to what the solver's account will still
|
|
3461
|
+
* pay for it, and puts the order back on the book at that size.
|
|
3462
|
+
*
|
|
3463
|
+
* The account tallies each order's payouts on the fill chain and refuses a fill
|
|
3464
|
+
* that would take the tally past the order's size. A fill that never settled
|
|
3465
|
+
* here, or a store restored from an older copy, leaves `remaining` above that
|
|
3466
|
+
* room and the order posted at a size the account will refuse. A tally behind
|
|
3467
|
+
* the store changes nothing: `remaining` is never raised.
|
|
3468
|
+
*
|
|
3469
|
+
* Answers how many orders were lowered.
|
|
3470
|
+
*/
|
|
3471
|
+
async reconcileTallies() {
|
|
3472
|
+
let lowered = 0;
|
|
3473
|
+
for (const order of await this.store.list({ status: "open" })) {
|
|
3474
|
+
try {
|
|
3475
|
+
const clamped = await this.clampToTally(order);
|
|
3476
|
+
if (!clamped) continue;
|
|
3477
|
+
lowered++;
|
|
3478
|
+
await this.resize(clamped, BigInt(order.remaining) - BigInt(clamped.remaining));
|
|
3479
|
+
} catch (err) {
|
|
3480
|
+
this.logger.error({ id: order.id, err }, "Could not bring the limit order in line with its on-chain tally");
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
return lowered;
|
|
3484
|
+
}
|
|
3485
|
+
/** The order as the store now holds it when its tally lowered it, null when it was left alone. */
|
|
3486
|
+
async clampToTally(order) {
|
|
3487
|
+
const token = this.assetRegistry.getAddress(limitOrderLegs(order).output, order.fillChain);
|
|
3488
|
+
if (!token) return null;
|
|
3489
|
+
const decimals = await this.contractService.getTokenDecimals(token, order.fillChain);
|
|
3490
|
+
const budget = budgetFor(order, token, decimals);
|
|
3491
|
+
const spent = await this.contractService.limitOrderSpent(order.fillChain, budget.budgetId);
|
|
3492
|
+
if (spent === null) return null;
|
|
3493
|
+
const room = budgetRoom(budget, spent, decimals);
|
|
3494
|
+
const over = BigInt(order.remaining) - (room + BigInt(order.reserved));
|
|
3495
|
+
if (over < toScaled(1n, decimals)) return null;
|
|
3496
|
+
const clamped = await this.store.clampRemaining(order.id, room.toString());
|
|
3497
|
+
if (!clamped) return null;
|
|
3498
|
+
this.logger.warn(
|
|
3499
|
+
{
|
|
3500
|
+
id: order.id,
|
|
3501
|
+
before: order.remaining,
|
|
3502
|
+
after: clamped.remaining,
|
|
3503
|
+
spent: spent.toString(),
|
|
3504
|
+
cap: budget.cap.toString(),
|
|
3505
|
+
reserved: clamped.reserved
|
|
3506
|
+
},
|
|
3507
|
+
"Limit order had more left than its on-chain tally allows; lowering it"
|
|
3508
|
+
);
|
|
3509
|
+
return clamped;
|
|
3510
|
+
}
|
|
3364
3511
|
/**
|
|
3365
3512
|
* Withdraws every order that has outlived the operator's own `expiresAt`.
|
|
3366
3513
|
*
|
|
@@ -3622,7 +3769,7 @@ async function patchRuntimeState(store, patch) {
|
|
|
3622
3769
|
}
|
|
3623
3770
|
|
|
3624
3771
|
// src/services/wallet/mpcvault.ts
|
|
3625
|
-
import { concatHex, keccak256, padHex, toHex } from "viem";
|
|
3772
|
+
import { concatHex, keccak256 as keccak2562, padHex, toHex } from "viem";
|
|
3626
3773
|
import * as grpc from "@grpc/grpc-js";
|
|
3627
3774
|
|
|
3628
3775
|
// src/proto/mpcvault/platform/v1/api.ts
|
|
@@ -12235,7 +12382,7 @@ var MpcVaultService = class {
|
|
|
12235
12382
|
const messageBytes = Buffer.from(messageHash.slice(2), "hex");
|
|
12236
12383
|
const prefix = Buffer.from(`Ethereum Signed Message:
|
|
12237
12384
|
${messageBytes.length}`);
|
|
12238
|
-
const ethSignedMessageHash =
|
|
12385
|
+
const ethSignedMessageHash = keccak2562(concatHex([toHex(prefix), messageHash]));
|
|
12239
12386
|
const ethSignedBytes = Buffer.from(ethSignedMessageHash.slice(2), "hex");
|
|
12240
12387
|
const uuid = await this.createSigningRequest({
|
|
12241
12388
|
vaultUuid: this.vaultUuid,
|
|
@@ -12323,11 +12470,12 @@ var SignerType = /* @__PURE__ */ ((SignerType2) => {
|
|
|
12323
12470
|
SignerType2["PrivateKey"] = "privateKey";
|
|
12324
12471
|
SignerType2["MpcVault"] = "mpcVault";
|
|
12325
12472
|
SignerType2["Turnkey"] = "turnkey";
|
|
12473
|
+
SignerType2["SecretPhrase"] = "secretPhrase";
|
|
12326
12474
|
return SignerType2;
|
|
12327
12475
|
})(SignerType || {});
|
|
12328
12476
|
|
|
12329
12477
|
// src/services/wallet/accounts/mpc.ts
|
|
12330
|
-
import { keccak256 as
|
|
12478
|
+
import { keccak256 as keccak2563, serializeTransaction } from "viem";
|
|
12331
12479
|
import { hashAuthorization } from "viem/utils";
|
|
12332
12480
|
function mpcVaultSigner(config) {
|
|
12333
12481
|
const service = new MpcVaultService({
|
|
@@ -12362,7 +12510,7 @@ function mpcVaultSigner(config) {
|
|
|
12362
12510
|
if (tx.authorizationList?.length) {
|
|
12363
12511
|
const serializable = { ...tx, type: "eip7702" };
|
|
12364
12512
|
const signature = await service.signRawHashComponents(
|
|
12365
|
-
|
|
12513
|
+
keccak2563(serializeTransaction(serializable))
|
|
12366
12514
|
);
|
|
12367
12515
|
return serializeTransaction(serializable, signature);
|
|
12368
12516
|
}
|
|
@@ -12444,6 +12592,51 @@ function privateKeySigner(privateKey) {
|
|
|
12444
12592
|
return viemSigner(privateKeyToAccount(privateKey));
|
|
12445
12593
|
}
|
|
12446
12594
|
|
|
12595
|
+
// src/services/wallet/accounts/secretphrase.ts
|
|
12596
|
+
import { validateMnemonic } from "@scure/bip39";
|
|
12597
|
+
import { english, mnemonicToAccount } from "viem/accounts";
|
|
12598
|
+
var WORD_COUNTS = [12, 15, 18, 21, 24];
|
|
12599
|
+
var MAX_DERIVATION_INDEX = 2 ** 31 - 1;
|
|
12600
|
+
function normaliseSecretPhrase(phrase) {
|
|
12601
|
+
return phrase.trim().split(/\s+/).join(" ").toLowerCase();
|
|
12602
|
+
}
|
|
12603
|
+
function validateSecretPhrase(phrase) {
|
|
12604
|
+
if (typeof phrase !== "string") throw new Error("Secret phrase must be a string");
|
|
12605
|
+
const normalised = normaliseSecretPhrase(phrase);
|
|
12606
|
+
const words = normalised === "" ? [] : normalised.split(" ");
|
|
12607
|
+
if (!WORD_COUNTS.includes(words.length)) {
|
|
12608
|
+
throw new Error(`Secret phrase must have ${WORD_COUNTS.join(", ")} words; got ${words.length}`);
|
|
12609
|
+
}
|
|
12610
|
+
if (words.some((word) => !english.includes(word))) {
|
|
12611
|
+
throw new Error("Secret phrase contains a word that is not in the BIP-39 English wordlist");
|
|
12612
|
+
}
|
|
12613
|
+
if (!validateMnemonic(normalised, english)) {
|
|
12614
|
+
throw new Error("Secret phrase checksum does not match; check the words and their order");
|
|
12615
|
+
}
|
|
12616
|
+
}
|
|
12617
|
+
function assertDerivationIndex(index, name = "Derivation index") {
|
|
12618
|
+
const expected = `${name} must be an integer between 0 and ${MAX_DERIVATION_INDEX}`;
|
|
12619
|
+
if (typeof index !== "number") throw new Error(expected);
|
|
12620
|
+
if (!Number.isInteger(index) || index < 0 || index > MAX_DERIVATION_INDEX) {
|
|
12621
|
+
throw new Error(`${expected}; got ${index}`);
|
|
12622
|
+
}
|
|
12623
|
+
}
|
|
12624
|
+
function secretPhraseSigner(config) {
|
|
12625
|
+
validateSecretPhrase(config.phrase);
|
|
12626
|
+
const phrase = normaliseSecretPhrase(config.phrase);
|
|
12627
|
+
const accountIndex = config.accountIndex ?? 0;
|
|
12628
|
+
assertDerivationIndex(accountIndex, "accountIndex");
|
|
12629
|
+
const derive = (index) => {
|
|
12630
|
+
assertDerivationIndex(index);
|
|
12631
|
+
return { ...viemSigner(mnemonicToAccount(phrase, { addressIndex: index })), mode: "secretPhrase" };
|
|
12632
|
+
};
|
|
12633
|
+
return { ...derive(accountIndex), accountIndex, derive };
|
|
12634
|
+
}
|
|
12635
|
+
function canDerive(signer) {
|
|
12636
|
+
const candidate = signer;
|
|
12637
|
+
return typeof candidate.derive === "function" && typeof candidate.accountIndex === "number";
|
|
12638
|
+
}
|
|
12639
|
+
|
|
12447
12640
|
// src/services/wallet/accounts/turnkey.ts
|
|
12448
12641
|
import { Turnkey } from "@turnkey/sdk-server";
|
|
12449
12642
|
import { createAccount } from "@turnkey/viem";
|
|
@@ -12467,6 +12660,7 @@ async function createSigner(config) {
|
|
|
12467
12660
|
if (config.type === "privateKey" /* PrivateKey */) return privateKeySigner(config.key);
|
|
12468
12661
|
if (config.type === "mpcVault" /* MpcVault */) return mpcVaultSigner(config);
|
|
12469
12662
|
if (config.type === "turnkey" /* Turnkey */) return turnkeySigner(config);
|
|
12663
|
+
if (config.type === "secretPhrase" /* SecretPhrase */) return secretPhraseSigner(config);
|
|
12470
12664
|
throw new Error(`Unsupported signer mode: ${config.type ?? "unknown"}`);
|
|
12471
12665
|
}
|
|
12472
12666
|
function validateSignerConfig(config) {
|
|
@@ -12492,13 +12686,21 @@ function validateSignerConfig(config) {
|
|
|
12492
12686
|
if (!config.signWith) throw new Error("simplex.signer.signWith is required");
|
|
12493
12687
|
return;
|
|
12494
12688
|
}
|
|
12689
|
+
if (config.type === "secretPhrase" /* SecretPhrase */) {
|
|
12690
|
+
if (!config.phrase) {
|
|
12691
|
+
throw new Error("simplex.signer.phrase is required when simplex.signer.type=secretPhrase");
|
|
12692
|
+
}
|
|
12693
|
+
validateSecretPhrase(config.phrase);
|
|
12694
|
+
if (config.accountIndex !== void 0) assertDerivationIndex(config.accountIndex, "simplex.signer.accountIndex");
|
|
12695
|
+
return;
|
|
12696
|
+
}
|
|
12495
12697
|
throw new Error(`Unsupported signer mode: ${config.type ?? "unknown"}`);
|
|
12496
12698
|
}
|
|
12497
12699
|
|
|
12498
12700
|
// src/services/wallet/account.ts
|
|
12499
12701
|
import {
|
|
12500
12702
|
hashTypedData,
|
|
12501
|
-
keccak256 as
|
|
12703
|
+
keccak256 as keccak2564,
|
|
12502
12704
|
serializeSignature,
|
|
12503
12705
|
serializeTransaction as serializeTransaction2
|
|
12504
12706
|
} from "viem";
|
|
@@ -12555,7 +12757,7 @@ function digestSigner(opts) {
|
|
|
12555
12757
|
),
|
|
12556
12758
|
signTransaction: async (tx) => {
|
|
12557
12759
|
const serializable = tx;
|
|
12558
|
-
const signature = await sign(
|
|
12760
|
+
const signature = await sign(keccak2564(serializeTransaction2(serializable)));
|
|
12559
12761
|
return serializeTransaction2(serializable, signature);
|
|
12560
12762
|
}
|
|
12561
12763
|
};
|
|
@@ -12585,6 +12787,12 @@ async function deriveSubstrateKeyPair(key) {
|
|
|
12585
12787
|
}
|
|
12586
12788
|
|
|
12587
12789
|
// src/services/CacheService.ts
|
|
12790
|
+
function budgetToCache(budget) {
|
|
12791
|
+
return { budgetId: budget.budgetId, cap: budget.cap.toString(), token: budget.token };
|
|
12792
|
+
}
|
|
12793
|
+
function budgetFromCache(budget) {
|
|
12794
|
+
return { budgetId: budget.budgetId, cap: BigInt(budget.cap), token: budget.token };
|
|
12795
|
+
}
|
|
12588
12796
|
var CacheService = class {
|
|
12589
12797
|
constructor(loggers = defaultLoggerContext()) {
|
|
12590
12798
|
this.CACHE_EXPIRY_MS = 1 * 60 * 1e3;
|
|
@@ -12742,7 +12950,24 @@ var CacheService = class {
|
|
|
12742
12950
|
return null;
|
|
12743
12951
|
}
|
|
12744
12952
|
}
|
|
12745
|
-
|
|
12953
|
+
/**
|
|
12954
|
+
* The budget of the limit order that priced the outputs cached for this order, or
|
|
12955
|
+
* null when no limit order did.
|
|
12956
|
+
*
|
|
12957
|
+
* Kept on the same entry as the outputs, so the two are replaced and expire
|
|
12958
|
+
* together: a bid can never sign one limit order's payout beside another's budget.
|
|
12959
|
+
*/
|
|
12960
|
+
getBidBudget(orderId) {
|
|
12961
|
+
try {
|
|
12962
|
+
const cache = this.cacheData.fillerOutputs[orderId];
|
|
12963
|
+
if (cache?.budget && this.isCacheValid(cache.timestamp)) return budgetFromCache(cache.budget);
|
|
12964
|
+
return null;
|
|
12965
|
+
} catch (error) {
|
|
12966
|
+
this.logger.error({ err: error }, "Error getting bid budget");
|
|
12967
|
+
return null;
|
|
12968
|
+
}
|
|
12969
|
+
}
|
|
12970
|
+
setFillerOutputs(orderId, outputs, inputs = [], budget) {
|
|
12746
12971
|
try {
|
|
12747
12972
|
this.cleanupStaleData();
|
|
12748
12973
|
this.cacheData.fillerOutputs[orderId] = {
|
|
@@ -12754,6 +12979,7 @@ var CacheService = class {
|
|
|
12754
12979
|
token: o.token,
|
|
12755
12980
|
amount: o.amount.toString()
|
|
12756
12981
|
})),
|
|
12982
|
+
...budget ? { budget: budgetToCache(budget) } : {},
|
|
12757
12983
|
timestamp: Date.now()
|
|
12758
12984
|
};
|
|
12759
12985
|
} catch (error) {
|
|
@@ -12799,7 +13025,8 @@ var CacheService = class {
|
|
|
12799
13025
|
data: call.data
|
|
12800
13026
|
})),
|
|
12801
13027
|
partialFill: plan.partialFill,
|
|
12802
|
-
profit: plan.profit
|
|
13028
|
+
profit: plan.profit,
|
|
13029
|
+
...plan.budget ? { budget: budgetFromCache(plan.budget) } : {}
|
|
12803
13030
|
}));
|
|
12804
13031
|
} catch (error) {
|
|
12805
13032
|
this.logger.error({ err: error }, "Error getting bid plans");
|
|
@@ -12828,7 +13055,8 @@ var CacheService = class {
|
|
|
12828
13055
|
data: call.data
|
|
12829
13056
|
})),
|
|
12830
13057
|
partialFill: plan.partialFill,
|
|
12831
|
-
profit: plan.profit
|
|
13058
|
+
profit: plan.profit,
|
|
13059
|
+
...plan.budget ? { budget: budgetToCache(plan.budget) } : {}
|
|
12832
13060
|
})),
|
|
12833
13061
|
timestamp: Date.now()
|
|
12834
13062
|
};
|
|
@@ -13661,6 +13889,44 @@ import {
|
|
|
13661
13889
|
bytes20ToBytes32
|
|
13662
13890
|
} from "@hyperbridge/sdk";
|
|
13663
13891
|
|
|
13892
|
+
// src/config/abis/SolverAccount.ts
|
|
13893
|
+
var SOLVER_ACCOUNT_ABI = [
|
|
13894
|
+
{
|
|
13895
|
+
inputs: [
|
|
13896
|
+
{ name: "orderId", type: "bytes32" },
|
|
13897
|
+
{ name: "cap", type: "uint256" },
|
|
13898
|
+
{ name: "token", type: "address" },
|
|
13899
|
+
{ name: "approved", type: "uint256" },
|
|
13900
|
+
{ name: "fee", type: "uint256" }
|
|
13901
|
+
],
|
|
13902
|
+
name: "debitOrder",
|
|
13903
|
+
outputs: [],
|
|
13904
|
+
stateMutability: "nonpayable",
|
|
13905
|
+
type: "function"
|
|
13906
|
+
},
|
|
13907
|
+
{
|
|
13908
|
+
inputs: [{ name: "orderId", type: "bytes32" }],
|
|
13909
|
+
name: "spent",
|
|
13910
|
+
outputs: [{ name: "", type: "uint256" }],
|
|
13911
|
+
stateMutability: "view",
|
|
13912
|
+
type: "function"
|
|
13913
|
+
},
|
|
13914
|
+
{
|
|
13915
|
+
inputs: [{ name: "sender", type: "address" }],
|
|
13916
|
+
name: "AccountUnauthorized",
|
|
13917
|
+
type: "error"
|
|
13918
|
+
},
|
|
13919
|
+
{
|
|
13920
|
+
inputs: [
|
|
13921
|
+
{ name: "orderId", type: "bytes32" },
|
|
13922
|
+
{ name: "total", type: "uint256" },
|
|
13923
|
+
{ name: "cap", type: "uint256" }
|
|
13924
|
+
],
|
|
13925
|
+
name: "LimitOrderExceeded",
|
|
13926
|
+
type: "error"
|
|
13927
|
+
}
|
|
13928
|
+
];
|
|
13929
|
+
|
|
13664
13930
|
// src/config/abis/EvmHost.ts
|
|
13665
13931
|
var EVM_HOST = [
|
|
13666
13932
|
{
|
|
@@ -18054,6 +18320,10 @@ function describeShortfall(balances) {
|
|
|
18054
18320
|
// src/services/ContractInteractionService.ts
|
|
18055
18321
|
Decimal2.config({ precision: 28, rounding: 4 });
|
|
18056
18322
|
var FUNDING_GAS_PER_CALL = 400000n;
|
|
18323
|
+
var DEBIT_ORDER_GAS = 60000n;
|
|
18324
|
+
function settles(budget) {
|
|
18325
|
+
return !!budget && budget.token.toLowerCase() !== ADDRESS_ZERO;
|
|
18326
|
+
}
|
|
18057
18327
|
var LIMIT_ORDER_CALL_GAS_LIMIT = 500000n;
|
|
18058
18328
|
var LIMIT_ORDER_VERIFICATION_GAS_LIMIT = 150000n;
|
|
18059
18329
|
var LIMIT_ORDER_PRE_VERIFICATION_GAS = 50000n;
|
|
@@ -18243,17 +18513,19 @@ var ContractInteractionService = class {
|
|
|
18243
18513
|
}
|
|
18244
18514
|
/**
|
|
18245
18515
|
* The call gas a bid needs: the shared estimate plus room for the funding calls
|
|
18246
|
-
* this bid prepends.
|
|
18516
|
+
* this bid prepends and for settling its limit order's budget.
|
|
18247
18517
|
*
|
|
18248
18518
|
* Funding calls are not simulated (the V4 PositionManager's flash accounting does
|
|
18249
18519
|
* not resolve in the bundler's estimation context), so each one adds a fixed
|
|
18250
18520
|
* allowance for its modifyLiquidities + take. They differ between the bids on one
|
|
18251
18521
|
* order, since each draws on its own limit order and so on its own funding, which
|
|
18252
18522
|
* is why this is worked out per bid rather than baked into the cached estimate.
|
|
18523
|
+
* The estimate is of a lone fillOrder, so the budget call is not in it either.
|
|
18253
18524
|
*/
|
|
18254
|
-
callGasLimitFor(order, baseCallGasLimit) {
|
|
18525
|
+
callGasLimitFor(order, baseCallGasLimit, budget) {
|
|
18255
18526
|
const funding = order.id ? this.cacheService.getFundingPrepends(order.id) : null;
|
|
18256
|
-
|
|
18527
|
+
const fundingGas = FUNDING_GAS_PER_CALL * BigInt(funding?.calls?.length ?? 0);
|
|
18528
|
+
return baseCallGasLimit + fundingGas + (settles(budget) ? DEBIT_ORDER_GAS : 0n);
|
|
18257
18529
|
}
|
|
18258
18530
|
/**
|
|
18259
18531
|
* Gets the fee token address and decimals for a given chain.
|
|
@@ -18435,6 +18707,27 @@ var ContractInteractionService = class {
|
|
|
18435
18707
|
{ maxRetries: 3, backoffMs: 250, logMessage: "Failed to read token balance" }
|
|
18436
18708
|
);
|
|
18437
18709
|
}
|
|
18710
|
+
/**
|
|
18711
|
+
* What the solver's account has tallied against a limit order's budget on `chain`, in
|
|
18712
|
+
* the paid token's own units, or null when it cannot be read.
|
|
18713
|
+
*
|
|
18714
|
+
* Null rather than a throw: an account whose implementation keeps no tally answers
|
|
18715
|
+
* with a revert or with no data, and the caller carries on without the figure either
|
|
18716
|
+
* way. Not retried, since that answer does not change on a second ask.
|
|
18717
|
+
*/
|
|
18718
|
+
async limitOrderSpent(chain, budgetId) {
|
|
18719
|
+
try {
|
|
18720
|
+
return await this.clientManager.getPublicClient(chain).readContract({
|
|
18721
|
+
address: this.solverAccountAddress,
|
|
18722
|
+
abi: SOLVER_ACCOUNT_ABI,
|
|
18723
|
+
functionName: "spent",
|
|
18724
|
+
args: [budgetId]
|
|
18725
|
+
});
|
|
18726
|
+
} catch (err) {
|
|
18727
|
+
this.logger.warn({ err, chain, budgetId }, "Could not read the limit order's tally from the solver account");
|
|
18728
|
+
return null;
|
|
18729
|
+
}
|
|
18730
|
+
}
|
|
18438
18731
|
async getSolverEntryPointBalance(chain) {
|
|
18439
18732
|
const entryPointAddress = this.configService.getEntryPointAddress(chain);
|
|
18440
18733
|
if (!entryPointAddress) {
|
|
@@ -18575,7 +18868,8 @@ var ContractInteractionService = class {
|
|
|
18575
18868
|
if (!cachedEstimate) {
|
|
18576
18869
|
throw new Error(`No cached gas estimate found for order ${order.id}. Call estimateGasFillPost first.`);
|
|
18577
18870
|
}
|
|
18578
|
-
const
|
|
18871
|
+
const budget = this.cacheService.getBidBudget(order.id);
|
|
18872
|
+
const callGasLimit = this.callGasLimitFor(order, cachedEstimate.callGasLimit, budget);
|
|
18579
18873
|
const cachedFillerOutputs = this.cacheService.getFillerOutputs(order.id);
|
|
18580
18874
|
const cachedFillerInputs = this.cacheService.getFillerInputs(order.id);
|
|
18581
18875
|
if (!cachedFillerOutputs || !cachedFillerInputs) {
|
|
@@ -18602,7 +18896,8 @@ var ContractInteractionService = class {
|
|
|
18602
18896
|
order,
|
|
18603
18897
|
cachedFillerOutputs,
|
|
18604
18898
|
fillOptions,
|
|
18605
|
-
cachedEstimate.totalCostInSourceFeeToken + dispatchFeeTokenAmount
|
|
18899
|
+
cachedEstimate.totalCostInSourceFeeToken + dispatchFeeTokenAmount,
|
|
18900
|
+
budget ? { budget, solverAccount: solverAccountAddress } : void 0
|
|
18606
18901
|
);
|
|
18607
18902
|
const commitment = orderCommitment2(order);
|
|
18608
18903
|
const nonce = await this.clientManager.getPublicClient(order.destination).readContract({
|
|
@@ -18794,8 +19089,13 @@ var ContractInteractionService = class {
|
|
|
18794
19089
|
* Same-chain fills release escrow locally with no Hyperbridge dispatch, so the
|
|
18795
19090
|
* gateway never pulls the fee token — its approval is skipped. Only cross-chain
|
|
18796
19091
|
* fills, which dispatch a RedeemEscrow message paid in the fee token, need it.
|
|
19092
|
+
*
|
|
19093
|
+
* A bid priced by a limit order ends with `debitOrder`, which takes the budget's id
|
|
19094
|
+
* as its `orderId`, works out what the fill paid from what is left of the allowance
|
|
19095
|
+
* and reverts the whole batch if the order's payouts would pass its size. It has to
|
|
19096
|
+
* come last, after the gateway has drawn on the allowance.
|
|
18797
19097
|
*/
|
|
18798
|
-
async buildApprovalAndFillCalldata(order, fillerOutputs, fillOptions, requiredFeeTokenAmount) {
|
|
19098
|
+
async buildApprovalAndFillCalldata(order, fillerOutputs, fillOptions, requiredFeeTokenAmount, settlement) {
|
|
18799
19099
|
const chain = order.destination;
|
|
18800
19100
|
const intentGatewayV2Address = this.configService.getIntentGatewayAddress(chain);
|
|
18801
19101
|
const perTokenRequired = /* @__PURE__ */ new Map();
|
|
@@ -18805,9 +19105,10 @@ var ContractInteractionService = class {
|
|
|
18805
19105
|
const key = addr.toLowerCase();
|
|
18806
19106
|
perTokenRequired.set(key, (perTokenRequired.get(key) ?? 0n) + output.amount);
|
|
18807
19107
|
}
|
|
19108
|
+
let feeKey;
|
|
18808
19109
|
if (order.source !== order.destination) {
|
|
18809
19110
|
const feeToken = await this.getFeeTokenWithDecimals(chain);
|
|
18810
|
-
|
|
19111
|
+
feeKey = feeToken.address.toLowerCase();
|
|
18811
19112
|
perTokenRequired.set(feeKey, (perTokenRequired.get(feeKey) ?? 0n) + requiredFeeTokenAmount);
|
|
18812
19113
|
}
|
|
18813
19114
|
const fundingPrepends = order.id ? this.cacheService.getFundingPrepends(order.id) : null;
|
|
@@ -18833,6 +19134,26 @@ var ContractInteractionService = class {
|
|
|
18833
19134
|
value: nativeOutputValue,
|
|
18834
19135
|
data: encodeFillOrder(transformOrderForContract(order), fillOptions)
|
|
18835
19136
|
});
|
|
19137
|
+
if (settlement && settles(settlement.budget)) {
|
|
19138
|
+
const { budget, solverAccount } = settlement;
|
|
19139
|
+
const budgetToken = budget.token.toLowerCase();
|
|
19140
|
+
const approved = perTokenRequired.get(budgetToken) ?? 0n;
|
|
19141
|
+
if (approved === 0n) {
|
|
19142
|
+
throw new Error(
|
|
19143
|
+
`Bid on order ${order.id} approves nothing in ${budget.token}, the token budget ${budget.budgetId} is measured in`
|
|
19144
|
+
);
|
|
19145
|
+
}
|
|
19146
|
+
const fee = feeKey === budgetToken ? fillOptions.relayerFee : 0n;
|
|
19147
|
+
calls.push({
|
|
19148
|
+
target: solverAccount,
|
|
19149
|
+
value: 0n,
|
|
19150
|
+
data: encodeFunctionData2({
|
|
19151
|
+
abi: SOLVER_ACCOUNT_ABI,
|
|
19152
|
+
functionName: "debitOrder",
|
|
19153
|
+
args: [budget.budgetId, budget.cap, budgetToken, approved, fee]
|
|
19154
|
+
})
|
|
19155
|
+
});
|
|
19156
|
+
}
|
|
18836
19157
|
return encodeERC7821ExecuteBatch2(calls);
|
|
18837
19158
|
}
|
|
18838
19159
|
};
|
|
@@ -20685,11 +21006,11 @@ function byBestRate(a, b) {
|
|
|
20685
21006
|
return left === right ? 0 : left > right ? -1 : 1;
|
|
20686
21007
|
}
|
|
20687
21008
|
function chargedFor(hold, delivered, released) {
|
|
20688
|
-
|
|
20689
|
-
|
|
20690
|
-
|
|
20691
|
-
const atRate = (
|
|
20692
|
-
return atRate > delivered ? atRate : delivered;
|
|
21009
|
+
const take = hold.take === void 0 ? 0n : BigInt(hold.take);
|
|
21010
|
+
if (take === 0n || released === 0n) return toScaledUp(delivered.raw, delivered.decimals);
|
|
21011
|
+
const offered = toRaw(BigInt(hold.amount), delivered.decimals);
|
|
21012
|
+
const atRate = (offered * released + take - 1n) / take;
|
|
21013
|
+
return toScaledUp(atRate > delivered.raw ? atRate : delivered.raw, delivered.decimals);
|
|
20693
21014
|
}
|
|
20694
21015
|
var IntentFiller = class {
|
|
20695
21016
|
constructor(chainConfigs, strategies, config, configService, chainClientManager, contractService, signer, scanners, rebalancingService, bidStorage, limitOrders, assetRegistry) {
|
|
@@ -20766,6 +21087,17 @@ var IntentFiller = class {
|
|
|
20766
21087
|
setLimitOrderService(service) {
|
|
20767
21088
|
this.limitOrderService = service;
|
|
20768
21089
|
}
|
|
21090
|
+
/**
|
|
21091
|
+
* Brings the limit orders' `remaining` in line with their on-chain tallies.
|
|
21092
|
+
*
|
|
21093
|
+
* On the settlement queue, so the pass and a fill never resize the same order at
|
|
21094
|
+
* once. Resolves when the pass has run.
|
|
21095
|
+
*/
|
|
21096
|
+
async reconcileLimitOrderTallies() {
|
|
21097
|
+
await this.settlementQueue.add(async () => {
|
|
21098
|
+
await this.limitOrderService?.reconcileTallies();
|
|
21099
|
+
});
|
|
21100
|
+
}
|
|
20769
21101
|
/** Whether the given chain id is configured for watch-only (monitor, never fill). */
|
|
20770
21102
|
isChainWatchOnly(chainId) {
|
|
20771
21103
|
const watchOnly = this.config.watchOnly;
|
|
@@ -21411,7 +21743,7 @@ var IntentFiller = class {
|
|
|
21411
21743
|
let reservation = [];
|
|
21412
21744
|
if (plan) {
|
|
21413
21745
|
if (order.id) {
|
|
21414
|
-
cache.setFillerOutputs(order.id, plan.fillerOutputs, plan.fillerInputs);
|
|
21746
|
+
cache.setFillerOutputs(order.id, plan.fillerOutputs, plan.fillerInputs, plan.budget);
|
|
21415
21747
|
cache.setPartialFill(order.id, plan.partialFill);
|
|
21416
21748
|
if (plan.fundingCalls.length > 0) cache.setFundingPrepends(order.id, plan.fundingCalls);
|
|
21417
21749
|
else cache.clearFundingPrepends(order.id);
|
|
@@ -21612,7 +21944,7 @@ var IntentFiller = class {
|
|
|
21612
21944
|
await this.releaseAll(claimed);
|
|
21613
21945
|
return;
|
|
21614
21946
|
}
|
|
21615
|
-
const settled = executedHold(claimed, delivered, released);
|
|
21947
|
+
const settled = executedHold(claimed, delivered.scaled, released);
|
|
21616
21948
|
const charged = chargedFor(settled, delivered, released);
|
|
21617
21949
|
const settledBid = rows.find(
|
|
21618
21950
|
(row) => row.reservations.some(
|
|
@@ -21665,15 +21997,15 @@ var IntentFiller = class {
|
|
|
21665
21997
|
if (delivered === null) return [];
|
|
21666
21998
|
const settled = executedHold(
|
|
21667
21999
|
holds.map((entry) => entry.hold),
|
|
21668
|
-
delivered,
|
|
22000
|
+
delivered.scaled,
|
|
21669
22001
|
released
|
|
21670
22002
|
);
|
|
21671
22003
|
const row = holds.find((entry) => entry.hold === settled).row;
|
|
21672
22004
|
return this.bidStorage.claimReservation(commitment, row.bid ?? void 0);
|
|
21673
22005
|
}
|
|
21674
22006
|
/**
|
|
21675
|
-
* What this fill delivered of the token the limit order pays,
|
|
21676
|
-
*
|
|
22007
|
+
* What this fill delivered of the token the limit order pays, or null when the
|
|
22008
|
+
* fill names none of it.
|
|
21677
22009
|
*/
|
|
21678
22010
|
async deliveredAgainst(limitOrderId, chainId, outputs) {
|
|
21679
22011
|
const order = await this.limitOrders?.get(limitOrderId);
|
|
@@ -21689,7 +22021,7 @@ var IntentFiller = class {
|
|
|
21689
22021
|
}
|
|
21690
22022
|
if (total === 0n) return null;
|
|
21691
22023
|
const decimals = await this.contractService.getTokenDecimals(address, chain);
|
|
21692
|
-
return toScaled(total, decimals);
|
|
22024
|
+
return { scaled: toScaled(total, decimals), raw: total, decimals };
|
|
21693
22025
|
}
|
|
21694
22026
|
/**
|
|
21695
22027
|
* Gives back whatever this bid still holds against its limit order.
|
|
@@ -21785,87 +22117,6 @@ import {
|
|
|
21785
22117
|
import { formatUnits as formatUnits4 } from "viem";
|
|
21786
22118
|
import { Decimal as Decimal6 } from "decimal.js";
|
|
21787
22119
|
|
|
21788
|
-
// src/orderbook/matching.ts
|
|
21789
|
-
function limitOrderLegs(order) {
|
|
21790
|
-
return order.side === "BID" ? { input: order.base, output: order.quote } : { input: order.quote, output: order.base };
|
|
21791
|
-
}
|
|
21792
|
-
function matchLimitOrders(orders, incoming, resolve, now = /* @__PURE__ */ new Date()) {
|
|
21793
|
-
const qualifying = orders.filter((order) => serves(order, incoming, resolve, now)).map((order) => {
|
|
21794
|
-
const offer = offerFor({
|
|
21795
|
-
side: order.side,
|
|
21796
|
-
inputAmount: incoming.inputNet,
|
|
21797
|
-
price: BigInt(order.price),
|
|
21798
|
-
outputDecimals: incoming.outputDecimals
|
|
21799
|
-
});
|
|
21800
|
-
const available = BigInt(order.remaining);
|
|
21801
|
-
return { order, offer, available, payout: offer < available ? offer : available };
|
|
21802
|
-
}).filter((candidate) => candidate.offer >= incoming.requestedOutput && candidate.payout > 0n).sort(byBestOfferFirst);
|
|
21803
|
-
return qualifying;
|
|
21804
|
-
}
|
|
21805
|
-
function byBestOfferFirst(a, b) {
|
|
21806
|
-
if (a.offer !== b.offer) return a.offer > b.offer ? -1 : 1;
|
|
21807
|
-
if (a.available !== b.available) return a.available > b.available ? -1 : 1;
|
|
21808
|
-
return a.order.id < b.order.id ? -1 : 1;
|
|
21809
|
-
}
|
|
21810
|
-
function whyUnmatched(orders, incoming, resolve, now = /* @__PURE__ */ new Date()) {
|
|
21811
|
-
let furthest = -1;
|
|
21812
|
-
for (const order of orders) {
|
|
21813
|
-
const failed = firstFailed(order, incoming, resolve, now);
|
|
21814
|
-
furthest = Math.max(furthest, failed === null ? CHECKS.length : CHECKS.indexOf(failed));
|
|
21815
|
-
}
|
|
21816
|
-
const { source, destination, inputSymbol } = incoming;
|
|
21817
|
-
if (furthest < CHECKS.length) {
|
|
21818
|
-
switch (CHECKS[Math.max(furthest, 0)]) {
|
|
21819
|
-
case "open":
|
|
21820
|
-
return "no limit order is open";
|
|
21821
|
-
case "chain":
|
|
21822
|
-
return `no open limit order fills on ${destination}`;
|
|
21823
|
-
case "input":
|
|
21824
|
-
return `no limit order on ${destination} takes ${inputSymbol} in`;
|
|
21825
|
-
case "output":
|
|
21826
|
-
return `no limit order on ${destination} taking ${inputSymbol} in pays out the token this order asks for`;
|
|
21827
|
-
case "source":
|
|
21828
|
-
return `no limit order for this pair on ${destination} accepts swaps from ${source}`;
|
|
21829
|
-
}
|
|
21830
|
-
}
|
|
21831
|
-
const priced = orders.filter((order) => firstFailed(order, incoming, resolve, now) === null).map((order) => ({
|
|
21832
|
-
order,
|
|
21833
|
-
offer: offerFor({
|
|
21834
|
-
side: order.side,
|
|
21835
|
-
inputAmount: incoming.inputNet,
|
|
21836
|
-
price: BigInt(order.price),
|
|
21837
|
-
outputDecimals: incoming.outputDecimals
|
|
21838
|
-
})
|
|
21839
|
-
}));
|
|
21840
|
-
if (priced.some(({ offer }) => offer >= incoming.requestedOutput)) {
|
|
21841
|
-
return "the limit orders that meet its rate have nothing left to pay out";
|
|
21842
|
-
}
|
|
21843
|
-
const best = priced.reduce((a, b) => b.offer > a.offer ? b : a);
|
|
21844
|
-
const symbol = limitOrderLegs(best.order).output;
|
|
21845
|
-
return `it asks for ${readable(incoming.requestedOutput)} ${symbol}; the best limit order offers ${readable(best.offer)} ${symbol}`;
|
|
21846
|
-
}
|
|
21847
|
-
var CHECKS = ["open", "chain", "input", "output", "source"];
|
|
21848
|
-
function serves(order, incoming, resolve, now) {
|
|
21849
|
-
return firstFailed(order, incoming, resolve, now) === null;
|
|
21850
|
-
}
|
|
21851
|
-
function firstFailed(order, incoming, resolve, now) {
|
|
21852
|
-
if (order.status !== "open") return "open";
|
|
21853
|
-
if (order.expiresAt !== null && new Date(order.expiresAt) <= now) return "open";
|
|
21854
|
-
if (order.fillChain !== incoming.destination) return "chain";
|
|
21855
|
-
const legs = limitOrderLegs(order);
|
|
21856
|
-
if (normalizeSymbol(legs.input) !== normalizeSymbol(incoming.inputSymbol)) return "input";
|
|
21857
|
-
const outputToken = resolve(legs.output, order.fillChain);
|
|
21858
|
-
if (!outputToken || outputToken.toLowerCase() !== incoming.outputToken.toLowerCase()) return "output";
|
|
21859
|
-
if (incoming.source === incoming.destination) return null;
|
|
21860
|
-
return order.acceptedSources.includes(incoming.source) ? null : "source";
|
|
21861
|
-
}
|
|
21862
|
-
function readable(amount) {
|
|
21863
|
-
const [whole, fraction] = toHuman(amount).split(".");
|
|
21864
|
-
const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
21865
|
-
const cut = fraction?.slice(0, 6).replace(/0+$/, "");
|
|
21866
|
-
return cut ? `${grouped}.${cut}` : grouped;
|
|
21867
|
-
}
|
|
21868
|
-
|
|
21869
22120
|
// src/orderbook/usd.ts
|
|
21870
22121
|
import { Decimal as Decimal5 } from "decimal.js";
|
|
21871
22122
|
function limitOrderUsdEdges(orders) {
|
|
@@ -21920,9 +22171,13 @@ var FXFiller = class _FXFiller {
|
|
|
21920
22171
|
*/
|
|
21921
22172
|
constructor(signer, configService, clientManager, contractService, pairs, registry, options) {
|
|
21922
22173
|
this.name = "FXFiller";
|
|
21923
|
-
/**
|
|
22174
|
+
/**
|
|
22175
|
+
* Consecutive orders where the overfill clamp activated. Dormant: the clamp
|
|
22176
|
+
* existed for venue-priced legs, which went with Uniswap V4 pricing, so every
|
|
22177
|
+
* outcome is now recorded unclamped and this never climbs. See `recordOrderOutcome`.
|
|
22178
|
+
*/
|
|
21924
22179
|
this.consecutiveClamps = 0;
|
|
21925
|
-
/** Once set, the filler refuses all orders until
|
|
22180
|
+
/** Once set, the filler refuses all orders until reset. Unreachable while the clamp is dormant. */
|
|
21926
22181
|
this.halted = false;
|
|
21927
22182
|
this.logger = moduleLogger(configService.loggers, "fx-simplex");
|
|
21928
22183
|
const { confirmationPolicy, fundingVenues = [], limitOrders } = options ?? {};
|
|
@@ -21971,10 +22226,9 @@ var FXFiller = class _FXFiller {
|
|
|
21971
22226
|
this.pairs.push(pair);
|
|
21972
22227
|
}
|
|
21973
22228
|
/**
|
|
21974
|
-
* Removes a market from the running engine.
|
|
21975
|
-
*
|
|
21976
|
-
*
|
|
21977
|
-
* rechecks of orders on the removed pair simply stop matching.
|
|
22229
|
+
* Removes a market from the running engine. At least one market must remain.
|
|
22230
|
+
* Matching reads the limit orders rather than the pairs, so removing a market
|
|
22231
|
+
* changes what the dashboard lists, not what the filler fills.
|
|
21978
22232
|
*/
|
|
21979
22233
|
removePair(pair) {
|
|
21980
22234
|
const index = this.pairs.indexOf(pair);
|
|
@@ -22030,22 +22284,20 @@ var FXFiller = class _FXFiller {
|
|
|
22030
22284
|
}
|
|
22031
22285
|
}
|
|
22032
22286
|
/**
|
|
22033
|
-
* Evaluates whether an order is profitable to fill
|
|
22034
|
-
*
|
|
22287
|
+
* Evaluates whether an order is profitable to fill against the operator's
|
|
22288
|
+
* limit orders and the filler's current token balances.
|
|
22035
22289
|
*
|
|
22036
22290
|
* High-level flow:
|
|
22037
|
-
* -
|
|
22038
|
-
* -
|
|
22039
|
-
*
|
|
22040
|
-
*
|
|
22041
|
-
*
|
|
22042
|
-
*
|
|
22043
|
-
*
|
|
22044
|
-
*
|
|
22045
|
-
*
|
|
22046
|
-
*
|
|
22047
|
-
* Note: we may intentionally overfill relative to the user's requested
|
|
22048
|
-
* outputs if the pair pricing makes that attractive. This is how we stay competitive.
|
|
22291
|
+
* - Match each (input, output) leg against the open limit orders.
|
|
22292
|
+
* - Build one bid per matching order, best offer first, each paying the
|
|
22293
|
+
* order's whole offer for the input at its own rate, capped by what the
|
|
22294
|
+
* order has left. The gateway credits the swapper the ask and splits
|
|
22295
|
+
* anything above it between the swapper and the protocol.
|
|
22296
|
+
* - Treat a payout below the ask as a partial fill, which only orders
|
|
22297
|
+
* without output calldata and without an earlier partial fill allow.
|
|
22298
|
+
* - Cap each payout by the filler's balance plus funding-venue withdrawals,
|
|
22299
|
+
* then gate it on fees (full fills) and the same-asset spread.
|
|
22300
|
+
* - Cache the resulting bids for later use in `executeOrder`.
|
|
22049
22301
|
*/
|
|
22050
22302
|
async calculateProfitability(order) {
|
|
22051
22303
|
if (order.id) this.contractService.cacheService.clearPartialFill(order.id);
|
|
@@ -22063,6 +22315,7 @@ var FXFiller = class _FXFiller {
|
|
|
22063
22315
|
const multiLeg = order.inputs.length > 1;
|
|
22064
22316
|
const plans = [];
|
|
22065
22317
|
const plannedOn = /* @__PURE__ */ new Map();
|
|
22318
|
+
const spentOn = /* @__PURE__ */ new Map();
|
|
22066
22319
|
for (let leg = 0; leg < order.inputs.length; leg++) {
|
|
22067
22320
|
const input = order.inputs[leg];
|
|
22068
22321
|
const output = order.output.assets[leg];
|
|
@@ -22084,7 +22337,30 @@ var FXFiller = class _FXFiller {
|
|
|
22084
22337
|
for (const candidate of matches) {
|
|
22085
22338
|
let partialFill = false;
|
|
22086
22339
|
const fundingCalls = [];
|
|
22087
|
-
const
|
|
22340
|
+
const budget = budgetFor(candidate.order, outputToken, outputDecimals);
|
|
22341
|
+
if (!spentOn.has(candidate.order.id)) {
|
|
22342
|
+
spentOn.set(
|
|
22343
|
+
candidate.order.id,
|
|
22344
|
+
await this.contractService.limitOrderSpent(destChain, budget.budgetId)
|
|
22345
|
+
);
|
|
22346
|
+
}
|
|
22347
|
+
const spent = spentOn.get(candidate.order.id) ?? null;
|
|
22348
|
+
const room = spent === null ? candidate.available : budgetRoom(budget, spent, outputDecimals);
|
|
22349
|
+
if (room === 0n) {
|
|
22350
|
+
this.logger.info(
|
|
22351
|
+
{
|
|
22352
|
+
orderId: order.id,
|
|
22353
|
+
leg,
|
|
22354
|
+
limitOrder: candidate.order.id,
|
|
22355
|
+
spent: spent?.toString(),
|
|
22356
|
+
cap: budget.cap.toString()
|
|
22357
|
+
},
|
|
22358
|
+
"Skipping a bid: the limit order has nothing left on chain"
|
|
22359
|
+
);
|
|
22360
|
+
continue;
|
|
22361
|
+
}
|
|
22362
|
+
const available = room < candidate.available ? room : candidate.available;
|
|
22363
|
+
const left = available - (plannedOn.get(candidate.order.id) ?? 0n);
|
|
22088
22364
|
if (left <= 0n) {
|
|
22089
22365
|
this.logger.info(
|
|
22090
22366
|
{ orderId: order.id, leg, limitOrder: candidate.order.id },
|
|
@@ -22119,10 +22395,7 @@ var FXFiller = class _FXFiller {
|
|
|
22119
22395
|
{
|
|
22120
22396
|
orderId: order.id,
|
|
22121
22397
|
limitOrder: candidate.order.id,
|
|
22122
|
-
available: formatUnits4(
|
|
22123
|
-
candidate.available,
|
|
22124
|
-
18
|
|
22125
|
-
),
|
|
22398
|
+
available: formatUnits4(available, 18),
|
|
22126
22399
|
userRequested: output.amount.toString(),
|
|
22127
22400
|
payout: targetOutput.toString(),
|
|
22128
22401
|
crossChain: sourceChain !== destChain
|
|
@@ -22167,7 +22440,10 @@ var FXFiller = class _FXFiller {
|
|
|
22167
22440
|
}
|
|
22168
22441
|
}
|
|
22169
22442
|
const effectiveBalance = walletContribution + credited;
|
|
22170
|
-
const finalOutputAmount =
|
|
22443
|
+
const finalOutputAmount = toRaw(
|
|
22444
|
+
toScaled(effectiveBalance > targetOutput ? targetOutput : effectiveBalance, outputDecimals),
|
|
22445
|
+
outputDecimals
|
|
22446
|
+
);
|
|
22171
22447
|
if (finalOutputAmount === 0n) {
|
|
22172
22448
|
this.logger.info(
|
|
22173
22449
|
{
|
|
@@ -22343,7 +22619,8 @@ var FXFiller = class _FXFiller {
|
|
|
22343
22619
|
fillerInputs,
|
|
22344
22620
|
fundingCalls: [...fundingCalls],
|
|
22345
22621
|
partialFill,
|
|
22346
|
-
profit: totalProfit
|
|
22622
|
+
profit: totalProfit,
|
|
22623
|
+
budget
|
|
22347
22624
|
});
|
|
22348
22625
|
if (!partialEligibleCheap) break;
|
|
22349
22626
|
}
|
|
@@ -22351,7 +22628,12 @@ var FXFiller = class _FXFiller {
|
|
|
22351
22628
|
if (plans.length === 0) return 0;
|
|
22352
22629
|
if (order.id) {
|
|
22353
22630
|
this.contractService.cacheService.setBidPlans(order.id, plans);
|
|
22354
|
-
this.contractService.cacheService.setFillerOutputs(
|
|
22631
|
+
this.contractService.cacheService.setFillerOutputs(
|
|
22632
|
+
order.id,
|
|
22633
|
+
plans[0].fillerOutputs,
|
|
22634
|
+
plans[0].fillerInputs,
|
|
22635
|
+
plans[0].budget
|
|
22636
|
+
);
|
|
22355
22637
|
this.contractService.cacheService.setPartialFill(order.id, plans[0].partialFill);
|
|
22356
22638
|
this.contractService.cacheService.setMatchedLimitOrder(order.id, [
|
|
22357
22639
|
{ limitOrderId: plans[0].limitOrderId, payout: plans[0].payout }
|
|
@@ -22446,10 +22728,14 @@ var FXFiller = class _FXFiller {
|
|
|
22446
22728
|
// =========================================================================
|
|
22447
22729
|
/**
|
|
22448
22730
|
* Update consecutive-clamp counter after a successful order evaluation.
|
|
22449
|
-
*
|
|
22450
|
-
*
|
|
22451
|
-
*
|
|
22452
|
-
*
|
|
22731
|
+
*
|
|
22732
|
+
* Dormant. Only venue-priced legs ever fed a clamped outcome here: a streak
|
|
22733
|
+
* of those meant a live market source had gone off (stale pool, manipulated
|
|
22734
|
+
* venue). Venue pricing went with Uniswap V4, and limit orders are prices the
|
|
22735
|
+
* operator set, so the only caller records `false` and the filler never
|
|
22736
|
+
* halts. `isHalted`, `resetHalt` and `[simplex.overfillProtection]
|
|
22737
|
+
* maxConsecutiveClamps` are kept so existing configs and the halt API still
|
|
22738
|
+
* load; they can go once nothing reads them.
|
|
22453
22739
|
*/
|
|
22454
22740
|
recordOrderOutcome(clamped, orderId) {
|
|
22455
22741
|
if (clamped) {
|
|
@@ -22475,18 +22761,6 @@ var FXFiller = class _FXFiller {
|
|
|
22475
22761
|
this.consecutiveClamps = 0;
|
|
22476
22762
|
this.logger.warn("FXFiller halt reset by operator \u2014 resuming order evaluation");
|
|
22477
22763
|
}
|
|
22478
|
-
/**
|
|
22479
|
-
* Given a single (input, output) leg and the remaining token0 budget of its
|
|
22480
|
-
* pair, computes how much token0 notional to allocate to this leg and the
|
|
22481
|
-
* corresponding maximum output amount at the pair's rate.
|
|
22482
|
-
*
|
|
22483
|
-
* `rate` is **token1 per 1 token0**:
|
|
22484
|
-
* - token0 input → token1 output: token0 × rate → token1 amount.
|
|
22485
|
-
* - token1 input → token0 output: token1 ÷ rate → token0 amount.
|
|
22486
|
-
*
|
|
22487
|
-
* Returns `null` when this leg cannot consume any of the pair's remaining
|
|
22488
|
-
* budget (e.g. the cap has already been exhausted).
|
|
22489
|
-
*/
|
|
22490
22764
|
/**
|
|
22491
22765
|
* Whether any solver has already delivered output against this order.
|
|
22492
22766
|
*
|
|
@@ -22824,9 +23098,9 @@ var LimitOrderLifecycle = class {
|
|
|
22824
23098
|
const reconcileMs = this.options.reconcileIntervalSecs * 1e3;
|
|
22825
23099
|
this.every(heartbeatMs, "heartbeat", () => this.service.heartbeat());
|
|
22826
23100
|
this.every(EXPIRY_SWEEP_MS, "expiry", () => this.expire());
|
|
22827
|
-
this.every(reconcileMs, "reconciliation", () => this.
|
|
23101
|
+
this.every(reconcileMs, "reconciliation", () => this.upkeep());
|
|
22828
23102
|
this.logger.info({ heartbeatMs, expiryMs: EXPIRY_SWEEP_MS, reconcileMs }, "Orderbook lifecycle started");
|
|
22829
|
-
void this.
|
|
23103
|
+
void this.upkeep();
|
|
22830
23104
|
}
|
|
22831
23105
|
stop() {
|
|
22832
23106
|
this.timers.forEach((timer) => clearInterval(timer));
|
|
@@ -22837,6 +23111,14 @@ var LimitOrderLifecycle = class {
|
|
|
22837
23111
|
const expired = await this.service.expireStale();
|
|
22838
23112
|
if (expired > 0) this.logger.info({ expired }, "Withdrew limit orders that had outlived their expiry");
|
|
22839
23113
|
}
|
|
23114
|
+
/**
|
|
23115
|
+
* The tally pass first, so reconciliation against the orderbook reads the sizes
|
|
23116
|
+
* it left. Each fails on its own.
|
|
23117
|
+
*/
|
|
23118
|
+
async upkeep() {
|
|
23119
|
+
await this.run("tally reconciliation", async () => this.options.reconcileTallies());
|
|
23120
|
+
await this.run("reconciliation", () => this.reconcile());
|
|
23121
|
+
}
|
|
22840
23122
|
async reconcile() {
|
|
22841
23123
|
const report = await this.service.reconcile();
|
|
22842
23124
|
if (report.cancelled || report.reposted || report.underFunded) {
|
|
@@ -23637,7 +23919,7 @@ async function bootFiller(config, options) {
|
|
|
23637
23919
|
const sharedCacheService = new CacheService(options.loggers);
|
|
23638
23920
|
if (!options.signer && !globalWatchOnly && !allChainsWatchOnly(watchOnlyConfig, resolvedChains)) {
|
|
23639
23921
|
throw new Error(
|
|
23640
|
-
"A signer is required unless every chain is watch-only: pass `signer` to Simplex.start (privateKeySigner, turnkeySigner, mpcVaultSigner, viemSigner, or your own Signer implementation). Running the binary? Configure it under [simplex.signer]."
|
|
23922
|
+
"A signer is required unless every chain is watch-only: pass `signer` to Simplex.start (privateKeySigner, secretPhraseSigner, turnkeySigner, mpcVaultSigner, viemSigner, or your own Signer implementation). Running the binary? Configure it under [simplex.signer]."
|
|
23641
23923
|
);
|
|
23642
23924
|
}
|
|
23643
23925
|
const chainClientManager = new ChainClientManager(configService, options.signer);
|
|
@@ -23776,7 +24058,8 @@ async function bootFiller(config, options) {
|
|
|
23776
24058
|
const lifecycle = new LimitOrderLifecycle(
|
|
23777
24059
|
limitOrderService,
|
|
23778
24060
|
{
|
|
23779
|
-
reconcileIntervalSecs: config.orderbook?.reconcileIntervalSecs ?? DEFAULT_RECONCILE_INTERVAL_SECS
|
|
24061
|
+
reconcileIntervalSecs: config.orderbook?.reconcileIntervalSecs ?? DEFAULT_RECONCILE_INTERVAL_SECS,
|
|
24062
|
+
reconcileTallies: () => intentFiller.reconcileLimitOrderTallies()
|
|
23780
24063
|
},
|
|
23781
24064
|
options.loggers
|
|
23782
24065
|
);
|
|
@@ -24247,6 +24530,14 @@ var MemoryLimitOrderStore = class {
|
|
|
24247
24530
|
const remaining = BigInt(order.remaining) - BigInt(amount);
|
|
24248
24531
|
return this.patch(id, { remaining: (remaining > 0n ? remaining : 0n).toString() });
|
|
24249
24532
|
}
|
|
24533
|
+
async clampRemaining(id, room) {
|
|
24534
|
+
const order = this.orders.get(id);
|
|
24535
|
+
if (!order || order.status !== "open") return null;
|
|
24536
|
+
const sum = BigInt(room) + BigInt(order.reserved);
|
|
24537
|
+
const target = sum > 0n ? sum : 0n;
|
|
24538
|
+
if (target >= BigInt(order.remaining)) return null;
|
|
24539
|
+
return this.patch(id, { remaining: target.toString() });
|
|
24540
|
+
}
|
|
24250
24541
|
async release(id, amount) {
|
|
24251
24542
|
const order = this.orders.get(id);
|
|
24252
24543
|
if (!order) return;
|
|
@@ -24755,7 +25046,8 @@ var OrderScanner = class _OrderScanner {
|
|
|
24755
25046
|
* back from its own endpoints, which also proves they answer for one chain
|
|
24756
25047
|
* before a filler ever depends on them.
|
|
24757
25048
|
*
|
|
24758
|
-
* @throws if an endpoint set is invalid
|
|
25049
|
+
* @throws if an endpoint set is invalid, if none of a chain's endpoints answer,
|
|
25050
|
+
* if its endpoints disagree on the chain, if two entries resolve
|
|
24759
25051
|
* to the same chain, or if `scanIntervalSecs` is below the minimum.
|
|
24760
25052
|
*/
|
|
24761
25053
|
static async create(options) {
|
|
@@ -24815,7 +25107,10 @@ var OrderScanner = class _OrderScanner {
|
|
|
24815
25107
|
async addChainNow(chain) {
|
|
24816
25108
|
if (this.closed) throw new Error("This OrderScanner is closed");
|
|
24817
25109
|
const rpcUrls = validateRpcUrls(chain.rpcUrls);
|
|
24818
|
-
const chainId = chain.chainId ?? await
|
|
25110
|
+
const chainId = chain.chainId ?? (await resolveChainConfigs([{ rpcUrls, bundlerUrl: chain.bundlerUrl ?? "" }], {
|
|
25111
|
+
loggers: this.loggers,
|
|
25112
|
+
tolerateUnreachable: true
|
|
25113
|
+
}))[0].chainId;
|
|
24819
25114
|
if (this.closed) throw new Error("This OrderScanner is closed");
|
|
24820
25115
|
if (this.scanners.has(chainId)) {
|
|
24821
25116
|
throw new Error(`Chain ${chainId} is already in this scanner`);
|
|
@@ -24915,7 +25210,7 @@ var LimitOrderController = class {
|
|
|
24915
25210
|
get service() {
|
|
24916
25211
|
const service = this.runtime.limitOrders;
|
|
24917
25212
|
if (!service) {
|
|
24918
|
-
throw new Error("No orderbook is configured \u2014 set [orderbook]
|
|
25213
|
+
throw new Error("No orderbook is configured \u2014 set [orderbook] url to use limit orders");
|
|
24919
25214
|
}
|
|
24920
25215
|
return service;
|
|
24921
25216
|
}
|
|
@@ -25380,7 +25675,7 @@ var Simplex = class _Simplex extends EventEmitter4 {
|
|
|
25380
25675
|
const tomlSigner = options.config.simplex?.signer;
|
|
25381
25676
|
if (tomlSigner && !options.signer) {
|
|
25382
25677
|
throw new Error(
|
|
25383
|
-
"config.simplex.signer describes a signer but none was passed. Simplex.start takes a Signer instance: `signer: await createSigner(config.simplex.signer)`, or build one directly with privateKeySigner/turnkeySigner/mpcVaultSigner/viemSigner."
|
|
25678
|
+
"config.simplex.signer describes a signer but none was passed. Simplex.start takes a Signer instance: `signer: await createSigner(config.simplex.signer)`, or build one directly with privateKeySigner/secretPhraseSigner/turnkeySigner/mpcVaultSigner/viemSigner."
|
|
25384
25679
|
);
|
|
25385
25680
|
}
|
|
25386
25681
|
const loggers = new LoggerContext({
|
|
@@ -25583,6 +25878,8 @@ var Simplex = class _Simplex extends EventEmitter4 {
|
|
|
25583
25878
|
export {
|
|
25584
25879
|
AssetController,
|
|
25585
25880
|
ChainController,
|
|
25881
|
+
LimitOrderController,
|
|
25882
|
+
LimitOrderValidationError,
|
|
25586
25883
|
LoggerContext,
|
|
25587
25884
|
MemoryDataStore,
|
|
25588
25885
|
OrderScanner,
|
|
@@ -25595,6 +25892,7 @@ export {
|
|
|
25595
25892
|
WalletController,
|
|
25596
25893
|
addLogSink,
|
|
25597
25894
|
assertConfirmationCoverage,
|
|
25895
|
+
canDerive,
|
|
25598
25896
|
configureLogger,
|
|
25599
25897
|
createSigner,
|
|
25600
25898
|
digestSigner,
|
|
@@ -25607,10 +25905,12 @@ export {
|
|
|
25607
25905
|
pickAnchorStable,
|
|
25608
25906
|
privateKeySigner,
|
|
25609
25907
|
registrySymbols,
|
|
25908
|
+
secretPhraseSigner,
|
|
25610
25909
|
turnkeySigner,
|
|
25611
25910
|
validateAssetDefinitions,
|
|
25612
25911
|
validateConfig,
|
|
25613
25912
|
validatePairConfigs,
|
|
25913
|
+
validateSecretPhrase,
|
|
25614
25914
|
validateSignerConfig,
|
|
25615
25915
|
validateVaultToml,
|
|
25616
25916
|
viemSigner
|