degate-cli 1.0.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/.env.example +10 -0
- package/cli.js +1082 -0
- package/package.json +47 -0
package/.env.example
ADDED
package/cli.js
ADDED
|
@@ -0,0 +1,1082 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const Table = require('cli-table3');
|
|
6
|
+
const ora = require('ora');
|
|
7
|
+
const { Wallet: EthersWallet, utils: ethersUtils } = require('ethers');
|
|
8
|
+
const bip39 = require('bip39');
|
|
9
|
+
const { hdkey } = require('ethereumjs-wallet');
|
|
10
|
+
const { sha256 } = require('ethereumjs-util');
|
|
11
|
+
const { derivePath } = require('ed25519-hd-key');
|
|
12
|
+
const { Keypair, VersionedMessage, VersionedTransaction } = require('@solana/web3.js');
|
|
13
|
+
const nacl = require('tweetnacl');
|
|
14
|
+
const axios = require('axios');
|
|
15
|
+
const inquirer = require('inquirer');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
|
|
19
|
+
require('dotenv').config({ path: path.join(__dirname, '.env') });
|
|
20
|
+
|
|
21
|
+
// ─── Config ───────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
const ENV_CONFIG = {
|
|
24
|
+
dev: {
|
|
25
|
+
DG_API: 'https://dev-backend.degate.com',
|
|
26
|
+
DEGATE_VERSION: 'Dev',
|
|
27
|
+
EXCHANGE_ADDRESS: '0x37e6969e1E8ACB307c68063d93fD929354D06140',
|
|
28
|
+
DEX_CHAIN_ID: 17000,
|
|
29
|
+
SIGN_TEMPLATE: 'S3',
|
|
30
|
+
API_SIGN_VALID_ADDRESS: '0xC1D393b94b6dcE9732f86042609FD698A0B00164',
|
|
31
|
+
},
|
|
32
|
+
prod: {
|
|
33
|
+
DG_API: 'https://v1-mainnet-backend.degate.com',
|
|
34
|
+
DEGATE_VERSION: 'V1',
|
|
35
|
+
EXCHANGE_ADDRESS: '0x5859cCb16DC791f79AAFadD5B9768bC400797A29',
|
|
36
|
+
DEX_CHAIN_ID: 1,
|
|
37
|
+
SIGN_TEMPLATE: 'S3',
|
|
38
|
+
API_SIGN_VALID_ADDRESS: '',
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function getConfig() {
|
|
43
|
+
const env = process.env.ENV || 'dev';
|
|
44
|
+
return ENV_CONFIG[env] || ENV_CONFIG.dev;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─── Crypto Utils ─────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
function formatSignDate(dateObj) {
|
|
50
|
+
const year = dateObj.getUTCFullYear();
|
|
51
|
+
const month = String(dateObj.getUTCMonth() + 1).padStart(2, '0');
|
|
52
|
+
const day = String(dateObj.getUTCDate()).padStart(2, '0');
|
|
53
|
+
const hours = String(dateObj.getUTCHours()).padStart(2, '0');
|
|
54
|
+
const minutes = String(dateObj.getUTCMinutes()).padStart(2, '0');
|
|
55
|
+
const seconds = String(dateObj.getUTCSeconds()).padStart(2, '0');
|
|
56
|
+
const milliseconds = String(dateObj.getUTCMilliseconds()).padStart(3, '0');
|
|
57
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds} UTC`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getSignMsg(keyNonce) {
|
|
61
|
+
const cfg = getConfig();
|
|
62
|
+
return `DeGate: ${cfg.DEGATE_VERSION}\nNonce: ${keyNonce}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function signEthereumMessage(message, privateKey) {
|
|
66
|
+
const wallet = new EthersWallet('0x' + privateKey);
|
|
67
|
+
return wallet.signMessage(message);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── Key Derivation ───────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
function deriveDGWallet(mnemonic) {
|
|
73
|
+
const seed = bip39.mnemonicToSeedSync(mnemonic);
|
|
74
|
+
const derivationPath = `m/44'/7999801'/0'/0/0`;
|
|
75
|
+
const hd = hdkey.fromMasterSeed(seed);
|
|
76
|
+
const wallet = hd.derivePath(derivationPath).getWallet();
|
|
77
|
+
const privateKey = wallet.getPrivateKey().toString('hex');
|
|
78
|
+
const ethWallet = new EthersWallet('0x' + privateKey);
|
|
79
|
+
return { chain: 'DGWallet', path: derivationPath, address: ethWallet.address, privateKey };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function deriveSolanaDA(mnemonic, exchangeAddress, time) {
|
|
83
|
+
const seed = bip39.mnemonicToSeedSync(mnemonic);
|
|
84
|
+
const derivationPath = `m/44'/501'/0'/0'`;
|
|
85
|
+
const derivedSeed = derivePath(derivationPath, seed.toString('hex')).key;
|
|
86
|
+
const keypair = Keypair.fromSeed(derivedSeed);
|
|
87
|
+
const address = keypair.publicKey.toBase58();
|
|
88
|
+
const privateKey = Buffer.from(keypair.secretKey).toString('hex');
|
|
89
|
+
const publicKey = Buffer.from(keypair.publicKey.toBytes()).toString('hex');
|
|
90
|
+
return { chain: 'SOLANA', path: derivationPath, address, publicKey, privateKey, time };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function deriveEthereumDA(mnemonic, exchangeAddress, time) {
|
|
94
|
+
const seed = bip39.mnemonicToSeedSync(mnemonic);
|
|
95
|
+
const derivationPath = `m/44'/60'/0'/0/0`;
|
|
96
|
+
const hd = hdkey.fromMasterSeed(seed);
|
|
97
|
+
const wallet = hd.derivePath(derivationPath).getWallet();
|
|
98
|
+
const privateKey = wallet.getPrivateKey().toString('hex');
|
|
99
|
+
const ethWallet = new EthersWallet('0x' + privateKey);
|
|
100
|
+
const signingKey = new ethersUtils.SigningKey('0x' + privateKey);
|
|
101
|
+
const publicKey = signingKey.publicKey.slice(2);
|
|
102
|
+
return { chain: 'ETHEREUM', path: derivationPath, address: ethWallet.address, publicKey, privateKey, time };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function deriveAllKeys(eoaPrivateKey, keyNonce) {
|
|
106
|
+
const cfg = getConfig();
|
|
107
|
+
const keyMessage = getSignMsg(keyNonce);
|
|
108
|
+
|
|
109
|
+
const eoaWallet = new EthersWallet(eoaPrivateKey);
|
|
110
|
+
const eoaAddress = eoaWallet.address;
|
|
111
|
+
const signature = await eoaWallet.signMessage(keyMessage);
|
|
112
|
+
|
|
113
|
+
const L0Seed = sha256(Buffer.from(ethersUtils.arrayify(signature)));
|
|
114
|
+
const daSeed = sha256(Buffer.concat([L0Seed, Buffer.from('solana')]));
|
|
115
|
+
const mnemonic = bip39.entropyToMnemonic(daSeed);
|
|
116
|
+
|
|
117
|
+
const time = Date.now();
|
|
118
|
+
const dgWallet = deriveDGWallet(mnemonic);
|
|
119
|
+
const solanaDA = deriveSolanaDA(mnemonic, cfg.EXCHANGE_ADDRESS, time);
|
|
120
|
+
const ethereumDA = deriveEthereumDA(mnemonic, cfg.EXCHANGE_ADDRESS, time);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
eoaAddress,
|
|
124
|
+
mnemonic,
|
|
125
|
+
DGWallet: dgWallet,
|
|
126
|
+
SOLANA: solanaDA,
|
|
127
|
+
ETHEREUM: ethereumDA,
|
|
128
|
+
BASE: { ...ethereumDA, chain: 'BASE' },
|
|
129
|
+
da_owner: `${dgWallet.address}_0`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ─── Transaction Signing ──────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
const EVM_CHAINS = ['ETHEREUM', 'BASE', 'BSC', 'ARBITRUM', 'OPTIMISM', 'AVALANCHE', 'POLYGON'];
|
|
136
|
+
|
|
137
|
+
function signSolanaTransaction(transactionData, mnemonic) {
|
|
138
|
+
const seed = bip39.mnemonicToSeedSync(mnemonic);
|
|
139
|
+
const derivedSeed = derivePath(`m/44'/501'/0'/0'`, seed.toString('hex')).key;
|
|
140
|
+
const keypair = Keypair.fromSeed(derivedSeed);
|
|
141
|
+
|
|
142
|
+
// 完全对齐 DgSolWallet.signTransaction 的两个分支:
|
|
143
|
+
// - hex → VersionedMessage.deserialize → sign → 返回 hex 签名(64 bytes)
|
|
144
|
+
// - base58 → VersionedTransaction.deserialize → sign → 返回完整 base58 signed tx
|
|
145
|
+
const isHex = /^[0-9a-fA-F]+$/.test(transactionData);
|
|
146
|
+
if (isHex) {
|
|
147
|
+
const msgBytes = Buffer.from(transactionData, 'hex');
|
|
148
|
+
const message = VersionedMessage.deserialize(msgBytes);
|
|
149
|
+
const tx = new VersionedTransaction(message);
|
|
150
|
+
tx.sign([keypair]);
|
|
151
|
+
return Buffer.from(tx.signatures[0]).toString('hex');
|
|
152
|
+
} else {
|
|
153
|
+
const txBytes = Buffer.from(ethersUtils.base58.decode(transactionData));
|
|
154
|
+
const tx = VersionedTransaction.deserialize(txBytes);
|
|
155
|
+
tx.sign([keypair]);
|
|
156
|
+
return ethersUtils.base58.encode(tx.serialize());
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function signEVMTransaction(transactionData, mnemonic) {
|
|
161
|
+
const seed = bip39.mnemonicToSeedSync(mnemonic);
|
|
162
|
+
const hd = hdkey.fromMasterSeed(seed);
|
|
163
|
+
const walletKey = hd.derivePath(`m/44'/60'/0'/0/0`).getWallet();
|
|
164
|
+
const privateKey = walletKey.getPrivateKey().toString('hex');
|
|
165
|
+
|
|
166
|
+
if (typeof transactionData === 'string') {
|
|
167
|
+
// raw hex digest → sign directly(与 dg-wallet/src/da/ethereum/index.ts 一致)
|
|
168
|
+
const signingKey = new ethersUtils.SigningKey('0x' + privateKey);
|
|
169
|
+
const rawDataHex = transactionData.startsWith('0x') ? transactionData : '0x' + transactionData;
|
|
170
|
+
const messageBytes = ethersUtils.arrayify(rawDataHex);
|
|
171
|
+
const signature = signingKey.signDigest(messageBytes);
|
|
172
|
+
const { r, s, v } = signature;
|
|
173
|
+
const normalizedV = (v >= 27 ? v - 27 : v).toString().padStart(2, '0');
|
|
174
|
+
return `${normalizedV}${r.slice(2).toLowerCase()}${s.slice(2).toLowerCase()}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// transaction object → 完整签名(对齐 DgEVMWallet.signTransaction)
|
|
178
|
+
const ethWallet = new EthersWallet('0x' + privateKey);
|
|
179
|
+
const txParams = { ...transactionData };
|
|
180
|
+
if (txParams.type !== undefined) txParams.type = Number(txParams.type);
|
|
181
|
+
const signedTx = await ethWallet.signTransaction(txParams);
|
|
182
|
+
return signedTx;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function signTransaction(chain, transactionData, mnemonic) {
|
|
186
|
+
if (chain === 'SOLANA') {
|
|
187
|
+
return signSolanaTransaction(transactionData, mnemonic);
|
|
188
|
+
}
|
|
189
|
+
if (EVM_CHAINS.includes(chain)) {
|
|
190
|
+
return signEVMTransaction(transactionData, mnemonic);
|
|
191
|
+
}
|
|
192
|
+
throw new Error(`Unsupported chain: ${chain}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ─── API Client ───────────────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
function createApiClient(jwtToken) {
|
|
198
|
+
const cfg = getConfig();
|
|
199
|
+
const client = axios.create({
|
|
200
|
+
baseURL: cfg.DG_API,
|
|
201
|
+
timeout: 30000,
|
|
202
|
+
headers: {
|
|
203
|
+
'Content-Type': 'application/json;charset=utf-8',
|
|
204
|
+
source: 'degate_cli',
|
|
205
|
+
...(jwtToken ? { Authorization: `Bearer ${jwtToken}` } : {}),
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
client.interceptors.response.use(
|
|
210
|
+
(resp) => {
|
|
211
|
+
const { data, code, message } = resp?.data ?? {};
|
|
212
|
+
if (code !== 0) throw { code, message, data };
|
|
213
|
+
return { data, code, message };
|
|
214
|
+
},
|
|
215
|
+
(error) => {
|
|
216
|
+
const { code, message } = error?.response?.data ?? {};
|
|
217
|
+
throw { code, message: message || error.message };
|
|
218
|
+
}
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
return client;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function getAccessToken(dgWalletPrivateKey) {
|
|
225
|
+
const time = new Date();
|
|
226
|
+
const keyMessage = `Sign to view\n${formatSignDate(time)}`;
|
|
227
|
+
const signature = await signEthereumMessage(keyMessage, dgWalletPrivateKey);
|
|
228
|
+
const dgWallet = new EthersWallet('0x' + dgWalletPrivateKey);
|
|
229
|
+
|
|
230
|
+
const client = createApiClient();
|
|
231
|
+
const resp = await client.post('/order-book-api/intent/access/token', {
|
|
232
|
+
wallet_id: dgWallet.address,
|
|
233
|
+
time: time.valueOf(),
|
|
234
|
+
ecdsa_signature: signature,
|
|
235
|
+
account_index: 0,
|
|
236
|
+
});
|
|
237
|
+
return resp.data.token;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function fetchChainInfo(client) {
|
|
241
|
+
const resp = await client.get('/order-book-api/intent/chains');
|
|
242
|
+
const { chains } = resp.data;
|
|
243
|
+
return (chains || []).filter(d => d.gas_token?.token_id).map(d => ({
|
|
244
|
+
chain: d.chain_name,
|
|
245
|
+
chainCode: d.chain_code,
|
|
246
|
+
isFungibleChain: d.is_support_fungible_usdc,
|
|
247
|
+
isEvmChain: d.is_evm_chain,
|
|
248
|
+
chainId: d.chain_id,
|
|
249
|
+
quoteToken: d.quote_token,
|
|
250
|
+
gasToken: d.gas_token,
|
|
251
|
+
solverAddress: null,
|
|
252
|
+
gasAddress: null,
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function fetchChainConfig(client) {
|
|
257
|
+
const resp = await client.get('/intent-omni/get_app_config_template');
|
|
258
|
+
return resp.data;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function getSolverAddresses(client) {
|
|
262
|
+
const [chainInfo, chainConfig] = await Promise.all([
|
|
263
|
+
fetchChainInfo(client),
|
|
264
|
+
fetchChainConfig(client),
|
|
265
|
+
]);
|
|
266
|
+
|
|
267
|
+
const configs = JSON.parse(chainConfig.data);
|
|
268
|
+
const result = {};
|
|
269
|
+
chainInfo.forEach(d => {
|
|
270
|
+
const config = configs.chains?.find(c => c.name === d.chain) || {};
|
|
271
|
+
result[d.chain] = {
|
|
272
|
+
solver: config.solverAddress || '',
|
|
273
|
+
gas: config.gasAddress || '',
|
|
274
|
+
chainId: config.chainId,
|
|
275
|
+
isEvmChain: d.isEvmChain,
|
|
276
|
+
gasToken: d.gasToken || null,
|
|
277
|
+
};
|
|
278
|
+
});
|
|
279
|
+
return { solverMap: result, chainInfo, chainConfig: configs };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function fetchDAOwner(eoaAddress) {
|
|
283
|
+
const client = createApiClient();
|
|
284
|
+
try {
|
|
285
|
+
const resp = await client.get('/order-book-api/intent/account', {
|
|
286
|
+
params: { l0_address: eoaAddress },
|
|
287
|
+
});
|
|
288
|
+
return resp.data?.owner || '';
|
|
289
|
+
} catch (err) {
|
|
290
|
+
if (err.code === 100000) return '';
|
|
291
|
+
throw err;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function fetchRegisteredDAs(authClient) {
|
|
296
|
+
try {
|
|
297
|
+
const resp = await authClient.get('/order-book-api/intent/daAddress');
|
|
298
|
+
const { key_nonce_when_addr_generate, addresses } = resp.data || {};
|
|
299
|
+
const das = {};
|
|
300
|
+
(addresses || []).forEach(item => {
|
|
301
|
+
const info = item.chain_addresses_info?.[0] || {};
|
|
302
|
+
das[item.chain_name?.toUpperCase()] = {
|
|
303
|
+
chain: item.chain_name?.toUpperCase(),
|
|
304
|
+
address: info.address,
|
|
305
|
+
publicKey: info.public_key,
|
|
306
|
+
path: info.address_path,
|
|
307
|
+
};
|
|
308
|
+
});
|
|
309
|
+
return { keyNonce: key_nonce_when_addr_generate, das };
|
|
310
|
+
} catch {
|
|
311
|
+
return { keyNonce: null, das: {} };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function signSolanaMessage(message, privateKey) {
|
|
316
|
+
const keyPair = Keypair.fromSecretKey(Buffer.from(privateKey, 'hex'));
|
|
317
|
+
const messageBytes = new TextEncoder().encode(message);
|
|
318
|
+
const signature = nacl.sign.detached(messageBytes, keyPair.secretKey);
|
|
319
|
+
return Buffer.from(signature).toString('hex');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function registerDAs(authClient, { eoaAddress, keys, keyNonce }) {
|
|
323
|
+
const cfg = getConfig();
|
|
324
|
+
const time = keys.SOLANA.time || Date.now();
|
|
325
|
+
|
|
326
|
+
const solSig = await signSolanaMessage(
|
|
327
|
+
cfg.EXCHANGE_ADDRESS + '-' + time,
|
|
328
|
+
keys.SOLANA.privateKey
|
|
329
|
+
);
|
|
330
|
+
const ethSig = await signEthereumMessage(
|
|
331
|
+
cfg.EXCHANGE_ADDRESS + '-' + time,
|
|
332
|
+
keys.ETHEREUM.privateKey
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
const chainDAs = {
|
|
336
|
+
SOLANA: { ...keys.SOLANA, signature: solSig },
|
|
337
|
+
ETHEREUM: { ...keys.ETHEREUM, signature: ethSig },
|
|
338
|
+
BASE: { ...keys.ETHEREUM, chain: 'BASE', signature: ethSig },
|
|
339
|
+
DGWallet: keys.DGWallet,
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const addresses = Object.values(chainDAs)
|
|
343
|
+
.filter(d => d?.address && d.chain !== 'DGWallet')
|
|
344
|
+
.map(d => d.address)
|
|
345
|
+
.join(',');
|
|
346
|
+
const daKeyMessage = `0,${addresses}\nTime: ${formatSignDate(new Date(time))}`;
|
|
347
|
+
const walletDaSig = await signEthereumMessage(daKeyMessage, keys.DGWallet.privateKey);
|
|
348
|
+
|
|
349
|
+
const daAddresses = Object.entries(chainDAs)
|
|
350
|
+
.filter(([, d]) => d?.chain && d.chain !== 'DGWallet')
|
|
351
|
+
.map(([, d]) => ({
|
|
352
|
+
chain_name: d.chain.toUpperCase(),
|
|
353
|
+
chain_addresses_info: [{
|
|
354
|
+
public_key: d.publicKey,
|
|
355
|
+
address: d.address,
|
|
356
|
+
address_path: d.path,
|
|
357
|
+
sig: d.signature,
|
|
358
|
+
}],
|
|
359
|
+
}));
|
|
360
|
+
|
|
361
|
+
const data = {
|
|
362
|
+
l0_address: eoaAddress,
|
|
363
|
+
wallet_id: keys.DGWallet.address,
|
|
364
|
+
account_index: 0,
|
|
365
|
+
wallet_da_signature: walletDaSig,
|
|
366
|
+
addresses: daAddresses,
|
|
367
|
+
sig_time_stamp_in_msec: time,
|
|
368
|
+
key_nonce_when_addr_generate: keyNonce,
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const resp = await authClient.post('/order-book-api/intent/daAddress', data);
|
|
372
|
+
return resp.data;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function fetchProducts(client) {
|
|
376
|
+
const resp = await client.get('/order-book-api/turbo-range/products', {
|
|
377
|
+
params: { limit: 100, offset: 0 },
|
|
378
|
+
});
|
|
379
|
+
return resp.data.items || [];
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function fetchPositions(client, status = 'OPEN') {
|
|
383
|
+
const resp = await client.get('/order-book-api/intent/turboRange', {
|
|
384
|
+
params: { limit: 100, offset: 0, status },
|
|
385
|
+
});
|
|
386
|
+
return resp.data;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function fetchPositionDetail(client, positionAddress) {
|
|
390
|
+
const resp = await client.get('/order-book-api/intent/turboRange/detail', {
|
|
391
|
+
params: { position: positionAddress },
|
|
392
|
+
});
|
|
393
|
+
return resp.data;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ─── Intent Flow ──────────────────────────────────────────────────────────────
|
|
397
|
+
|
|
398
|
+
async function intentTryCreate(client, order) {
|
|
399
|
+
const resp = await client.post('/order-book-api/intent/v2/compact_try_create', order, {
|
|
400
|
+
headers: { Version: '0.0.1' },
|
|
401
|
+
});
|
|
402
|
+
return resp.data;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function intentSign(client, signData) {
|
|
406
|
+
const resp = await client.post('/order-book-api/intent/v2/sign', signData, {
|
|
407
|
+
headers: { Version: '0.0.1' },
|
|
408
|
+
});
|
|
409
|
+
return resp.data;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function verifyApiSign(message, sig) {
|
|
413
|
+
const cfg = getConfig();
|
|
414
|
+
if (!cfg.API_SIGN_VALID_ADDRESS) return true;
|
|
415
|
+
try {
|
|
416
|
+
const recovered = ethersUtils.verifyMessage(message, sig);
|
|
417
|
+
return recovered.toLowerCase() === cfg.API_SIGN_VALID_ADDRESS.toLowerCase();
|
|
418
|
+
} catch {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async function signAndSubmitIntent(client, tryResp, mnemonic, das) {
|
|
424
|
+
const intentItem = tryResp.result;
|
|
425
|
+
if (!intentItem) throw new Error('compact_try_create 未返回 result');
|
|
426
|
+
|
|
427
|
+
if (!verifyApiSign(intentItem.data, intentItem.signature)) {
|
|
428
|
+
throw new Error('API 签名验证失败');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const data = JSON.parse(intentItem.data);
|
|
432
|
+
const { action, add_on } = data;
|
|
433
|
+
|
|
434
|
+
const addOnTxs = (add_on || []).reduce((re, t) => re.concat(t.more_transactions || []), []);
|
|
435
|
+
const allTxs = (action?.more_transactions || []).concat(addOnTxs);
|
|
436
|
+
|
|
437
|
+
const myTxs = allTxs.filter(t => {
|
|
438
|
+
const daAddr = das[t.chain]?.address?.toLowerCase();
|
|
439
|
+
return daAddr && t.signers?.some(s => s?.toLowerCase() === daAddr);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
if (!myTxs.length) throw new Error('没有需要签名的交易');
|
|
443
|
+
|
|
444
|
+
const txHashes = {};
|
|
445
|
+
const signatures = await Promise.all(myTxs.map(async tx => {
|
|
446
|
+
const sig = await signTransaction(tx.chain, tx.data, mnemonic);
|
|
447
|
+
if (tx.chain === 'SOLANA') {
|
|
448
|
+
// Solana tx hash = 已签名 tx 的第一个 signature(base58)
|
|
449
|
+
try {
|
|
450
|
+
const signedTxBytes = Buffer.from(ethersUtils.base58.decode(sig));
|
|
451
|
+
const signedTx = VersionedTransaction.deserialize(signedTxBytes);
|
|
452
|
+
txHashes[tx.transaction_uuid] = ethersUtils.base58.encode(signedTx.signatures[0]);
|
|
453
|
+
} catch {}
|
|
454
|
+
} else if (EVM_CHAINS.includes(tx.chain) && typeof sig === 'string' && sig.startsWith('0x')) {
|
|
455
|
+
// EVM tx hash = keccak256(signed RLP bytes)
|
|
456
|
+
try {
|
|
457
|
+
txHashes[tx.transaction_uuid] = ethersUtils.keccak256(ethersUtils.arrayify(sig));
|
|
458
|
+
} catch {}
|
|
459
|
+
}
|
|
460
|
+
return { uuid: tx.transaction_uuid, signature: sig };
|
|
461
|
+
}));
|
|
462
|
+
|
|
463
|
+
const signResp = await intentSign(client, {
|
|
464
|
+
uuid: intentItem.uuid,
|
|
465
|
+
expand_signatures: signatures,
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
return { ...signResp, txHashes };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ─── Wallet Session ───────────────────────────────────────────────────────────
|
|
472
|
+
|
|
473
|
+
const SESSION_FILE = path.join(__dirname, '.session.json');
|
|
474
|
+
|
|
475
|
+
function saveSession(session) {
|
|
476
|
+
fs.writeFileSync(SESSION_FILE, JSON.stringify(session, null, 2));
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function loadSession() {
|
|
480
|
+
if (!fs.existsSync(SESSION_FILE)) return null;
|
|
481
|
+
try {
|
|
482
|
+
return JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8'));
|
|
483
|
+
} catch { return null; }
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function buildSession(privateKey, keyNonce = 1, env = 'dev') {
|
|
487
|
+
if (env) process.env.ENV = env;
|
|
488
|
+
const spinner = ora('派生密钥...').start();
|
|
489
|
+
try {
|
|
490
|
+
const keys = await deriveAllKeys(privateKey, keyNonce);
|
|
491
|
+
|
|
492
|
+
spinner.text = '获取 Access Token...';
|
|
493
|
+
const jwt = await getAccessToken(keys.DGWallet.privateKey);
|
|
494
|
+
const client = createApiClient(jwt);
|
|
495
|
+
|
|
496
|
+
spinner.text = '获取 DA Owner...';
|
|
497
|
+
const da_owner = await fetchDAOwner(keys.eoaAddress) || `${keys.DGWallet.address}_0`;
|
|
498
|
+
|
|
499
|
+
spinner.text = '检查 DA 注册状态...';
|
|
500
|
+
const { das: registeredDAs } = await fetchRegisteredDAs(client);
|
|
501
|
+
const needsSync = !registeredDAs.SOLANA?.address || !registeredDAs.ETHEREUM?.address;
|
|
502
|
+
if (needsSync) {
|
|
503
|
+
spinner.text = '注册 DA 地址到后端...';
|
|
504
|
+
await registerDAs(client, { eoaAddress: keys.eoaAddress, keys, keyNonce });
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
spinner.text = '获取链配置...';
|
|
508
|
+
const { solverMap } = await getSolverAddresses(client);
|
|
509
|
+
|
|
510
|
+
const session = {
|
|
511
|
+
env,
|
|
512
|
+
eoaAddress: keys.eoaAddress,
|
|
513
|
+
da_owner,
|
|
514
|
+
dgWalletAddress: keys.DGWallet.address,
|
|
515
|
+
dgWalletPrivateKey: keys.DGWallet.privateKey,
|
|
516
|
+
solanaAddress: keys.SOLANA.address,
|
|
517
|
+
ethereumDAAddress: keys.ETHEREUM.address,
|
|
518
|
+
mnemonic: keys.mnemonic,
|
|
519
|
+
jwt,
|
|
520
|
+
solverMap,
|
|
521
|
+
jwtCreatedAt: Date.now(),
|
|
522
|
+
das: {
|
|
523
|
+
SOLANA: { address: keys.SOLANA.address, privateKey: keys.SOLANA.privateKey },
|
|
524
|
+
ETHEREUM: { address: keys.ETHEREUM.address, privateKey: keys.ETHEREUM.privateKey },
|
|
525
|
+
BASE: { address: keys.ETHEREUM.address, privateKey: keys.ETHEREUM.privateKey },
|
|
526
|
+
BSC: { address: keys.ETHEREUM.address, privateKey: keys.ETHEREUM.privateKey },
|
|
527
|
+
ARBITRUM: { address: keys.ETHEREUM.address, privateKey: keys.ETHEREUM.privateKey },
|
|
528
|
+
OPTIMISM: { address: keys.ETHEREUM.address, privateKey: keys.ETHEREUM.privateKey },
|
|
529
|
+
AVALANCHE: { address: keys.ETHEREUM.address, privateKey: keys.ETHEREUM.privateKey },
|
|
530
|
+
POLYGON: { address: keys.ETHEREUM.address, privateKey: keys.ETHEREUM.privateKey },
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
saveSession(session);
|
|
534
|
+
spinner.succeed('认证成功');
|
|
535
|
+
|
|
536
|
+
console.log(chalk.gray(` EOA: ${keys.eoaAddress}`));
|
|
537
|
+
console.log(chalk.gray(` DGWallet: ${keys.DGWallet.address}`));
|
|
538
|
+
console.log(chalk.gray(` DA Owner: ${da_owner}`));
|
|
539
|
+
console.log(chalk.gray(` Solana DA: ${keys.SOLANA.address}`));
|
|
540
|
+
return session;
|
|
541
|
+
} catch (err) {
|
|
542
|
+
spinner.fail('认证失败');
|
|
543
|
+
console.error(chalk.red(err.message || JSON.stringify(err)));
|
|
544
|
+
process.exit(1);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async function ensureSession() {
|
|
549
|
+
let session = loadSession();
|
|
550
|
+
if (session) {
|
|
551
|
+
if (session.env) process.env.ENV = session.env;
|
|
552
|
+
return session;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const pk = process.env.PRIVATE_KEY;
|
|
556
|
+
if (pk) {
|
|
557
|
+
const keyNonce = parseInt(process.env.KEY_NONCE || '1', 10);
|
|
558
|
+
return buildSession(pk, keyNonce, process.env.ENV || 'dev');
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
console.log(chalk.yellow('未找到 session,请先运行: node cli.js setup'));
|
|
562
|
+
process.exit(1);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async function getAuthClient(session) {
|
|
566
|
+
const ONE_HOUR = 60 * 60 * 1000;
|
|
567
|
+
let jwt = session.jwt;
|
|
568
|
+
|
|
569
|
+
if (Date.now() - (session.jwtCreatedAt || 0) > ONE_HOUR) {
|
|
570
|
+
const spinner = ora('刷新 Access Token...').start();
|
|
571
|
+
try {
|
|
572
|
+
jwt = await getAccessToken(session.dgWalletPrivateKey);
|
|
573
|
+
session.jwt = jwt;
|
|
574
|
+
session.jwtCreatedAt = Date.now();
|
|
575
|
+
saveSession(session);
|
|
576
|
+
spinner.succeed('Token 已刷新');
|
|
577
|
+
} catch (err) {
|
|
578
|
+
spinner.fail('Token 刷新失败,请重新 init');
|
|
579
|
+
throw err;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return createApiClient(jwt);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// ─── Explorer Utils ───────────────────────────────────────────────────────────
|
|
587
|
+
|
|
588
|
+
const EVM_EXPLORERS = {
|
|
589
|
+
ETHEREUM: { mainnet: 'https://etherscan.io/tx/', testnet: 'https://sepolia.etherscan.io/tx/' },
|
|
590
|
+
BASE: { mainnet: 'https://basescan.org/tx/', testnet: 'https://sepolia.basescan.org/tx/' },
|
|
591
|
+
BSC: { mainnet: 'https://bscscan.com/tx/', testnet: 'https://testnet.bscscan.com/tx/' },
|
|
592
|
+
ARBITRUM: { mainnet: 'https://arbiscan.io/tx/', testnet: 'https://sepolia.arbiscan.io/tx/' },
|
|
593
|
+
OPTIMISM: { mainnet: 'https://optimistic.etherscan.io/tx/', testnet: 'https://sepolia-optimism.etherscan.io/tx/' },
|
|
594
|
+
AVALANCHE:{ mainnet: 'https://snowtrace.io/tx/', testnet: 'https://testnet.snowtrace.io/tx/' },
|
|
595
|
+
POLYGON: { mainnet: 'https://polygonscan.com/tx/', testnet: 'https://amoy.polygonscan.com/tx/' },
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
function buildExplorerUrl(chain, txHash) {
|
|
599
|
+
const cfg = getConfig();
|
|
600
|
+
const isProd = cfg.DEX_CHAIN_ID === 1;
|
|
601
|
+
if (chain === 'SOLANA') {
|
|
602
|
+
return `https://solscan.io/tx/${txHash}${isProd ? '' : '?cluster=devnet'}`;
|
|
603
|
+
}
|
|
604
|
+
const exp = EVM_EXPLORERS[chain];
|
|
605
|
+
if (exp) return (isProd ? exp.mainnet : exp.testnet) + txHash;
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// ─── Commands ─────────────────────────────────────────────────────────────────
|
|
610
|
+
|
|
611
|
+
async function cmdSetup() {
|
|
612
|
+
console.log(chalk.cyan.bold('\n🔐 DeGate CLI Setup\n'));
|
|
613
|
+
|
|
614
|
+
const existing = loadSession();
|
|
615
|
+
if (existing) {
|
|
616
|
+
const { overwrite } = await inquirer.prompt([{
|
|
617
|
+
type: 'confirm',
|
|
618
|
+
name: 'overwrite',
|
|
619
|
+
message: `已有 session (${existing.eoaAddress}),要覆盖吗?`,
|
|
620
|
+
default: false,
|
|
621
|
+
}]);
|
|
622
|
+
if (!overwrite) {
|
|
623
|
+
console.log(chalk.gray('已取消'));
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const answers = await inquirer.prompt([
|
|
629
|
+
{
|
|
630
|
+
type: 'password',
|
|
631
|
+
name: 'privateKey',
|
|
632
|
+
message: '输入你的 EOA 私钥 (MetaMask 私钥):',
|
|
633
|
+
mask: '*',
|
|
634
|
+
validate: (v) => {
|
|
635
|
+
const key = v.startsWith('0x') ? v : '0x' + v;
|
|
636
|
+
try { new EthersWallet(key); return true; } catch { return '私钥格式不对'; }
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
type: 'list',
|
|
641
|
+
name: 'env',
|
|
642
|
+
message: '选择环境:',
|
|
643
|
+
choices: [
|
|
644
|
+
{ name: 'Dev (dev-backend.degate.com)', value: 'dev' },
|
|
645
|
+
{ name: 'Prod (v1-mainnet-backend.degate.com)', value: 'prod' },
|
|
646
|
+
],
|
|
647
|
+
default: 'dev',
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
type: 'number',
|
|
651
|
+
name: 'keyNonce',
|
|
652
|
+
message: 'Key Nonce (首次注册填 1):',
|
|
653
|
+
default: 1,
|
|
654
|
+
},
|
|
655
|
+
]);
|
|
656
|
+
|
|
657
|
+
const pk = answers.privateKey.startsWith('0x') ? answers.privateKey : '0x' + answers.privateKey;
|
|
658
|
+
await buildSession(pk, answers.keyNonce, answers.env);
|
|
659
|
+
console.log(chalk.green('\n✅ Setup 完成! 现在可以使用 products / deposit / withdraw 等命令'));
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
async function cmdProducts() {
|
|
663
|
+
const session = await ensureSession();
|
|
664
|
+
const client = await getAuthClient(session);
|
|
665
|
+
const spinner = ora('获取产品列表...').start();
|
|
666
|
+
|
|
667
|
+
try {
|
|
668
|
+
const products = await fetchProducts(client);
|
|
669
|
+
spinner.stop();
|
|
670
|
+
|
|
671
|
+
if (!products.length) {
|
|
672
|
+
console.log(chalk.yellow('暂无产品'));
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const table = new Table({
|
|
677
|
+
head: ['#', 'Pool', 'Chain', 'Price', '7d APY', 'Range', 'Fee', 'Pool Address'].map(h => chalk.cyan(h)),
|
|
678
|
+
style: { head: [] },
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
products.forEach((p, i) => {
|
|
682
|
+
const baseSymbol = p.token_a?.symbol || '?';
|
|
683
|
+
const quoteSymbol = p.token_b?.symbol || '?';
|
|
684
|
+
const chain = p.chain_name || '-';
|
|
685
|
+
const price = parseFloat(p.current_price || 0).toFixed(p.priceDecimals || 4);
|
|
686
|
+
const apy = p.week_apr ? `${parseFloat(p.week_apr).toFixed(2)}%` : '-';
|
|
687
|
+
const range = `${p.price_percentage_min || 0}%-${p.price_percentage_max || 0}%`;
|
|
688
|
+
const fee = p.fee_percentage ? `${p.fee_percentage}%` : '-';
|
|
689
|
+
table.push([
|
|
690
|
+
i + 1,
|
|
691
|
+
`${baseSymbol}/${quoteSymbol}`,
|
|
692
|
+
chain,
|
|
693
|
+
price,
|
|
694
|
+
apy,
|
|
695
|
+
range,
|
|
696
|
+
fee,
|
|
697
|
+
p.pool_address?.slice(0, 16) + '...',
|
|
698
|
+
]);
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
console.log(chalk.bold('\n📊 Turbo Range 产品列表\n'));
|
|
702
|
+
console.log(table.toString());
|
|
703
|
+
console.log(chalk.gray(`\n共 ${products.length} 个产品`));
|
|
704
|
+
} catch (err) {
|
|
705
|
+
spinner.fail('获取产品失败');
|
|
706
|
+
console.error(chalk.red(err.message || JSON.stringify(err)));
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
async function cmdDeposit(options) {
|
|
711
|
+
const session = await ensureSession();
|
|
712
|
+
const client = await getAuthClient(session);
|
|
713
|
+
|
|
714
|
+
const spinner = ora('获取产品信息...').start();
|
|
715
|
+
try {
|
|
716
|
+
const products = await fetchProducts(client);
|
|
717
|
+
let product;
|
|
718
|
+
|
|
719
|
+
if (options.pool) {
|
|
720
|
+
product = products.find(p =>
|
|
721
|
+
p.pool_address?.toLowerCase() === options.pool.toLowerCase() ||
|
|
722
|
+
p.token_a?.symbol?.toLowerCase() === options.pool.toLowerCase()
|
|
723
|
+
);
|
|
724
|
+
} else if (options.index) {
|
|
725
|
+
product = products[parseInt(options.index, 10) - 1];
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (!product) {
|
|
729
|
+
spinner.fail('未找到产品,请用 --pool <address> 或 --index <num>');
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const currentPrice = parseFloat(product.current_price);
|
|
734
|
+
const chain = product.chain_name;
|
|
735
|
+
const poolAddress = product.pool_address;
|
|
736
|
+
const amount = options.amount;
|
|
737
|
+
const showDecimals = product.priceDecimals || 4;
|
|
738
|
+
|
|
739
|
+
if (!amount) {
|
|
740
|
+
spinner.fail('请指定 --amount');
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
let minPrice, maxPrice;
|
|
745
|
+
if (options.range) {
|
|
746
|
+
const rangePct = parseFloat(options.range) / 100;
|
|
747
|
+
minPrice = (currentPrice * (1 - rangePct)).toFixed(showDecimals);
|
|
748
|
+
maxPrice = (currentPrice * (1 + rangePct)).toFixed(showDecimals);
|
|
749
|
+
} else if (options.min && options.max) {
|
|
750
|
+
minPrice = options.min;
|
|
751
|
+
maxPrice = options.max;
|
|
752
|
+
} else {
|
|
753
|
+
spinner.fail('请指定 --range <百分比> 或 --min/--max');
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const solver = session.solverMap[chain] || {};
|
|
758
|
+
const da = session.das[chain];
|
|
759
|
+
if (!da) {
|
|
760
|
+
spinner.fail(`不支持的链: ${chain}`);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const usdcToken = product.token_b;
|
|
765
|
+
const decimals = usdcToken?.decimals || 6;
|
|
766
|
+
const volume = ethersUtils.parseUnits(amount, decimals).toString();
|
|
767
|
+
const usdcChain = usdcToken.chain || chain;
|
|
768
|
+
const usdcDa = session.das[usdcChain] || da;
|
|
769
|
+
const gasToken = solver.gasToken;
|
|
770
|
+
|
|
771
|
+
const intentOrder = {
|
|
772
|
+
owner: session.da_owner,
|
|
773
|
+
intent: {
|
|
774
|
+
modifiable: false,
|
|
775
|
+
type: 'LP_DEPOSIT',
|
|
776
|
+
chain,
|
|
777
|
+
gas_payer_address: solver.gas || '',
|
|
778
|
+
token_out: [{
|
|
779
|
+
chain: usdcChain,
|
|
780
|
+
token: usdcToken.code,
|
|
781
|
+
address: usdcDa.address,
|
|
782
|
+
amount: volume,
|
|
783
|
+
decimals,
|
|
784
|
+
side: 'OUT',
|
|
785
|
+
}],
|
|
786
|
+
extra_data: {
|
|
787
|
+
fee_address: solver.solver || '',
|
|
788
|
+
caller: da.address,
|
|
789
|
+
deposit_amount: volume,
|
|
790
|
+
token_mint: usdcToken.code,
|
|
791
|
+
fee_percent: `${product.fee_percentage || 0}`,
|
|
792
|
+
zap_in: {
|
|
793
|
+
pool: poolAddress,
|
|
794
|
+
slippage: `${product.price_slippage || '0.01'}`,
|
|
795
|
+
deposit_token_mint: usdcToken.code,
|
|
796
|
+
user_da: da.address,
|
|
797
|
+
deposit_amount: volume,
|
|
798
|
+
tick_upper_price: maxPrice,
|
|
799
|
+
tick_lower_price: minPrice,
|
|
800
|
+
token_a: product.token_a.code,
|
|
801
|
+
token_b: product.token_b.code,
|
|
802
|
+
},
|
|
803
|
+
},
|
|
804
|
+
},
|
|
805
|
+
gas_payment_token: gasToken ? { chain: gasToken.chain, token: gasToken.code } : undefined,
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
spinner.text = `开仓: ${product.token_a?.symbol}/${product.token_b?.symbol} | ${amount} USDC | 区间 ${minPrice}-${maxPrice}`;
|
|
809
|
+
|
|
810
|
+
if (options.verbose) {
|
|
811
|
+
spinner.stop();
|
|
812
|
+
console.log(chalk.yellow('\n[VERBOSE] 注册的 DA 地址:'));
|
|
813
|
+
const { das: regDAs } = await fetchRegisteredDAs(client);
|
|
814
|
+
Object.entries(regDAs).forEach(([ch, d]) => {
|
|
815
|
+
console.log(chalk.gray(` ${ch}: ${d.address} (path: ${d.path})`));
|
|
816
|
+
});
|
|
817
|
+
console.log(chalk.yellow('\n[VERBOSE] 本地派生 DA 地址:'));
|
|
818
|
+
Object.entries(session.das).forEach(([ch, d]) => {
|
|
819
|
+
console.log(chalk.gray(` ${ch}: ${d.address}`));
|
|
820
|
+
});
|
|
821
|
+
console.log(chalk.yellow('\n[VERBOSE] Intent Order:'));
|
|
822
|
+
console.log(chalk.gray(JSON.stringify(intentOrder, null, 2)));
|
|
823
|
+
spinner.start();
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const tryResp = await intentTryCreate(client, intentOrder);
|
|
827
|
+
|
|
828
|
+
spinner.text = '签名交易...';
|
|
829
|
+
const signResp = await signAndSubmitIntent(client, tryResp, session.mnemonic, session.das);
|
|
830
|
+
|
|
831
|
+
spinner.succeed('开仓成功!');
|
|
832
|
+
|
|
833
|
+
const txHash = Object.values(signResp?.txHashes || {})[0];
|
|
834
|
+
const explorerUrl = txHash ? buildExplorerUrl(chain, txHash) : null;
|
|
835
|
+
|
|
836
|
+
const resultTable = new Table({ style: { head: [] } });
|
|
837
|
+
resultTable.push(
|
|
838
|
+
{ '产品': `${product.token_a?.symbol}/${product.token_b?.symbol}` },
|
|
839
|
+
{ '链': chain },
|
|
840
|
+
{ '金额': `${amount} USDC` },
|
|
841
|
+
{ '价格区间': `${minPrice} - ${maxPrice}` },
|
|
842
|
+
{ '当前价格': currentPrice.toFixed(showDecimals) },
|
|
843
|
+
{ 'Intent ID': signResp?.intent?.intent_id || '-' },
|
|
844
|
+
{ '状态': chalk.green('SUCCESS') },
|
|
845
|
+
);
|
|
846
|
+
if (txHash) {
|
|
847
|
+
resultTable.push({ 'TxHash': txHash });
|
|
848
|
+
resultTable.push({ 'Explorer': chalk.cyan(explorerUrl) });
|
|
849
|
+
}
|
|
850
|
+
console.log('\n' + resultTable.toString());
|
|
851
|
+
|
|
852
|
+
return signResp;
|
|
853
|
+
} catch (err) {
|
|
854
|
+
spinner.fail('开仓失败');
|
|
855
|
+
console.error(chalk.red(`code: ${err.code}, message: ${err.message}`));
|
|
856
|
+
if (err.data) console.error(chalk.gray(JSON.stringify(err.data, null, 2)));
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
async function cmdPositions(options) {
|
|
861
|
+
const session = await ensureSession();
|
|
862
|
+
const client = await getAuthClient(session);
|
|
863
|
+
const spinner = ora('获取持仓...').start();
|
|
864
|
+
|
|
865
|
+
try {
|
|
866
|
+
const status = options.closed ? 'CLOSED' : 'OPEN';
|
|
867
|
+
const resp = await fetchPositions(client, status);
|
|
868
|
+
const positions = resp.positions || [];
|
|
869
|
+
spinner.stop();
|
|
870
|
+
|
|
871
|
+
if (!positions.length) {
|
|
872
|
+
console.log(chalk.yellow(`暂无${status === 'OPEN' ? '活跃' : '已关闭'}持仓`));
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const table = new Table({
|
|
877
|
+
head: ['#', 'Pool', 'Position', 'Range', 'Value', 'Yield', 'APY', 'Status'].map(h => chalk.cyan(h)),
|
|
878
|
+
style: { head: [] },
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
positions.forEach((p, i) => {
|
|
882
|
+
const baseSymbol = p.token_a?.symbol || '?';
|
|
883
|
+
const quoteSymbol = p.token_b?.symbol || '?';
|
|
884
|
+
const poolName = `${baseSymbol}/${quoteSymbol}`;
|
|
885
|
+
const posAddr = (p.position || '').slice(0, 16) + '...';
|
|
886
|
+
const range = `${p.tick_lower || '-'} - ${p.tick_upper || '-'}`;
|
|
887
|
+
const totalYield = p.total_yields || p.total_yield_usd;
|
|
888
|
+
const value = p.position_value ? `$${parseFloat(p.position_value).toFixed(2)}` : '-';
|
|
889
|
+
const yield_ = totalYield ? `$${parseFloat(totalYield).toFixed(4)}` : '-';
|
|
890
|
+
const apy = p.apy ? `${parseFloat(p.apy).toFixed(2)}%` : '-';
|
|
891
|
+
table.push([i + 1, poolName, posAddr, range, value, yield_, apy, p.status]);
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
console.log(chalk.bold(`\n📈 ${status === 'OPEN' ? '活跃' : '已关闭'}持仓\n`));
|
|
895
|
+
console.log(table.toString());
|
|
896
|
+
console.log(chalk.gray(`\n共 ${positions.length} 个持仓`));
|
|
897
|
+
|
|
898
|
+
if (status === 'OPEN') {
|
|
899
|
+
console.log(chalk.gray('\n使用 withdraw --index <序号> 或 withdraw --position <address> 来平仓'));
|
|
900
|
+
}
|
|
901
|
+
} catch (err) {
|
|
902
|
+
spinner.fail('获取持仓失败');
|
|
903
|
+
console.error(chalk.red(err.message || JSON.stringify(err)));
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
async function cmdWithdraw(options) {
|
|
908
|
+
const session = await ensureSession();
|
|
909
|
+
const client = await getAuthClient(session);
|
|
910
|
+
|
|
911
|
+
let positionAddress = options.position;
|
|
912
|
+
|
|
913
|
+
// --index 选择
|
|
914
|
+
if (!positionAddress && options.index) {
|
|
915
|
+
const spinner = ora('获取持仓列表...').start();
|
|
916
|
+
const resp = await fetchPositions(client, 'OPEN');
|
|
917
|
+
const positions = resp.positions || [];
|
|
918
|
+
spinner.stop();
|
|
919
|
+
const idx = parseInt(options.index, 10) - 1;
|
|
920
|
+
if (idx < 0 || idx >= positions.length) {
|
|
921
|
+
console.log(chalk.red(`序号 ${options.index} 不存在,共 ${positions.length} 个活跃持仓`));
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
positionAddress = positions[idx].position;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// 无参数:交互式选择
|
|
928
|
+
if (!positionAddress) {
|
|
929
|
+
const spinner = ora('获取持仓列表...').start();
|
|
930
|
+
const resp = await fetchPositions(client, 'OPEN');
|
|
931
|
+
const positions = resp.positions || [];
|
|
932
|
+
spinner.stop();
|
|
933
|
+
|
|
934
|
+
if (!positions.length) {
|
|
935
|
+
console.log(chalk.yellow('暂无活跃持仓'));
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const { selected } = await inquirer.prompt([{
|
|
940
|
+
type: 'list',
|
|
941
|
+
name: 'selected',
|
|
942
|
+
message: '选择要平仓的持仓:',
|
|
943
|
+
choices: positions.map((p, i) => {
|
|
944
|
+
const base = p.token_a?.symbol || '?';
|
|
945
|
+
const quote = p.token_b?.symbol || '?';
|
|
946
|
+
const value = p.position_value ? `$${parseFloat(p.position_value).toFixed(2)}` : '-';
|
|
947
|
+
const apy = p.apy ? `${parseFloat(p.apy).toFixed(2)}%` : '-';
|
|
948
|
+
return {
|
|
949
|
+
name: `[${i + 1}] ${base}/${quote} ${value} APY ${apy} ${p.position}`,
|
|
950
|
+
value: p.position,
|
|
951
|
+
};
|
|
952
|
+
}),
|
|
953
|
+
}]);
|
|
954
|
+
positionAddress = selected;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const spinner = ora('获取持仓详情...').start();
|
|
958
|
+
try {
|
|
959
|
+
const detail = await fetchPositionDetail(client, positionAddress);
|
|
960
|
+
const position = { ...detail, ...(detail.position || {}) };
|
|
961
|
+
|
|
962
|
+
if (!position.pool_address) {
|
|
963
|
+
spinner.fail('未找到该持仓');
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
const chain = position.chain_name || position.chain;
|
|
968
|
+
const solver = session.solverMap[chain] || {};
|
|
969
|
+
const da = session.das[chain];
|
|
970
|
+
if (!da) {
|
|
971
|
+
spinner.fail(`不支持的链: ${chain}`);
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
const products = await fetchProducts(client);
|
|
976
|
+
const product = products.find(p => p.pool_address?.toLowerCase() === position.pool_address?.toLowerCase());
|
|
977
|
+
|
|
978
|
+
const usdcCode = product?.token_b?.code || position.token_b?.code;
|
|
979
|
+
const convertToUsdc = options.convertUsdc !== false;
|
|
980
|
+
const gasToken = solver.gasToken;
|
|
981
|
+
|
|
982
|
+
const intentOrder = {
|
|
983
|
+
owner: session.da_owner,
|
|
984
|
+
intent: {
|
|
985
|
+
chain,
|
|
986
|
+
type: 'LP_WITHDRAW',
|
|
987
|
+
gas_payer_address: solver.gas || '',
|
|
988
|
+
token_out: [],
|
|
989
|
+
extra_data: {
|
|
990
|
+
pool: position.pool_address,
|
|
991
|
+
caller: da.address,
|
|
992
|
+
convert_to_usdc: convertToUsdc,
|
|
993
|
+
nft_mint: positionAddress,
|
|
994
|
+
fee_address: solver.solver || '',
|
|
995
|
+
fee_percent: `${product?.fee_percentage || 0}`,
|
|
996
|
+
token_mint: usdcCode,
|
|
997
|
+
slippage: `${product?.price_slippage || '0.01'}`,
|
|
998
|
+
},
|
|
999
|
+
},
|
|
1000
|
+
gas_payment_token: gasToken ? { chain: gasToken.chain, token: gasToken.code } : undefined,
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
spinner.text = '提交 withdraw...';
|
|
1004
|
+
const tryResp = await intentTryCreate(client, intentOrder);
|
|
1005
|
+
|
|
1006
|
+
spinner.text = '签名交易...';
|
|
1007
|
+
const signResp = await signAndSubmitIntent(client, tryResp, session.mnemonic, session.das);
|
|
1008
|
+
|
|
1009
|
+
spinner.succeed('平仓成功!');
|
|
1010
|
+
|
|
1011
|
+
const txHash = Object.values(signResp?.txHashes || {})[0];
|
|
1012
|
+
const explorerUrl = txHash ? buildExplorerUrl(chain, txHash) : null;
|
|
1013
|
+
|
|
1014
|
+
const resultTable = new Table({ style: { head: [] } });
|
|
1015
|
+
resultTable.push(
|
|
1016
|
+
{ 'Position': positionAddress.slice(0, 20) + '...' },
|
|
1017
|
+
{ 'Pool': position.pool_name || position.pool_address?.slice(0, 20) },
|
|
1018
|
+
{ '链': chain },
|
|
1019
|
+
{ '转换为USDC': convertToUsdc ? '是' : '否' },
|
|
1020
|
+
{ 'Intent ID': signResp?.intent?.intent_id || '-' },
|
|
1021
|
+
{ '状态': chalk.green('SUCCESS') },
|
|
1022
|
+
);
|
|
1023
|
+
if (txHash) {
|
|
1024
|
+
resultTable.push({ 'TxHash': txHash });
|
|
1025
|
+
resultTable.push({ 'Explorer': chalk.cyan(explorerUrl) });
|
|
1026
|
+
}
|
|
1027
|
+
console.log('\n' + resultTable.toString());
|
|
1028
|
+
|
|
1029
|
+
return signResp;
|
|
1030
|
+
} catch (err) {
|
|
1031
|
+
spinner.fail('平仓失败');
|
|
1032
|
+
console.error(chalk.red(err.message || JSON.stringify(err)));
|
|
1033
|
+
if (err.data) console.error(chalk.gray(JSON.stringify(err.data, null, 2)));
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// ─── CLI Program ──────────────────────────────────────────────────────────────
|
|
1038
|
+
|
|
1039
|
+
const program = new Command();
|
|
1040
|
+
|
|
1041
|
+
program
|
|
1042
|
+
.name('degate-cli')
|
|
1043
|
+
.description('DeGate Turbo Range CLI - 开仓/平仓工具')
|
|
1044
|
+
.version('1.0.0');
|
|
1045
|
+
|
|
1046
|
+
program
|
|
1047
|
+
.command('setup')
|
|
1048
|
+
.description('交互式初始化:输入私钥,自动派生 DA + 获取 JWT')
|
|
1049
|
+
.action(cmdSetup);
|
|
1050
|
+
|
|
1051
|
+
program
|
|
1052
|
+
.command('products')
|
|
1053
|
+
.description('查看 Turbo Range 产品列表')
|
|
1054
|
+
.action(cmdProducts);
|
|
1055
|
+
|
|
1056
|
+
program
|
|
1057
|
+
.command('deposit')
|
|
1058
|
+
.description('开仓 (LP_DEPOSIT)')
|
|
1059
|
+
.option('--pool <address>', '产品 pool 地址或 token symbol')
|
|
1060
|
+
.option('--index <num>', '产品编号(从 products 列表获取)')
|
|
1061
|
+
.option('--amount <usdc>', 'USDC 金额')
|
|
1062
|
+
.option('--range <percent>', '快捷区间百分比(如 5 = 当前价格 ±5%)')
|
|
1063
|
+
.option('--min <price>', '最低价格(自定义区间)')
|
|
1064
|
+
.option('--max <price>', '最高价格(自定义区间)')
|
|
1065
|
+
.option('--verbose', '打印完整请求/响应数据')
|
|
1066
|
+
.action(cmdDeposit);
|
|
1067
|
+
|
|
1068
|
+
program
|
|
1069
|
+
.command('positions')
|
|
1070
|
+
.description('查看持仓')
|
|
1071
|
+
.option('--closed', '查看已关闭持仓')
|
|
1072
|
+
.action(cmdPositions);
|
|
1073
|
+
|
|
1074
|
+
program
|
|
1075
|
+
.command('withdraw')
|
|
1076
|
+
.description('平仓 (LP_WITHDRAW)')
|
|
1077
|
+
.option('--position <address>', '持仓地址 (position address)')
|
|
1078
|
+
.option('--index <num>', '持仓序号(从 positions 列表获取)')
|
|
1079
|
+
.option('--no-convert-usdc', '不转换为 USDC')
|
|
1080
|
+
.action(cmdWithdraw);
|
|
1081
|
+
|
|
1082
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "degate-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "DeGate Turbo Range CLI - deposit/withdraw LP positions across Solana & EVM chains",
|
|
5
|
+
"main": "cli.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"degate-cli": "./cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node cli.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"cli.js",
|
|
14
|
+
".env.example",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"degate",
|
|
19
|
+
"defi",
|
|
20
|
+
"lp",
|
|
21
|
+
"solana",
|
|
22
|
+
"evm",
|
|
23
|
+
"turbo-range",
|
|
24
|
+
"cli"
|
|
25
|
+
],
|
|
26
|
+
"author": "coptercc793",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18.0.0"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@solana/web3.js": "^1.95.0",
|
|
33
|
+
"axios": "^1.7.0",
|
|
34
|
+
"bip39": "^3.1.0",
|
|
35
|
+
"chalk": "^4.1.2",
|
|
36
|
+
"cli-table3": "^0.6.5",
|
|
37
|
+
"commander": "^12.0.0",
|
|
38
|
+
"dotenv": "^16.4.0",
|
|
39
|
+
"ed25519-hd-key": "^1.3.0",
|
|
40
|
+
"ethereumjs-util": "^7.1.5",
|
|
41
|
+
"ethereumjs-wallet": "^1.0.2",
|
|
42
|
+
"ethers": "^5.7.2",
|
|
43
|
+
"inquirer": "^8.2.6",
|
|
44
|
+
"ora": "^5.4.1",
|
|
45
|
+
"tweetnacl": "^1.0.3"
|
|
46
|
+
}
|
|
47
|
+
}
|