@ekubo/yul-router-sdk 0.1.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/index.d.ts +62 -0
- package/dist/index.js +182 -0
- package/package.json +33 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Address, Hex } from "viem";
|
|
2
|
+
export declare const MIN_SQRT_RATIO = 4611797791050542631n;
|
|
3
|
+
export declare const MAX_SQRT_RATIO = 79227682466138141934206691491n;
|
|
4
|
+
export declare const MIN_CALCULATED_AMOUNT_THRESHOLD: bigint;
|
|
5
|
+
export declare const MAX_CALCULATED_AMOUNT_THRESHOLD: bigint;
|
|
6
|
+
export declare const MAX_MULTIHOP_LENGTH = 256;
|
|
7
|
+
export declare const MAX_HOP_LENGTH = 256;
|
|
8
|
+
export interface PoolKey {
|
|
9
|
+
token0: Address;
|
|
10
|
+
token1: Address;
|
|
11
|
+
config: Hex;
|
|
12
|
+
}
|
|
13
|
+
export interface CoreHop {
|
|
14
|
+
type: "core";
|
|
15
|
+
poolKey: PoolKey;
|
|
16
|
+
sqrtRatioLimit?: bigint;
|
|
17
|
+
skipAhead?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface ForwardedHop {
|
|
20
|
+
type: "forwarded";
|
|
21
|
+
forwardee: Address;
|
|
22
|
+
poolKey: PoolKey;
|
|
23
|
+
sqrtRatioLimit?: bigint;
|
|
24
|
+
skipAhead?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface Ve33Hop {
|
|
27
|
+
type: "ve33";
|
|
28
|
+
forwardee: Address;
|
|
29
|
+
poolKey: PoolKey;
|
|
30
|
+
sqrtRatioLimit?: bigint;
|
|
31
|
+
skipAhead?: number;
|
|
32
|
+
}
|
|
33
|
+
export interface WrapperHop {
|
|
34
|
+
type: "wrapper";
|
|
35
|
+
underlying: Address;
|
|
36
|
+
wrapped: Address;
|
|
37
|
+
}
|
|
38
|
+
export type Hop = CoreHop | ForwardedHop | Ve33Hop | WrapperHop;
|
|
39
|
+
export interface MultiHop {
|
|
40
|
+
specifiedAmount: bigint;
|
|
41
|
+
hops: readonly Hop[];
|
|
42
|
+
}
|
|
43
|
+
export interface EncodeRoutesParameters {
|
|
44
|
+
specifiedToken: Address;
|
|
45
|
+
calculatedToken: Address;
|
|
46
|
+
calculatedAmountThreshold?: bigint;
|
|
47
|
+
recipient?: Address;
|
|
48
|
+
multiHops: readonly MultiHop[];
|
|
49
|
+
}
|
|
50
|
+
export interface EncodeRouteParameters {
|
|
51
|
+
specifiedToken: Address;
|
|
52
|
+
calculatedToken: Address;
|
|
53
|
+
specifiedAmount: bigint;
|
|
54
|
+
calculatedAmountThreshold?: bigint;
|
|
55
|
+
recipient?: Address;
|
|
56
|
+
hops: readonly Hop[];
|
|
57
|
+
}
|
|
58
|
+
export type Parameters = EncodeRoutesParameters;
|
|
59
|
+
export declare function encodeRoute(params: EncodeRouteParameters): Hex;
|
|
60
|
+
export declare function generateCalldata(params: EncodeRoutesParameters): Hex;
|
|
61
|
+
export declare function encodeRoutes(params: EncodeRoutesParameters): Hex;
|
|
62
|
+
export declare function calldataSize(data: Hex): number;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { concatHex, getAddress, hexToBigInt, maxInt128, minInt128, numberToHex, padHex, size, } from "viem";
|
|
2
|
+
export const MIN_SQRT_RATIO = 4611797791050542631n;
|
|
3
|
+
export const MAX_SQRT_RATIO = 79227682466138141934206691491n;
|
|
4
|
+
export const MIN_CALCULATED_AMOUNT_THRESHOLD = minInt128;
|
|
5
|
+
export const MAX_CALCULATED_AMOUNT_THRESHOLD = maxInt128;
|
|
6
|
+
export const MAX_MULTIHOP_LENGTH = 256;
|
|
7
|
+
export const MAX_HOP_LENGTH = 256;
|
|
8
|
+
export function encodeRoute(params) {
|
|
9
|
+
const { specifiedAmount, hops, ...shared } = params;
|
|
10
|
+
return encodeRoutes({
|
|
11
|
+
...shared,
|
|
12
|
+
multiHops: [{ specifiedAmount, hops }],
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export function generateCalldata(params) {
|
|
16
|
+
return encodeRoutes(params);
|
|
17
|
+
}
|
|
18
|
+
export function encodeRoutes(params) {
|
|
19
|
+
const { specifiedToken, calculatedToken, calculatedAmountThreshold, recipient, multiHops, } = params;
|
|
20
|
+
if (multiHops.length < 1 || multiHops.length > MAX_MULTIHOP_LENGTH) {
|
|
21
|
+
throw new Error(`multiHops length must be between 1 and ${MAX_MULTIHOP_LENGTH}`);
|
|
22
|
+
}
|
|
23
|
+
const specified = getAddress(specifiedToken);
|
|
24
|
+
const calculated = getAddress(calculatedToken);
|
|
25
|
+
let isExactOut;
|
|
26
|
+
const encodedMultiHops = [];
|
|
27
|
+
for (const multiHop of multiHops) {
|
|
28
|
+
const { specifiedAmount, hops } = multiHop;
|
|
29
|
+
assertInt128(specifiedAmount, "specifiedAmount");
|
|
30
|
+
if (specifiedAmount !== 0n) {
|
|
31
|
+
const multiHopExactOut = specifiedAmount < 0n;
|
|
32
|
+
if (isExactOut !== undefined && isExactOut !== multiHopExactOut) {
|
|
33
|
+
throw new Error("mixed exact-out / exact-in multi-hops");
|
|
34
|
+
}
|
|
35
|
+
isExactOut = multiHopExactOut;
|
|
36
|
+
}
|
|
37
|
+
if (hops.length < 1 || hops.length > MAX_HOP_LENGTH) {
|
|
38
|
+
throw new Error(`each multi-hop needs between 1 and ${MAX_HOP_LENGTH} hops`);
|
|
39
|
+
}
|
|
40
|
+
let currentToken = specified;
|
|
41
|
+
const encodedHops = [];
|
|
42
|
+
for (const hop of hops) {
|
|
43
|
+
switch (hop.type) {
|
|
44
|
+
case "core": {
|
|
45
|
+
const { nextToken } = resolvePoolHop(currentToken, hop.poolKey);
|
|
46
|
+
encodedHops.push(encodeSwapHop("00", hop.poolKey, hop.sqrtRatioLimit, hop.skipAhead));
|
|
47
|
+
currentToken = nextToken;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
case "forwarded": {
|
|
51
|
+
const { poolKey, forwardee } = hop;
|
|
52
|
+
const { nextToken } = resolvePoolHop(currentToken, poolKey);
|
|
53
|
+
encodedHops.push(concatHex([
|
|
54
|
+
"0x01",
|
|
55
|
+
encodeAddress(forwardee),
|
|
56
|
+
encodePoolKey(poolKey),
|
|
57
|
+
encodeSqrtRatioLimit(hop.sqrtRatioLimit),
|
|
58
|
+
encodeSkipAhead(hop.skipAhead),
|
|
59
|
+
]));
|
|
60
|
+
currentToken = nextToken;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case "ve33": {
|
|
64
|
+
const { poolKey, forwardee } = hop;
|
|
65
|
+
const { nextToken } = resolvePoolHop(currentToken, poolKey);
|
|
66
|
+
encodedHops.push(concatHex([
|
|
67
|
+
"0x03",
|
|
68
|
+
encodeAddress(forwardee),
|
|
69
|
+
encodePoolKey(poolKey),
|
|
70
|
+
encodeSqrtRatioLimit(hop.sqrtRatioLimit),
|
|
71
|
+
encodeSkipAhead(hop.skipAhead),
|
|
72
|
+
]));
|
|
73
|
+
currentToken = nextToken;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case "wrapper": {
|
|
77
|
+
const underlying = getAddress(hop.underlying);
|
|
78
|
+
const wrapped = getAddress(hop.wrapped);
|
|
79
|
+
if (hexToBigInt(underlying) === hexToBigInt(wrapped)) {
|
|
80
|
+
throw new Error("underlying and wrapped token must differ");
|
|
81
|
+
}
|
|
82
|
+
if (currentToken === underlying) {
|
|
83
|
+
currentToken = wrapped;
|
|
84
|
+
}
|
|
85
|
+
else if (currentToken === wrapped) {
|
|
86
|
+
currentToken = underlying;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
throw new Error("wrapper hop is disconnected");
|
|
90
|
+
}
|
|
91
|
+
encodedHops.push(concatHex(["0x02", encodeAddress(underlying), encodeAddress(wrapped)]));
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (currentToken !== calculated) {
|
|
97
|
+
throw new Error("calculatedToken does not match multi-hop output");
|
|
98
|
+
}
|
|
99
|
+
encodedMultiHops.push(concatHex([
|
|
100
|
+
encodeInt128(specifiedAmount),
|
|
101
|
+
numberToHex(hops.length - 1, { size: 1 }),
|
|
102
|
+
...encodedHops,
|
|
103
|
+
]));
|
|
104
|
+
}
|
|
105
|
+
const threshold = calculatedAmountThreshold ??
|
|
106
|
+
(isExactOut === true ? MIN_CALCULATED_AMOUNT_THRESHOLD : 0n);
|
|
107
|
+
assertInt128(threshold, "calculatedAmountThreshold");
|
|
108
|
+
if (threshold !== 0n && isExactOut !== undefined && (threshold < 0n) !== isExactOut) {
|
|
109
|
+
throw new Error("calculatedAmountThreshold sign and specified amount signs have to match");
|
|
110
|
+
}
|
|
111
|
+
const flags = recipient ? 1 : 0;
|
|
112
|
+
const header = concatHex([
|
|
113
|
+
numberToHex(flags, { size: 1 }),
|
|
114
|
+
numberToHex(multiHops.length - 1, { size: 1 }),
|
|
115
|
+
encodeAddress(specified),
|
|
116
|
+
encodeAddress(calculated),
|
|
117
|
+
encodeInt128(threshold),
|
|
118
|
+
...(recipient ? [encodeAddress(recipient)] : []),
|
|
119
|
+
]);
|
|
120
|
+
return concatHex([header, ...encodedMultiHops]);
|
|
121
|
+
}
|
|
122
|
+
function resolvePoolHop(currentToken, poolKey) {
|
|
123
|
+
const token0 = getAddress(poolKey.token0);
|
|
124
|
+
const token1 = getAddress(poolKey.token1);
|
|
125
|
+
if (hexToBigInt(token0) >= hexToBigInt(token1)) {
|
|
126
|
+
throw new Error("poolKey tokens must be sorted");
|
|
127
|
+
}
|
|
128
|
+
if (currentToken === token0) {
|
|
129
|
+
return { nextToken: token1 };
|
|
130
|
+
}
|
|
131
|
+
if (currentToken === token1) {
|
|
132
|
+
return { nextToken: token0 };
|
|
133
|
+
}
|
|
134
|
+
throw new Error("pool hop is disconnected");
|
|
135
|
+
}
|
|
136
|
+
function encodeSwapHop(kind, poolKey, sqrtRatioLimit, skipAhead) {
|
|
137
|
+
return concatHex([
|
|
138
|
+
`0x${kind}`,
|
|
139
|
+
encodePoolKey(poolKey),
|
|
140
|
+
encodeSqrtRatioLimit(sqrtRatioLimit),
|
|
141
|
+
encodeSkipAhead(skipAhead),
|
|
142
|
+
]);
|
|
143
|
+
}
|
|
144
|
+
function encodePoolKey(poolKey) {
|
|
145
|
+
const token0 = getAddress(poolKey.token0);
|
|
146
|
+
const token1 = getAddress(poolKey.token1);
|
|
147
|
+
if (hexToBigInt(token0) >= hexToBigInt(token1)) {
|
|
148
|
+
throw new Error("poolKey tokens must be sorted");
|
|
149
|
+
}
|
|
150
|
+
const config = padHex(poolKey.config, { size: 32 });
|
|
151
|
+
return concatHex([encodeAddress(token0), encodeAddress(token1), config]);
|
|
152
|
+
}
|
|
153
|
+
function encodeAddress(address) {
|
|
154
|
+
return getAddress(address);
|
|
155
|
+
}
|
|
156
|
+
function encodeSqrtRatioLimit(value) {
|
|
157
|
+
if (value === undefined || value === 0n) {
|
|
158
|
+
return "0x000000000000000000000000";
|
|
159
|
+
}
|
|
160
|
+
if (value < MIN_SQRT_RATIO || value > MAX_SQRT_RATIO) {
|
|
161
|
+
throw new Error("invalid sqrtRatioLimit");
|
|
162
|
+
}
|
|
163
|
+
return numberToHex(value, { size: 12 });
|
|
164
|
+
}
|
|
165
|
+
function encodeSkipAhead(skipAhead = 0) {
|
|
166
|
+
if (!Number.isInteger(skipAhead) || skipAhead < 0 || skipAhead > 0x7fffffff) {
|
|
167
|
+
throw new Error("skipAhead must fit into uint31");
|
|
168
|
+
}
|
|
169
|
+
return numberToHex(skipAhead, { size: 4 });
|
|
170
|
+
}
|
|
171
|
+
function assertInt128(value, name) {
|
|
172
|
+
if (value < minInt128 || value > maxInt128) {
|
|
173
|
+
throw new Error(`${name} must fit into int128`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function encodeInt128(value) {
|
|
177
|
+
assertInt128(value, "value");
|
|
178
|
+
return numberToHex(BigInt.asUintN(128, value), { size: 16 });
|
|
179
|
+
}
|
|
180
|
+
export function calldataSize(data) {
|
|
181
|
+
return size(data);
|
|
182
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ekubo/yul-router-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist/index.js",
|
|
17
|
+
"dist/index.d.ts"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.json",
|
|
21
|
+
"generate-foundry-testdata": "npm run --silent build && node scripts/generate-foundry-testdata.mjs",
|
|
22
|
+
"prepublishOnly": "npm run build && npm test",
|
|
23
|
+
"test": "vitest run"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"viem": "^2.31.7"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"typescript": "^5.8.3",
|
|
30
|
+
"viem": "^2.31.7",
|
|
31
|
+
"vitest": "^3.2.4"
|
|
32
|
+
}
|
|
33
|
+
}
|