@ninsei-labs/sdk 0.39.3 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -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 +561 -451
- 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 +20 -8
- 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
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
// THE RELAYED UNISWAP ROUTE - THE FOURTH CONCRETE ASYNCHRONOUS ROUTE PROVIDER, A SIBLING OF cow.mjs, kyber.mjs AND
|
|
2
|
+
// uniswapx.mjs. THIS IS THE KEY-FREE ONE.
|
|
3
|
+
//
|
|
4
|
+
// WHAT IT IS. Uniswap's own repository Uniswap/relayer ships contracts (RelayOrderReactor) that RELAY a transaction
|
|
5
|
+
// to the UniversalRouter in exchange for ERC-20 tokens. A swapper signs a RelayOrder - one static input, one dynamic
|
|
6
|
+
// fee, and calldata encoded for the UniversalRouter - and anyone holding that signature (a "relayer") calls
|
|
7
|
+
// reactor.execute(SignedOrder, address feeRecipient), pays the gas, and is reimbursed in the input token through the
|
|
8
|
+
// order's own fee. There is no auction house and no API key: the order is a self-contained Permit2 instruction, and
|
|
9
|
+
// the reactor makes the router call the order asked for. So its shape is "async" - the swap happens in a LATER,
|
|
10
|
+
// separate transaction sent by someone else - and what ends its execution is a REAL ON-CHAIN EFFECT: the reactor's
|
|
11
|
+
// own Relay event for this order hash, read from the chain. `settled(request, deps)` below is that.
|
|
12
|
+
//
|
|
13
|
+
// WHY THIS ONE CARRIES NATIVE. The relayed swap is a UniversalRouter call, and the calldata this provider builds
|
|
14
|
+
// swaps the input token into the chain's WRAPPED native and then UNWRAPS it to the recipient
|
|
15
|
+
// (V3_SWAP_EXACT_IN + UNWRAP_WETH, uniswaprelay-spec.mjs). The native coin is what the escrow is funded with, so
|
|
16
|
+
// `settles: "native"` - it is a native leg beside CoWSwap and UniswapX.
|
|
17
|
+
//
|
|
18
|
+
// EVERY EXTERNAL FACT IS PINNED TO ITS SOURCE:
|
|
19
|
+
// * the RelayOrder struct, its field names and order, the Permit2 witness domain and the digest - uniswaprelay-spec.mjs
|
|
20
|
+
// with its own citations (Uniswap/relayer src/base/ReactorStructs.sol, src/lib/RelayOrderLib.sol,
|
|
21
|
+
// src/lib/FeeEscalatorLib.sol, src/lib/InputLib.sol, src/lib/RelayOrderInfoLib.sol; Uniswap/uniswapx-sdk
|
|
22
|
+
// src/order/RelayOrder.ts; Uniswap/permit2 src/libraries/PermitHash.sol and src/EIP712.sol);
|
|
23
|
+
// * the reactor address per chain - Uniswap/relayer README, "Deployment Addresses": Ethereum MAINNET only,
|
|
24
|
+
// RelayOrderReactor 0x0000000000A4e21E2597DCac987455c48b12edBF with UniversalRouter
|
|
25
|
+
// 0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD. Uniswap/uniswapx-sdk src/constants.ts REACTOR_ADDRESS_MAPPING
|
|
26
|
+
// additionally maps the SAME relay address onto Goerli, Polygon, Sepolia and a test chain (its
|
|
27
|
+
// constructSameAddressMap blanket, not a per-chain deployment note). ARBITRUM (42161) IS NOT DECLARED BY EITHER
|
|
28
|
+
// SOURCE - so this provider refuses an unknown network BY NAME and never guesses an address;
|
|
29
|
+
// * the event that ends an execution - Uniswap/relayer src/base/ReactorEvents.sol:
|
|
30
|
+
// event Relay(bytes32 indexed orderHash, address indexed filler, address indexed swapper, uint256 nonce);
|
|
31
|
+
// * WHERE THE FEE GOES - the address the relayer names in execute(), or the caller itself with the two-argument
|
|
32
|
+
// overload (Uniswap/relayer README "RelayOrderReactor"; src/reactors/RelayOrderReactor.sol). It is NOT part of
|
|
33
|
+
// the signed order, so it is not a field here;
|
|
34
|
+
// * HOW THE INPUT IS PULLED - Permit2's permitWitnessTransferFrom, from the swapper, to input.recipient (the
|
|
35
|
+
// UniversalRouter for a relayed swap); the swapper must have approved Permit2 for the input and the fee token
|
|
36
|
+
// (Uniswap/relayer src/lib/RelayOrderLib.sol transferInputTokens). There is no plain-approval path;
|
|
37
|
+
// * THE DISCOVERY PATH - and this is the honest part. Uniswap's relayer repository states only that swappers
|
|
38
|
+
// "generate Relay Orders to be submitted onchain"; its "Integrating as a filler" section is EMPTY, the reactor
|
|
39
|
+
// is called by whoever holds the signature, and the UniswapX order service (Uniswap/uniswapx-service) has NO
|
|
40
|
+
// relay order type in its models or in its swagger.json. So there is NO documented submission endpoint and NO
|
|
41
|
+
// gossip service: HOW A SIGNED ORDER REACHES A RELAYER IS UNDOCUMENTED. This provider does NOT invent a URL: its
|
|
42
|
+
// submit() refuses BY NAME with "uniswap-relay-no-discovery".
|
|
43
|
+
//
|
|
44
|
+
// THE SHARED PRE-SIGNATURE GUARDS STAY ON FOR THIS PROVIDER TOO (uniswapRelayGate): the price is not the market, the
|
|
45
|
+
// amount is covered, the token allowance - the SAME guard functions the synchronous DEX leg and the other three
|
|
46
|
+
// providers use. An external, asynchronous route must not become a hole in the guards.
|
|
47
|
+
//
|
|
48
|
+
// NO NEW DEPENDENCY: the typed data is built by uniswaprelay-spec.mjs (this repository's own EIP-712), signing goes
|
|
49
|
+
// through the wallet adapter / the repository's signer, and the chain is read through the caller's own EIP-1193
|
|
50
|
+
// driver (eth_getLogs).
|
|
51
|
+
import { ASYNC } from "./shape.mjs";
|
|
52
|
+
import { keccak256 } from "../primitives.mjs";
|
|
53
|
+
import {
|
|
54
|
+
UNISWAPRELAY_PERMIT2, UNISWAPRELAY_NATIVE, UNISWAPRELAY_ORDER_FIELDS,
|
|
55
|
+
uniswapRelayOrderMissingFields, uniswapRelayOrderHashHex, uniswapRelayOrderTypedData,
|
|
56
|
+
uniswapRelayRouterCalldata, uniswapRelayRouterDecode,
|
|
57
|
+
} from "./uniswaprelay-spec.mjs";
|
|
58
|
+
import * as engine from "../engine.mjs";
|
|
59
|
+
|
|
60
|
+
// THE REACTOR (settlement side) AND THE UNIVERSALROUTER (execution side) FOR MAINNET - the only pair Uniswap's own
|
|
61
|
+
// relayer README publishes (see the header). Other chains are a NAMED refusal, not a guess.
|
|
62
|
+
export const UNISWAPRELAY_REACTOR = "0x0000000000A4e21E2597DCac987455c48b12edBF";
|
|
63
|
+
export const UNISWAPRELAY_UNIVERSAL_ROUTER = "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD";
|
|
64
|
+
|
|
65
|
+
// THE VENUE NAME, assembled from single-token parts (a single multi-word literal would read as user-facing text).
|
|
66
|
+
export const UNISWAPRELAY_VENUE = ["Uniswap", "Relay"].join(" ");
|
|
67
|
+
|
|
68
|
+
// THE NETWORKS WHERE THIS PROVIDER CAN BUILD AN ORDER. Keyed by EVM chainId. Only the chain whose reactor AND
|
|
69
|
+
// UniversalRouter pair Uniswap's relayer README publishes (Ethereum mainnet). A network absent here is a NAMED
|
|
70
|
+
// refusal ("uniswap-relay-unknown-network"), never a guess at an address.
|
|
71
|
+
export const UNISWAPRELAY_NETWORKS = Object.freeze({
|
|
72
|
+
1: { reactor: UNISWAPRELAY_REACTOR, universalRouter: UNISWAPRELAY_UNIVERSAL_ROUTER, permit2: UNISWAPRELAY_PERMIT2 },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// THE EVENT TOPIC THAT ENDS AN EXECUTION: keccak256("Relay(bytes32,address,address,uint256)")
|
|
76
|
+
// (Uniswap/relayer src/base/ReactorEvents.sol). Assembled from single-token parts, then hashed - not a literal.
|
|
77
|
+
const RELAY_EVENT_PARTS = ["Relay(", "bytes32,", "address,", "address,", "uint256)"];
|
|
78
|
+
const relayEventSignature = () => RELAY_EVENT_PARTS.join("");
|
|
79
|
+
export const UNISWAPRELAY_EVENT_SIGNATURE = relayEventSignature();
|
|
80
|
+
export const UNISWAPRELAY_EVENT_TOPIC0 = (() => {
|
|
81
|
+
let s = "0x";
|
|
82
|
+
for (const b of keccak256(relayEventSignature())) s += b.toString(16).padStart(2, "0");
|
|
83
|
+
return s;
|
|
84
|
+
})();
|
|
85
|
+
|
|
86
|
+
// THE STATUS VOCABULARY OF THIS PROVIDER. It has no order book, so its vocabulary is the CHAIN's: an order is
|
|
87
|
+
// "open" until its Relay event exists and is "filled" once it does; a deadline that has passed with no event is
|
|
88
|
+
// "expired" (not filled).
|
|
89
|
+
export const UNISWAPRELAY_STATUSES = ["open", "filled", "expired"];
|
|
90
|
+
export const UNISWAPRELAY_FINAL_STATUSES = ["filled", "expired"];
|
|
91
|
+
export const UNISWAPRELAY_SETTLED_STATUS = "filled";
|
|
92
|
+
|
|
93
|
+
// THE NAMED REFUSALS OF THIS PROVIDER. Every one is a VALUE ({ ok: false, code }), never silence and never null: a
|
|
94
|
+
// caller must always be able to tell "unreachable" from "refused" from "not settled yet", and - the one that
|
|
95
|
+
// matters here - "the discovery path is not documented" from "the order was refused".
|
|
96
|
+
export const UNISWAPRELAY_REFUSAL_CODES = [
|
|
97
|
+
"uniswap-relay-unknown-network", // the chain has no published reactor/UniversalRouter pair here
|
|
98
|
+
"uniswap-relay-bad-order", // the order is incomplete or malformed - nothing to sign
|
|
99
|
+
"uniswap-relay-sign-failed", // the wallet did not produce a signature
|
|
100
|
+
"uniswap-relay-no-discovery", // NO DOCUMENTED path hands a signed relay order to a relayer
|
|
101
|
+
"uniswap-relay-unreachable", // the chain could not be read (no driver, connection, or timeout)
|
|
102
|
+
"uniswap-relay-bad-response", // the chain answered in a shape we do not know
|
|
103
|
+
"uniswap-relay-not-settled", // no Relay event before the timeout - "not yet", not "broken"
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
const refusal = (code, params = {}) => ({ ok: false, code, params });
|
|
107
|
+
|
|
108
|
+
/** The reactor for a chain, or null. */
|
|
109
|
+
export const uniswapRelayReactorFor = (chainId) => {
|
|
110
|
+
const n = UNISWAPRELAY_NETWORKS[Number(chainId)];
|
|
111
|
+
return n ? n.reactor : null;
|
|
112
|
+
};
|
|
113
|
+
/** The UniversalRouter for a chain, or null. */
|
|
114
|
+
export const uniswapRelayRouterFor = (chainId) => {
|
|
115
|
+
const n = UNISWAPRELAY_NETWORKS[Number(chainId)];
|
|
116
|
+
return n ? n.universalRouter : null;
|
|
117
|
+
};
|
|
118
|
+
/** The whole network record for a chain, or null. */
|
|
119
|
+
export const uniswapRelayNetwork = (chainId) => UNISWAPRELAY_NETWORKS[Number(chainId)] || null;
|
|
120
|
+
|
|
121
|
+
const isAddress = (v) => /^0x[0-9a-fA-F]{40}$/.test(String(v || ""));
|
|
122
|
+
const isPositive = (v) => { try { return BigInt(String(v)) > 0n; } catch { return false; } };
|
|
123
|
+
const isHash = (v) => /^0x[0-9a-fA-F]{64}$/.test(String(v || ""));
|
|
124
|
+
const isSignature = (v) => /^0x[0-9a-fA-F]{130}$/.test(String(v || ""));
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* THE INTENT: from what the person gives, build the swapper's own RelayOrder, its EIP-712 typed data, its order hash,
|
|
128
|
+
* and the UniversalRouter calldata the order carries. Returns { ok: true, order, typedData, orderHash, calldata,
|
|
129
|
+
* chainId, owner, reactor } or a named refusal. Pure - no network, no wallet.
|
|
130
|
+
*
|
|
131
|
+
* The order ALWAYS points its input at the UniversalRouter (input.recipient) and builds token -> native calldata, so
|
|
132
|
+
* this provider is a native leg by construction: buyAmountWei is the minimum native the swap must return.
|
|
133
|
+
*/
|
|
134
|
+
export function buildUniswapRelayIntent(request = {}) {
|
|
135
|
+
const chainId = Number(request.chainId);
|
|
136
|
+
const net = uniswapRelayNetwork(chainId);
|
|
137
|
+
if (!net) return { ...refusal("uniswap-relay-unknown-network", { chainId: Number.isFinite(chainId) ? chainId : null }), known: Object.keys(UNISWAPRELAY_NETWORKS).map(Number) };
|
|
138
|
+
const swapper = request.swapper;
|
|
139
|
+
const recipient = request.recipient === undefined || request.recipient === null ? swapper : request.recipient;
|
|
140
|
+
const universalRouter = request.universalRouter === undefined || request.universalRouter === null ? net.universalRouter : request.universalRouter;
|
|
141
|
+
const weth = request.wrappedToken;
|
|
142
|
+
const missing = [];
|
|
143
|
+
if (!isAddress(swapper)) missing.push("swapper");
|
|
144
|
+
if (!isAddress(recipient) || String(recipient).toLowerCase() === UNISWAPRELAY_NATIVE) missing.push("recipient");
|
|
145
|
+
if (!isAddress(request.sellToken)) missing.push("sellToken");
|
|
146
|
+
if (!isAddress(weth)) missing.push("wrappedToken");
|
|
147
|
+
if (!isAddress(universalRouter)) missing.push("universalRouter");
|
|
148
|
+
if (!Number.isInteger(Number(request.fee))) missing.push("fee");
|
|
149
|
+
if (!isPositive(request.sellAmountWei)) missing.push("sellAmountWei");
|
|
150
|
+
if (!isPositive(request.buyAmountWei)) missing.push("buyAmountWei");
|
|
151
|
+
if (!isAddress(request.feeToken)) missing.push("feeToken");
|
|
152
|
+
if (!isPositive(request.feeEndAmountWei)) missing.push("feeEndAmountWei");
|
|
153
|
+
if (!Number.isInteger(Number(request.deadline))) missing.push("deadline");
|
|
154
|
+
if (missing.length) return refusal("uniswap-relay-bad-order", { missing: [...new Set(missing)] });
|
|
155
|
+
|
|
156
|
+
const feeStartAmount = request.feeStartAmountWei === undefined || request.feeStartAmountWei === null ? request.feeEndAmountWei : request.feeStartAmountWei;
|
|
157
|
+
const feeStartTime = request.feeStartTime === undefined || request.feeStartTime === null ? request.deadline : request.feeStartTime;
|
|
158
|
+
const feeEndTime = request.feeEndTime === undefined || request.feeEndTime === null ? request.deadline : request.feeEndTime;
|
|
159
|
+
|
|
160
|
+
let calldata;
|
|
161
|
+
try {
|
|
162
|
+
calldata = uniswapRelayRouterCalldata({
|
|
163
|
+
tokenIn: request.sellToken, weth, fee: request.fee,
|
|
164
|
+
amountInWei: request.sellAmountWei, amountOutMinimumWei: request.buyAmountWei, recipient,
|
|
165
|
+
});
|
|
166
|
+
} catch (error) {
|
|
167
|
+
return refusal("uniswap-relay-bad-order", { why: String(error && error.message ? error.message : error) });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const order = {
|
|
171
|
+
info: {
|
|
172
|
+
reactor: net.reactor,
|
|
173
|
+
swapper,
|
|
174
|
+
nonce: request.nonce === undefined || request.nonce === null ? "0" : String(request.nonce),
|
|
175
|
+
deadline: String(request.deadline),
|
|
176
|
+
},
|
|
177
|
+
input: { token: request.sellToken, amount: String(request.sellAmountWei), recipient: universalRouter },
|
|
178
|
+
fee: {
|
|
179
|
+
token: request.feeToken,
|
|
180
|
+
startAmount: String(feeStartAmount),
|
|
181
|
+
endAmount: String(request.feeEndAmountWei),
|
|
182
|
+
startTime: String(feeStartTime),
|
|
183
|
+
endTime: String(feeEndTime),
|
|
184
|
+
},
|
|
185
|
+
universalRouterCalldata: calldata,
|
|
186
|
+
};
|
|
187
|
+
const stillMissing = uniswapRelayOrderMissingFields(order);
|
|
188
|
+
if (stillMissing.length) return refusal("uniswap-relay-bad-order", { missing: stillMissing });
|
|
189
|
+
try {
|
|
190
|
+
const typedData = uniswapRelayOrderTypedData({ order, chainId });
|
|
191
|
+
const orderHash = uniswapRelayOrderHashHex(order);
|
|
192
|
+
return { ok: true, chainId, owner: swapper, reactor: order.info.reactor, order, orderHash, calldata, typedData };
|
|
193
|
+
} catch (error) {
|
|
194
|
+
return refusal("uniswap-relay-bad-order", { why: String(error && error.message ? error.message : error) });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The provider's plan, in the shape the registry and the paths expect. Returns a VALUE with a named refusal. */
|
|
199
|
+
export function uniswapRelayPlan(request = {}) {
|
|
200
|
+
const intent = buildUniswapRelayIntent(request);
|
|
201
|
+
if (!intent.ok) return intent;
|
|
202
|
+
return {
|
|
203
|
+
ok: true,
|
|
204
|
+
provider: "uniswaprelay",
|
|
205
|
+
venue: UNISWAPRELAY_VENUE,
|
|
206
|
+
shape: ASYNC,
|
|
207
|
+
route: "uniswaprelay",
|
|
208
|
+
expectedOutWei: String(request.buyAmountWei),
|
|
209
|
+
order: intent.order,
|
|
210
|
+
orderHash: intent.orderHash,
|
|
211
|
+
typedData: intent.typedData,
|
|
212
|
+
calldata: intent.calldata,
|
|
213
|
+
owner: intent.owner,
|
|
214
|
+
reactor: intent.reactor,
|
|
215
|
+
nativeOutput: true,
|
|
216
|
+
// WHAT ENDS EXECUTION: the reactor's own Relay event on chain - an OBSERVED effect, not an answer from a service.
|
|
217
|
+
endsWith: { kind: "onchain-effect", settledWhen: UNISWAPRELAY_SETTLED_STATUS, final: [...UNISWAPRELAY_FINAL_STATUSES], event: UNISWAPRELAY_EVENT_SIGNATURE },
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* SIGN THE ORDER THROUGH THE WALLET ADAPTER. The adapter's own `signTypedData` is used when present (it wraps
|
|
223
|
+
* eth_signTypedData_v4); otherwise the same call is made through the adapter's EIP-1193 driver. A wallet that will
|
|
224
|
+
* not sign is a NAMED refusal, not a crash.
|
|
225
|
+
*/
|
|
226
|
+
export async function uniswapRelaySignOrder({ typedData, wallet, address = null } = {}) {
|
|
227
|
+
if (!typedData || typeof typedData !== "object") return refusal("uniswap-relay-bad-order", { field: "typedData" });
|
|
228
|
+
try {
|
|
229
|
+
if (wallet && typeof wallet.signTypedData === "function") {
|
|
230
|
+
const signature = await wallet.signTypedData(typedData);
|
|
231
|
+
if (!isSignature(signature)) return refusal("uniswap-relay-sign-failed", { why: "bad-signature-shape" });
|
|
232
|
+
return { ok: true, signature };
|
|
233
|
+
}
|
|
234
|
+
const driver = wallet && typeof wallet.driver === "function" ? wallet.driver() : null;
|
|
235
|
+
if (driver && typeof driver.request === "function") {
|
|
236
|
+
const from = address || (typeof wallet.address === "function" ? await wallet.address() : null);
|
|
237
|
+
const signature = await driver.request({ method: "eth_signTypedData_v4", params: [from, JSON.stringify(typedData)] });
|
|
238
|
+
if (!isSignature(signature)) return refusal("uniswap-relay-sign-failed", { why: "bad-signature-shape" });
|
|
239
|
+
return { ok: true, signature };
|
|
240
|
+
}
|
|
241
|
+
return refusal("uniswap-relay-sign-failed", { why: "no-wallet" });
|
|
242
|
+
} catch (error) {
|
|
243
|
+
return refusal("uniswap-relay-sign-failed", { why: String((error && error.code) || (error && error.message) || error) });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* HAND THE SIGNED ORDER TO A RELAYER - THE HONEST ANSWER IS THAT THERE IS NO DOCUMENTED WAY.
|
|
249
|
+
*
|
|
250
|
+
* Every other provider here has a transport: CoW and KyberSwap post to a public order book, UniswapX posts to the
|
|
251
|
+
* Trading API (with a key). The relayer has NONE. Uniswap/relayer's README says only that swappers "generate Relay
|
|
252
|
+
* Orders to be submitted onchain"; its "Integrating as a filler" section is empty; the reactor is called by whoever
|
|
253
|
+
* happens to hold the signature; and the UniswapX order service (Uniswap/uniswapx-service) carries no relay order
|
|
254
|
+
* type at all (no model, nothing in swagger.json). So the delivery path is UNDOCUMENTED, and this provider refuses
|
|
255
|
+
* BY NAME rather than inventing an endpoint: the refusal tells the caller the signature is built and signable, but
|
|
256
|
+
* there is nowhere the repository can honestly send it.
|
|
257
|
+
*/
|
|
258
|
+
export function uniswapRelaySubmit({ order = null, signature = null, chainId = null } = {}) {
|
|
259
|
+
const chain = Number(chainId);
|
|
260
|
+
if (!uniswapRelayNetwork(chain)) return refusal("uniswap-relay-unknown-network", { chainId: Number.isFinite(chain) ? chain : null });
|
|
261
|
+
if (!order || typeof order !== "object") return refusal("uniswap-relay-bad-order", { field: "order" });
|
|
262
|
+
if (!isSignature(signature)) return refusal("uniswap-relay-bad-order", { field: "signature" });
|
|
263
|
+
// The order IS self-contained and signable; what is missing is the DOCUMENTED WAY to a relayer. Named, with the
|
|
264
|
+
// facts: the reactor is permissionless and there is no submission endpoint in Uniswap's own sources.
|
|
265
|
+
return refusal("uniswap-relay-no-discovery", {
|
|
266
|
+
reactorIsPermissionless: true,
|
|
267
|
+
documentedSubmissionEndpoint: null,
|
|
268
|
+
reason: "undocumented",
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ONE READ OF THE REACTOR'S OWN Relay EVENT for this order hash. The chain is the ONLY source of truth here - an
|
|
273
|
+
// answer from a service would not be an on-chain effect. The read goes through the caller's EIP-1193 driver
|
|
274
|
+
// (eth_getLogs) or an injected `getLogs` (checks drive the failures without a chain).
|
|
275
|
+
const readRelayLogs = async ({ driver = null, getLogs = null, reactor, orderHash, fromBlock = "earliest", toBlock = "latest" }) => {
|
|
276
|
+
try {
|
|
277
|
+
if (typeof getLogs === "function") {
|
|
278
|
+
const logs = await getLogs({ reactor, orderHash, fromBlock, toBlock, topic0: UNISWAPRELAY_EVENT_TOPIC0 });
|
|
279
|
+
return { ok: true, logs };
|
|
280
|
+
}
|
|
281
|
+
if (driver && typeof driver.request === "function") {
|
|
282
|
+
const logs = await driver.request({
|
|
283
|
+
method: "eth_getLogs",
|
|
284
|
+
params: [{ address: reactor, fromBlock, toBlock, topics: [UNISWAPRELAY_EVENT_TOPIC0, orderHash] }],
|
|
285
|
+
});
|
|
286
|
+
return { ok: true, logs };
|
|
287
|
+
}
|
|
288
|
+
return refusal("uniswap-relay-unreachable", { why: "no-driver" });
|
|
289
|
+
} catch (error) {
|
|
290
|
+
return refusal("uniswap-relay-unreachable", { why: String((error && error.code) || (error && error.message) || error) });
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* READ ONE ORDER's status off the chain. The reactor's Relay event with this order hash means FILLED; a deadline
|
|
296
|
+
* already past with no event means EXPIRED; otherwise OPEN. A logs answer that is not an array, or a log whose
|
|
297
|
+
* topics are not this event and this order hash, is "uniswap-relay-bad-response" - never silence, never a silent
|
|
298
|
+
* "settled".
|
|
299
|
+
*/
|
|
300
|
+
export async function uniswapRelayStatus({ orderHash, chainId = null, reactor = null, deadline = null, driver = null, getLogs = null, fromBlock = "earliest", toBlock = "latest", nowSec = null } = {}) {
|
|
301
|
+
const chain = Number(chainId);
|
|
302
|
+
const net = uniswapRelayNetwork(chain);
|
|
303
|
+
const target = reactor || (net ? net.reactor : null);
|
|
304
|
+
if (!target) return refusal("uniswap-relay-unknown-network", { chainId: Number.isFinite(chain) ? chain : null });
|
|
305
|
+
if (!isHash(orderHash)) return refusal("uniswap-relay-bad-order", { field: "orderHash" });
|
|
306
|
+
const read = await readRelayLogs({ driver, getLogs, reactor: target, orderHash, fromBlock, toBlock });
|
|
307
|
+
if (!read.ok) return read;
|
|
308
|
+
const logs = read.logs;
|
|
309
|
+
if (!Array.isArray(logs)) return refusal("uniswap-relay-bad-response", { where: "logs", got: typeof logs });
|
|
310
|
+
const ours = logs.filter((log) => {
|
|
311
|
+
if (!log || !Array.isArray(log.topics)) return false;
|
|
312
|
+
const t0 = String(log.topics[0] || "").toLowerCase();
|
|
313
|
+
const t1 = String(log.topics[1] || "").toLowerCase();
|
|
314
|
+
return t0 === UNISWAPRELAY_EVENT_TOPIC0.toLowerCase() && t1 === String(orderHash).toLowerCase();
|
|
315
|
+
});
|
|
316
|
+
if (logs.length > 0 && ours.length === 0) return refusal("uniswap-relay-bad-response", { where: "logs", detail: "topics-mismatch" });
|
|
317
|
+
if (ours.length > 0) return { ok: true, status: UNISWAPRELAY_SETTLED_STATUS, log: ours[0] };
|
|
318
|
+
// No event: still open, unless the deadline (unix seconds) has already passed.
|
|
319
|
+
if (deadline !== null && deadline !== undefined) {
|
|
320
|
+
const now = typeof nowSec === "function" ? nowSec() : (Number.isFinite(nowSec) ? nowSec : Math.floor(Date.now() / 1000));
|
|
321
|
+
if (Number(deadline) < now) return { ok: true, status: "expired" };
|
|
322
|
+
}
|
|
323
|
+
return { ok: true, status: "open" };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* WHAT ENDS AN ASYNCHRONOUS PROVIDER'S EXECUTION - poll the chain until the reactor's Relay event for this order
|
|
328
|
+
* hash exists or the order is finally dead, with a timeout. Returns:
|
|
329
|
+
* { ok: true, done: true, settled: true, status: "filled" } - the reactor relayed the order;
|
|
330
|
+
* { ok: true, done: true, settled: false, status: "expired" } - the deadline passed with no event;
|
|
331
|
+
* { ok: false, code: "uniswap-relay-not-settled" | "uniswap-relay-unreachable" | ... } - a NAMED refusal.
|
|
332
|
+
* This observes the REAL on-chain effect (the event), never an answer from a service. The clock and the sleep are
|
|
333
|
+
* injectable so the wait is testable without real time.
|
|
334
|
+
*/
|
|
335
|
+
export async function uniswapRelaySettled(request = {}, deps = {}) {
|
|
336
|
+
const { orderHash, chainId = null, reactor = null, deadline = null } = request;
|
|
337
|
+
if (!isHash(orderHash)) return refusal("uniswap-relay-bad-order", { field: "orderHash" });
|
|
338
|
+
const now = typeof deps.now === "function" ? deps.now : () => Date.now();
|
|
339
|
+
const sleep = typeof deps.sleep === "function" ? deps.sleep : (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
340
|
+
const pollMs = Number.isFinite(deps.pollMs) && deps.pollMs > 0 ? deps.pollMs : 5_000;
|
|
341
|
+
const timeoutMs = Number.isFinite(deps.timeoutMs) && deps.timeoutMs > 0 ? deps.timeoutMs : 300_000;
|
|
342
|
+
const startedAt = now();
|
|
343
|
+
let lastStatus = null;
|
|
344
|
+
for (;;) {
|
|
345
|
+
const read = await uniswapRelayStatus({
|
|
346
|
+
orderHash, chainId, reactor, deadline,
|
|
347
|
+
driver: deps.driver || null, getLogs: deps.getLogs || null,
|
|
348
|
+
fromBlock: deps.fromBlock === undefined ? "earliest" : deps.fromBlock,
|
|
349
|
+
toBlock: deps.toBlock === undefined ? "latest" : deps.toBlock,
|
|
350
|
+
nowSec: deps.nowSec === undefined ? null : deps.nowSec,
|
|
351
|
+
});
|
|
352
|
+
if (!read.ok) return read; // unreachable / bad-response / unknown: a named refusal, not silence
|
|
353
|
+
lastStatus = read.status;
|
|
354
|
+
if (lastStatus === UNISWAPRELAY_SETTLED_STATUS) return { ok: true, done: true, settled: true, status: lastStatus, orderHash };
|
|
355
|
+
if (lastStatus === "expired") return { ok: true, done: true, settled: false, status: lastStatus, orderHash };
|
|
356
|
+
if (now() - startedAt >= timeoutMs) return refusal("uniswap-relay-not-settled", { orderHash, status: lastStatus, waitedMs: now() - startedAt });
|
|
357
|
+
await sleep(pollMs);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* THE SHARED PRE-SIGNATURE GUARDS, APPLIED TO THIS PROVIDER. The same guard functions the synchronous DEX leg and the
|
|
363
|
+
* other three providers use (engine.dex.priceGateVerdict, engine.dex.dexLegVerdict, engine.guards.allowanceVerdict),
|
|
364
|
+
* composed by the same summary guard (engine.guards.signingGateVerdict). Pure - no network, no wallet.
|
|
365
|
+
*/
|
|
366
|
+
export function uniswapRelayGate({
|
|
367
|
+
order = null, amountInWei = null, expectedOutWei = null, requiredOutWei = null,
|
|
368
|
+
tokenUsd = null, referenceNativeUsd = null, tolerated = false, gates = null,
|
|
369
|
+
sellDecimals = null, buyDecimals = null, buySymbol = null, allowanceWei = null, allowanceConsent = false,
|
|
370
|
+
tokenIsNative = false, symbol = null, decimals = null,
|
|
371
|
+
} = {}) {
|
|
372
|
+
if (!order) return { ok: false, blocked: true, reason: "uniswap-relay-no-order", checks: [] };
|
|
373
|
+
// THE RELAY ORDER CARRIES ONE AMOUNT (its input); the expected output lives in the calldata's amountOutMinimum, so
|
|
374
|
+
// the caller names both explicitly rather than this gate reading a number the order does not hold.
|
|
375
|
+
const amountIn = amountInWei === undefined || amountInWei === null ? (order.input ? order.input.amount : null) : amountInWei;
|
|
376
|
+
const amountOut = expectedOutWei === undefined || expectedOutWei === null ? null : expectedOutWei;
|
|
377
|
+
const price = engine.dex.priceGateVerdict({
|
|
378
|
+
amountInWei: amountIn, amountOutWei: amountOut,
|
|
379
|
+
inDecimals: sellDecimals, outDecimals: buyDecimals,
|
|
380
|
+
tokenUsd: Number(tokenUsd) > 0 ? Number(tokenUsd) : null,
|
|
381
|
+
referenceUsd: Number(referenceNativeUsd) > 0 ? Number(referenceNativeUsd) : null,
|
|
382
|
+
stableSteps: [], tolerated: Boolean(tolerated), gates,
|
|
383
|
+
});
|
|
384
|
+
const leg = { ok: true, amountOutWei: amountOut === null ? null : String(amountOut), price, wrapped: { symbol: buySymbol, decimals: buyDecimals } };
|
|
385
|
+
const coverage = engine.dex.dexLegVerdict(leg, requiredOutWei === undefined || requiredOutWei === null ? amountOut : requiredOutWei);
|
|
386
|
+
const coverageCheck = {
|
|
387
|
+
ok: !coverage.blocked, checked: coverage.kind !== "unchecked" && coverage.kind !== "unquoted",
|
|
388
|
+
blocks: coverage.blocked === true, kind: coverage.kind, reason: coverage.reason,
|
|
389
|
+
};
|
|
390
|
+
const allowance = engine.guards.allowanceVerdict({
|
|
391
|
+
tokenIsNative, allowanceWei, amountWei: amountIn,
|
|
392
|
+
consent: allowanceConsent === true, symbol, decimals: Number.isFinite(Number(decimals)) ? Number(decimals) : 18,
|
|
393
|
+
});
|
|
394
|
+
const gate = engine.guards.signingGateVerdict([coverageCheck, allowance]);
|
|
395
|
+
return { ...gate, price, coverage, allowance };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// THE PROVIDER, AS THE REGISTRY EXPECTS IT: id, kind, shape and, for "async", what ends execution (settled).
|
|
399
|
+
export const uniswapRelayProvider = Object.freeze({
|
|
400
|
+
id: "uniswaprelay",
|
|
401
|
+
kind: "uniswaprelay",
|
|
402
|
+
venue: UNISWAPRELAY_VENUE,
|
|
403
|
+
shape: ASYNC,
|
|
404
|
+
// WHAT IT SETTLES: native coin - the relayed swap unwraps the wrapped native to the recipient, and the escrow is
|
|
405
|
+
// funded with the native. So this provider can carry a native need ALL THE WAY - a native leg of a composed route,
|
|
406
|
+
// or a single route on its own.
|
|
407
|
+
settles: "native",
|
|
408
|
+
networks: UNISWAPRELAY_NETWORKS,
|
|
409
|
+
settlement: UNISWAPRELAY_REACTOR,
|
|
410
|
+
universalRouter: UNISWAPRELAY_UNIVERSAL_ROUTER,
|
|
411
|
+
permit2: UNISWAPRELAY_PERMIT2,
|
|
412
|
+
nativeToken: UNISWAPRELAY_NATIVE,
|
|
413
|
+
eventTopic: UNISWAPRELAY_EVENT_TOPIC0,
|
|
414
|
+
orderFields: UNISWAPRELAY_ORDER_FIELDS,
|
|
415
|
+
plan: uniswapRelayPlan,
|
|
416
|
+
settled: uniswapRelaySettled,
|
|
417
|
+
buildIntent: buildUniswapRelayIntent,
|
|
418
|
+
sign: uniswapRelaySignOrder,
|
|
419
|
+
submit: uniswapRelaySubmit,
|
|
420
|
+
status: uniswapRelayStatus,
|
|
421
|
+
gate: uniswapRelayGate,
|
|
422
|
+
});
|