@switch-win/sdk 1.2.9 → 1.3.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 +26 -0
- package/GASLESS.md +407 -0
- package/LIMIT-ORDERS.md +213 -205
- package/README.md +131 -105
- package/ROBINHOOD.md +201 -198
- package/abi/GaslessSwitchV2ABI.json +1095 -0
- package/dist/constants.d.ts +276 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +403 -0
- package/dist/constants.js.map +1 -0
- package/dist/gasless-client.d.ts +81 -0
- package/dist/gasless-client.d.ts.map +1 -0
- package/dist/gasless-client.js +467 -0
- package/dist/gasless-client.js.map +1 -0
- package/dist/gasless-index.d.ts +5 -0
- package/dist/gasless-index.d.ts.map +1 -0
- package/dist/gasless-index.js +5 -0
- package/dist/gasless-index.js.map +1 -0
- package/dist/gasless-operators.d.ts +32 -0
- package/dist/gasless-operators.d.ts.map +1 -0
- package/dist/gasless-operators.js +132 -0
- package/dist/gasless-operators.js.map +1 -0
- package/dist/gasless-store.d.ts +18 -0
- package/dist/gasless-store.d.ts.map +1 -0
- package/dist/gasless-store.js +83 -0
- package/dist/gasless-store.js.map +1 -0
- package/dist/gasless-types.d.ts +218 -0
- package/dist/gasless-types.d.ts.map +1 -0
- package/dist/gasless-types.js +2 -0
- package/dist/gasless-types.js.map +1 -0
- package/dist/gasless.d.ts +135 -0
- package/dist/gasless.d.ts.map +1 -0
- package/dist/gasless.js +513 -0
- package/dist/gasless.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/limit-orders.d.ts +440 -0
- package/dist/limit-orders.d.ts.map +1 -0
- package/dist/limit-orders.js +387 -0
- package/dist/limit-orders.js.map +1 -0
- package/dist/networks/index.d.ts +3 -0
- package/dist/networks/index.d.ts.map +1 -0
- package/dist/networks/index.js +2 -0
- package/dist/networks/index.js.map +1 -0
- package/dist/networks/robinhood.d.ts +536 -0
- package/dist/networks/robinhood.d.ts.map +1 -0
- package/dist/networks/robinhood.js +489 -0
- package/dist/networks/robinhood.js.map +1 -0
- package/dist/types.d.ts +436 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +19 -0
- package/dist/types.js.map +1 -0
- package/examples/gasless-nextjs-proxy.ts +87 -0
- package/examples/gasless-operator.ts +76 -0
- package/examples/gasless-swap.ts +170 -0
- package/examples/limit-order-ethers.ts +195 -0
- package/examples/nextjs-proxy.ts +82 -0
- package/examples/react-hooks.tsx +396 -0
- package/examples/swap-ethers.ts +209 -0
- package/examples/swap-web3py.py +267 -0
- package/package.json +53 -12
- package/src/constants.ts +184 -182
- package/src/gasless-client.ts +823 -0
- package/src/gasless-index.ts +4 -0
- package/src/gasless-operators.ts +227 -0
- package/src/gasless-store.ts +137 -0
- package/src/gasless-types.ts +258 -0
- package/src/gasless.ts +756 -0
- package/src/index.ts +69 -63
- package/src/limit-orders.ts +206 -195
- package/src/types.ts +80 -55
package/src/gasless.ts
ADDED
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Contract,
|
|
3
|
+
Interface,
|
|
4
|
+
TypedDataEncoder,
|
|
5
|
+
ZeroAddress,
|
|
6
|
+
getAddress,
|
|
7
|
+
id,
|
|
8
|
+
keccak256,
|
|
9
|
+
type Provider,
|
|
10
|
+
} from "ethers";
|
|
11
|
+
import {
|
|
12
|
+
LIMIT_ORDER_EIP712_TYPES,
|
|
13
|
+
NATIVE_PLS,
|
|
14
|
+
SWITCH_ROUTER,
|
|
15
|
+
WPLS,
|
|
16
|
+
GO_SWITCH_ABI,
|
|
17
|
+
} from "./constants.js";
|
|
18
|
+
import {
|
|
19
|
+
ROBINHOOD_CHAIN,
|
|
20
|
+
ROBINHOOD_SWITCH_CONTRACTS,
|
|
21
|
+
ROBINHOOD_TOKENS,
|
|
22
|
+
selectRobinhoodFeeOnOutput,
|
|
23
|
+
} from "./networks/robinhood.js";
|
|
24
|
+
import type { LimitOrderConfigResponse } from "./types.js";
|
|
25
|
+
import type { LimitOrderNetwork } from "./limit-orders.js";
|
|
26
|
+
import type {
|
|
27
|
+
GaslessChainState,
|
|
28
|
+
GaslessDeployment,
|
|
29
|
+
GaslessEligibility,
|
|
30
|
+
GaslessOrder,
|
|
31
|
+
GaslessQuoteDecision,
|
|
32
|
+
GaslessQuoteResponse,
|
|
33
|
+
GaslessTaxInfo,
|
|
34
|
+
GaslessTrade,
|
|
35
|
+
PreparedGaslessSwap,
|
|
36
|
+
} from "./gasless-types.js";
|
|
37
|
+
|
|
38
|
+
export * from "./gasless-types.js";
|
|
39
|
+
|
|
40
|
+
/** Fixed on-chain gasless protocol fee: 10 bps (0.1%), not an integration setting.
|
|
41
|
+
* Integrations that do not want this fee policy should offer regular swaps only. */
|
|
42
|
+
export const GASLESS_PROTOCOL_FEE_BPS = 10;
|
|
43
|
+
export const GASLESS_MAX_OPERATOR_PAYOUT_BPS = 5;
|
|
44
|
+
export const GASLESS_ORDER_LIFETIME_SECONDS = 180;
|
|
45
|
+
export const GASLESS_QUOTE_MAX_AGE_MS = 30_000;
|
|
46
|
+
export const GASLESS_MAX_SLIPPAGE_BPS = 100;
|
|
47
|
+
export const GASLESS_GAS_PRICE_BOOST_BPS = 13_000;
|
|
48
|
+
export const GASLESS_GAS_LIMIT_BPS = 13_000;
|
|
49
|
+
export const GASLESS_GAS_HEADROOM_BPS = 11_000;
|
|
50
|
+
export const GASLESS_ORDER_TYPES = LIMIT_ORDER_EIP712_TYPES;
|
|
51
|
+
export const GASLESS_ORDER_TUPLE =
|
|
52
|
+
"(address maker,address tokenIn,address tokenOut,uint256 amountIn,uint256 minAmountOut,uint256 deadline,uint256 nonce,bool feeOnOutput,address recipient,bool unwrapOutput,address partnerAddress)";
|
|
53
|
+
export const GASLESS_ROUTES_TUPLE =
|
|
54
|
+
"(uint256 amountIn,(address tokenIn,address tokenOut,(address adapter,uint256 amountIn,uint24 fee,bytes data)[] legs)[] hops)[]";
|
|
55
|
+
/** V2 only: fillOrder returns maker output, nominal payout, and the realized cap base. */
|
|
56
|
+
export const GASLESS_SWITCH_ABI = [
|
|
57
|
+
"function settlementVersion() pure returns (uint256)",
|
|
58
|
+
"function SWITCH_ROUTER() view returns (address)",
|
|
59
|
+
"function WNATIVE() view returns (address)",
|
|
60
|
+
"function getFee() pure returns (uint256)",
|
|
61
|
+
"function MAX_OPERATOR_EXCESS_BPS() pure returns (uint256)",
|
|
62
|
+
"function paused() view returns (bool)",
|
|
63
|
+
"function operatorGateEnabled() pure returns (bool)",
|
|
64
|
+
"function hasRole(bytes32,address) view returns (bool)",
|
|
65
|
+
"function domainSeparator() view returns (bytes32)",
|
|
66
|
+
"function isNonceUsed(address maker,uint256 nonce) view returns (bool)",
|
|
67
|
+
"function invalidateNonce(uint256 nonce)",
|
|
68
|
+
"function invalidateNonces(uint256[] nonces)",
|
|
69
|
+
`function canFillOrder(${GASLESS_ORDER_TUPLE} order,bytes signature) view returns (bool)`,
|
|
70
|
+
`function fillOrder(${GASLESS_ORDER_TUPLE} order,bytes signature,${GASLESS_ROUTES_TUPLE} routes,bool excessOnInput) returns (uint256 makerOutput,uint256 operatorPayout,uint256 capBase)`,
|
|
71
|
+
"event OrderFilled(address indexed maker,address indexed filler,uint256 indexed nonce,address tokenIn,address tokenOut,uint256 amountIn,uint256 makerAmountOut,uint256 fillerProfit,bool excessOnInput,address partnerAddress,bool directFill)",
|
|
72
|
+
"event NonceCancelled(address indexed maker,uint256 indexed nonce)",
|
|
73
|
+
"event GaslessFeePaid(address indexed maker,uint256 indexed nonce,address indexed token,uint256 nominalFee,uint256 protocolReceived,uint256 partnerReceived)",
|
|
74
|
+
...[
|
|
75
|
+
"InvalidOrder",
|
|
76
|
+
"InvalidSignature",
|
|
77
|
+
"NonceAlreadyUsed",
|
|
78
|
+
"OperatorOnly",
|
|
79
|
+
"InvalidRoute",
|
|
80
|
+
"InsufficientOutput",
|
|
81
|
+
"UnsupportedToken",
|
|
82
|
+
"TradingPaused",
|
|
83
|
+
"RouterFeeNotExempt",
|
|
84
|
+
"NativeTransferFailed",
|
|
85
|
+
].map((name) => `error ${name}()`),
|
|
86
|
+
] as const;
|
|
87
|
+
|
|
88
|
+
/** Pinned V2 deployments. A future migration requires an explicit trusted override. */
|
|
89
|
+
export const GASLESS_DEPLOYMENTS: Readonly<
|
|
90
|
+
Record<LimitOrderNetwork, Readonly<GaslessDeployment>>
|
|
91
|
+
> = {
|
|
92
|
+
pulsechain: {
|
|
93
|
+
network: "pulsechain",
|
|
94
|
+
chainId: 369,
|
|
95
|
+
contract: "0xB5bd12E009D625d8b122d0Ff9EdeD8bA2cEdeaeB",
|
|
96
|
+
router: SWITCH_ROUTER,
|
|
97
|
+
wrappedNative: WPLS,
|
|
98
|
+
explorer: "https://scan.pulsechain.com",
|
|
99
|
+
},
|
|
100
|
+
robinhood: {
|
|
101
|
+
network: "robinhood",
|
|
102
|
+
chainId: ROBINHOOD_CHAIN.id,
|
|
103
|
+
contract: "0xc39cB531f71059fd427a63bF8a2f4b822D6B9b02",
|
|
104
|
+
router: ROBINHOOD_SWITCH_CONTRACTS.router,
|
|
105
|
+
wrappedNative: ROBINHOOD_TOKENS.WETH.address,
|
|
106
|
+
explorer: ROBINHOOD_CHAIN.blockExplorerUrl,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** Historical V1 gasless addresses for indexing/cancellation, never new submissions. */
|
|
111
|
+
export const GASLESS_LEGACY_CONTRACTS: Readonly<
|
|
112
|
+
Record<LimitOrderNetwork, readonly string[]>
|
|
113
|
+
> = {
|
|
114
|
+
pulsechain: ["0xC28C70527740A85E98f7C8f800694f05c43814C5"],
|
|
115
|
+
robinhood: ["0xC9daFAA2ef5482Eab0014fAc7c44E7F8C9005A3F"],
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export class GaslessError extends Error {
|
|
119
|
+
constructor(
|
|
120
|
+
public readonly code: string,
|
|
121
|
+
message: string,
|
|
122
|
+
public readonly retryable = false,
|
|
123
|
+
) {
|
|
124
|
+
super(message);
|
|
125
|
+
this.name = "GaslessError";
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const sameAddress = (a: unknown, b: string): boolean =>
|
|
130
|
+
typeof a === "string" &&
|
|
131
|
+
/^0x[\da-f]{40}$/i.test(a) &&
|
|
132
|
+
a.toLowerCase() === b.toLowerCase();
|
|
133
|
+
|
|
134
|
+
export function gaslessUint(value: unknown, allowZero = false): bigint {
|
|
135
|
+
if (typeof value !== "string" || !/^(0|[1-9]\d{0,77})$/.test(value))
|
|
136
|
+
throw new GaslessError(
|
|
137
|
+
"INVALID_INTEGER",
|
|
138
|
+
"Expected a uint256 decimal string.",
|
|
139
|
+
);
|
|
140
|
+
const n = BigInt(value);
|
|
141
|
+
if (n >= 1n << 256n || (!allowZero && n === 0n))
|
|
142
|
+
throw new GaslessError(
|
|
143
|
+
"INVALID_INTEGER",
|
|
144
|
+
"Amount is outside the supported uint256 range.",
|
|
145
|
+
);
|
|
146
|
+
return n;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function gaslessAddress(value: string, allowZero = false): string {
|
|
150
|
+
// Reject invalid mixed-case checksums and implicit name resolution.
|
|
151
|
+
if (
|
|
152
|
+
typeof value !== "string" ||
|
|
153
|
+
!/^0x[\da-f]{40}$/i.test(value) ||
|
|
154
|
+
(!allowZero && sameAddress(value, ZeroAddress))
|
|
155
|
+
)
|
|
156
|
+
throw new GaslessError(
|
|
157
|
+
"INVALID_ADDRESS",
|
|
158
|
+
"Expected an explicit nonzero address.",
|
|
159
|
+
);
|
|
160
|
+
try {
|
|
161
|
+
return getAddress(value);
|
|
162
|
+
} catch {
|
|
163
|
+
throw new GaslessError("INVALID_ADDRESS", "Address checksum is invalid.");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function normalizeGaslessTrade(
|
|
168
|
+
trade: GaslessTrade,
|
|
169
|
+
): Required<GaslessTrade> {
|
|
170
|
+
const result = {
|
|
171
|
+
maker: gaslessAddress(trade.maker),
|
|
172
|
+
tokenIn: gaslessAddress(trade.tokenIn),
|
|
173
|
+
tokenOut: gaslessAddress(trade.tokenOut),
|
|
174
|
+
amountIn: gaslessUint(trade.amountIn).toString(),
|
|
175
|
+
recipient: gaslessAddress(trade.recipient ?? trade.maker),
|
|
176
|
+
partnerAddress: gaslessAddress(trade.partnerAddress ?? ZeroAddress, true),
|
|
177
|
+
slippageBps: trade.slippageBps ?? 50,
|
|
178
|
+
};
|
|
179
|
+
if (
|
|
180
|
+
!Number.isInteger(result.slippageBps) ||
|
|
181
|
+
result.slippageBps < 0 ||
|
|
182
|
+
result.slippageBps > GASLESS_MAX_SLIPPAGE_BPS
|
|
183
|
+
)
|
|
184
|
+
throw new GaslessError(
|
|
185
|
+
"INVALID_SLIPPAGE",
|
|
186
|
+
"Gasless supports 0–100 bps of price slippage.",
|
|
187
|
+
);
|
|
188
|
+
if (sameAddress(result.tokenIn, result.tokenOut))
|
|
189
|
+
throw new GaslessError("SAME_TOKEN", "Select two different tokens.");
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function getGaslessSigningParams(deployment: GaslessDeployment) {
|
|
194
|
+
return {
|
|
195
|
+
domain: {
|
|
196
|
+
name: "GaslessSwitch",
|
|
197
|
+
version: "2",
|
|
198
|
+
chainId: deployment.chainId,
|
|
199
|
+
verifyingContract: gaslessAddress(deployment.contract),
|
|
200
|
+
},
|
|
201
|
+
// Mutable arrays are accepted by ethers v6 and v5 signTypedData implementations.
|
|
202
|
+
types: {
|
|
203
|
+
LimitOrder: GASLESS_ORDER_TYPES.LimitOrder.map((field) => ({ ...field })),
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function validateGaslessConfig(
|
|
209
|
+
config: LimitOrderConfigResponse,
|
|
210
|
+
deployment: GaslessDeployment,
|
|
211
|
+
): void {
|
|
212
|
+
const domain = config.gaslessEip712Domain;
|
|
213
|
+
if (
|
|
214
|
+
!sameAddress(config.gaslessContract, deployment.contract) ||
|
|
215
|
+
!sameAddress(config.gaslessApprovalSpender, deployment.contract) ||
|
|
216
|
+
!sameAddress(config.limitOrderRouter, deployment.router) ||
|
|
217
|
+
config.gaslessSettlementVersion !== 2 ||
|
|
218
|
+
config.gaslessTaxTokensSupported !== true ||
|
|
219
|
+
config.gaslessProtocolFeeBps !== 10 ||
|
|
220
|
+
config.gaslessMaxOperatorPayoutBps !== 5 ||
|
|
221
|
+
domain?.name !== "GaslessSwitch" ||
|
|
222
|
+
domain.version !== "2" ||
|
|
223
|
+
domain.chainId !== deployment.chainId ||
|
|
224
|
+
!sameAddress(domain.verifyingContract, deployment.contract) ||
|
|
225
|
+
!config.orderSubmissionLimitOrderContracts?.some((address) =>
|
|
226
|
+
sameAddress(address, deployment.contract),
|
|
227
|
+
)
|
|
228
|
+
) {
|
|
229
|
+
throw new GaslessError(
|
|
230
|
+
"CONFIGURATION_CHANGED",
|
|
231
|
+
"Gasless deployment or fee policy changed. Use Regular until the integration is updated.",
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const PULSE_FEE_PRIORITY = new Map([
|
|
237
|
+
[WPLS.toLowerCase(), 1],
|
|
238
|
+
[NATIVE_PLS.toLowerCase(), 1],
|
|
239
|
+
["0xefd766ccb38eaf1dfd701853bfce31359239f305", 2],
|
|
240
|
+
["0x15d38573d2feeb82e7ad5187ab8c1d52810b1f07", 2],
|
|
241
|
+
["0x0cb6f5a34ad42ec934882a05265a7d5f59b51a2f", 2],
|
|
242
|
+
["0x95b303987a60c71504d99aa1b13b4da07b0790ab", 3],
|
|
243
|
+
["0x02dcdd04e3f455d838cd1249292c58f3b79e3c3c", 4],
|
|
244
|
+
["0xb17d901469b9208b17d916112988a3fed19b5ca1", 5],
|
|
245
|
+
["0x2b591e99afe9f32eaa6214f7b7629768c40eeb39", 6],
|
|
246
|
+
["0x57fde0a71132198bbec939b98976993d8d89d225", 7],
|
|
247
|
+
["0xf6f8db0aba00007681f8faf16a0fda1c9b030b11", 8],
|
|
248
|
+
]);
|
|
249
|
+
|
|
250
|
+
export function validateGaslessTaxInfo(tax: GaslessTaxInfo): void {
|
|
251
|
+
if (
|
|
252
|
+
!tax ||
|
|
253
|
+
typeof tax.isTaxToken !== "boolean" ||
|
|
254
|
+
![tax.sellTaxBps, tax.buyTaxBps].every(
|
|
255
|
+
(n) => Number.isInteger(n) && n >= 0 && n < 10000,
|
|
256
|
+
) ||
|
|
257
|
+
(!tax.isTaxToken && (tax.sellTaxBps !== 0 || tax.buyTaxBps !== 0))
|
|
258
|
+
)
|
|
259
|
+
throw new GaslessError(
|
|
260
|
+
"TAX_CHECK_UNAVAILABLE",
|
|
261
|
+
"Reliable token tax information is unavailable.",
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Same directional tax rules and preferred fee tokens as the Switch frontend. */
|
|
266
|
+
export function selectGaslessFeeOnOutput(
|
|
267
|
+
network: LimitOrderNetwork,
|
|
268
|
+
tokenIn: string,
|
|
269
|
+
tokenOut: string,
|
|
270
|
+
inputTax: GaslessTaxInfo,
|
|
271
|
+
outputTax: GaslessTaxInfo,
|
|
272
|
+
): boolean {
|
|
273
|
+
validateGaslessTaxInfo(inputTax);
|
|
274
|
+
validateGaslessTaxInfo(outputTax);
|
|
275
|
+
if (network === "robinhood")
|
|
276
|
+
return selectRobinhoodFeeOnOutput(tokenIn, tokenOut, inputTax, outputTax);
|
|
277
|
+
if (outputTax.isTaxToken && outputTax.buyTaxBps > 0) return false;
|
|
278
|
+
if (inputTax.isTaxToken && inputTax.sellTaxBps > 0) return true;
|
|
279
|
+
const a = PULSE_FEE_PRIORITY.get(tokenIn.toLowerCase()) ?? Infinity;
|
|
280
|
+
const b = PULSE_FEE_PRIORITY.get(tokenOut.toLowerCase()) ?? Infinity;
|
|
281
|
+
return b < a || (a === b && a !== Infinity);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export const gaslessCeil = (n: bigint, denominator: bigint): bigint =>
|
|
285
|
+
(n + denominator - 1n) / denominator;
|
|
286
|
+
|
|
287
|
+
/** gasPrice from the quote is already boosted: take the maximum, never boost it twice. */
|
|
288
|
+
export function gaslessGasBudget(
|
|
289
|
+
estimate: bigint,
|
|
290
|
+
rawGasPrice: bigint,
|
|
291
|
+
quoteGasPrice: bigint,
|
|
292
|
+
) {
|
|
293
|
+
if (estimate <= 0n || rawGasPrice <= 0n || quoteGasPrice <= 0n)
|
|
294
|
+
throw new GaslessError("INVALID_GAS", "Positive gas inputs are required.");
|
|
295
|
+
const gasLimit = gaslessCeil(estimate * 13000n, 10000n);
|
|
296
|
+
const boosted = gaslessCeil(rawGasPrice * 13000n, 10000n);
|
|
297
|
+
const gasPrice = boosted > quoteGasPrice ? boosted : quoteGasPrice;
|
|
298
|
+
return {
|
|
299
|
+
gasLimit,
|
|
300
|
+
gasPrice,
|
|
301
|
+
gasBudgetNative: gaslessCeil(gasLimit * gasPrice * 11000n, 10000n),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Current contract state, not just an API-provided address or USD estimate. */
|
|
306
|
+
export async function readGaslessChainState(
|
|
307
|
+
provider: Provider,
|
|
308
|
+
deployment: GaslessDeployment,
|
|
309
|
+
now = Date.now(),
|
|
310
|
+
): Promise<GaslessChainState> {
|
|
311
|
+
const [network, block, prices] = await Promise.all([
|
|
312
|
+
provider.getNetwork(),
|
|
313
|
+
provider.getBlock("latest"),
|
|
314
|
+
provider.getFeeData(),
|
|
315
|
+
]);
|
|
316
|
+
if (network.chainId !== BigInt(deployment.chainId))
|
|
317
|
+
throw new GaslessError(
|
|
318
|
+
"WRONG_CHAIN",
|
|
319
|
+
"RPC or wallet is on the wrong network.",
|
|
320
|
+
);
|
|
321
|
+
if (
|
|
322
|
+
!block ||
|
|
323
|
+
now / 1000 - block.timestamp > 90 ||
|
|
324
|
+
block.timestamp > now / 1000 + 15 ||
|
|
325
|
+
!prices.gasPrice ||
|
|
326
|
+
prices.gasPrice <= 0n
|
|
327
|
+
)
|
|
328
|
+
throw new GaslessError(
|
|
329
|
+
"STALE_CHAIN",
|
|
330
|
+
"A recent block and current gas price are required.",
|
|
331
|
+
true,
|
|
332
|
+
);
|
|
333
|
+
const c = new Contract(deployment.contract, GASLESS_SWITCH_ABI, provider);
|
|
334
|
+
const router = new Contract(
|
|
335
|
+
deployment.router,
|
|
336
|
+
["function FEE_EXEMPT(address) view returns (bool)"],
|
|
337
|
+
provider,
|
|
338
|
+
);
|
|
339
|
+
const overrides = { blockTag: block.number };
|
|
340
|
+
const [
|
|
341
|
+
version,
|
|
342
|
+
fee,
|
|
343
|
+
cap,
|
|
344
|
+
paused,
|
|
345
|
+
boundRouter,
|
|
346
|
+
wrapped,
|
|
347
|
+
gate,
|
|
348
|
+
domain,
|
|
349
|
+
exempt,
|
|
350
|
+
] = await Promise.all([
|
|
351
|
+
c.settlementVersion(overrides),
|
|
352
|
+
c.getFee(overrides),
|
|
353
|
+
c.MAX_OPERATOR_EXCESS_BPS(overrides),
|
|
354
|
+
c.paused(overrides),
|
|
355
|
+
c.SWITCH_ROUTER(overrides),
|
|
356
|
+
c.WNATIVE(overrides),
|
|
357
|
+
c.operatorGateEnabled(overrides),
|
|
358
|
+
c.domainSeparator(overrides),
|
|
359
|
+
router.FEE_EXEMPT(deployment.contract, overrides),
|
|
360
|
+
]);
|
|
361
|
+
if (
|
|
362
|
+
version !== 2n ||
|
|
363
|
+
fee !== 10n ||
|
|
364
|
+
cap !== 5n ||
|
|
365
|
+
paused ||
|
|
366
|
+
!gate ||
|
|
367
|
+
!exempt ||
|
|
368
|
+
!sameAddress(boundRouter, deployment.router) ||
|
|
369
|
+
!sameAddress(wrapped, deployment.wrappedNative) ||
|
|
370
|
+
domain.toLowerCase() !==
|
|
371
|
+
TypedDataEncoder.hashDomain(
|
|
372
|
+
getGaslessSigningParams(deployment).domain,
|
|
373
|
+
).toLowerCase()
|
|
374
|
+
) {
|
|
375
|
+
throw new GaslessError(
|
|
376
|
+
"CONFIGURATION_CHANGED",
|
|
377
|
+
"Gasless is paused or its on-chain fee, domain, or router policy does not match.",
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
checkedAt: now,
|
|
382
|
+
blockNumber: block.number,
|
|
383
|
+
blockTimestamp: block.timestamp,
|
|
384
|
+
rawGasPriceWei: prices.gasPrice.toString(),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const routerInterface = new Interface(GO_SWITCH_ABI);
|
|
389
|
+
|
|
390
|
+
/** Bind every execution check to the exact trade, route bytes, fee side, and chain. */
|
|
391
|
+
export function assessGaslessQuote(
|
|
392
|
+
quote: GaslessQuoteResponse,
|
|
393
|
+
tradeInput: GaslessTrade,
|
|
394
|
+
deployment: GaslessDeployment,
|
|
395
|
+
state: GaslessChainState,
|
|
396
|
+
now = Date.now(),
|
|
397
|
+
): GaslessQuoteDecision {
|
|
398
|
+
let eligibility: GaslessEligibility | undefined;
|
|
399
|
+
const fail = (
|
|
400
|
+
code: Extract<GaslessQuoteDecision, { eligible: false }>["code"],
|
|
401
|
+
reason: string,
|
|
402
|
+
retryable = false,
|
|
403
|
+
): GaslessQuoteDecision => ({
|
|
404
|
+
eligible: false,
|
|
405
|
+
code,
|
|
406
|
+
reason,
|
|
407
|
+
retryable,
|
|
408
|
+
...(eligibility ? { eligibility } : {}),
|
|
409
|
+
});
|
|
410
|
+
try {
|
|
411
|
+
const trade = normalizeGaslessTrade(tradeInput);
|
|
412
|
+
if (sameAddress(trade.tokenIn, NATIVE_PLS))
|
|
413
|
+
return fail(
|
|
414
|
+
"NATIVE_INPUT_REQUIRES_WRAP",
|
|
415
|
+
"Wrap native currency first (wrapping requires gas), or use Regular.",
|
|
416
|
+
);
|
|
417
|
+
const nativeOut = sameAddress(trade.tokenOut, NATIVE_PLS);
|
|
418
|
+
const tokenOut = nativeOut ? deployment.wrappedNative : trade.tokenOut;
|
|
419
|
+
if (
|
|
420
|
+
sameAddress(trade.tokenIn, tokenOut) ||
|
|
421
|
+
sameAddress(trade.recipient, deployment.contract)
|
|
422
|
+
)
|
|
423
|
+
return fail("INVALID_PREVIEW", "Unsupported token pair or recipient.");
|
|
424
|
+
const p = quote?.gaslessExecution;
|
|
425
|
+
eligibility = quote?.gaslessEligibility ?? p?.eligibility;
|
|
426
|
+
const summaries = [quote?.gaslessEligibility, p?.eligibility].filter(
|
|
427
|
+
(summary): summary is GaslessEligibility => summary !== undefined,
|
|
428
|
+
);
|
|
429
|
+
for (const summary of summaries) {
|
|
430
|
+
if (
|
|
431
|
+
!summary ||
|
|
432
|
+
summary.policyVersion !== 1 ||
|
|
433
|
+
!["eligible", "ineligible", "unavailable", "not_checked"].includes(
|
|
434
|
+
summary.status,
|
|
435
|
+
) ||
|
|
436
|
+
typeof summary.eligible !== "boolean" ||
|
|
437
|
+
summary.eligible !== (summary.status === "eligible") ||
|
|
438
|
+
summary.recommendation !== (summary.eligible ? "gasless" : "regular") ||
|
|
439
|
+
(summary.reasonCode === "READY") !== summary.eligible ||
|
|
440
|
+
typeof summary.reason !== "string" ||
|
|
441
|
+
!summary.reason.length ||
|
|
442
|
+
summary.reason.length > 1000 ||
|
|
443
|
+
summary.nativeSymbol !==
|
|
444
|
+
(deployment.network === "pulsechain" ? "PLS" : "ETH") ||
|
|
445
|
+
summary.protocolFeeBps !== 10 ||
|
|
446
|
+
summary.maxOperatorPayoutBps !== 5 ||
|
|
447
|
+
summary.requiredGasCoverageBps !== 20000 ||
|
|
448
|
+
summary.payoutValuationHaircutBps !== 1000 ||
|
|
449
|
+
summary.minimumSlippageBps !== 50 ||
|
|
450
|
+
summary.operatorLivenessVerified !== false ||
|
|
451
|
+
!Number.isSafeInteger(summary.checkedAt) ||
|
|
452
|
+
summary.checkedAt <= 0 ||
|
|
453
|
+
!Number.isSafeInteger(summary.expiresAt) ||
|
|
454
|
+
summary.expiresAt <= summary.checkedAt ||
|
|
455
|
+
summary.expiresAt > summary.checkedAt + GASLESS_QUOTE_MAX_AGE_MS
|
|
456
|
+
)
|
|
457
|
+
throw Error("Invalid server eligibility summary");
|
|
458
|
+
if (summary.checkedAt > now || summary.expiresAt <= now)
|
|
459
|
+
return fail(
|
|
460
|
+
"STALE_QUOTE",
|
|
461
|
+
"The server gasless eligibility check expired. Refresh before signing.",
|
|
462
|
+
true,
|
|
463
|
+
);
|
|
464
|
+
if (
|
|
465
|
+
eligibility &&
|
|
466
|
+
[
|
|
467
|
+
"status",
|
|
468
|
+
"eligible",
|
|
469
|
+
"recommendation",
|
|
470
|
+
"reasonCode",
|
|
471
|
+
"checkedAt",
|
|
472
|
+
"expiresAt",
|
|
473
|
+
].some(
|
|
474
|
+
(key) =>
|
|
475
|
+
summary[key as keyof GaslessEligibility] !==
|
|
476
|
+
eligibility![key as keyof GaslessEligibility],
|
|
477
|
+
)
|
|
478
|
+
)
|
|
479
|
+
throw Error("Conflicting server eligibility summaries");
|
|
480
|
+
}
|
|
481
|
+
if (eligibility && !eligibility.eligible)
|
|
482
|
+
return fail(
|
|
483
|
+
"SERVER_INELIGIBLE",
|
|
484
|
+
eligibility.reason,
|
|
485
|
+
eligibility.status !== "ineligible" ||
|
|
486
|
+
["STALE_PREVIEW", "OPERATOR_GAS_UNAVAILABLE"].includes(
|
|
487
|
+
eligibility.reasonCode,
|
|
488
|
+
),
|
|
489
|
+
);
|
|
490
|
+
if (!p || p.status !== "ready")
|
|
491
|
+
return fail(
|
|
492
|
+
"PREVIEW_UNAVAILABLE",
|
|
493
|
+
p?.reason ||
|
|
494
|
+
"Operator execution could not be verified. Retry or use Regular.",
|
|
495
|
+
true,
|
|
496
|
+
);
|
|
497
|
+
if (
|
|
498
|
+
!Number.isSafeInteger(now) ||
|
|
499
|
+
now <= 0 ||
|
|
500
|
+
!Number.isSafeInteger(state.blockNumber) ||
|
|
501
|
+
state.blockNumber <= 0 ||
|
|
502
|
+
!Number.isSafeInteger(state.blockTimestamp) ||
|
|
503
|
+
state.blockTimestamp <= 0 ||
|
|
504
|
+
!Number.isSafeInteger(p.checkedAt) ||
|
|
505
|
+
p.checkedAt > now ||
|
|
506
|
+
now - p.checkedAt > GASLESS_QUOTE_MAX_AGE_MS ||
|
|
507
|
+
!Number.isSafeInteger(state.checkedAt) ||
|
|
508
|
+
state.checkedAt > now ||
|
|
509
|
+
now - state.checkedAt > GASLESS_QUOTE_MAX_AGE_MS ||
|
|
510
|
+
now / 1000 - state.blockTimestamp > 90 ||
|
|
511
|
+
state.blockTimestamp > now / 1000 + 15
|
|
512
|
+
)
|
|
513
|
+
return fail(
|
|
514
|
+
"STALE_QUOTE",
|
|
515
|
+
"The operator execution check expired. Refresh before signing.",
|
|
516
|
+
true,
|
|
517
|
+
);
|
|
518
|
+
if (
|
|
519
|
+
quote.protocolFeeBps !== 10 ||
|
|
520
|
+
typeof quote.feeOnOutput !== "boolean" ||
|
|
521
|
+
!sameAddress(quote.fromToken, trade.tokenIn) ||
|
|
522
|
+
!(
|
|
523
|
+
sameAddress(quote.toToken, tokenOut) ||
|
|
524
|
+
(nativeOut && sameAddress(quote.toToken, NATIVE_PLS))
|
|
525
|
+
) ||
|
|
526
|
+
quote.totalAmountIn !== trade.amountIn
|
|
527
|
+
)
|
|
528
|
+
throw Error("Quote does not match the trade");
|
|
529
|
+
const tx = quote.selectedTx;
|
|
530
|
+
if (
|
|
531
|
+
!tx ||
|
|
532
|
+
!sameAddress(tx.to, deployment.router) ||
|
|
533
|
+
gaslessUint(tx.value, true) !== 0n ||
|
|
534
|
+
typeof tx.data !== "string" ||
|
|
535
|
+
tx.data.length > 200000
|
|
536
|
+
)
|
|
537
|
+
throw Error("Invalid router transaction");
|
|
538
|
+
const decoded = routerInterface.decodeFunctionData("goSwitch", tx.data);
|
|
539
|
+
if (
|
|
540
|
+
!sameAddress(decoded[1], trade.recipient) ||
|
|
541
|
+
BigInt(decoded[3]) !== 10n ||
|
|
542
|
+
decoded[4] !== quote.feeOnOutput ||
|
|
543
|
+
decoded[5] !== nativeOut ||
|
|
544
|
+
!sameAddress(decoded[6], trade.partnerAddress)
|
|
545
|
+
)
|
|
546
|
+
throw Error("Router arguments do not match the trade");
|
|
547
|
+
const routes = decoded[0];
|
|
548
|
+
if (routes.length === 0 || routes.length > 16)
|
|
549
|
+
throw Error("Invalid routes");
|
|
550
|
+
let total = 0n,
|
|
551
|
+
legs = 0;
|
|
552
|
+
for (const route of routes) {
|
|
553
|
+
if (
|
|
554
|
+
route.amountIn <= 0n ||
|
|
555
|
+
route.hops.length === 0 ||
|
|
556
|
+
route.hops.length > 8 ||
|
|
557
|
+
!sameAddress(route.hops[0].tokenIn, trade.tokenIn) ||
|
|
558
|
+
!sameAddress(route.hops[route.hops.length - 1].tokenOut, tokenOut)
|
|
559
|
+
)
|
|
560
|
+
throw Error("Invalid route endpoints");
|
|
561
|
+
total += route.amountIn;
|
|
562
|
+
let previous = trade.tokenIn;
|
|
563
|
+
for (const hop of route.hops) {
|
|
564
|
+
if (!sameAddress(hop.tokenIn, previous) || !hop.legs.length)
|
|
565
|
+
throw Error("Disconnected route");
|
|
566
|
+
gaslessAddress(hop.tokenOut);
|
|
567
|
+
previous = hop.tokenOut;
|
|
568
|
+
for (const leg of hop.legs) {
|
|
569
|
+
gaslessAddress(leg.adapter);
|
|
570
|
+
if (leg.amountIn <= 0n) throw Error("Empty leg");
|
|
571
|
+
legs++;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (
|
|
576
|
+
total.toString() !== trade.amountIn ||
|
|
577
|
+
legs > 64 ||
|
|
578
|
+
p.version !== 2 ||
|
|
579
|
+
p.settlementVersion !== 2 ||
|
|
580
|
+
p.chainId !== deployment.chainId ||
|
|
581
|
+
!sameAddress(p.contract, deployment.contract) ||
|
|
582
|
+
!sameAddress(p.maker, trade.maker) ||
|
|
583
|
+
!sameAddress(p.tokenIn, trade.tokenIn) ||
|
|
584
|
+
!sameAddress(p.tokenOut, tokenOut) ||
|
|
585
|
+
p.amountIn !== trade.amountIn ||
|
|
586
|
+
!sameAddress(p.recipient, trade.recipient) ||
|
|
587
|
+
p.unwrapOutput !== nativeOut ||
|
|
588
|
+
p.feeOnOutput !== quote.feeOnOutput ||
|
|
589
|
+
p.excessOnInput !== !quote.feeOnOutput ||
|
|
590
|
+
!sameAddress(
|
|
591
|
+
p.payoutToken,
|
|
592
|
+
quote.feeOnOutput ? tokenOut : trade.tokenIn,
|
|
593
|
+
) ||
|
|
594
|
+
p.routeHash?.toLowerCase() !== keccak256(tx.data).toLowerCase() ||
|
|
595
|
+
p.quoteBlockNumber !== quote.quoteBlockNumber ||
|
|
596
|
+
!Number.isSafeInteger(p.quoteBlockNumber) ||
|
|
597
|
+
p.quoteBlockNumber <= 0 ||
|
|
598
|
+
p.quoteBlockNumber > state.blockNumber + 3 ||
|
|
599
|
+
p.slippageBps !== trade.slippageBps ||
|
|
600
|
+
p.gasPriceBoostBps !== 13000 ||
|
|
601
|
+
p.gasLimitBps !== 13000 ||
|
|
602
|
+
p.headroomBps !== 11000
|
|
603
|
+
)
|
|
604
|
+
throw Error("Settlement binding mismatch");
|
|
605
|
+
const expected = gaslessUint(p.expectedOutputAmount),
|
|
606
|
+
received = gaslessUint(p.recipientAmountOut);
|
|
607
|
+
const minimum = (expected * BigInt(10000 - trade.slippageBps)) / 10000n;
|
|
608
|
+
const payout = gaslessUint(p.operatorPayout),
|
|
609
|
+
nominal = gaslessUint(p.nominalOperatorPayout),
|
|
610
|
+
capBase = gaslessUint(p.payoutCapBase);
|
|
611
|
+
const nativePayout = gaslessUint(p.operatorPayoutNative),
|
|
612
|
+
limit = gaslessUint(p.gasLimit),
|
|
613
|
+
price = gaslessUint(p.gasPriceWei),
|
|
614
|
+
budget = gaslessUint(p.gasBudgetNative);
|
|
615
|
+
const estimate = gaslessUint(p.estimatedGas),
|
|
616
|
+
raw = gaslessUint(state.rawGasPriceWei),
|
|
617
|
+
quotePrice = gaslessUint(quote.quoteGasPrice);
|
|
618
|
+
if (
|
|
619
|
+
minimum <= 0n ||
|
|
620
|
+
p.minAmountOut !== minimum.toString() ||
|
|
621
|
+
received < minimum ||
|
|
622
|
+
payout > nominal ||
|
|
623
|
+
nominal > (capBase * 5n) / 10000n ||
|
|
624
|
+
limit < gaslessCeil(estimate * 13000n, 10000n) ||
|
|
625
|
+
limit > 20000000n ||
|
|
626
|
+
budget < gaslessCeil(limit * price * 11000n, 10000n)
|
|
627
|
+
)
|
|
628
|
+
throw Error("Settlement accounting mismatch");
|
|
629
|
+
const boosted = gaslessCeil(raw * 13000n, 10000n);
|
|
630
|
+
const gasPrice = [price, quotePrice, boosted].reduce((a, b) =>
|
|
631
|
+
a > b ? a : b,
|
|
632
|
+
);
|
|
633
|
+
const freshBudget = gaslessCeil(limit * gasPrice * 11000n, 10000n);
|
|
634
|
+
const gasBudget = freshBudget > budget ? freshBudget : budget;
|
|
635
|
+
for (const summary of summaries) {
|
|
636
|
+
const economics = summary.economics,
|
|
637
|
+
operator = summary.operator;
|
|
638
|
+
if (
|
|
639
|
+
summary.checkedAt !== p.checkedAt ||
|
|
640
|
+
p.eligible !== true ||
|
|
641
|
+
trade.slippageBps < summary.minimumSlippageBps ||
|
|
642
|
+
!economics ||
|
|
643
|
+
!operator ||
|
|
644
|
+
operator.gasAvailable !== true ||
|
|
645
|
+
economics.gasCovered !== true ||
|
|
646
|
+
economics.safetyMarginCovered !== true ||
|
|
647
|
+
economics.operatorPayoutNativeWei !== p.operatorPayoutNative ||
|
|
648
|
+
economics.estimatedGasUnits !== p.estimatedGas ||
|
|
649
|
+
economics.gasLimit !== p.gasLimit ||
|
|
650
|
+
economics.gasPriceWei !== p.gasPriceWei ||
|
|
651
|
+
economics.gasBudgetNativeWei !== p.gasBudgetNative ||
|
|
652
|
+
gaslessUint(economics.conservativeGasBudgetNativeWei) !== budget * 2n ||
|
|
653
|
+
gaslessUint(economics.stressedOperatorPayoutNativeWei) !==
|
|
654
|
+
(nativePayout * 9000n) / 10000n
|
|
655
|
+
)
|
|
656
|
+
throw Error(
|
|
657
|
+
"Eligibility economics do not match the settlement preview",
|
|
658
|
+
);
|
|
659
|
+
gaslessAddress(operator.address);
|
|
660
|
+
// Preserve the server's safety margin if gas rose while the quote was being fetched.
|
|
661
|
+
const reserve = gasBudget * 2n;
|
|
662
|
+
if (gaslessUint(operator.nativeBalanceWei, true) < reserve)
|
|
663
|
+
return fail(
|
|
664
|
+
"INSUFFICIENT_OPERATOR_PAYOUT",
|
|
665
|
+
"The checked operator's gas funds no longer cover the conservative reserve. Refresh or use Regular.",
|
|
666
|
+
true,
|
|
667
|
+
);
|
|
668
|
+
if ((nativePayout * 9000n) / 10000n <= reserve)
|
|
669
|
+
return fail(
|
|
670
|
+
"INSUFFICIENT_OPERATOR_PAYOUT",
|
|
671
|
+
"The 0.05% payout no longer covers the server's conservative reserve at the current gas price. Refresh or use Regular.",
|
|
672
|
+
true,
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
if (p.eligible !== true || nativePayout <= gasBudget)
|
|
676
|
+
return fail(
|
|
677
|
+
"INSUFFICIENT_OPERATOR_PAYOUT",
|
|
678
|
+
p.reason ||
|
|
679
|
+
"The 0.05% total operator payout does not cover the padded gas budget and leave a positive incentive. Use Regular.",
|
|
680
|
+
);
|
|
681
|
+
return {
|
|
682
|
+
eligible: true,
|
|
683
|
+
...(eligibility ? { eligibility } : {}),
|
|
684
|
+
prepared: {
|
|
685
|
+
deployment,
|
|
686
|
+
trade,
|
|
687
|
+
quote,
|
|
688
|
+
preview: p,
|
|
689
|
+
feeOnOutput: quote.feeOnOutput,
|
|
690
|
+
expectedOutput: received.toString(),
|
|
691
|
+
minAmountOut: minimum.toString(),
|
|
692
|
+
payoutToken: p.payoutToken,
|
|
693
|
+
operatorPayout: payout.toString(),
|
|
694
|
+
gasPriceWei: gasPrice.toString(),
|
|
695
|
+
gasLimit: limit.toString(),
|
|
696
|
+
gasBudgetNative: gasBudget.toString(),
|
|
697
|
+
chainState: state,
|
|
698
|
+
},
|
|
699
|
+
};
|
|
700
|
+
} catch {
|
|
701
|
+
return fail(
|
|
702
|
+
"INVALID_PREVIEW",
|
|
703
|
+
"The quote and operator execution check do not match this swap. Refresh or use Regular.",
|
|
704
|
+
true,
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** Random 48-bit nonce stays exact in the hosted API's safe-integer database. */
|
|
710
|
+
export function createGaslessNonce(): number {
|
|
711
|
+
return globalThis.crypto
|
|
712
|
+
.getRandomValues(new Uint8Array(6))
|
|
713
|
+
.reduce((n, byte) => n * 256 + byte, 0);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
export function buildGaslessOrder(
|
|
717
|
+
prepared: PreparedGaslessSwap,
|
|
718
|
+
options: { nonce?: number; now?: number } = {},
|
|
719
|
+
): GaslessOrder {
|
|
720
|
+
const now = options.now ?? Date.now();
|
|
721
|
+
const decision = assessGaslessQuote(
|
|
722
|
+
prepared.quote,
|
|
723
|
+
prepared.trade,
|
|
724
|
+
prepared.deployment,
|
|
725
|
+
prepared.chainState,
|
|
726
|
+
now,
|
|
727
|
+
);
|
|
728
|
+
if (!decision.eligible)
|
|
729
|
+
throw new GaslessError(decision.code, decision.reason, decision.retryable);
|
|
730
|
+
const nonce = options.nonce ?? createGaslessNonce();
|
|
731
|
+
if (!Number.isSafeInteger(nonce) || nonce < 0)
|
|
732
|
+
throw new GaslessError(
|
|
733
|
+
"INVALID_NONCE",
|
|
734
|
+
"Nonce must be a nonnegative safe integer.",
|
|
735
|
+
);
|
|
736
|
+
const t = decision.prepared.trade;
|
|
737
|
+
return {
|
|
738
|
+
maker: t.maker,
|
|
739
|
+
tokenIn: t.tokenIn,
|
|
740
|
+
tokenOut: sameAddress(t.tokenOut, NATIVE_PLS)
|
|
741
|
+
? prepared.deployment.wrappedNative
|
|
742
|
+
: t.tokenOut,
|
|
743
|
+
amountIn: t.amountIn,
|
|
744
|
+
minAmountOut: decision.prepared.minAmountOut,
|
|
745
|
+
deadline:
|
|
746
|
+
Math.min(Math.floor(now / 1000), prepared.chainState.blockTimestamp) +
|
|
747
|
+
GASLESS_ORDER_LIFETIME_SECONDS,
|
|
748
|
+
nonce,
|
|
749
|
+
feeOnOutput: decision.prepared.feeOnOutput,
|
|
750
|
+
recipient: t.recipient,
|
|
751
|
+
unwrapOutput: sameAddress(t.tokenOut, NATIVE_PLS),
|
|
752
|
+
partnerAddress: t.partnerAddress,
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
export const GASLESS_OPERATOR_ROLE = id("OPERATOR_ROLE");
|