@ninsei-labs/sdk 0.39.3 → 0.40.0-develop.1
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/CHANGELOG.md +35 -0
- package/engine/atomic/dleq2.js +86 -111
- package/engine/atomic/halfenc.js +58 -72
- package/engine/atomic/halves.js +31 -53
- package/engine/atomic/order-client.js +15 -16
- package/engine/atomic/order.js +44 -62
- package/engine/atomic/orderContext.js +26 -50
- package/engine/atomic/orderHalfCommit.js +17 -18
- package/engine/core/chainSource.js +54 -69
- package/engine/core/clock.js +11 -12
- package/engine/core/config.js +554 -449
- package/engine/core/finality.js +34 -53
- package/engine/core/format.js +35 -36
- package/engine/core/preSignGuards.js +109 -117
- package/engine/core/rfqSource.js +57 -75
- package/engine/core/sellWindow.js +23 -24
- package/engine/core/store.js +20 -21
- package/engine/core/swap.js +242 -243
- package/engine/evm/amounts.js +50 -51
- package/engine/evm/asyncExec.js +577 -0
- package/engine/evm/asyncLegs.js +308 -0
- package/engine/evm/asyncRoute.js +179 -0
- package/engine/evm/auth.js +41 -48
- package/engine/evm/claimGas.js +28 -47
- package/engine/evm/depositor.js +39 -43
- package/engine/evm/dex.js +266 -267
- package/engine/evm/escrow.js +113 -129
- package/engine/evm/factory.js +71 -77
- package/engine/evm/fees.js +41 -42
- package/engine/evm/funding.js +160 -162
- package/engine/evm/gasReserve.js +67 -36
- package/engine/evm/index.js +46 -53
- package/engine/evm/injected.js +34 -42
- package/engine/evm/permit.js +129 -56
- package/engine/evm/prices.js +30 -45
- package/engine/evm/session.js +188 -189
- package/engine/mock/chain.js +17 -18
- package/engine/monero/address.js +33 -48
- package/engine/monero/halvesSweep.js +25 -43
- package/engine/monero/node.js +14 -15
- package/engine/monero/scan.js +82 -95
- package/engine/monero/swapKeys.js +30 -33
- package/engine/monero/unlock.js +17 -20
- package/engine/monero/wallet.js +32 -42
- package/engine/recovery/recoveryFile.js +154 -161
- package/engine/recovery/restore.js +50 -61
- package/engine/sdk/bridge.js +244 -234
- package/engine/ui/dom.js +4 -5
- package/engine/ui/walletModal.js +20 -31
- package/index.d.ts +8 -0
- package/package.json +1 -1
- package/src/adapters/evm-wallet.mjs +16 -0
- package/src/adapters/monero-wallet.mjs +1 -1
- package/src/adapters/monero.mjs +1 -1
- package/src/adapters/node-test-evm-wallet.mjs +5 -5
- package/src/config.mjs +1 -1
- package/src/engine.mjs +4 -0
- package/src/legs/cow-spec.mjs +254 -0
- package/src/legs/cow.mjs +315 -0
- package/src/legs/evm.mjs +86 -2
- package/src/legs/kyber-spec.mjs +265 -0
- package/src/legs/kyber.mjs +425 -0
- package/src/legs/shape.mjs +35 -0
- package/src/legs/uniswaprelay-spec.mjs +541 -0
- package/src/legs/uniswaprelay.mjs +422 -0
- package/src/legs/uniswapx-spec.mjs +329 -0
- package/src/legs/uniswapx.mjs +362 -0
- package/src/quotes.mjs +1 -1
- package/src/swaps.mjs +1 -1
package/engine/evm/permit.js
CHANGED
|
@@ -1,35 +1,39 @@
|
|
|
1
1
|
// GENERATED FILE - a byte-for-byte copy of the engine module www/js/evm/permit.js for the SDK package.
|
|
2
|
-
// Edit the source under www/js
|
|
3
|
-
// tools/check-sdk-engine.mjs reddens on any drift, so a stale copy cannot ship silently.
|
|
2
|
+
// Edit the source under www/js and regenerate the mirror; a drift guard reddens on any difference.
|
|
4
3
|
// Everything below this header is identical to the source.
|
|
5
4
|
|
|
6
|
-
//
|
|
5
|
+
// TOKEN ALLOWANCE FOR BUYING WITHOUT OWN ETH: what the token supports and what is missing.
|
|
7
6
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// «неизвестно» - третьим значением, а не одним из двух.
|
|
7
|
+
// WHAT IS HERE AND WHAT IS NOT. Only selectors, calldata assembly and an HONEST verdict on whether a signature
|
|
8
|
+
// allowance suffices. No conclusion like "USDC has permit" is written from memory: that is a guess, and a guess
|
|
9
|
+
// about a token's accounting function costs money. The capability is PROBED by reading the chain, and the verdict
|
|
10
|
+
// can say "unknown" as a third value.
|
|
13
11
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// финансирования, а не потому, что этот своп его вызывает.
|
|
12
|
+
// WHAT IS ALREADY CHECKED FOR OUR SWAP LEG: the allowance is needed by the ROUTER as a normal approve. The
|
|
13
|
+
// router of this build does NOT know Permit2 (permit2() reverts; the transfer is paid by TransferHelper.safeTransferFrom
|
|
14
|
+
// from msg.sender). So Permit2 sits in the registry for a FUTURE funding path.
|
|
18
15
|
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
16
|
+
// WHY THERE IS NO SINGLE-TRANSACTION PATH HERE, AND IT IS NOT AN OVERSIGHT. The router sees msg.sender as the payer.
|
|
17
|
+
// A user without ETH cannot send approve, swap or deposit. Only an EXECUTOR CONTRACT could deposit for them and
|
|
18
|
+
// pull USDC by their signature (an EOA cannot present someone else's signature). No such contract exists here, so
|
|
19
|
+
// the SINGLE-TRANSACTION path stays refused and says what is missing (kind "no-orchestrator"). BUT A PERSON WITH
|
|
20
|
+
// NO OWN NATIVE COIN IS NO LONGER SENT AWAY BY THAT ALONE: the wallet's balance decides the path first, and an
|
|
21
|
+
// ASYNCHRONOUS provider (an intent auction, whose settlement arrives later) carries the purchase when the wallet
|
|
22
|
+
// cannot cover the gas - the swap settles first and the escrow is funded after it. See usdcWithoutEthVerdict below
|
|
23
|
+
// and www/js/evm/asyncRoute.js - and it is a NAMED refusal only when no provider serves the network.
|
|
25
24
|
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// отдельного разрешения ПО ТОКЕНУ (approve или permit), и оно адресуется тому, кто тянет токен.
|
|
25
|
+
// THE DEPOSITOR SIGNATURE IS A DIFFERENT ALLOWANCE. It lets a relayer SEND the order-creation transaction, but
|
|
26
|
+
// not pull the token. Pulling USDC needs a separate TOKEN allowance (approve or permit) addressed to the puller.
|
|
29
27
|
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
28
|
+
// THE BALANCE-DRIVEN PATH CHOICE. Whether a provider serves a network ASYNCHRONOUSLY (an intent auction whose
|
|
29
|
+
// settlement arrives later - see www/js/evm/asyncRoute.js) is what decides the path for a person with no own
|
|
30
|
+
// native coin: covered gas -> the ordinary route; short native + a served network -> the asynchronous route;
|
|
31
|
+
// short native + an unserved network -> a NAMED refusal. That answer comes from the same table the package's
|
|
32
|
+
// provider layer pins (sdk/src/legs/cow.mjs).
|
|
33
|
+
import { asyncRouteVerdict } from "./asyncRoute.js";
|
|
34
|
+
|
|
35
|
+
// ERC-20 and EIP-2612 selectors. Next to each is the signature string it was computed from, and a check
|
|
36
|
+
// recomputes keccak256 of those strings, so a selector cannot diverge from its signature silently.
|
|
33
37
|
export const ERC20_METHOD = {
|
|
34
38
|
approve: { hex: "0x095ea7b3", signature: "approve(address,uint256)" },
|
|
35
39
|
allowance: { hex: "0xdd62ed3e", signature: "allowance(address,address)" },
|
|
@@ -47,20 +51,26 @@ const addrWord = (a) => hexBody(a).toLowerCase().padStart(64, "0");
|
|
|
47
51
|
const uintWord = (v) => BigInt(v).toString(16).padStart(64, "0");
|
|
48
52
|
const wordBytes = (hex) => {
|
|
49
53
|
const b = hexBody(hex);
|
|
50
|
-
//
|
|
51
|
-
// показать это как «нет домена» значило бы принять незнание за отказ токена.
|
|
54
|
+
// AN EMPTY ANSWER IS "NOT READ", NOT "ZERO BYTES". This is how eth_call answers for an address without code.
|
|
52
55
|
if (b.length === 0) return null;
|
|
53
56
|
return b.length % 2 ? null : b.length / 2;
|
|
54
57
|
};
|
|
55
58
|
|
|
56
|
-
//
|
|
59
|
+
// A quantity as BigInt, or null. An empty/unreadable value is null ("not known"), never zero: a zero balance
|
|
60
|
+
// and an unread balance are different states, and the path choice must not confuse them.
|
|
61
|
+
const toBig = (v) => {
|
|
62
|
+
if (v === null || v === undefined || v === "") return null;
|
|
63
|
+
try { return BigInt(typeof v === "bigint" ? v : String(v)); } catch { return null; }
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// calldata for a signature allowance (EIP-2612). Signature is r || s || v (65 bytes); v in {27,28}.
|
|
57
67
|
export function encodePermit({ owner, spender, value, deadline, signature }) {
|
|
58
|
-
if (!isAddr(owner) || !isAddr(spender)) throw new Error("permit: owner
|
|
68
|
+
if (!isAddr(owner) || !isAddr(spender)) throw new Error("permit: owner and spender must be addresses");
|
|
59
69
|
const hex = hexBody(signature);
|
|
60
|
-
if (hex.length !== 130) throw new Error("permit:
|
|
70
|
+
if (hex.length !== 130) throw new Error("permit: the signature must be 65 bytes, got " + hex.length / 2);
|
|
61
71
|
let v = parseInt(hex.slice(128, 130), 16);
|
|
62
72
|
if (v < 27) v += 27;
|
|
63
|
-
if (v !== 27 && v !== 28) throw new Error("permit: v
|
|
73
|
+
if (v !== 27 && v !== 28) throw new Error("permit: v outside {27,28}: " + v);
|
|
64
74
|
return EIP2612_METHOD.permit.hex +
|
|
65
75
|
addrWord(owner) + addrWord(spender) + uintWord(value) + uintWord(deadline) +
|
|
66
76
|
uintWord(v) + hex.slice(0, 64) + hex.slice(64, 128);
|
|
@@ -71,55 +81,118 @@ export const encodeDomainSeparator = () => EIP2612_METHOD.DOMAIN_SEPARATOR.hex;
|
|
|
71
81
|
export const encodeApprove = ({ spender, amountWei }) => ERC20_METHOD.approve.hex + addrWord(spender) + uintWord(amountWei);
|
|
72
82
|
|
|
73
83
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* supported === true -
|
|
77
|
-
*
|
|
78
|
-
* supported ===
|
|
79
|
-
* supported === null - прочитать не удалось (нет кода/пустой ответ): это НЕ «нет permit», это «не знаем».
|
|
84
|
+
* Whether THIS address supports a signature allowance (EIP-2612). The answer has THREE states: "not checked"
|
|
85
|
+
* must differ from "no".
|
|
86
|
+
* supported === true - code exists, DOMAIN_SEPARATOR answers 32 bytes, nonces(owner) answers 32 bytes, and the bytecode has the permit selector;
|
|
87
|
+
* supported === false - code exists, but a signal is MISSING (then a signature allowance is unavailable);
|
|
88
|
+
* supported === null - could not read (no code / empty answer): this is NOT "no permit", it is "unknown".
|
|
80
89
|
*/
|
|
81
90
|
export function permitSupportVerdict({ code = null, domainSeparator = null, nonces = null, owner = null } = {}) {
|
|
82
91
|
const codeHex = hexBody(code);
|
|
83
|
-
if (codeHex.length === 0) return { supported: null, kind: "no-code", reason: "
|
|
84
|
-
if (!isAddr(owner)) return { supported: null, kind: "no-owner", reason: "
|
|
92
|
+
if (codeHex.length === 0) return { supported: null, kind: "no-code", reason: "no code at the token address - not a token, the allowance cannot be read" };
|
|
93
|
+
if (!isAddr(owner)) return { supported: null, kind: "no-owner", reason: "no owner given - cannot read nonces(owner)" };
|
|
85
94
|
const dsBytes = wordBytes(domainSeparator);
|
|
86
|
-
if (dsBytes === null) return { supported: null, kind: "domain-unread", reason: "DOMAIN_SEPARATOR
|
|
87
|
-
if (dsBytes !== 32) return { supported: false, kind: "domain-empty", reason: "DOMAIN_SEPARATOR
|
|
95
|
+
if (dsBytes === null) return { supported: null, kind: "domain-unread", reason: "DOMAIN_SEPARATOR did not answer 32 bytes: could not read" };
|
|
96
|
+
if (dsBytes !== 32) return { supported: false, kind: "domain-empty", reason: "DOMAIN_SEPARATOR returned " + dsBytes + " bytes instead of 32: no EIP-712 domain declared" };
|
|
88
97
|
const nonceBytes = wordBytes(nonces);
|
|
89
|
-
if (nonceBytes === null) return { supported: null, kind: "nonces-unread", reason: "nonces(owner)
|
|
90
|
-
if (nonceBytes !== 32) return { supported: false, kind: "nonces-empty", reason: "nonces(owner)
|
|
98
|
+
if (nonceBytes === null) return { supported: null, kind: "nonces-unread", reason: "nonces(owner) did not answer: could not read" };
|
|
99
|
+
if (nonceBytes !== 32) return { supported: false, kind: "nonces-empty", reason: "nonces(owner) returned " + nonceBytes + " bytes instead of 32: no EIP-2612 counter" };
|
|
91
100
|
if (!codeHex.toLowerCase().includes(EIP2612_METHOD.permit.hex.slice(2))) {
|
|
92
|
-
return { supported: false, kind: "permit-selector-absent", reason: "
|
|
101
|
+
return { supported: false, kind: "permit-selector-absent", reason: "the token bytecode has no permit selector: no signature allowance" };
|
|
93
102
|
}
|
|
94
|
-
return { supported: true, kind: "eip2612", reason: "
|
|
103
|
+
return { supported: true, kind: "eip2612", reason: "the token declares an EIP-712 domain, a nonces counter and the permit selector" };
|
|
95
104
|
}
|
|
96
105
|
|
|
97
106
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
107
|
+
* WHICH WAY A "BUY WITHOUT OWN NATIVE COIN" GOES - DECIDED BY THE WALLET'S BALANCE, NOT BY A FIXED REFUSAL.
|
|
108
|
+
*
|
|
109
|
+
* The wallet's own native coin decides the path BEFORE any token question:
|
|
110
|
+
* * the wallet covers the gas the deal needs (`gasNeedWei`) - the ordinary, synchronous path is taken: the
|
|
111
|
+
* person pays with their own gas and nothing has to be asked of anyone (`kind: "own-gas"`, `path: "sync"`);
|
|
112
|
+
* * the wallet does NOT cover it - the ASYNCHRONOUS path is taken (`kind: "async-route"`, `path: "async"`):
|
|
113
|
+
* the swap settles FIRST and the escrow is funded AFTER it, so the deposit and its gas come later. This is
|
|
114
|
+
* NOT a refusal as long as a provider serves the network - the "stop being a refusal" rule;
|
|
115
|
+
* * no provider serves the network - a NAMED refusal (`kind: "no-async-provider"`, blocked): "the wallet cannot
|
|
116
|
+
* cover the gas and there is no asynchronous path here" - never silence and never a silent "yes".
|
|
117
|
+
*
|
|
118
|
+
* An unread balance or an unmeasured gas need is its OWN state: "not known" must not look like "covered", but it
|
|
119
|
+
* is not a refusal either - the caller reads `blocked`.
|
|
120
|
+
*
|
|
121
|
+
* The single-transaction executor branch lives BELOW and stays reachable when the balance question cannot be
|
|
122
|
+
* answered at all (no gas need given): an executor contract could still carry the whole purchase in one
|
|
123
|
+
* transaction. That branch is the older verdict and is kept so existing callers see the same kinds.
|
|
124
|
+
*
|
|
125
|
+
* @param {{ orchestrator?: string|null, permitSupported?: boolean|null, token?: string|null,
|
|
126
|
+
* nativeBalanceWei?: *, gasNeedWei?: *, asyncProvider?: object|null, chainId?: * }} args
|
|
101
127
|
*/
|
|
102
|
-
export function usdcWithoutEthVerdict({
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
128
|
+
export function usdcWithoutEthVerdict({
|
|
129
|
+
orchestrator = null, permitSupported = null, token = null,
|
|
130
|
+
nativeBalanceWei = null, gasNeedWei = null, asyncProvider = null, chainId = null,
|
|
131
|
+
} = {}) {
|
|
132
|
+
const need = toBig(gasNeedWei);
|
|
133
|
+
// THE ASYNC ANSWER - from the caller's explicit verdict (the package layer's own answer) or from the engine's
|
|
134
|
+
// table by chainId. Either way it is a VALUE with a named reason, never silence.
|
|
135
|
+
const async = asyncProvider && typeof asyncProvider === "object"
|
|
136
|
+
? asyncProvider
|
|
137
|
+
: (chainId === null || chainId === undefined ? null : asyncRouteVerdict({ chainId }));
|
|
138
|
+
const asyncOk = Boolean(async && async.ok === true);
|
|
139
|
+
const asyncReason = async && async.ok === false && async.reason ? String(async.reason) : "no-async-provider";
|
|
140
|
+
|
|
141
|
+
if (need !== null && need > 0n) {
|
|
142
|
+
const have = toBig(nativeBalanceWei);
|
|
143
|
+
if (have === null) {
|
|
144
|
+
return {
|
|
145
|
+
ok: false, blocked: false, checked: false, kind: "balance-unread", path: null,
|
|
146
|
+
gasNeedWei: need.toString(),
|
|
147
|
+
reason: "the wallet's native balance was not read, so it is unknown which path fits - read the balance before choosing",
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (have >= need) {
|
|
151
|
+
return {
|
|
152
|
+
ok: true, blocked: false, checked: true, kind: "own-gas", path: "sync",
|
|
153
|
+
nativeBalanceWei: have.toString(), gasNeedWei: need.toString(),
|
|
154
|
+
reason: "the wallet's own native covers the gas this deal needs, so the ordinary synchronous path is taken",
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const base = {
|
|
158
|
+
checked: true, nativeBalanceWei: have.toString(), gasNeedWei: need.toString(),
|
|
159
|
+
shortfallWei: (need - have).toString(),
|
|
160
|
+
};
|
|
161
|
+
if (asyncOk) {
|
|
162
|
+
return {
|
|
163
|
+
ok: true, blocked: false, ...base, kind: "async-route", path: "async",
|
|
164
|
+
provider: async.provider ?? null, venue: async.venue ?? null,
|
|
165
|
+
reason: "the wallet's own native does not cover the gas this deal needs, but an asynchronous provider serves this network: the swap settles first and the escrow is funded after it, so the deposit's gas is paid later",
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
ok: false, blocked: true, ...base, kind: "no-async-provider", path: null,
|
|
170
|
+
reason: "the wallet's own native does not cover the gas this deal needs and no asynchronous provider serves this network (" + asyncReason + "): there is no path for a purchase without own native coin here",
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// GAS NOT MEASURED: the balance cannot decide anything - fall through to the single-transaction executor
|
|
175
|
+
// verdict (unchanged behaviour for callers that pass no gas need).
|
|
176
|
+
// THE REFUSAL WITHOUT AN EXECUTOR, and it is not about the token. The swap pays msg.sender (the router), and
|
|
177
|
+
// only a CONTRACT can present someone else's pull signature. Without an executor the one-transaction path
|
|
178
|
+
// does not exist AT ALL, regardless of the token.
|
|
106
179
|
if (!isAddr(orchestrator)) {
|
|
107
180
|
return {
|
|
108
|
-
ok: false, blocked: true, kind: "no-orchestrator",
|
|
109
|
-
reason: "
|
|
181
|
+
ok: false, blocked: true, kind: "no-orchestrator", path: null,
|
|
182
|
+
reason: "no executor contract: only a contract could pull a token by the user signature and deposit native into the escrow in one transaction, and this build has none. The depositor signature without it lets a relayer SEND the order creation, but not pull the token",
|
|
110
183
|
};
|
|
111
184
|
}
|
|
112
185
|
if (permitSupported === false) {
|
|
113
186
|
return {
|
|
114
187
|
ok: false, blocked: true, kind: "token-no-permit",
|
|
115
|
-
reason: "
|
|
188
|
+
reason: "the token has no signature allowance (EIP-2612): the allowance must be a separate approve, i.e. a separate transaction - the path does not assemble into one transaction",
|
|
116
189
|
};
|
|
117
190
|
}
|
|
118
191
|
if (permitSupported !== true) {
|
|
119
192
|
return {
|
|
120
193
|
ok: false, blocked: true, kind: "permit-unproven",
|
|
121
|
-
reason: "
|
|
194
|
+
reason: "unknown whether the token supports a signature allowance: \"not checked\" is not \"possible\". The capability must be read from the chain before assembling the path",
|
|
122
195
|
};
|
|
123
196
|
}
|
|
124
|
-
return { ok: true, blocked: false, kind: "one-tx-possible", reason: "
|
|
197
|
+
return { ok: true, blocked: false, kind: "one-tx-possible", path: "sync", reason: "an executor is present and the token declares EIP-2612: the one-transaction path assembles" };
|
|
125
198
|
}
|
package/engine/evm/prices.js
CHANGED
|
@@ -1,32 +1,23 @@
|
|
|
1
1
|
// GENERATED FILE - a byte-for-byte copy of the engine module www/js/evm/prices.js for the SDK package.
|
|
2
|
-
// Edit the source under www/js
|
|
3
|
-
// tools/check-sdk-engine.mjs reddens on any drift, so a stale copy cannot ship silently.
|
|
2
|
+
// Edit the source under www/js and regenerate the mirror; a drift guard reddens on any difference.
|
|
4
3
|
// Everything below this header is identical to the source.
|
|
5
4
|
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// - нет ключей, лимитов и «мы упали, потому что у CoinGecko 429»;
|
|
12
|
-
// - значение нельзя перепутать с ценой другой сети: адрес фида свой у каждой цепи.
|
|
13
|
-
// Адреса фидов НЕ выдуманы: каждый подтверждён вызовом в сети - описание `description()` должно
|
|
14
|
-
// совпасть с ожидаемой парой, а ответ - попасть в разумный коридор и быть свежим. Проверка живёт
|
|
15
|
-
// в tools/check-prices.mjs (работает на заглушке RPC, без сети).
|
|
16
|
-
//
|
|
17
|
-
// ЧЕГО ЗДЕСЬ НЕТ: если фида для сети нет или он молчит - возвращаем null, и вызывающий код обязан
|
|
18
|
-
// показать, что число демонстрационное, а не подставить вчерашнюю константу молча.
|
|
5
|
+
// LIVE MARKET NUMBERS FOR THE EVM LEG: the native price (Chainlink on-chain) and the gas cost (RPC).
|
|
6
|
+
// WHY ON-CHAIN, NOT AN API: the price is a contract call over the same RPC already allowed by the CSP
|
|
7
|
+
// (connect-src), so no new external domain appears, there are no keys or rate limits, and the feed address is
|
|
8
|
+
// per chain. The feed addresses are NOT invented: each is confirmed by a call on chain (description() must match).
|
|
9
|
+
// WHAT IS NOT HERE: if a network has no feed or it is silent, return null and let the caller mark the number as demo.
|
|
19
10
|
|
|
20
11
|
const SELECTOR = {
|
|
21
12
|
description: "0x7284e416", // description()
|
|
22
13
|
latestRoundData: "0xfeaf968c", // latestRoundData() -> (roundId, answer, startedAt, updatedAt, answeredInRound)
|
|
23
14
|
};
|
|
24
15
|
|
|
25
|
-
const CACHE_MS = 60_000; //
|
|
26
|
-
const GAS_CACHE_MS = 30_000; //
|
|
27
|
-
const MAX_AGE_MS = 24 * 60 * 60 * 1000; //
|
|
16
|
+
const CACHE_MS = 60_000; // price: a minute balances freshness and request count
|
|
17
|
+
const GAS_CACHE_MS = 30_000; // gas changes more often, but it need not run on every keystroke
|
|
18
|
+
const MAX_AGE_MS = 24 * 60 * 60 * 1000; // older than a day, the feed is stale and not shown as live
|
|
28
19
|
|
|
29
|
-
const cache = new Map(); //
|
|
20
|
+
const cache = new Map(); // key: chainId + request kind
|
|
30
21
|
|
|
31
22
|
function cached(key, ttlMs) {
|
|
32
23
|
const hit = cache.get(key);
|
|
@@ -47,14 +38,14 @@ async function rpc(chain, method, params = []) {
|
|
|
47
38
|
});
|
|
48
39
|
if (!res.ok) throw new Error(method + ": HTTP " + res.status);
|
|
49
40
|
const j = await res.json();
|
|
50
|
-
if (j.error) throw new Error(method + ": " + (j.error.message || "
|
|
41
|
+
if (j.error) throw new Error(method + ": " + (j.error.message || "RPC error"));
|
|
51
42
|
return j.result;
|
|
52
43
|
}
|
|
53
44
|
|
|
54
45
|
const word = (data, i) => BigInt("0x" + String(data).slice(2).slice(i * 64, (i + 1) * 64));
|
|
55
46
|
|
|
56
|
-
//
|
|
57
|
-
//
|
|
47
|
+
// ABI string decode: [offset][length][bytes]. Needed to tell the ETH/USD feed from another one. TextDecoder,
|
|
48
|
+
// not Buffer: this code runs in the browser.
|
|
58
49
|
function decodeString(data) {
|
|
59
50
|
const s = String(data || "");
|
|
60
51
|
if (s.length < 2 + 128) return "";
|
|
@@ -65,7 +56,7 @@ function decodeString(data) {
|
|
|
65
56
|
return new TextDecoder().decode(bytes);
|
|
66
57
|
}
|
|
67
58
|
|
|
68
|
-
//
|
|
59
|
+
// Native price in USD. null if the feed is missing, silent, or fails validation.
|
|
69
60
|
export async function nativeUsd(chain, { force = false } = {}) {
|
|
70
61
|
const feed = chain && chain.priceFeed;
|
|
71
62
|
if (!feed || !feed.address) return null;
|
|
@@ -87,7 +78,7 @@ export async function nativeUsd(chain, { force = false } = {}) {
|
|
|
87
78
|
const fresh = updatedAt > 0 && Date.now() - updatedAt < MAX_AGE_MS;
|
|
88
79
|
const sane = value >= (feed.min || 0.01) && value <= (feed.max || 1e7);
|
|
89
80
|
if (desc !== feed.pair || !fresh || !sane) {
|
|
90
|
-
return put(key, null); // null,
|
|
81
|
+
return put(key, null); // null, not "roughly this": silence is more honest than a guess
|
|
91
82
|
}
|
|
92
83
|
return put(key, { usd: value, updatedAt, source: "chainlink", pair: desc });
|
|
93
84
|
} catch {
|
|
@@ -95,9 +86,8 @@ export async function nativeUsd(chain, { force = false } = {}) {
|
|
|
95
86
|
}
|
|
96
87
|
}
|
|
97
88
|
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
// отдельный вызов NodeInterface не нужен - обычная оценка даёт сопоставимую с кошельками цифру.
|
|
89
|
+
// Gas cost in USD: limit estimate x gas price x native price. On Arbitrum the L1 part is already in the gas
|
|
90
|
+
// price, so no separate NodeInterface call is needed.
|
|
101
91
|
export async function gasUsd(chain, { from = null, payToken = null, amountWei = null } = {}) {
|
|
102
92
|
const key = chain.id + ":gas";
|
|
103
93
|
const hit = cached(key, GAS_CACHE_MS);
|
|
@@ -108,15 +98,12 @@ export async function gasUsd(chain, { from = null, payToken = null, amountWei =
|
|
|
108
98
|
const gasPrice = BigInt(await rpc(chain, "eth_gasPrice"));
|
|
109
99
|
let gasLimit = null;
|
|
110
100
|
let measured = false;
|
|
111
|
-
//
|
|
112
|
-
//
|
|
101
|
+
// We estimate the transaction the person will actually sign: a transfer of the paying token.
|
|
102
|
+
// The escrow is not deployed yet, so this is an honest lower bound, not the swap gas.
|
|
113
103
|
try {
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
// ВАЖНО: признак "перевод токена" и признак "есть аккаунт" - РАЗНЫЕ условия. Раньше они
|
|
118
|
-
// были слиты в одну ветку, и без аккаунта перевод токена получал лимит нативного перевода
|
|
119
|
-
// (21000 вместо 65000) - поймал tools/check-prices.mjs.
|
|
104
|
+
// Without an account eth_estimateGas uses the ZERO address and a token transfer fails with
|
|
105
|
+
// "ERC20: transfer from the zero address". So without `from` we do not estimate: we take a typical
|
|
106
|
+
// limit and mark it as unmeasured. NOTE: "token transfer" and "has account" are DIFFERENT conditions.
|
|
120
107
|
const tokenTransfer = Boolean(payToken && !payToken.native);
|
|
121
108
|
if (tokenTransfer && from) {
|
|
122
109
|
// transfer(address,uint256) = 0xa9059cbb
|
|
@@ -128,7 +115,7 @@ export async function gasUsd(chain, { from = null, payToken = null, amountWei =
|
|
|
128
115
|
gasLimit = BigInt(await rpc(chain, "eth_estimateGas", [{ from, to: from, value: "0x1" }]));
|
|
129
116
|
measured = true;
|
|
130
117
|
} else {
|
|
131
|
-
//
|
|
118
|
+
// No account and no measurement: a typical limit and an honest 'not measured' flag.
|
|
132
119
|
gasLimit = BigInt(chain.gasLimitFallback || (tokenTransfer ? 65000 : 21000));
|
|
133
120
|
}
|
|
134
121
|
} catch {
|
|
@@ -142,21 +129,19 @@ export async function gasUsd(chain, { from = null, payToken = null, amountWei =
|
|
|
142
129
|
}
|
|
143
130
|
}
|
|
144
131
|
|
|
145
|
-
//
|
|
132
|
+
// One refresh point: fetch price and gas for the network and return what the interface can sign with.
|
|
146
133
|
export async function refreshChainPrices(chain, opts = {}) {
|
|
147
134
|
if (!chain || !chain.rpcUrl) return { nativeUsd: null, gasUsd: null, source: "none" };
|
|
148
135
|
const [price, gas] = await Promise.all([nativeUsd(chain), gasUsd(chain, opts)]);
|
|
149
|
-
//
|
|
150
|
-
//
|
|
136
|
+
// The native price is shared by all network maths (balances, '~ $', gas), so it is written into the
|
|
137
|
+
// network config, else the same number would live in two places and drift.
|
|
151
138
|
if (price && Number.isFinite(price.usd)) chain.native.usd = price.usd;
|
|
152
139
|
return {
|
|
153
140
|
nativeUsd: price ? price.usd : null,
|
|
154
141
|
nativeUpdatedAt: price ? price.updatedAt : null,
|
|
155
142
|
gasUsd: gas ? gas.usd : null,
|
|
156
|
-
//
|
|
157
|
-
// (
|
|
158
|
-
// «стоимость одной оценки» их не заменяет. Нет газа у цепи - null, и вызывающий обязан сказать, что запас
|
|
159
|
-
// не измерен, а не подставить ноль.
|
|
143
|
+
// GAS PRICE IN WEI GOES OUT, not just the estimate cost. The order-action gas reserve is derived from it
|
|
144
|
+
// (gasReserve.js): the mark and refund are different transactions with their own limits. No gas - null.
|
|
160
145
|
gasPriceWei: gas ? gas.gasPrice : null,
|
|
161
146
|
gasSource: gas ? gas.source : null,
|
|
162
147
|
gasMeasured: gas ? Boolean(gas.measured) : false,
|
|
@@ -164,8 +149,8 @@ export async function refreshChainPrices(chain, opts = {}) {
|
|
|
164
149
|
};
|
|
165
150
|
}
|
|
166
151
|
|
|
167
|
-
//
|
|
168
|
-
//
|
|
152
|
+
// Short and one line, but covering BOTH halves: the string is "Gas & price", so both gas and price must be
|
|
153
|
+
// in the value. A live price shows as Chainlink, a demo one as 'demo values'.
|
|
169
154
|
export function priceLabel(info) {
|
|
170
155
|
if (!info || !info.source || info.source === "none") return "demo values";
|
|
171
156
|
if (info.source !== "chainlink") return "demo values";
|