@dhedge/v2-sdk 2.1.4 → 2.1.6
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/README.md +180 -45
- package/dist/config.d.ts +10 -0
- package/dist/entities/pool.d.ts +110 -1
- package/dist/services/hyperliquid/constants.d.ts +16 -0
- package/dist/services/hyperliquid/index.d.ts +6 -0
- package/dist/services/hyperliquid/marketData.d.ts +12 -0
- package/dist/services/hyperliquid/positionData.d.ts +1 -0
- package/dist/services/odos/index.d.ts +15 -1
- package/dist/services/toros/limitOrder.d.ts +8 -0
- package/dist/test/constants.d.ts +7 -0
- package/dist/test/wallet.d.ts +1 -0
- package/dist/types.d.ts +12 -2
- package/dist/v2-sdk.cjs.development.js +4256 -1335
- package/dist/v2-sdk.cjs.development.js.map +1 -1
- package/dist/v2-sdk.cjs.production.min.js +1 -1
- package/dist/v2-sdk.cjs.production.min.js.map +1 -1
- package/dist/v2-sdk.esm.js +5007 -2087
- package/dist/v2-sdk.esm.js.map +1 -1
- package/package.json +3 -2
- package/src/abi/hyperliquid/ICoreDepositWallet.json +130 -0
- package/src/abi/hyperliquid/ICoreWriter.json +1 -0
- package/src/abi/odos/OdosRouterV3.json +1351 -0
- package/src/abi/toros/IPoolLimitOrderManager.json +78 -0
- package/src/config.ts +44 -13
- package/src/entities/pool.ts +348 -4
- package/src/services/hyperliquid/constants.ts +23 -0
- package/src/services/hyperliquid/index.ts +176 -0
- package/src/services/hyperliquid/marketData.ts +157 -0
- package/src/services/hyperliquid/positionData.ts +33 -0
- package/src/services/odos/index.ts +97 -13
- package/src/services/toros/completeWithdrawal.ts +1 -1
- package/src/services/toros/initWithdrawal.ts +1 -1
- package/src/services/toros/limitOrder.ts +86 -0
- package/src/services/toros/swapData.ts +83 -12
- package/src/test/constants.ts +10 -3
- package/src/test/hyperliquid.test.ts +107 -0
- package/src/test/odos.test.ts +43 -12
- package/src/test/pool.test.ts +37 -45
- package/src/test/torosLimitOrder.test.ts +130 -0
- package/src/test/wallet.ts +2 -1
- package/src/types.ts +13 -2
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { ethers } from "ethers";
|
|
2
|
+
import ICoreDepositWalletAbi from "../../abi/hyperliquid/ICoreDepositWallet.json";
|
|
3
|
+
import ICoreWriterAbi from "../../abi/hyperliquid/ICoreWriter.json";
|
|
4
|
+
import {
|
|
5
|
+
HYPERLIQUID_VERSION,
|
|
6
|
+
LIMIT_ORDER_ACTION,
|
|
7
|
+
LIMIT_ORDER_TIF_IOC,
|
|
8
|
+
SEND_ASSET_ACTION,
|
|
9
|
+
SPOT_DEX_ID,
|
|
10
|
+
SPOT_SEND_ACTION,
|
|
11
|
+
USDC_CORE_ADDRESS,
|
|
12
|
+
USDC_TOKEN_ID
|
|
13
|
+
} from "./constants";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
calculatePrice,
|
|
17
|
+
calculateSize,
|
|
18
|
+
getAssetInfo,
|
|
19
|
+
getMidPrice,
|
|
20
|
+
isSpotAsset,
|
|
21
|
+
scaleSize
|
|
22
|
+
} from "./marketData";
|
|
23
|
+
import { getPositionSize } from "./positionData";
|
|
24
|
+
|
|
25
|
+
const depositWallet = new ethers.utils.Interface(ICoreDepositWalletAbi);
|
|
26
|
+
const coreWriter = new ethers.utils.Interface(ICoreWriterAbi);
|
|
27
|
+
|
|
28
|
+
export const getDepositHyperliquidTxData = (
|
|
29
|
+
dexId: number,
|
|
30
|
+
amount: ethers.BigNumber | string
|
|
31
|
+
): string => {
|
|
32
|
+
return depositWallet.encodeFunctionData("deposit", [amount, dexId]);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const getWithdrawSpotHyperliquidTxData = (
|
|
36
|
+
amount: ethers.BigNumber | string
|
|
37
|
+
): string => {
|
|
38
|
+
const coreAmount = ethers.BigNumber.from(amount).mul(100); //USDC on Core has two more decimals
|
|
39
|
+
//Hardcoded to USDC address and id on Hyperliquid Core
|
|
40
|
+
//From Spot to EVM
|
|
41
|
+
const innerEncoded = ethers.utils.defaultAbiCoder.encode(
|
|
42
|
+
//to, token, amount
|
|
43
|
+
["address", "uint64", "uint64"],
|
|
44
|
+
[USDC_CORE_ADDRESS, USDC_TOKEN_ID, coreAmount]
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const rawTXData = ethers.utils.solidityPack(
|
|
48
|
+
["uint8", "uint24", "bytes"],
|
|
49
|
+
[HYPERLIQUID_VERSION, SPOT_SEND_ACTION, innerEncoded]
|
|
50
|
+
);
|
|
51
|
+
return coreWriter.encodeFunctionData("sendRawAction", [rawTXData]);
|
|
52
|
+
};
|
|
53
|
+
export const getPerpToSpotHyperliquidTxData = (
|
|
54
|
+
dexId: number,
|
|
55
|
+
receiver: string,
|
|
56
|
+
amount: ethers.BigNumber | string
|
|
57
|
+
): string => {
|
|
58
|
+
const coreAmount = ethers.BigNumber.from(amount).mul(100); //USDC on Core has two more decimals
|
|
59
|
+
//From Perp to Spot
|
|
60
|
+
const innerEncoded = ethers.utils.defaultAbiCoder.encode(
|
|
61
|
+
//destination, subAccount, sourceDex, destinationDex, token, amount
|
|
62
|
+
["address", "address", "uint32", "uint32", "uint64", "uint64"],
|
|
63
|
+
[
|
|
64
|
+
receiver,
|
|
65
|
+
ethers.constants.AddressZero,
|
|
66
|
+
dexId,
|
|
67
|
+
SPOT_DEX_ID,
|
|
68
|
+
USDC_TOKEN_ID,
|
|
69
|
+
coreAmount
|
|
70
|
+
]
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const rawTXData = ethers.utils.solidityPack(
|
|
74
|
+
["uint8", "uint24", "bytes"],
|
|
75
|
+
[HYPERLIQUID_VERSION, SEND_ASSET_ACTION, innerEncoded]
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return coreWriter.encodeFunctionData("sendRawAction", [rawTXData]);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const getLimitOrderHyperliquidTxData = async (
|
|
82
|
+
assetId: number,
|
|
83
|
+
isLong: boolean,
|
|
84
|
+
changeAmount: number,
|
|
85
|
+
slippage: number
|
|
86
|
+
): Promise<string> => {
|
|
87
|
+
let isBuy = isLong;
|
|
88
|
+
let reduceOnly = false;
|
|
89
|
+
if (changeAmount < 0) {
|
|
90
|
+
changeAmount = changeAmount * -1;
|
|
91
|
+
isBuy = !isLong;
|
|
92
|
+
reduceOnly = !isSpotAsset(assetId);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
//Calculate price with slippage
|
|
96
|
+
const { assetName, szDecimals } = await getAssetInfo(assetId);
|
|
97
|
+
const midPrice = await getMidPrice(assetId, assetName);
|
|
98
|
+
const price = calculatePrice(
|
|
99
|
+
isSpotAsset(assetId),
|
|
100
|
+
szDecimals,
|
|
101
|
+
midPrice,
|
|
102
|
+
isBuy,
|
|
103
|
+
slippage
|
|
104
|
+
);
|
|
105
|
+
const size = calculateSize(szDecimals, changeAmount, midPrice);
|
|
106
|
+
|
|
107
|
+
const innerEncoded = ethers.utils.defaultAbiCoder.encode(
|
|
108
|
+
//assetIndex, isBuy, price, size, reduceOnly, tif, clientOrderId
|
|
109
|
+
["uint32", "bool", "uint64", "uint64", "bool", "uint8", "uint128"],
|
|
110
|
+
[
|
|
111
|
+
assetId,
|
|
112
|
+
isBuy,
|
|
113
|
+
price,
|
|
114
|
+
size,
|
|
115
|
+
reduceOnly,
|
|
116
|
+
LIMIT_ORDER_TIF_IOC, // immediate or cancel
|
|
117
|
+
ethers.BigNumber.from(0) //client order id
|
|
118
|
+
]
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const rawTXData = ethers.utils.solidityPack(
|
|
122
|
+
["uint8", "uint24", "bytes"],
|
|
123
|
+
[HYPERLIQUID_VERSION, LIMIT_ORDER_ACTION, innerEncoded]
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
return coreWriter.encodeFunctionData("sendRawAction", [rawTXData]);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const getClosePositionHyperliquidTxData = async (
|
|
130
|
+
assetId: number,
|
|
131
|
+
percentageToClose: number,
|
|
132
|
+
slippage: number,
|
|
133
|
+
poolAddress: string
|
|
134
|
+
): Promise<string> => {
|
|
135
|
+
const isSpot = isSpotAsset(assetId);
|
|
136
|
+
const { assetName, szDecimals, baseTokenName } = await getAssetInfo(assetId);
|
|
137
|
+
const positionSize = await getPositionSize(
|
|
138
|
+
assetId,
|
|
139
|
+
isSpot,
|
|
140
|
+
baseTokenName ?? assetName,
|
|
141
|
+
poolAddress
|
|
142
|
+
);
|
|
143
|
+
const isBuy = positionSize < 0; // if position size is negative, we need to buy to close, otherwise sell
|
|
144
|
+
const sizeRaw = scaleSize(szDecimals, positionSize, percentageToClose);
|
|
145
|
+
|
|
146
|
+
//Calculate price with slippage
|
|
147
|
+
const midPrice = await getMidPrice(assetId, assetName);
|
|
148
|
+
const price = calculatePrice(
|
|
149
|
+
isSpotAsset(assetId),
|
|
150
|
+
szDecimals,
|
|
151
|
+
midPrice,
|
|
152
|
+
isBuy,
|
|
153
|
+
slippage
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const innerEncoded = ethers.utils.defaultAbiCoder.encode(
|
|
157
|
+
//assetIndex, isBuy, price, size, reduceOnly, tif, clientOrderId
|
|
158
|
+
["uint32", "bool", "uint64", "uint64", "bool", "uint8", "uint128"],
|
|
159
|
+
[
|
|
160
|
+
assetId,
|
|
161
|
+
positionSize < 0, // if position size is negative, we need to buy to close, otherwise sell
|
|
162
|
+
price,
|
|
163
|
+
sizeRaw,
|
|
164
|
+
!isSpot,
|
|
165
|
+
LIMIT_ORDER_TIF_IOC, // immediate or cancel
|
|
166
|
+
ethers.BigNumber.from(0) //client order id
|
|
167
|
+
]
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const rawTXData = ethers.utils.solidityPack(
|
|
171
|
+
["uint8", "uint24", "bytes"],
|
|
172
|
+
[HYPERLIQUID_VERSION, LIMIT_ORDER_ACTION, innerEncoded]
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
return coreWriter.encodeFunctionData("sendRawAction", [rawTXData]);
|
|
176
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { API_URL, dexIdNameMap } from "./constants";
|
|
3
|
+
import { BigNumber } from "bignumber.js";
|
|
4
|
+
import { ApiError } from "../..";
|
|
5
|
+
|
|
6
|
+
export const perpDexIndex = (assetId: number): number => {
|
|
7
|
+
return Math.max(Math.floor((assetId - 100000) / 10000), 0);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const assetIndex = (assetId: number): number => {
|
|
11
|
+
if (assetId > 100000) {
|
|
12
|
+
//builder-deployed perps
|
|
13
|
+
return (assetId - 100000) % 10000;
|
|
14
|
+
} else return assetId;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const spotAssetIndex = (assetId: number): number => {
|
|
18
|
+
return assetId - 10000;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const isSpotAsset = (assetId: number): boolean => {
|
|
22
|
+
return assetId > 10000 && assetId < 100000;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const getMidPrice = async (
|
|
26
|
+
assetId: number,
|
|
27
|
+
assetName: string
|
|
28
|
+
): Promise<number> => {
|
|
29
|
+
const response = await axios.post(API_URL, {
|
|
30
|
+
type: "allMids",
|
|
31
|
+
dex: dexIdNameMap[perpDexIndex(assetId)]
|
|
32
|
+
});
|
|
33
|
+
const raw = response.data[assetName];
|
|
34
|
+
if (raw === undefined || raw === null) {
|
|
35
|
+
throw new ApiError(
|
|
36
|
+
`Hyperliquid allMids response missing price for asset "${assetName}"`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
const price = +raw;
|
|
40
|
+
if (isNaN(price)) {
|
|
41
|
+
throw new ApiError(
|
|
42
|
+
`Hyperliquid allMids returned non-numeric price for asset "${assetName}": ${raw}`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return price;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const getAssetInfo = async (
|
|
49
|
+
assetId: number
|
|
50
|
+
): Promise<{
|
|
51
|
+
assetName: string;
|
|
52
|
+
szDecimals: number;
|
|
53
|
+
baseTokenName?: string;
|
|
54
|
+
}> => {
|
|
55
|
+
if (isSpotAsset(assetId)) {
|
|
56
|
+
const response = await axios.post(API_URL, {
|
|
57
|
+
type: "spotMeta"
|
|
58
|
+
});
|
|
59
|
+
const asset = response.data.universe.find(
|
|
60
|
+
(e: { index: number }) => e.index === spotAssetIndex(assetId)
|
|
61
|
+
);
|
|
62
|
+
if (!asset) {
|
|
63
|
+
throw new ApiError(
|
|
64
|
+
`Hyperliquid spotMeta response contains no asset for assetId ${assetId} (index ${spotAssetIndex(
|
|
65
|
+
assetId
|
|
66
|
+
)})`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const baseToken = response.data.tokens[asset.tokens[0]];
|
|
70
|
+
return {
|
|
71
|
+
assetName: asset.name,
|
|
72
|
+
szDecimals: baseToken.szDecimals,
|
|
73
|
+
baseTokenName: baseToken.name
|
|
74
|
+
};
|
|
75
|
+
} else {
|
|
76
|
+
const dex = dexIdNameMap[perpDexIndex(assetId)];
|
|
77
|
+
const response = await axios.post(API_URL, {
|
|
78
|
+
type: "metaAndAssetCtxs",
|
|
79
|
+
dex
|
|
80
|
+
});
|
|
81
|
+
if (!Array.isArray(response.data) || response.data.length === 0) {
|
|
82
|
+
throw new ApiError(
|
|
83
|
+
`Hyperliquid metaAndAssetCtxs response has invalid data for assetId ${assetId} (dex ${String(
|
|
84
|
+
dex
|
|
85
|
+
)})`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const meta = response.data[0];
|
|
89
|
+
if (!meta || !Array.isArray(meta.universe)) {
|
|
90
|
+
throw new ApiError(
|
|
91
|
+
`Hyperliquid metaAndAssetCtxs response contains no universe for assetId ${assetId} (dex ${String(
|
|
92
|
+
dex
|
|
93
|
+
)})`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
const assets = meta.universe;
|
|
97
|
+
const index = assetIndex(assetId);
|
|
98
|
+
if (index < 0 || index >= assets.length) {
|
|
99
|
+
throw new ApiError(
|
|
100
|
+
`Hyperliquid metaAndAssetCtxs response contains no asset for assetId ${assetId} (dex ${String(
|
|
101
|
+
dex
|
|
102
|
+
)}, index ${index}, universe length ${assets.length})`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
const asset = assets[index];
|
|
106
|
+
return {
|
|
107
|
+
assetName: asset.name,
|
|
108
|
+
szDecimals: asset.szDecimals
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const calculatePrice = (
|
|
114
|
+
isSpotAsset: boolean,
|
|
115
|
+
szDecimals: number,
|
|
116
|
+
midPrice: number,
|
|
117
|
+
isBuy: boolean,
|
|
118
|
+
slippage: number
|
|
119
|
+
): string => {
|
|
120
|
+
// 1. Apply slippage
|
|
121
|
+
const price = midPrice * (isBuy ? 1 + slippage / 100 : 1 - slippage / 100);
|
|
122
|
+
|
|
123
|
+
// 2. Round to 5 significant figures
|
|
124
|
+
const roundedSignificant = parseFloat(price.toPrecision(5));
|
|
125
|
+
|
|
126
|
+
// 3. For perp base decimals = 6
|
|
127
|
+
const baseDecimals = isSpotAsset ? 8 : 6;
|
|
128
|
+
const finalDecimals = baseDecimals - szDecimals;
|
|
129
|
+
const factor = Math.pow(10, finalDecimals);
|
|
130
|
+
const roundedDecimals = Math.round(roundedSignificant * factor) / factor;
|
|
131
|
+
return new BigNumber(roundedDecimals).times(1e8).toFixed(0);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const calculateSize = (
|
|
135
|
+
szDecimals: number,
|
|
136
|
+
value: number,
|
|
137
|
+
price: number
|
|
138
|
+
): string => {
|
|
139
|
+
const factor = Math.pow(10, szDecimals);
|
|
140
|
+
return new BigNumber(Math.round((value / price) * factor) / factor)
|
|
141
|
+
.times(1e8)
|
|
142
|
+
.toFixed(0);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const scaleSize = (
|
|
146
|
+
szDecimals: number,
|
|
147
|
+
positionSize: number,
|
|
148
|
+
percentageToClose: number
|
|
149
|
+
): string => {
|
|
150
|
+
const factor = Math.pow(10, szDecimals);
|
|
151
|
+
return new BigNumber(
|
|
152
|
+
Math.round(((positionSize * percentageToClose) / 100) * factor) / factor
|
|
153
|
+
)
|
|
154
|
+
.times(1e8)
|
|
155
|
+
.abs()
|
|
156
|
+
.toFixed(0);
|
|
157
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { API_URL, dexIdNameMap } from "./constants";
|
|
3
|
+
import { perpDexIndex } from "./marketData";
|
|
4
|
+
|
|
5
|
+
export const getPositionSize = async (
|
|
6
|
+
assetId: number,
|
|
7
|
+
isSpot: boolean,
|
|
8
|
+
assetName: string,
|
|
9
|
+
user: string
|
|
10
|
+
): Promise<number> => {
|
|
11
|
+
if (isSpot) {
|
|
12
|
+
const response = await axios.post(API_URL, {
|
|
13
|
+
type: "spotClearinghouseState",
|
|
14
|
+
user
|
|
15
|
+
});
|
|
16
|
+
const balance = response.data.balances.find(
|
|
17
|
+
(e: { coin: string }) => e.coin === assetName
|
|
18
|
+
);
|
|
19
|
+
if (!balance) throw new Error(`No balance found for asset ${assetName}`);
|
|
20
|
+
return +balance.total;
|
|
21
|
+
} else {
|
|
22
|
+
const response = await axios.post(API_URL, {
|
|
23
|
+
type: "clearinghouseState",
|
|
24
|
+
user,
|
|
25
|
+
dex: dexIdNameMap[perpDexIndex(assetId)]
|
|
26
|
+
});
|
|
27
|
+
const position = response.data.assetPositions.find(
|
|
28
|
+
(e: { position: { coin: string } }) => e.position.coin === assetName
|
|
29
|
+
);
|
|
30
|
+
if (!position) throw new Error(`No position found for asset ${assetName}`);
|
|
31
|
+
return +position.position.szi;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import axios from "axios";
|
|
3
3
|
import { ApiError, ethers } from "../..";
|
|
4
|
-
import { networkChainIdMap } from "../../config";
|
|
4
|
+
import { networkChainIdMap, OdosSwapFeeRecipient } from "../../config";
|
|
5
5
|
import { Pool } from "../../entities";
|
|
6
|
+
import OdosRouterV3Abi from "../../abi/odos/OdosRouterV3.json";
|
|
7
|
+
import BigNumber from "bignumber.js";
|
|
6
8
|
|
|
7
|
-
export const odosBaseUrl = "https://api.odos.xyz/sor";
|
|
9
|
+
export const odosBaseUrl = "https://enterprise-api.odos.xyz/sor";
|
|
10
|
+
|
|
11
|
+
// Types for Odos Router V3 swap function parameters
|
|
12
|
+
export interface SwapTokenInfo {
|
|
13
|
+
inputToken: string;
|
|
14
|
+
inputAmount: ethers.BigNumber;
|
|
15
|
+
inputReceiver: string;
|
|
16
|
+
outputToken: string;
|
|
17
|
+
outputQuote: ethers.BigNumber;
|
|
18
|
+
outputMin: ethers.BigNumber;
|
|
19
|
+
outputReceiver: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SwapReferralInfo {
|
|
23
|
+
code: ethers.BigNumber;
|
|
24
|
+
fee: ethers.BigNumber;
|
|
25
|
+
feeRecipient: string;
|
|
26
|
+
}
|
|
8
27
|
|
|
9
28
|
export async function getOdosSwapTxData(
|
|
10
29
|
pool: Pool,
|
|
@@ -13,13 +32,13 @@ export async function getOdosSwapTxData(
|
|
|
13
32
|
amountIn: ethers.BigNumber | string,
|
|
14
33
|
slippage: number
|
|
15
34
|
): Promise<{ swapTxData: string; minAmountOut: string }> {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
process.env.ODOS_REFERAL_CODE &&
|
|
19
|
-
Number(process.env.ODOS_REFERAL_CODE) > 0
|
|
20
|
-
) {
|
|
21
|
-
referralCode = Number(process.env.ODOS_REFERAL_CODE);
|
|
35
|
+
if (!process.env.ODOS_API_KEY) {
|
|
36
|
+
throw new Error("ODOS_API_KEY is not set");
|
|
22
37
|
}
|
|
38
|
+
const ODOS_API_KEY = process.env.ODOS_API_KEY;
|
|
39
|
+
|
|
40
|
+
const referralFeeBips = 2; // 2 basis points = 0.02%
|
|
41
|
+
|
|
23
42
|
const quoteParams = {
|
|
24
43
|
chainId: networkChainIdMap[pool.network],
|
|
25
44
|
inputTokens: [
|
|
@@ -36,12 +55,20 @@ export async function getOdosSwapTxData(
|
|
|
36
55
|
],
|
|
37
56
|
slippageLimitPercent: slippage,
|
|
38
57
|
userAddr: pool.address,
|
|
39
|
-
|
|
58
|
+
compact: false,
|
|
59
|
+
referralFeeRecipient: OdosSwapFeeRecipient[pool.network],
|
|
60
|
+
referralFee: referralFeeBips // 0.02% fee
|
|
40
61
|
};
|
|
41
62
|
try {
|
|
42
63
|
const quoteResult = await axios.post(
|
|
43
|
-
`${odosBaseUrl}/quote/
|
|
44
|
-
quoteParams
|
|
64
|
+
`${odosBaseUrl}/quote/v3`,
|
|
65
|
+
quoteParams,
|
|
66
|
+
{
|
|
67
|
+
headers: {
|
|
68
|
+
"Content-Type": "application/json",
|
|
69
|
+
"x-api-key": ODOS_API_KEY
|
|
70
|
+
}
|
|
71
|
+
}
|
|
45
72
|
);
|
|
46
73
|
|
|
47
74
|
const assembleParams = {
|
|
@@ -51,10 +78,67 @@ export async function getOdosSwapTxData(
|
|
|
51
78
|
|
|
52
79
|
const assembleResult = await axios.post(
|
|
53
80
|
`${odosBaseUrl}/assemble`,
|
|
54
|
-
assembleParams
|
|
81
|
+
assembleParams,
|
|
82
|
+
{
|
|
83
|
+
headers: {
|
|
84
|
+
"Content-Type": "application/json",
|
|
85
|
+
"x-api-key": ODOS_API_KEY
|
|
86
|
+
}
|
|
87
|
+
}
|
|
55
88
|
);
|
|
89
|
+
|
|
90
|
+
const txData = assembleResult.data.transaction.data;
|
|
91
|
+
|
|
92
|
+
// Decode the transaction data
|
|
93
|
+
const iface = new ethers.utils.Interface(OdosRouterV3Abi.abi);
|
|
94
|
+
const decodedData = iface.parseTransaction({ data: txData });
|
|
95
|
+
|
|
96
|
+
const tokenInfo = decodedData.args[0] as SwapTokenInfo;
|
|
97
|
+
const pathDefinition = decodedData.args[1] as string;
|
|
98
|
+
const executor = decodedData.args[2] as string;
|
|
99
|
+
const referralInfo = decodedData.args[3] as SwapReferralInfo;
|
|
100
|
+
|
|
101
|
+
if (
|
|
102
|
+
referralInfo.fee.lte(
|
|
103
|
+
ethers.BigNumber.from((referralFeeBips * 1e18) / 10000)
|
|
104
|
+
)
|
|
105
|
+
) {
|
|
106
|
+
// Referral fee is already correct, return original txData
|
|
107
|
+
return {
|
|
108
|
+
swapTxData: assembleResult.data.transaction.data,
|
|
109
|
+
minAmountOut: assembleResult.data.outputTokens[0].amount
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const FEE_DENOM = new BigNumber(1e18);
|
|
114
|
+
const correctedFee = new BigNumber((referralFeeBips * 1e18) / 10000);
|
|
115
|
+
const factor = 1.1;
|
|
116
|
+
const correctedOutputQuote = new BigNumber(tokenInfo.outputQuote.toString())
|
|
117
|
+
.times(
|
|
118
|
+
FEE_DENOM.minus(correctedFee).div(
|
|
119
|
+
FEE_DENOM.minus(referralInfo.fee.toString())
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
.times(factor);
|
|
123
|
+
|
|
124
|
+
// example referralInfo.fee could be 0.0005 * 1e18 = 500000000000000, which is 0.05%
|
|
125
|
+
// Create corrected referral info
|
|
126
|
+
const correctedTxData = iface.encodeFunctionData(decodedData.name, [
|
|
127
|
+
{
|
|
128
|
+
...tokenInfo,
|
|
129
|
+
outputQuote: correctedOutputQuote.toFixed(0)
|
|
130
|
+
},
|
|
131
|
+
pathDefinition,
|
|
132
|
+
executor,
|
|
133
|
+
{
|
|
134
|
+
code: referralInfo.code,
|
|
135
|
+
fee: correctedFee.toFixed(0), // align with referralFeeBips
|
|
136
|
+
feeRecipient: referralInfo.feeRecipient
|
|
137
|
+
}
|
|
138
|
+
]);
|
|
139
|
+
|
|
56
140
|
return {
|
|
57
|
-
swapTxData:
|
|
141
|
+
swapTxData: correctedTxData,
|
|
58
142
|
minAmountOut: assembleResult.data.outputTokens[0].amount
|
|
59
143
|
};
|
|
60
144
|
} catch (e) {
|
|
@@ -25,7 +25,7 @@ const getSwapWithdrawData = async (
|
|
|
25
25
|
swapDestMinDestAmount: BigNumber
|
|
26
26
|
) => {
|
|
27
27
|
const srcData = [];
|
|
28
|
-
const routerKey = ethers.utils.formatBytes32String("
|
|
28
|
+
const routerKey = ethers.utils.formatBytes32String("ODOS_V3");
|
|
29
29
|
// const destData
|
|
30
30
|
for (const { token, balance } of trackedAssets) {
|
|
31
31
|
if (token.toLowerCase() === receiveToken.toLowerCase()) {
|
|
@@ -46,7 +46,7 @@ const getAaveAssetWithdrawData = async (
|
|
|
46
46
|
const { srcData, dstData } = swapDataParams;
|
|
47
47
|
|
|
48
48
|
const srcDataToEncode: unknown[] = [];
|
|
49
|
-
const routerKey = ethers.utils.formatBytes32String("
|
|
49
|
+
const routerKey = ethers.utils.formatBytes32String("ODOS_V3");
|
|
50
50
|
for (const { asset, amount } of srcData) {
|
|
51
51
|
const swapData = await retry({
|
|
52
52
|
fn: () => {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { ethers } from "ethers";
|
|
3
|
+
import { Pool } from "../..";
|
|
4
|
+
import { limitOrderAddress } from "../../config";
|
|
5
|
+
import { LimitOrderInfo } from "../../types";
|
|
6
|
+
import IPoolLimitOrderManager from "../../abi/toros/IPoolLimitOrderManager.json";
|
|
7
|
+
|
|
8
|
+
const iface = new ethers.utils.Interface(IPoolLimitOrderManager);
|
|
9
|
+
|
|
10
|
+
export function getLimitOrderId(
|
|
11
|
+
userAddress: string,
|
|
12
|
+
vaultAddress: string
|
|
13
|
+
): string {
|
|
14
|
+
return ethers.utils.solidityKeccak256(
|
|
15
|
+
["address", "address"],
|
|
16
|
+
[userAddress, vaultAddress]
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getCreateLimitOrderTxData(info: LimitOrderInfo): string {
|
|
21
|
+
return iface.encodeFunctionData("createLimitOrder", [
|
|
22
|
+
[
|
|
23
|
+
info.amount,
|
|
24
|
+
info.stopLossPriceD18,
|
|
25
|
+
info.takeProfitPriceD18,
|
|
26
|
+
info.user,
|
|
27
|
+
info.pool,
|
|
28
|
+
info.pricingAsset
|
|
29
|
+
]
|
|
30
|
+
]);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getModifyLimitOrderTxData(info: LimitOrderInfo): string {
|
|
34
|
+
return iface.encodeFunctionData("modifyLimitOrder", [
|
|
35
|
+
[
|
|
36
|
+
info.amount,
|
|
37
|
+
info.stopLossPriceD18,
|
|
38
|
+
info.takeProfitPriceD18,
|
|
39
|
+
info.user,
|
|
40
|
+
info.pool,
|
|
41
|
+
info.pricingAsset
|
|
42
|
+
]
|
|
43
|
+
]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getDeleteLimitOrderTxData(vaultAddress: string): string {
|
|
47
|
+
return iface.encodeFunctionData("deleteLimitOrder", [vaultAddress]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function getTorosLimitOrder(
|
|
51
|
+
pool: Pool,
|
|
52
|
+
userAddress: string,
|
|
53
|
+
vaultAddress: string
|
|
54
|
+
): Promise<LimitOrderInfo | null> {
|
|
55
|
+
const managerAddress = limitOrderAddress[pool.network];
|
|
56
|
+
if (!managerAddress) return null;
|
|
57
|
+
|
|
58
|
+
const orderId = getLimitOrderId(userAddress, vaultAddress);
|
|
59
|
+
const contract = new ethers.Contract(
|
|
60
|
+
managerAddress,
|
|
61
|
+
IPoolLimitOrderManager,
|
|
62
|
+
pool.signer
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const result = await contract.limitOrders(orderId);
|
|
66
|
+
// If amount is zero, the order doesn't exist
|
|
67
|
+
if (result.amount.isZero()) return null;
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
amount: result.amount,
|
|
71
|
+
stopLossPriceD18: result.stopLossPriceD18,
|
|
72
|
+
takeProfitPriceD18: result.takeProfitPriceD18,
|
|
73
|
+
user: result.user,
|
|
74
|
+
pool: result.pool,
|
|
75
|
+
pricingAsset: result.pricingAsset
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function hasActiveTorosLimitOrder(
|
|
80
|
+
pool: Pool,
|
|
81
|
+
userAddress: string,
|
|
82
|
+
vaultAddress: string
|
|
83
|
+
): Promise<boolean> {
|
|
84
|
+
const order = await getTorosLimitOrder(pool, userAddress, vaultAddress);
|
|
85
|
+
return order !== null;
|
|
86
|
+
}
|