@rabby-wallet/rabby-swap 0.0.88 → 0.0.89-alpha.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/dist/abi.d.ts +2 -0
- package/dist/abi.js +2 -0
- package/dist/calldata/chain.d.ts +1 -0
- package/dist/calldata/chain.js +9 -0
- package/dist/calldata/magpie/abi.d.ts +1 -0
- package/dist/calldata/magpie/abi.js +3 -0
- package/dist/calldata/magpie/dexAggregator.d.ts +3 -0
- package/dist/calldata/magpie/dexAggregator.js +32 -0
- package/dist/calldata/magpie/index.d.ts +3 -0
- package/dist/calldata/magpie/index.js +65 -0
- package/dist/calldata/magpie/packed.d.ts +16 -0
- package/dist/calldata/magpie/packed.js +30 -0
- package/dist/calldata/types.d.ts +22 -0
- package/dist/calldata/types.js +1 -0
- package/dist/calldata/uni/abi.d.ts +3 -0
- package/dist/calldata/uni/abi.js +6 -0
- package/dist/calldata/uni/assets.d.ts +5 -0
- package/dist/calldata/uni/assets.js +65 -0
- package/dist/calldata/uni/index.d.ts +2 -0
- package/dist/calldata/uni/index.js +40 -0
- package/dist/calldata/uni/shared.d.ts +45 -0
- package/dist/calldata/uni/shared.js +223 -0
- package/dist/calldata/uni/universalRouter/commands.d.ts +3 -0
- package/dist/calldata/uni/universalRouter/commands.js +95 -0
- package/dist/calldata/uni/universalRouter/profiles.d.ts +6 -0
- package/dist/calldata/uni/universalRouter/profiles.js +27 -0
- package/dist/calldata/uni/universalRouter/sdk.d.ts +11 -0
- package/dist/calldata/uni/universalRouter/sdk.js +21 -0
- package/dist/calldata/uni/universalRouter/v2.d.ts +1 -0
- package/dist/calldata/uni/universalRouter/v2.js +24 -0
- package/dist/calldata/uni/universalRouter/v4/index.d.ts +3 -0
- package/dist/calldata/uni/universalRouter/v4/index.js +139 -0
- package/dist/consts.d.ts +2 -0
- package/dist/consts.js +29 -0
- package/dist/dexs/magpie.d.ts +1 -2
- package/dist/dexs/magpie.js +1 -3
- package/dist/dexs/uni.d.ts +1 -2
- package/dist/dexs/uni.js +1 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/quote.d.ts +3 -20
- package/dist/quote.js +3 -3
- package/dist/verify/calldata.d.ts +9 -13
- package/dist/verify/calldata.js +44 -22
- package/dist/verify/sdk.d.ts +2 -1
- package/dist/verify/sdk.js +4 -1
- package/package.json +6 -3
package/dist/abi.d.ts
CHANGED
package/dist/abi.js
CHANGED
|
@@ -1652,6 +1652,8 @@ export const ZeroXABIV2 = [
|
|
|
1652
1652
|
{ stateMutability: "payable", type: "fallback" },
|
|
1653
1653
|
];
|
|
1654
1654
|
export const SushiABI = [{ "inputs": [], "stateMutability": "nonpayable", "type": "constructor" }, { "inputs": [{ "internalType": "address", "name": "tokenOut", "type": "address" }, { "internalType": "uint256", "name": "amountOut", "type": "uint256" }], "name": "MinimalOutputBalanceViolation", "type": "error" }, { "inputs": [], "name": "safeExecutor", "outputs": [{ "internalType": "contract SafeExecutor", "name": "", "type": "address" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "contract IERC20", "name": "tokenIn", "type": "address" }, { "internalType": "uint256", "name": "amountIn", "type": "uint256" }, { "internalType": "address", "name": "recipient", "type": "address" }, { "internalType": "contract IERC20", "name": "tokenOut", "type": "address" }, { "internalType": "uint256", "name": "amountOutMin", "type": "uint256" }, { "internalType": "address", "name": "executor", "type": "address" }, { "internalType": "bytes", "name": "executorData", "type": "bytes" }], "name": "snwap", "outputs": [{ "internalType": "uint256", "name": "amountOut", "type": "uint256" }], "stateMutability": "payable", "type": "function" }, { "inputs": [{ "components": [{ "internalType": "contract IERC20", "name": "token", "type": "address" }, { "internalType": "uint256", "name": "amountIn", "type": "uint256" }, { "internalType": "address", "name": "transferTo", "type": "address" }], "internalType": "struct InputToken[]", "name": "inputTokens", "type": "tuple[]" }, { "components": [{ "internalType": "contract IERC20", "name": "token", "type": "address" }, { "internalType": "address", "name": "recipient", "type": "address" }, { "internalType": "uint256", "name": "amountOutMin", "type": "uint256" }], "internalType": "struct OutputToken[]", "name": "outputTokens", "type": "tuple[]" }, { "components": [{ "internalType": "address", "name": "executor", "type": "address" }, { "internalType": "uint256", "name": "value", "type": "uint256" }, { "internalType": "bytes", "name": "data", "type": "bytes" }], "internalType": "struct Executor[]", "name": "executors", "type": "tuple[]" }], "name": "snwapMultiple", "outputs": [{ "internalType": "uint256[]", "name": "amountOut", "type": "uint256[]" }], "stateMutability": "payable", "type": "function" }];
|
|
1655
|
+
export { UniABI } from "./calldata/uni/abi";
|
|
1656
|
+
export { MagpieABI } from "./calldata/magpie/abi";
|
|
1655
1657
|
// IAggregator.swapExactIn from the official router's verified implementation:
|
|
1656
1658
|
// https://sourcify.dev/server/v2/contract/56/0x88b20da52208607e3bf06a59605acebbe7d2e3d6?fields=abi,sources
|
|
1657
1659
|
export const PancakeSwapABI = [
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const getNativeTokenAddress: (chainId: number, nativeTokenAddress?: string) => string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { CHAINS } from "@debank/common";
|
|
2
|
+
export const getNativeTokenAddress = (chainId, nativeTokenAddress) => {
|
|
3
|
+
var _a;
|
|
4
|
+
const native = nativeTokenAddress !== null && nativeTokenAddress !== void 0 ? nativeTokenAddress : (_a = Object.values(CHAINS).find((chain) => chain.id === chainId)) === null || _a === void 0 ? void 0 : _a.nativeTokenAddress;
|
|
5
|
+
if (!native) {
|
|
6
|
+
throw new Error(`Native token is not configured for chain ${chainId}`);
|
|
7
|
+
}
|
|
8
|
+
return native;
|
|
9
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const MagpieABI: string[];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import BigNumber from "bignumber.js";
|
|
2
|
+
// DexAggregator LibRouter.getData(), verified Arbitrum source:
|
|
3
|
+
// https://sourcify.dev/server/v2/contract/42161/0x20F6ee51340aDEed01A59B0e65cB3703f3dc860c?fields=sources
|
|
4
|
+
export const decodeDexAggregator = (data) => ({
|
|
5
|
+
receiver: data.address(72),
|
|
6
|
+
fromAsset: data.address(92),
|
|
7
|
+
toAsset: data.address(112),
|
|
8
|
+
amountIn: data.uint(132),
|
|
9
|
+
amountOutMin: data.compact(167, 168),
|
|
10
|
+
});
|
|
11
|
+
// LibRouter.getData(): byte 274 holds the fee count, followed by three
|
|
12
|
+
// arrays of compact descriptors (asset, receiver, amount), 3 bytes per entry.
|
|
13
|
+
// transferFees() transfers these separately from the swap's amountIn.
|
|
14
|
+
// Only input-asset fees fit the single-asset payment checked against the quote.
|
|
15
|
+
// https://sourcify.dev/server/v2/contract/42161/0x20F6ee51340aDEed01A59B0e65cB3703f3dc860c?fields=sources
|
|
16
|
+
// https://docs.fly.trade/developers/api-reference/evm-swap-integration-guide#step-1-get-a-quote
|
|
17
|
+
export const decodeDexAggregatorInputFee = (data) => {
|
|
18
|
+
const count = Number(data.uint(274, 1));
|
|
19
|
+
const inputAsset = new BigNumber(data.address(92).slice(2), 16);
|
|
20
|
+
let total = new BigNumber(0);
|
|
21
|
+
for (let index = 0; index < count; index++) {
|
|
22
|
+
const assetOffset = 275 + index * 3;
|
|
23
|
+
const amountOffset = 275 + (count * 2 + index) * 3;
|
|
24
|
+
const asset = data.compact(assetOffset, assetOffset + 1);
|
|
25
|
+
const amount = data.compact(amountOffset, amountOffset + 1);
|
|
26
|
+
if (inputAsset.eq(asset))
|
|
27
|
+
total = total.plus(amount);
|
|
28
|
+
else if (new BigNumber(amount).gt(0))
|
|
29
|
+
throw new Error("Non-input-asset Magpie fees are not supported");
|
|
30
|
+
}
|
|
31
|
+
return total.toFixed(0);
|
|
32
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { DecodeCalldataOptions, DecodeCalldataResult, TxWithChainId } from "../types";
|
|
2
|
+
export declare const decodeInputFee: (tx: TxWithChainId, decoded?: DecodeCalldataResult) => string;
|
|
3
|
+
export declare const decodeCalldata: (tx: TxWithChainId, options?: DecodeCalldataOptions) => DecodeCalldataResult;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Interface } from "@ethersproject/abi";
|
|
2
|
+
import BigNumber from "bignumber.js";
|
|
3
|
+
import { getNativeTokenAddress } from "../chain";
|
|
4
|
+
import { isSameAddress, NULL_ADDRESS } from "../../utils";
|
|
5
|
+
import { MagpieABI } from "./abi";
|
|
6
|
+
import { decodeDexAggregator, decodeDexAggregatorInputFee, } from "./dexAggregator";
|
|
7
|
+
import { PackedReader } from "./packed";
|
|
8
|
+
const magpieInterface = new Interface(MagpieABI);
|
|
9
|
+
const dexAggregatorSelector = magpieInterface.getSighash("swapWithBackendSignature");
|
|
10
|
+
// Keep parsing details private and tied to the decoded result's lifetime.
|
|
11
|
+
const decodedPayloads = new WeakMap();
|
|
12
|
+
const readPayload = (tx) => {
|
|
13
|
+
const selector = tx.data.slice(0, 10).toLowerCase();
|
|
14
|
+
if (selector !== dexAggregatorSelector) {
|
|
15
|
+
throw new Error(`Unsupported Magpie calldata selector: ${selector}`);
|
|
16
|
+
}
|
|
17
|
+
const transaction = magpieInterface.parseTransaction({ data: tx.data });
|
|
18
|
+
const payloadLength = (transaction.args[0].length - 2) / 2;
|
|
19
|
+
return new PackedReader(tx.data, payloadLength);
|
|
20
|
+
};
|
|
21
|
+
export const decodeInputFee = (tx, decoded) => {
|
|
22
|
+
var _a;
|
|
23
|
+
const cached = decoded && decodedPayloads.get(decoded);
|
|
24
|
+
if (cached && cached.data === tx.data) {
|
|
25
|
+
return ((_a = cached.inputFee) !== null && _a !== void 0 ? _a : (cached.inputFee = decodeDexAggregatorInputFee(cached.payload)));
|
|
26
|
+
}
|
|
27
|
+
return decodeDexAggregatorInputFee(readPayload(tx));
|
|
28
|
+
};
|
|
29
|
+
export const decodeCalldata = (tx, options = {}) => {
|
|
30
|
+
const native = getNativeTokenAddress(tx.chainId, options.nativeTokenAddress);
|
|
31
|
+
const payload = readPayload(tx);
|
|
32
|
+
const swap = decodeDexAggregator(payload);
|
|
33
|
+
const nativeInput = isSameAddress(swap.fromAsset, NULL_ADDRESS);
|
|
34
|
+
let amountIn = new BigNumber(swap.amountIn);
|
|
35
|
+
// DexAggregator.adjustAmountIn(): zero means automatic input, not a zero
|
|
36
|
+
// debit. Native input is known from value minus encoded native fees;
|
|
37
|
+
// ERC20 input depends on min(balance, allowance), unavailable offline.
|
|
38
|
+
// https://sourcify.dev/server/v2/contract/42161/0x20F6ee51340aDEed01A59B0e65cB3703f3dc860c?fields=sources
|
|
39
|
+
const inputFee = nativeInput ? decodeDexAggregatorInputFee(payload) : "0";
|
|
40
|
+
if (amountIn.isZero()) {
|
|
41
|
+
if (!nativeInput)
|
|
42
|
+
throw new Error("Automatic ERC20 input depends on onchain balance and allowance; it cannot be determined from calldata alone");
|
|
43
|
+
amountIn = new BigNumber(tx.value).minus(inputFee);
|
|
44
|
+
}
|
|
45
|
+
if (!amountIn.gt(0))
|
|
46
|
+
throw new Error("Original input amount is zero or unresolved");
|
|
47
|
+
if (nativeInput && !amountIn.plus(inputFee).eq(tx.value))
|
|
48
|
+
throw new Error("Native transaction value does not equal input amount plus encoded native fees");
|
|
49
|
+
const decoded = {
|
|
50
|
+
fromToken: isSameAddress(swap.fromAsset, NULL_ADDRESS)
|
|
51
|
+
? native
|
|
52
|
+
: swap.fromAsset,
|
|
53
|
+
fromTokenAmount: amountIn.toFixed(0),
|
|
54
|
+
toToken: isSameAddress(swap.toAsset, NULL_ADDRESS) ? native : swap.toAsset,
|
|
55
|
+
minReceiveToTokenAmount: swap.amountOutMin,
|
|
56
|
+
// LibRouter reads toAddress literally; zero is not a msg.sender sentinel.
|
|
57
|
+
toTokenReceiver: swap.receiver,
|
|
58
|
+
};
|
|
59
|
+
decodedPayloads.set(decoded, {
|
|
60
|
+
data: tx.data,
|
|
61
|
+
payload,
|
|
62
|
+
inputFee: nativeInput ? inputFee : undefined,
|
|
63
|
+
});
|
|
64
|
+
return decoded;
|
|
65
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface PackedSwap {
|
|
2
|
+
receiver: string;
|
|
3
|
+
fromAsset: string;
|
|
4
|
+
toAsset: string;
|
|
5
|
+
amountIn: string;
|
|
6
|
+
amountOutMin: string;
|
|
7
|
+
}
|
|
8
|
+
export declare class PackedReader {
|
|
9
|
+
private readonly payloadLength;
|
|
10
|
+
private readonly hex;
|
|
11
|
+
constructor(data: string, payloadLength: number);
|
|
12
|
+
private read;
|
|
13
|
+
address(offset: number): string;
|
|
14
|
+
uint(offset: number, length?: number): string;
|
|
15
|
+
compact(shiftOffset: number, pointerOffset: number): string;
|
|
16
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import BigNumber from "bignumber.js";
|
|
2
|
+
// Offsets include the selector. Reads must stay inside the declared bytes payload.
|
|
3
|
+
export class PackedReader {
|
|
4
|
+
constructor(data, payloadLength) {
|
|
5
|
+
this.payloadLength = payloadLength;
|
|
6
|
+
this.hex = data.slice(2);
|
|
7
|
+
}
|
|
8
|
+
read(offset, length) {
|
|
9
|
+
if (offset < 68 || offset + length > 68 + this.payloadLength) {
|
|
10
|
+
throw new Error(`Packed field points outside the Magpie bytes payload (offset ${offset}, length ${length})`);
|
|
11
|
+
}
|
|
12
|
+
return this.hex.slice(offset * 2, (offset + length) * 2);
|
|
13
|
+
}
|
|
14
|
+
address(offset) {
|
|
15
|
+
return `0x${this.read(offset, 20)}`.toLowerCase();
|
|
16
|
+
}
|
|
17
|
+
uint(offset, length = 32) {
|
|
18
|
+
return new BigNumber(this.read(offset, length), 16).toFixed(0);
|
|
19
|
+
}
|
|
20
|
+
compact(shiftOffset, pointerOffset) {
|
|
21
|
+
const shift = Number(this.uint(shiftOffset, 1));
|
|
22
|
+
const pointer = Number(this.uint(pointerOffset, 2));
|
|
23
|
+
// Only the high (256 - shift) bits survive Solidity's shr.
|
|
24
|
+
const significantBits = 256 - shift;
|
|
25
|
+
const length = Math.ceil(significantBits / 8);
|
|
26
|
+
return new BigNumber(this.uint(pointer, length))
|
|
27
|
+
.idiv(new BigNumber(2).pow(length * 8 - significantBits))
|
|
28
|
+
.toFixed(0);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface Tx {
|
|
2
|
+
from: string;
|
|
3
|
+
to: string;
|
|
4
|
+
data: string;
|
|
5
|
+
value: string;
|
|
6
|
+
chainId?: number;
|
|
7
|
+
gas?: number;
|
|
8
|
+
gasPrice?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface TxWithChainId extends Tx {
|
|
11
|
+
chainId: number;
|
|
12
|
+
}
|
|
13
|
+
export interface DecodeCalldataOptions {
|
|
14
|
+
nativeTokenAddress?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface DecodeCalldataResult {
|
|
17
|
+
fromToken: string;
|
|
18
|
+
fromTokenAmount: string;
|
|
19
|
+
toToken: string;
|
|
20
|
+
minReceiveToTokenAmount: string;
|
|
21
|
+
toTokenReceiver: string;
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { SwapRouter } from "@uniswap/universal-router-sdk";
|
|
2
|
+
// The current quote flow uses execute directly or through SwapProxy.
|
|
3
|
+
// Keep the official ABI as the source of truth, excluding signed execution.
|
|
4
|
+
export const UniversalRouterABI = Object.values(SwapRouter.INTERFACE.functions).filter((fragment) => fragment.name === "execute");
|
|
5
|
+
export const SwapProxyABI = Object.values(SwapRouter.PROXY_INTERFACE.functions);
|
|
6
|
+
export const UniABI = [...UniversalRouterABI, ...SwapProxyABI];
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { DecodeCalldataResult } from "../types";
|
|
2
|
+
export declare const getArcNativeInputAmount: (token: string, chainId: number, txValue: string) => string | undefined;
|
|
3
|
+
export declare const isSameUniAsset: (a: string, b: string, chainId: number, native: string) => boolean;
|
|
4
|
+
export declare const normalizeUniAssets: (result: DecodeCalldataResult, chainId: number, native: string) => DecodeCalldataResult;
|
|
5
|
+
export declare const getComparableUniInputAmount: (requestedAmount: string, decoded: DecodeCalldataResult, chainId: number, native: string) => string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import BigNumber from "bignumber.js";
|
|
2
|
+
import { isSameAddress } from "../../utils";
|
|
3
|
+
import { ARC_ERC20_USDC } from "../../consts";
|
|
4
|
+
const ARC_CHAIN_ID = 5042;
|
|
5
|
+
const ARC_NATIVE_SCALE = "1000000000000";
|
|
6
|
+
// Arc funds its ERC20 USDC route through msg.value without a WETH-style wrap.
|
|
7
|
+
// Fix the complete input before reading individual router-funded split legs,
|
|
8
|
+
// just as SwapProxy ingress / WRAP_ETH does on other routes. Keep ERC20 units
|
|
9
|
+
// until normalizeUniAssets converts the final result to native units.
|
|
10
|
+
// https://docs.arc.io/arc/concepts/stablecoin-native-model#two-interfaces-one-balance
|
|
11
|
+
export const getArcNativeInputAmount = (token, chainId, txValue) => chainId === ARC_CHAIN_ID &&
|
|
12
|
+
isSameAddress(token, ARC_ERC20_USDC) &&
|
|
13
|
+
new BigNumber(txValue).gt(0)
|
|
14
|
+
? new BigNumber(txValue).idiv(ARC_NATIVE_SCALE).toFixed(0)
|
|
15
|
+
: undefined;
|
|
16
|
+
// Compare asset identity for refunds only. Do not merge raw native (18) and
|
|
17
|
+
// ERC20 (6) amounts or balances: they share an asset, but use different units.
|
|
18
|
+
// https://docs.arc.io/arc/concepts/stablecoin-native-model#two-interfaces-one-balance
|
|
19
|
+
export const isSameUniAsset = (a, b, chainId, native) => {
|
|
20
|
+
const identity = (token) => chainId === ARC_CHAIN_ID && isSameAddress(token, ARC_ERC20_USDC)
|
|
21
|
+
? native
|
|
22
|
+
: token;
|
|
23
|
+
return isSameAddress(identity(a), identity(b));
|
|
24
|
+
};
|
|
25
|
+
// Arc exposes one USDC balance as native (18 decimals) and ERC20 (6 decimals).
|
|
26
|
+
// Normalize both the asset and its amount together, after route aggregation.
|
|
27
|
+
// https://docs.arc.io/arc/concepts/stablecoin-native-model#two-interfaces-one-balance
|
|
28
|
+
// Tempo pathUSD is a real 6-decimal TIP20; it must retain its contract address.
|
|
29
|
+
export const normalizeUniAssets = (result, chainId, native) => {
|
|
30
|
+
if (chainId !== ARC_CHAIN_ID || isSameAddress(native, ARC_ERC20_USDC))
|
|
31
|
+
return result;
|
|
32
|
+
const nativeIn = isSameAddress(result.fromToken, ARC_ERC20_USDC);
|
|
33
|
+
const nativeOut = isSameAddress(result.toToken, ARC_ERC20_USDC);
|
|
34
|
+
return Object.assign(Object.assign(Object.assign({}, result), (nativeIn
|
|
35
|
+
? {
|
|
36
|
+
fromToken: native,
|
|
37
|
+
fromTokenAmount: new BigNumber(result.fromTokenAmount)
|
|
38
|
+
.times(ARC_NATIVE_SCALE)
|
|
39
|
+
.toFixed(),
|
|
40
|
+
}
|
|
41
|
+
: {})), (nativeOut
|
|
42
|
+
? {
|
|
43
|
+
toToken: native,
|
|
44
|
+
minReceiveToTokenAmount: new BigNumber(result.minReceiveToTokenAmount)
|
|
45
|
+
.times(ARC_NATIVE_SCALE)
|
|
46
|
+
.toFixed(),
|
|
47
|
+
}
|
|
48
|
+
: {}));
|
|
49
|
+
};
|
|
50
|
+
export const getComparableUniInputAmount = (requestedAmount, decoded, chainId, native) => {
|
|
51
|
+
// Keep exact native-value inputs intact. Only Arc's native USDC can also
|
|
52
|
+
// travel through its 6-decimal ERC20 interface, which truncates 12 digits.
|
|
53
|
+
// Compare against that exact integer conversion, without a general tolerance.
|
|
54
|
+
// https://www.arc.io/blog/arc-compatibility-guide-for-existing-evm-apps
|
|
55
|
+
if (decoded.fromTokenAmount !== requestedAmount &&
|
|
56
|
+
chainId === ARC_CHAIN_ID &&
|
|
57
|
+
isSameAddress(decoded.fromToken, native) &&
|
|
58
|
+
!isSameAddress(native, ARC_ERC20_USDC)) {
|
|
59
|
+
return new BigNumber(requestedAmount)
|
|
60
|
+
.idiv(ARC_NATIVE_SCALE)
|
|
61
|
+
.times(ARC_NATIVE_SCALE)
|
|
62
|
+
.toFixed(0);
|
|
63
|
+
}
|
|
64
|
+
return requestedAmount;
|
|
65
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Interface } from "@ethersproject/abi";
|
|
2
|
+
import BigNumber from "bignumber.js";
|
|
3
|
+
import { getNativeTokenAddress } from "../chain";
|
|
4
|
+
import { UniABI } from "./abi";
|
|
5
|
+
import { chainToken, finishSwap } from "./shared";
|
|
6
|
+
import { walkCommands } from "./universalRouter/commands";
|
|
7
|
+
import { getRouterProfile } from "./universalRouter/profiles";
|
|
8
|
+
import { normalizeUniAssets } from "./assets";
|
|
9
|
+
const uniInterface = new Interface(UniABI);
|
|
10
|
+
export const decodeCalldata = (tx, options = {}) => {
|
|
11
|
+
var _a;
|
|
12
|
+
const native = getNativeTokenAddress(tx.chainId, options.nativeTokenAddress);
|
|
13
|
+
const transaction = uniInterface.parseTransaction({ data: tx.data });
|
|
14
|
+
const ctx = {
|
|
15
|
+
chainId: tx.chainId,
|
|
16
|
+
native,
|
|
17
|
+
sender: tx.from,
|
|
18
|
+
// SwapProxy calls execute normally, so the Router's msgSender() is the
|
|
19
|
+
// proxy, while tx.from remains the user whose requested payout we verify.
|
|
20
|
+
// https://github.com/Uniswap/universal-router/blob/main/contracts/SwapProxy.sol
|
|
21
|
+
routerCaller: transaction.args.router ? tx.to : tx.from,
|
|
22
|
+
// SwapProxy passes the actual Universal Router as an explicit argument.
|
|
23
|
+
router: transaction.args.router || tx.to,
|
|
24
|
+
proxyToken: transaction.args.token,
|
|
25
|
+
proxyAmount: (_a = transaction.args.amount) === null || _a === void 0 ? void 0 : _a.toString(),
|
|
26
|
+
txValue: tx.value,
|
|
27
|
+
};
|
|
28
|
+
const profile = getRouterProfile(ctx.chainId, ctx.router);
|
|
29
|
+
const swap = {};
|
|
30
|
+
if (new BigNumber(ctx.txValue).gt(0))
|
|
31
|
+
swap.pendingMinimums = { [ctx.native.toLowerCase()]: ctx.txValue };
|
|
32
|
+
if (ctx.proxyToken && ctx.proxyAmount && ctx.proxyAmount !== "0") {
|
|
33
|
+
swap.fromToken = ctx.proxyToken;
|
|
34
|
+
swap.fromTokenAmount = ctx.proxyAmount;
|
|
35
|
+
swap.inputAmountLocked = true;
|
|
36
|
+
(swap.pendingMinimums || (swap.pendingMinimums = {}))[chainToken(ctx.proxyToken, ctx.native).toLowerCase()] = ctx.proxyAmount;
|
|
37
|
+
}
|
|
38
|
+
walkCommands(transaction.args.commands, transaction.args.inputs, swap, ctx, profile);
|
|
39
|
+
return normalizeUniAssets(finishSwap(swap), tx.chainId, native);
|
|
40
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import BigNumber from "bignumber.js";
|
|
2
|
+
import type { DecodeCalldataResult } from "../types";
|
|
3
|
+
export declare const MSG_SENDER = "0x0000000000000000000000000000000000000001";
|
|
4
|
+
export declare const ROUTER = "0x0000000000000000000000000000000000000002";
|
|
5
|
+
export declare const CONTRACT_BALANCE: BigNumber;
|
|
6
|
+
export type Minimums = Record<string, string>;
|
|
7
|
+
export interface Swap {
|
|
8
|
+
fromToken?: string;
|
|
9
|
+
fromTokenAmount?: string;
|
|
10
|
+
toToken?: string;
|
|
11
|
+
minReceive?: string;
|
|
12
|
+
receiver?: string;
|
|
13
|
+
inputAmountLocked?: boolean;
|
|
14
|
+
hasSwap?: boolean;
|
|
15
|
+
pendingMinimums?: Minimums;
|
|
16
|
+
payouts?: Record<string, {
|
|
17
|
+
token: string;
|
|
18
|
+
minimum: string;
|
|
19
|
+
receiver: string;
|
|
20
|
+
}>;
|
|
21
|
+
}
|
|
22
|
+
export interface Context {
|
|
23
|
+
chainId: number;
|
|
24
|
+
native: string;
|
|
25
|
+
sender: string;
|
|
26
|
+
routerCaller: string;
|
|
27
|
+
router: string;
|
|
28
|
+
txValue: string;
|
|
29
|
+
proxyToken?: string;
|
|
30
|
+
proxyAmount?: string;
|
|
31
|
+
}
|
|
32
|
+
export declare const chainToken: (token: string, native: string) => string;
|
|
33
|
+
export declare const realAmount: (amount: string, fallback?: string) => string;
|
|
34
|
+
export declare const assertInputAsset: (swap: Swap, ctx: Context, token: string) => void;
|
|
35
|
+
export declare const setSwap: (swap: Swap, ctx: Context, fromToken: string, toToken: string, amountA: string, amountB: string, payment?: {
|
|
36
|
+
recipient?: string;
|
|
37
|
+
payerIsUser?: boolean;
|
|
38
|
+
inputPair?: string;
|
|
39
|
+
minimums?: Minimums;
|
|
40
|
+
}) => void;
|
|
41
|
+
export declare const setOutput: (swap: Swap, ctx: Context, token: string, recipient: string, minimum?: string, exactAmount?: boolean, poolMinimums?: Minimums) => void;
|
|
42
|
+
export declare const payPortion: (swap: Swap, ctx: Context, token: string, recipient: string, portion: string, denominator: string, poolMinimums?: Minimums) => void;
|
|
43
|
+
export declare const wrapNative: (swap: Swap, ctx: Context, amount: string, recipient: string) => void;
|
|
44
|
+
export declare const unwrapNative: (swap: Swap, ctx: Context, recipient: string, minimum: string) => void;
|
|
45
|
+
export declare const finishSwap: (swap: Swap) => DecodeCalldataResult;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import BigNumber from "bignumber.js";
|
|
2
|
+
import { isSameAddress, NULL_ADDRESS } from "../../utils";
|
|
3
|
+
import { getArcNativeInputAmount, isSameUniAsset } from "./assets";
|
|
4
|
+
export const MSG_SENDER = "0x0000000000000000000000000000000000000001";
|
|
5
|
+
export const ROUTER = "0x0000000000000000000000000000000000000002";
|
|
6
|
+
const EETH = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
|
|
7
|
+
export const CONTRACT_BALANCE = new BigNumber(2).pow(255);
|
|
8
|
+
export const chainToken = (token, native) => isSameAddress(token, NULL_ADDRESS) || isSameAddress(token, EETH)
|
|
9
|
+
? native
|
|
10
|
+
: token;
|
|
11
|
+
export const realAmount = (amount, fallback) => CONTRACT_BALANCE.eq(amount) && fallback ? fallback : amount;
|
|
12
|
+
// Dispatcher.map() and BaseActionsRouter._mapRecipient() use the Router's
|
|
13
|
+
// execution caller, which is SwapProxy for proxied swaps, not the outer user.
|
|
14
|
+
// https://github.com/Uniswap/universal-router/blob/main/contracts/base/Dispatcher.sol
|
|
15
|
+
// https://github.com/Uniswap/v4-periphery/blob/main/src/base/BaseActionsRouter.sol
|
|
16
|
+
const resolveRecipient = (recipient, ctx) => isSameAddress(recipient, MSG_SENDER)
|
|
17
|
+
? ctx.routerCaller
|
|
18
|
+
: isSameAddress(recipient, ROUTER)
|
|
19
|
+
? ctx.router
|
|
20
|
+
: recipient;
|
|
21
|
+
const isRouter = (recipient, ctx) => isSameAddress(resolveRecipient(recipient, ctx), ctx.router);
|
|
22
|
+
const isInputRefund = (swap, ctx, output) => swap.hasSwap &&
|
|
23
|
+
isSameUniAsset(output, swap.fromToken || "", ctx.chainId, ctx.native) &&
|
|
24
|
+
!isSameUniAsset(output, swap.toToken || "", ctx.chainId, ctx.native);
|
|
25
|
+
const recordPayout = (swap, ctx, token, recipient, minimum) => {
|
|
26
|
+
var _a;
|
|
27
|
+
const receiver = resolveRecipient(recipient, ctx);
|
|
28
|
+
const key = `${token.toLowerCase()}:${receiver.toLowerCase()}`;
|
|
29
|
+
const payouts = (swap.payouts || (swap.payouts = {}));
|
|
30
|
+
payouts[key] = {
|
|
31
|
+
token,
|
|
32
|
+
receiver,
|
|
33
|
+
minimum: new BigNumber(((_a = payouts[key]) === null || _a === void 0 ? void 0 : _a.minimum) || 0).plus(minimum).toFixed(),
|
|
34
|
+
};
|
|
35
|
+
swap.toToken = token;
|
|
36
|
+
swap.minReceive = payouts[key].minimum;
|
|
37
|
+
swap.receiver = receiver;
|
|
38
|
+
};
|
|
39
|
+
export const assertInputAsset = (swap, ctx, token) => {
|
|
40
|
+
if (swap.inputAmountLocked)
|
|
41
|
+
throw new Error("Additional user-funded debit after prepaid input cannot be represented by the locked input amount");
|
|
42
|
+
if (swap.fromToken &&
|
|
43
|
+
!isSameUniAsset(chainToken(token, ctx.native), swap.fromToken, ctx.chainId, ctx.native)) {
|
|
44
|
+
throw new Error("Multiple user-funded input assets cannot be represented as a single swap result");
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
export const setSwap = (swap, ctx, fromToken, toToken, amountA, amountB, payment = {}) => {
|
|
48
|
+
var _a;
|
|
49
|
+
const input = chainToken(fromToken, ctx.native);
|
|
50
|
+
const output = chainToken(toToken, ctx.native);
|
|
51
|
+
const pending = payment.minimums || (swap.pendingMinimums || (swap.pendingMinimums = {}));
|
|
52
|
+
const inputKey = input.toLowerCase();
|
|
53
|
+
const poolCredit = payment.minimums && pending[inputKey] !== undefined;
|
|
54
|
+
if (payment.payerIsUser === true)
|
|
55
|
+
assertInputAsset(swap, ctx, input);
|
|
56
|
+
if (payment.inputPair && payment.payerIsUser === false) {
|
|
57
|
+
// V3 / V4 can pay this token directly into the next V2 pair. Remove only
|
|
58
|
+
// that proven handoff, retaining payments to every other token or recipient.
|
|
59
|
+
const key = `${inputKey}:${payment.inputPair.toLowerCase()}`;
|
|
60
|
+
(_a = swap.payouts) === null || _a === void 0 ? true : delete _a[key];
|
|
61
|
+
}
|
|
62
|
+
if (!swap.inputAmountLocked) {
|
|
63
|
+
if (!swap.hasSwap) {
|
|
64
|
+
const nativeInputAmount = payment.payerIsUser === true
|
|
65
|
+
? undefined
|
|
66
|
+
: isSameAddress(input, ctx.native) && new BigNumber(ctx.txValue).gt(0)
|
|
67
|
+
? ctx.txValue
|
|
68
|
+
: getArcNativeInputAmount(input, ctx.chainId, ctx.txValue);
|
|
69
|
+
const amount = nativeInputAmount !== null && nativeInputAmount !== void 0 ? nativeInputAmount : realAmount(amountA, swap.fromTokenAmount || ctx.proxyAmount || ctx.txValue);
|
|
70
|
+
if (!new BigNumber(amount).gt(0) || CONTRACT_BALANCE.eq(amount)) {
|
|
71
|
+
throw new Error("Cannot determine the original input amount from a dynamic router balance");
|
|
72
|
+
}
|
|
73
|
+
swap.fromToken = input;
|
|
74
|
+
swap.fromTokenAmount = amount;
|
|
75
|
+
if (nativeInputAmount !== undefined)
|
|
76
|
+
swap.inputAmountLocked = true;
|
|
77
|
+
}
|
|
78
|
+
else if (payment.payerIsUser !== false &&
|
|
79
|
+
!poolCredit &&
|
|
80
|
+
isSameAddress(input, swap.fromToken || "")) {
|
|
81
|
+
const amount = amountA;
|
|
82
|
+
// Only explicit root inputs can be summed. Intermediate OPEN_DELTA (0)
|
|
83
|
+
// and CONTRACT_BALANCE values describe router funds, not another user debit.
|
|
84
|
+
if (!CONTRACT_BALANCE.eq(amount)) {
|
|
85
|
+
swap.fromTokenAmount = new BigNumber(swap.fromTokenAmount)
|
|
86
|
+
.plus(amount)
|
|
87
|
+
.toFixed();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (payment.payerIsUser !== true && pending[inputKey] !== undefined) {
|
|
92
|
+
const amount = amountA;
|
|
93
|
+
pending[inputKey] =
|
|
94
|
+
amount === "0" || CONTRACT_BALANCE.eq(amount)
|
|
95
|
+
? "0"
|
|
96
|
+
: BigNumber.max(0, new BigNumber(pending[inputKey]).minus(amount)).toFixed();
|
|
97
|
+
}
|
|
98
|
+
swap.toToken = output;
|
|
99
|
+
const minimum = amountB;
|
|
100
|
+
swap.hasSwap = true;
|
|
101
|
+
const recipient = payment.recipient || ROUTER;
|
|
102
|
+
if (isRouter(recipient, ctx)) {
|
|
103
|
+
const key = output.toLowerCase();
|
|
104
|
+
pending[key] = new BigNumber(pending[key] || 0).plus(minimum).toFixed();
|
|
105
|
+
swap.minReceive = pending[key];
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
recordPayout(swap, ctx, output, recipient, minimum);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
export const setOutput = (swap, ctx, token, recipient, minimum, exactAmount = false, poolMinimums) => {
|
|
112
|
+
var _a;
|
|
113
|
+
const output = chainToken(token, ctx.native);
|
|
114
|
+
if (isRouter(recipient, ctx) && !poolMinimums)
|
|
115
|
+
return;
|
|
116
|
+
const pending = poolMinimums || (swap.pendingMinimums || (swap.pendingMinimums = {}));
|
|
117
|
+
const key = output.toLowerCase();
|
|
118
|
+
const amount = (_a = minimum !== null && minimum !== void 0 ? minimum : pending[key]) !== null && _a !== void 0 ? _a : "0";
|
|
119
|
+
pending[key] = exactAmount
|
|
120
|
+
? BigNumber.max(0, new BigNumber(pending[key] || 0).minus(amount)).toFixed()
|
|
121
|
+
: "0";
|
|
122
|
+
if (isRouter(recipient, ctx)) {
|
|
123
|
+
// TAKE moves pool credit into the router; it is not another pool credit
|
|
124
|
+
// or a payout to the user. Keep the two balances separate.
|
|
125
|
+
const routerMinimums = (swap.pendingMinimums || (swap.pendingMinimums = {}));
|
|
126
|
+
routerMinimums[key] = new BigNumber(routerMinimums[key] || 0)
|
|
127
|
+
.plus(amount)
|
|
128
|
+
.toFixed();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// Only input returned to the user is a refund; third-party transfers must
|
|
132
|
+
// remain visible to the final payout checks.
|
|
133
|
+
if (isInputRefund(swap, ctx, output) &&
|
|
134
|
+
isSameAddress(resolveRecipient(recipient, ctx), ctx.sender))
|
|
135
|
+
return;
|
|
136
|
+
recordPayout(swap, ctx, output, recipient, amount);
|
|
137
|
+
};
|
|
138
|
+
// Payments.sol and V4Router TAKE_PORTION transfer floor(balance * portion /
|
|
139
|
+
// denominator). Subtract that fee before a later balance-based payout.
|
|
140
|
+
// https://github.com/Uniswap/universal-router/blob/main/contracts/modules/Payments.sol
|
|
141
|
+
// https://github.com/Uniswap/v4-periphery/blob/main/src/V4Router.sol
|
|
142
|
+
export const payPortion = (swap, ctx, token, recipient, portion, denominator, poolMinimums) => {
|
|
143
|
+
const ratio = new BigNumber(portion);
|
|
144
|
+
if (!ratio.isInteger() || ratio.lt(0) || ratio.gt(denominator))
|
|
145
|
+
throw new Error("Invalid payment portion");
|
|
146
|
+
if (isRouter(recipient, ctx) && !poolMinimums)
|
|
147
|
+
return;
|
|
148
|
+
const pending = poolMinimums || (swap.pendingMinimums || (swap.pendingMinimums = {}));
|
|
149
|
+
const output = chainToken(token, ctx.native);
|
|
150
|
+
const key = output.toLowerCase();
|
|
151
|
+
const minimum = new BigNumber(pending[key] || 0);
|
|
152
|
+
// The remaining balance and the fee are monotone functions of the balance;
|
|
153
|
+
// their lower bounds can both be computed from its known minimum.
|
|
154
|
+
const fee = minimum.times(ratio).idiv(denominator).toFixed(0);
|
|
155
|
+
pending[key] = minimum.minus(fee).toFixed(0);
|
|
156
|
+
if (poolMinimums && isRouter(recipient, ctx)) {
|
|
157
|
+
const routerMinimums = (swap.pendingMinimums || (swap.pendingMinimums = {}));
|
|
158
|
+
routerMinimums[key] = new BigNumber(routerMinimums[key] || 0)
|
|
159
|
+
.plus(fee)
|
|
160
|
+
.toFixed();
|
|
161
|
+
}
|
|
162
|
+
else if (isSameAddress(resolveRecipient(recipient, ctx), ctx.sender)) {
|
|
163
|
+
if (!isInputRefund(swap, ctx, output))
|
|
164
|
+
recordPayout(swap, ctx, output, recipient, fee);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
export const wrapNative = (swap, ctx, amount, recipient) => {
|
|
168
|
+
if (!isRouter(recipient, ctx))
|
|
169
|
+
throw new Error("WRAP_ETH to an external recipient is not modeled as a single swap input");
|
|
170
|
+
const pending = (swap.pendingMinimums || (swap.pendingMinimums = {}));
|
|
171
|
+
const key = ctx.native.toLowerCase();
|
|
172
|
+
pending[key] = CONTRACT_BALANCE.eq(amount)
|
|
173
|
+
? "0"
|
|
174
|
+
: BigNumber.max(0, new BigNumber(pending[key] || 0).minus(amount)).toFixed();
|
|
175
|
+
if (swap.hasSwap || swap.inputAmountLocked)
|
|
176
|
+
return;
|
|
177
|
+
swap.fromToken = ctx.native;
|
|
178
|
+
// msg.value is the complete native budget, including subsequent split wraps
|
|
179
|
+
// and any input eventually refunded, rather than just the first wrapped leg.
|
|
180
|
+
swap.fromTokenAmount = new BigNumber(ctx.txValue).gt(0)
|
|
181
|
+
? ctx.txValue
|
|
182
|
+
: realAmount(amount, ctx.txValue);
|
|
183
|
+
swap.inputAmountLocked = true;
|
|
184
|
+
};
|
|
185
|
+
export const unwrapNative = (swap, ctx, recipient, minimum) => {
|
|
186
|
+
setOutput(swap, ctx, ctx.native, recipient, minimum);
|
|
187
|
+
};
|
|
188
|
+
export const finishSwap = (swap) => {
|
|
189
|
+
const payouts = Object.values(swap.payouts || {});
|
|
190
|
+
if (payouts.length > 1) {
|
|
191
|
+
throw new Error("Multiple output assets or recipients cannot be represented as a single swap result");
|
|
192
|
+
}
|
|
193
|
+
if (payouts.length === 1) {
|
|
194
|
+
swap.toToken = payouts[0].token;
|
|
195
|
+
swap.minReceive = payouts[0].minimum;
|
|
196
|
+
swap.receiver = payouts[0].receiver;
|
|
197
|
+
}
|
|
198
|
+
const required = [
|
|
199
|
+
"fromToken",
|
|
200
|
+
"fromTokenAmount",
|
|
201
|
+
"toToken",
|
|
202
|
+
"minReceive",
|
|
203
|
+
"receiver",
|
|
204
|
+
];
|
|
205
|
+
const missing = required.filter((field) => !swap[field]);
|
|
206
|
+
if (missing.length) {
|
|
207
|
+
throw new Error(`Incomplete swap data: missing ${missing.join(", ")}`);
|
|
208
|
+
}
|
|
209
|
+
if (!new BigNumber(swap.fromTokenAmount).gt(0) ||
|
|
210
|
+
CONTRACT_BALANCE.eq(swap.fromTokenAmount)) {
|
|
211
|
+
throw new Error("Original input amount is zero or unresolved");
|
|
212
|
+
}
|
|
213
|
+
if (isSameAddress(swap.fromToken, swap.toToken)) {
|
|
214
|
+
throw new Error("The route does not describe a swap between two distinct assets");
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
fromToken: swap.fromToken,
|
|
218
|
+
fromTokenAmount: swap.fromTokenAmount,
|
|
219
|
+
toToken: swap.toToken,
|
|
220
|
+
minReceiveToTokenAmount: swap.minReceive,
|
|
221
|
+
toTokenReceiver: swap.receiver,
|
|
222
|
+
};
|
|
223
|
+
};
|