imm-cli 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/README.md +315 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +112 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +251 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/immbook.d.ts +16 -0
- package/dist/commands/immbook.d.ts.map +1 -0
- package/dist/commands/immbook.js +795 -0
- package/dist/commands/immbook.js.map +1 -0
- package/dist/commands/jaine.d.ts +3 -0
- package/dist/commands/jaine.d.ts.map +1 -0
- package/dist/commands/jaine.js +1397 -0
- package/dist/commands/jaine.js.map +1 -0
- package/dist/commands/send.d.ts +3 -0
- package/dist/commands/send.d.ts.map +1 -0
- package/dist/commands/send.js +229 -0
- package/dist/commands/send.js.map +1 -0
- package/dist/commands/setup.d.ts +3 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +83 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/slop-app.d.ts +9 -0
- package/dist/commands/slop-app.d.ts.map +1 -0
- package/dist/commands/slop-app.js +793 -0
- package/dist/commands/slop-app.js.map +1 -0
- package/dist/commands/slop.d.ts +3 -0
- package/dist/commands/slop.d.ts.map +1 -0
- package/dist/commands/slop.js +1053 -0
- package/dist/commands/slop.js.map +1 -0
- package/dist/commands/wallet.d.ts +3 -0
- package/dist/commands/wallet.d.ts.map +1 -0
- package/dist/commands/wallet.js +298 -0
- package/dist/commands/wallet.js.map +1 -0
- package/dist/config/paths.d.ts +6 -0
- package/dist/config/paths.d.ts.map +1 -0
- package/dist/config/paths.js +24 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/store.d.ts +44 -0
- package/dist/config/store.d.ts.map +1 -0
- package/dist/config/store.js +109 -0
- package/dist/config/store.js.map +1 -0
- package/dist/constants/chain.d.ts +56 -0
- package/dist/constants/chain.d.ts.map +1 -0
- package/dist/constants/chain.js +50 -0
- package/dist/constants/chain.js.map +1 -0
- package/dist/errors.d.ts +86 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +100 -0
- package/dist/errors.js.map +1 -0
- package/dist/immbook/api.d.ts +38 -0
- package/dist/immbook/api.d.ts.map +1 -0
- package/dist/immbook/api.js +86 -0
- package/dist/immbook/api.js.map +1 -0
- package/dist/immbook/auth.d.ts +31 -0
- package/dist/immbook/auth.d.ts.map +1 -0
- package/dist/immbook/auth.js +93 -0
- package/dist/immbook/auth.js.map +1 -0
- package/dist/immbook/comments.d.ts +26 -0
- package/dist/immbook/comments.d.ts.map +1 -0
- package/dist/immbook/comments.js +20 -0
- package/dist/immbook/comments.js.map +1 -0
- package/dist/immbook/follows.d.ts +19 -0
- package/dist/immbook/follows.d.ts.map +1 -0
- package/dist/immbook/follows.js +21 -0
- package/dist/immbook/follows.js.map +1 -0
- package/dist/immbook/jwtCache.d.ts +15 -0
- package/dist/immbook/jwtCache.d.ts.map +1 -0
- package/dist/immbook/jwtCache.js +63 -0
- package/dist/immbook/jwtCache.js.map +1 -0
- package/dist/immbook/points.d.ts +35 -0
- package/dist/immbook/points.d.ts.map +1 -0
- package/dist/immbook/points.js +20 -0
- package/dist/immbook/points.js.map +1 -0
- package/dist/immbook/posts.d.ts +46 -0
- package/dist/immbook/posts.d.ts.map +1 -0
- package/dist/immbook/posts.js +43 -0
- package/dist/immbook/posts.js.map +1 -0
- package/dist/immbook/profile.d.ts +29 -0
- package/dist/immbook/profile.d.ts.map +1 -0
- package/dist/immbook/profile.js +14 -0
- package/dist/immbook/profile.js.map +1 -0
- package/dist/immbook/submolts.d.ts +22 -0
- package/dist/immbook/submolts.d.ts.map +1 -0
- package/dist/immbook/submolts.js +24 -0
- package/dist/immbook/submolts.js.map +1 -0
- package/dist/immbook/tradeProof.d.ts +21 -0
- package/dist/immbook/tradeProof.d.ts.map +1 -0
- package/dist/immbook/tradeProof.js +14 -0
- package/dist/immbook/tradeProof.js.map +1 -0
- package/dist/immbook/votes.d.ts +17 -0
- package/dist/immbook/votes.d.ts.map +1 -0
- package/dist/immbook/votes.js +20 -0
- package/dist/immbook/votes.js.map +1 -0
- package/dist/intents/store.d.ts +22 -0
- package/dist/intents/store.d.ts.map +1 -0
- package/dist/intents/store.js +76 -0
- package/dist/intents/store.js.map +1 -0
- package/dist/intents/types.d.ts +21 -0
- package/dist/intents/types.d.ts.map +1 -0
- package/dist/intents/types.js +2 -0
- package/dist/intents/types.js.map +1 -0
- package/dist/jaine/abi/erc20.d.ts +90 -0
- package/dist/jaine/abi/erc20.d.ts.map +1 -0
- package/dist/jaine/abi/erc20.js +65 -0
- package/dist/jaine/abi/erc20.js.map +1 -0
- package/dist/jaine/abi/factory.d.ts +38 -0
- package/dist/jaine/abi/factory.d.ts.map +1 -0
- package/dist/jaine/abi/factory.js +26 -0
- package/dist/jaine/abi/factory.js.map +1 -0
- package/dist/jaine/abi/index.d.ts +11 -0
- package/dist/jaine/abi/index.d.ts.map +1 -0
- package/dist/jaine/abi/index.js +11 -0
- package/dist/jaine/abi/index.js.map +1 -0
- package/dist/jaine/abi/nftManager.d.ts +282 -0
- package/dist/jaine/abi/nftManager.d.ts.map +1 -0
- package/dist/jaine/abi/nftManager.js +182 -0
- package/dist/jaine/abi/nftManager.js.map +1 -0
- package/dist/jaine/abi/pool.d.ts +77 -0
- package/dist/jaine/abi/pool.d.ts.map +1 -0
- package/dist/jaine/abi/pool.js +56 -0
- package/dist/jaine/abi/pool.js.map +1 -0
- package/dist/jaine/abi/quoter.d.ts +84 -0
- package/dist/jaine/abi/quoter.d.ts.map +1 -0
- package/dist/jaine/abi/quoter.js +53 -0
- package/dist/jaine/abi/quoter.js.map +1 -0
- package/dist/jaine/abi/router.d.ts +135 -0
- package/dist/jaine/abi/router.d.ts.map +1 -0
- package/dist/jaine/abi/router.js +88 -0
- package/dist/jaine/abi/router.js.map +1 -0
- package/dist/jaine/abi/w0g.d.ts +41 -0
- package/dist/jaine/abi/w0g.d.ts.map +1 -0
- package/dist/jaine/abi/w0g.js +34 -0
- package/dist/jaine/abi/w0g.js.map +1 -0
- package/dist/jaine/allowance.d.ts +48 -0
- package/dist/jaine/allowance.d.ts.map +1 -0
- package/dist/jaine/allowance.js +192 -0
- package/dist/jaine/allowance.js.map +1 -0
- package/dist/jaine/coreTokens.d.ts +32 -0
- package/dist/jaine/coreTokens.d.ts.map +1 -0
- package/dist/jaine/coreTokens.js +91 -0
- package/dist/jaine/coreTokens.js.map +1 -0
- package/dist/jaine/pathEncoding.d.ts +39 -0
- package/dist/jaine/pathEncoding.d.ts.map +1 -0
- package/dist/jaine/pathEncoding.js +98 -0
- package/dist/jaine/pathEncoding.js.map +1 -0
- package/dist/jaine/paths.d.ts +11 -0
- package/dist/jaine/paths.d.ts.map +1 -0
- package/dist/jaine/paths.js +20 -0
- package/dist/jaine/paths.js.map +1 -0
- package/dist/jaine/poolCache.d.ts +42 -0
- package/dist/jaine/poolCache.d.ts.map +1 -0
- package/dist/jaine/poolCache.js +164 -0
- package/dist/jaine/poolCache.js.map +1 -0
- package/dist/jaine/routing.d.ts +41 -0
- package/dist/jaine/routing.d.ts.map +1 -0
- package/dist/jaine/routing.js +247 -0
- package/dist/jaine/routing.js.map +1 -0
- package/dist/jaine/userTokens.d.ts +27 -0
- package/dist/jaine/userTokens.d.ts.map +1 -0
- package/dist/jaine/userTokens.js +89 -0
- package/dist/jaine/userTokens.js.map +1 -0
- package/dist/slop/abi/factory.d.ts +128 -0
- package/dist/slop/abi/factory.d.ts.map +1 -0
- package/dist/slop/abi/factory.js +70 -0
- package/dist/slop/abi/factory.js.map +1 -0
- package/dist/slop/abi/feeCollector.d.ts +95 -0
- package/dist/slop/abi/feeCollector.d.ts.map +1 -0
- package/dist/slop/abi/feeCollector.js +71 -0
- package/dist/slop/abi/feeCollector.js.map +1 -0
- package/dist/slop/abi/index.d.ts +5 -0
- package/dist/slop/abi/index.d.ts.map +1 -0
- package/dist/slop/abi/index.js +5 -0
- package/dist/slop/abi/index.js.map +1 -0
- package/dist/slop/abi/registry.d.ts +135 -0
- package/dist/slop/abi/registry.d.ts.map +1 -0
- package/dist/slop/abi/registry.js +90 -0
- package/dist/slop/abi/registry.js.map +1 -0
- package/dist/slop/abi/token.d.ts +320 -0
- package/dist/slop/abi/token.d.ts.map +1 -0
- package/dist/slop/abi/token.js +251 -0
- package/dist/slop/abi/token.js.map +1 -0
- package/dist/slop/quote.d.ts +80 -0
- package/dist/slop/quote.d.ts.map +1 -0
- package/dist/slop/quote.js +174 -0
- package/dist/slop/quote.js.map +1 -0
- package/dist/utils/canonicalJson.d.ts +8 -0
- package/dist/utils/canonicalJson.d.ts.map +1 -0
- package/dist/utils/canonicalJson.js +20 -0
- package/dist/utils/canonicalJson.js.map +1 -0
- package/dist/utils/env.d.ts +11 -0
- package/dist/utils/env.d.ts.map +1 -0
- package/dist/utils/env.js +20 -0
- package/dist/utils/env.js.map +1 -0
- package/dist/utils/http.d.ts +19 -0
- package/dist/utils/http.d.ts.map +1 -0
- package/dist/utils/http.js +61 -0
- package/dist/utils/http.js.map +1 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +21 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/output.d.ts +19 -0
- package/dist/utils/output.d.ts.map +1 -0
- package/dist/utils/output.js +37 -0
- package/dist/utils/output.js.map +1 -0
- package/dist/utils/respond.d.ts +19 -0
- package/dist/utils/respond.d.ts.map +1 -0
- package/dist/utils/respond.js +25 -0
- package/dist/utils/respond.js.map +1 -0
- package/dist/utils/ui.d.ts +38 -0
- package/dist/utils/ui.d.ts.map +1 -0
- package/dist/utils/ui.js +126 -0
- package/dist/utils/ui.js.map +1 -0
- package/dist/wallet/client.d.ts +4 -0
- package/dist/wallet/client.d.ts.map +1 -0
- package/dist/wallet/client.js +53 -0
- package/dist/wallet/client.js.map +1 -0
- package/dist/wallet/keystore.d.ts +21 -0
- package/dist/wallet/keystore.d.ts.map +1 -0
- package/dist/wallet/keystore.js +111 -0
- package/dist/wallet/keystore.js.map +1 -0
- package/package.json +56 -0
- package/skills/imm/SKILL.md +617 -0
|
@@ -0,0 +1,1397 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { isAddress, getAddress, parseUnits, formatUnits, maxUint256, encodeFunctionData, } from "viem";
|
|
3
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
4
|
+
import { createWalletClient, http } from "viem";
|
|
5
|
+
import { loadConfig } from "../config/store.js";
|
|
6
|
+
import { getPublicClient } from "../wallet/client.js";
|
|
7
|
+
import { loadKeystore, decryptPrivateKey } from "../wallet/keystore.js";
|
|
8
|
+
import { requireKeystorePassword } from "../utils/env.js";
|
|
9
|
+
import { ImmError, ErrorCodes } from "../errors.js";
|
|
10
|
+
import { isHeadless, writeJsonSuccess, writeStderr } from "../utils/output.js";
|
|
11
|
+
import { spinner, successBox, infoBox, colors, formatBalance, createTable } from "../utils/ui.js";
|
|
12
|
+
// Jaine module imports
|
|
13
|
+
import { resolveToken, getTokenSymbol } from "../jaine/coreTokens.js";
|
|
14
|
+
import { loadUserTokens, addUserAlias, removeUserAlias, getMergedTokens } from "../jaine/userTokens.js";
|
|
15
|
+
import { loadPoolsCache, savePoolsCache, scanCorePools, findPoolsForToken, findPoolsBetweenTokens, } from "../jaine/poolCache.js";
|
|
16
|
+
import { POOLS_CACHE_FILE } from "../jaine/paths.js";
|
|
17
|
+
import { FEE_TIERS } from "../jaine/abi/factory.js";
|
|
18
|
+
import { ERC20_EXTENDED_ABI } from "../jaine/abi/erc20.js";
|
|
19
|
+
import { W0G_ABI } from "../jaine/abi/w0g.js";
|
|
20
|
+
import { ROUTER_ABI } from "../jaine/abi/router.js";
|
|
21
|
+
import { NFT_MANAGER_ABI } from "../jaine/abi/nftManager.js";
|
|
22
|
+
import { POOL_ABI } from "../jaine/abi/pool.js";
|
|
23
|
+
import { getAllAllowances, ensureAllowance, revokeApproval, getSpenderAddress, } from "../jaine/allowance.js";
|
|
24
|
+
import { findBestRouteExactInput, findBestRouteExactOutput, formatRoute, } from "../jaine/routing.js";
|
|
25
|
+
// Validation helpers
|
|
26
|
+
function validateFeeTier(fee) {
|
|
27
|
+
if (!FEE_TIERS.includes(fee)) {
|
|
28
|
+
throw new ImmError(ErrorCodes.INVALID_FEE_TIER, `Invalid fee tier: ${fee}`, `Valid fee tiers: ${FEE_TIERS.join(", ")}`);
|
|
29
|
+
}
|
|
30
|
+
return fee;
|
|
31
|
+
}
|
|
32
|
+
function validateSlippage(bps) {
|
|
33
|
+
if (bps < 0 || bps > 5000) {
|
|
34
|
+
throw new ImmError(ErrorCodes.INVALID_SLIPPAGE, `Invalid slippage: ${bps} bps`, "Slippage must be between 0 and 5000 bps (0-50%)");
|
|
35
|
+
}
|
|
36
|
+
return bps;
|
|
37
|
+
}
|
|
38
|
+
function parseIntSafe(value, name) {
|
|
39
|
+
const n = parseInt(value, 10);
|
|
40
|
+
if (!Number.isFinite(n)) {
|
|
41
|
+
throw new ImmError(ErrorCodes.INVALID_AMOUNT, `Invalid ${name}: ${value}`);
|
|
42
|
+
}
|
|
43
|
+
return n;
|
|
44
|
+
}
|
|
45
|
+
async function getTokenDecimals(token) {
|
|
46
|
+
const client = getPublicClient();
|
|
47
|
+
const decimals = await client.readContract({
|
|
48
|
+
address: token,
|
|
49
|
+
abi: ERC20_EXTENDED_ABI,
|
|
50
|
+
functionName: "decimals",
|
|
51
|
+
});
|
|
52
|
+
const n = Number(decimals);
|
|
53
|
+
// Guard: NaN or out of range → fallback 18
|
|
54
|
+
if (!Number.isFinite(n) || n < 0 || n > 255) {
|
|
55
|
+
return 18;
|
|
56
|
+
}
|
|
57
|
+
return n;
|
|
58
|
+
}
|
|
59
|
+
async function getTokenSymbolOnChain(token) {
|
|
60
|
+
const client = getPublicClient();
|
|
61
|
+
try {
|
|
62
|
+
return await client.readContract({
|
|
63
|
+
address: token,
|
|
64
|
+
abi: ERC20_EXTENDED_ABI,
|
|
65
|
+
functionName: "symbol",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return getTokenSymbol(token);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function requireWalletAndKeystore() {
|
|
73
|
+
const cfg = loadConfig();
|
|
74
|
+
if (!cfg.wallet.address) {
|
|
75
|
+
throw new ImmError(ErrorCodes.WALLET_NOT_CONFIGURED, "No wallet configured.", "Run: imm wallet create --json");
|
|
76
|
+
}
|
|
77
|
+
const password = requireKeystorePassword();
|
|
78
|
+
const keystore = loadKeystore();
|
|
79
|
+
if (!keystore) {
|
|
80
|
+
throw new ImmError(ErrorCodes.KEYSTORE_NOT_FOUND, "Keystore not found.", "Run: imm wallet create --json");
|
|
81
|
+
}
|
|
82
|
+
const privateKey = decryptPrivateKey(keystore, password);
|
|
83
|
+
return { address: cfg.wallet.address, privateKey };
|
|
84
|
+
}
|
|
85
|
+
function createJaineWalletClient(privateKey) {
|
|
86
|
+
const cfg = loadConfig();
|
|
87
|
+
const account = privateKeyToAccount(privateKey);
|
|
88
|
+
return createWalletClient({
|
|
89
|
+
account,
|
|
90
|
+
chain: {
|
|
91
|
+
id: cfg.chain.chainId,
|
|
92
|
+
name: "0G",
|
|
93
|
+
nativeCurrency: { name: "0G", symbol: "0G", decimals: 18 },
|
|
94
|
+
rpcUrls: { default: { http: [cfg.chain.rpcUrl] } },
|
|
95
|
+
},
|
|
96
|
+
transport: http(cfg.chain.rpcUrl, {
|
|
97
|
+
timeout: 30_000,
|
|
98
|
+
retryCount: 2,
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
export function createJaineCommand() {
|
|
103
|
+
const jaine = new Command("jaine")
|
|
104
|
+
.description("Jaine DEX operations (swap, LP, pools)")
|
|
105
|
+
.exitOverride();
|
|
106
|
+
// ============ TOKENS SUBCOMMAND ============
|
|
107
|
+
const tokens = new Command("tokens")
|
|
108
|
+
.description("Manage token aliases")
|
|
109
|
+
.exitOverride();
|
|
110
|
+
tokens
|
|
111
|
+
.command("list")
|
|
112
|
+
.description("List all known tokens (core + user aliases)")
|
|
113
|
+
.action(async () => {
|
|
114
|
+
const merged = getMergedTokens();
|
|
115
|
+
const userConfig = loadUserTokens();
|
|
116
|
+
if (isHeadless()) {
|
|
117
|
+
writeJsonSuccess({
|
|
118
|
+
tokens: Object.entries(merged).map(([symbol, address]) => ({
|
|
119
|
+
symbol,
|
|
120
|
+
address,
|
|
121
|
+
isUserAlias: !!userConfig.aliases[symbol],
|
|
122
|
+
})),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
const table = createTable([
|
|
127
|
+
{ header: "Symbol", width: 15 },
|
|
128
|
+
{ header: "Address", width: 45 },
|
|
129
|
+
{ header: "Source", width: 10 },
|
|
130
|
+
]);
|
|
131
|
+
for (const [symbol, address] of Object.entries(merged).sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
132
|
+
const source = userConfig.aliases[symbol] ? colors.info("user") : colors.muted("core");
|
|
133
|
+
table.push([symbol, address, source]);
|
|
134
|
+
}
|
|
135
|
+
writeStderr(table.toString());
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
tokens
|
|
139
|
+
.command("add-alias <symbol> <address>")
|
|
140
|
+
.description("Add a user token alias")
|
|
141
|
+
.action(async (symbol, address) => {
|
|
142
|
+
if (!isAddress(address)) {
|
|
143
|
+
throw new ImmError(ErrorCodes.INVALID_ADDRESS, `Invalid address: ${address}`);
|
|
144
|
+
}
|
|
145
|
+
addUserAlias(symbol, getAddress(address));
|
|
146
|
+
if (isHeadless()) {
|
|
147
|
+
writeJsonSuccess({ symbol, address: getAddress(address) });
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
successBox("Token Alias Added", `${colors.info(symbol)} → ${colors.address(address)}`);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
tokens
|
|
154
|
+
.command("remove-alias <symbol>")
|
|
155
|
+
.description("Remove a user token alias")
|
|
156
|
+
.action(async (symbol) => {
|
|
157
|
+
const removed = removeUserAlias(symbol);
|
|
158
|
+
if (!removed) {
|
|
159
|
+
throw new ImmError(ErrorCodes.TOKEN_NOT_FOUND, `Alias not found: ${symbol}`);
|
|
160
|
+
}
|
|
161
|
+
if (isHeadless()) {
|
|
162
|
+
writeJsonSuccess({ symbol, removed: true });
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
successBox("Token Alias Removed", `Removed alias: ${colors.info(symbol)}`);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
jaine.addCommand(tokens);
|
|
169
|
+
// ============ POOLS SUBCOMMAND ============
|
|
170
|
+
const pools = new Command("pools")
|
|
171
|
+
.description("Pool discovery and cache management")
|
|
172
|
+
.exitOverride();
|
|
173
|
+
pools
|
|
174
|
+
.command("scan-core")
|
|
175
|
+
.description("Scan factory for pools between core tokens")
|
|
176
|
+
.option("--fee-tiers <tiers>", "Comma-separated fee tiers", FEE_TIERS.join(","))
|
|
177
|
+
.action(async (options) => {
|
|
178
|
+
const feeTiers = options.feeTiers.split(",").map((t) => validateFeeTier(parseIntSafe(t.trim(), "feeTier")));
|
|
179
|
+
const spin = spinner("Scanning pools...");
|
|
180
|
+
spin.start();
|
|
181
|
+
const cfg = loadConfig();
|
|
182
|
+
const foundPools = await scanCorePools(feeTiers, (found, scanned) => {
|
|
183
|
+
if (!isHeadless()) {
|
|
184
|
+
spin.text = `Scanning pools... (${found} found, ${scanned} pairs scanned)`;
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
const cache = {
|
|
188
|
+
version: 1,
|
|
189
|
+
chainId: cfg.chain.chainId,
|
|
190
|
+
generatedAt: new Date().toISOString(),
|
|
191
|
+
pools: foundPools,
|
|
192
|
+
};
|
|
193
|
+
savePoolsCache(cache);
|
|
194
|
+
spin.succeed(`Found ${foundPools.length} pools`);
|
|
195
|
+
if (isHeadless()) {
|
|
196
|
+
writeJsonSuccess({
|
|
197
|
+
poolsFound: foundPools.length,
|
|
198
|
+
generatedAt: cache.generatedAt,
|
|
199
|
+
pools: foundPools,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
infoBox("Pool Cache Updated", `Found: ${colors.value(foundPools.length.toString())} pools\n` +
|
|
204
|
+
`Saved to: ${colors.muted(POOLS_CACHE_FILE)}`);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
pools
|
|
208
|
+
.command("for-token <token>")
|
|
209
|
+
.description("Find pools containing a specific token")
|
|
210
|
+
.action(async (token) => {
|
|
211
|
+
const userTokens = loadUserTokens();
|
|
212
|
+
const tokenAddr = resolveToken(token, userTokens.aliases);
|
|
213
|
+
const cache = loadPoolsCache();
|
|
214
|
+
if (!cache) {
|
|
215
|
+
throw new ImmError(ErrorCodes.NO_ROUTE_FOUND, "Pool cache is empty", "Run: imm jaine pools scan-core");
|
|
216
|
+
}
|
|
217
|
+
const matchingPools = findPoolsForToken(tokenAddr, cache);
|
|
218
|
+
if (isHeadless()) {
|
|
219
|
+
writeJsonSuccess({
|
|
220
|
+
token: tokenAddr,
|
|
221
|
+
pools: matchingPools,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
if (matchingPools.length === 0) {
|
|
226
|
+
infoBox("No Pools Found", `No pools found for ${getTokenSymbol(tokenAddr, userTokens.aliases)}`);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
const table = createTable([
|
|
230
|
+
{ header: "Pool", width: 45 },
|
|
231
|
+
{ header: "Token0", width: 12 },
|
|
232
|
+
{ header: "Token1", width: 12 },
|
|
233
|
+
{ header: "Fee", width: 8 },
|
|
234
|
+
]);
|
|
235
|
+
for (const pool of matchingPools) {
|
|
236
|
+
table.push([
|
|
237
|
+
pool.address,
|
|
238
|
+
getTokenSymbol(pool.token0, userTokens.aliases),
|
|
239
|
+
getTokenSymbol(pool.token1, userTokens.aliases),
|
|
240
|
+
`${(pool.fee / 10000).toFixed(2)}%`,
|
|
241
|
+
]);
|
|
242
|
+
}
|
|
243
|
+
writeStderr(table.toString());
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
pools
|
|
248
|
+
.command("find <tokenIn> <tokenOut>")
|
|
249
|
+
.description("Find pools between two tokens")
|
|
250
|
+
.option("--amount-in <amount>", "Amount in for quote")
|
|
251
|
+
.action(async (tokenIn, tokenOut, options) => {
|
|
252
|
+
const userTokens = loadUserTokens();
|
|
253
|
+
const tokenInAddr = resolveToken(tokenIn, userTokens.aliases);
|
|
254
|
+
const tokenOutAddr = resolveToken(tokenOut, userTokens.aliases);
|
|
255
|
+
const directPools = findPoolsBetweenTokens(tokenInAddr, tokenOutAddr);
|
|
256
|
+
if (options.amountIn) {
|
|
257
|
+
const decimals = await getTokenDecimals(tokenInAddr);
|
|
258
|
+
const amountIn = parseUnits(options.amountIn, decimals);
|
|
259
|
+
const spin = spinner("Finding best route...");
|
|
260
|
+
spin.start();
|
|
261
|
+
const bestRoute = await findBestRouteExactInput(tokenInAddr, tokenOutAddr, amountIn);
|
|
262
|
+
spin.succeed("Route found");
|
|
263
|
+
if (!bestRoute) {
|
|
264
|
+
throw new ImmError(ErrorCodes.NO_ROUTE_FOUND, "No route found for this swap");
|
|
265
|
+
}
|
|
266
|
+
const decimalsOut = await getTokenDecimals(tokenOutAddr);
|
|
267
|
+
if (isHeadless()) {
|
|
268
|
+
writeJsonSuccess({
|
|
269
|
+
tokenIn: tokenInAddr,
|
|
270
|
+
tokenOut: tokenOutAddr,
|
|
271
|
+
amountIn: amountIn.toString(),
|
|
272
|
+
amountOut: bestRoute.amountOut.toString(),
|
|
273
|
+
route: formatRoute(bestRoute, userTokens.aliases),
|
|
274
|
+
hops: bestRoute.tokens.length - 1,
|
|
275
|
+
directPools,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
infoBox("Best Route", `${colors.value(options.amountIn)} ${getTokenSymbol(tokenInAddr, userTokens.aliases)} → ` +
|
|
280
|
+
`${colors.value(formatUnits(bestRoute.amountOut, decimalsOut))} ${getTokenSymbol(tokenOutAddr, userTokens.aliases)}\n\n` +
|
|
281
|
+
`Route: ${formatRoute(bestRoute, userTokens.aliases)}\n` +
|
|
282
|
+
`Hops: ${bestRoute.tokens.length - 1}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
if (isHeadless()) {
|
|
287
|
+
writeJsonSuccess({
|
|
288
|
+
tokenIn: tokenInAddr,
|
|
289
|
+
tokenOut: tokenOutAddr,
|
|
290
|
+
directPools,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
if (directPools.length === 0) {
|
|
295
|
+
infoBox("No Direct Pools", "No direct pools found. Multi-hop routing may still work.");
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
const table = createTable([
|
|
299
|
+
{ header: "Pool", width: 45 },
|
|
300
|
+
{ header: "Fee", width: 10 },
|
|
301
|
+
]);
|
|
302
|
+
for (const pool of directPools) {
|
|
303
|
+
table.push([pool.address, `${(pool.fee / 10000).toFixed(2)}%`]);
|
|
304
|
+
}
|
|
305
|
+
writeStderr(table.toString());
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
jaine.addCommand(pools);
|
|
311
|
+
// ============ W0G SUBCOMMAND ============
|
|
312
|
+
const w0g = new Command("w0g")
|
|
313
|
+
.description("Wrap/unwrap native 0G to w0G")
|
|
314
|
+
.exitOverride();
|
|
315
|
+
w0g
|
|
316
|
+
.command("balance")
|
|
317
|
+
.description("Show native 0G and w0G balances")
|
|
318
|
+
.action(async () => {
|
|
319
|
+
const cfg = loadConfig();
|
|
320
|
+
if (!cfg.wallet.address) {
|
|
321
|
+
throw new ImmError(ErrorCodes.WALLET_NOT_CONFIGURED, "No wallet configured.");
|
|
322
|
+
}
|
|
323
|
+
const client = getPublicClient();
|
|
324
|
+
const w0gAddr = cfg.protocol.w0g;
|
|
325
|
+
const [nativeBalance, w0gBalance] = await Promise.all([
|
|
326
|
+
client.getBalance({ address: cfg.wallet.address }),
|
|
327
|
+
client.readContract({
|
|
328
|
+
address: w0gAddr,
|
|
329
|
+
abi: W0G_ABI,
|
|
330
|
+
functionName: "balanceOf",
|
|
331
|
+
args: [cfg.wallet.address],
|
|
332
|
+
}),
|
|
333
|
+
]);
|
|
334
|
+
if (isHeadless()) {
|
|
335
|
+
writeJsonSuccess({
|
|
336
|
+
native0G: nativeBalance.toString(),
|
|
337
|
+
w0G: w0gBalance.toString(),
|
|
338
|
+
formatted: {
|
|
339
|
+
native0G: formatUnits(nativeBalance, 18),
|
|
340
|
+
w0G: formatUnits(w0gBalance, 18),
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
infoBox("0G Balances", `Native 0G: ${colors.value(formatBalance(nativeBalance, 18))} 0G\n` +
|
|
346
|
+
`Wrapped w0G: ${colors.value(formatBalance(w0gBalance, 18))} w0G`);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
w0g
|
|
350
|
+
.command("wrap")
|
|
351
|
+
.description("Wrap native 0G to w0G")
|
|
352
|
+
.requiredOption("--amount <0G>", "Amount of native 0G to wrap")
|
|
353
|
+
.requiredOption("--yes", "Confirm the transaction")
|
|
354
|
+
.action(async (options) => {
|
|
355
|
+
if (!options.yes) {
|
|
356
|
+
throw new ImmError(ErrorCodes.CONFIRMATION_REQUIRED, "Add --yes to confirm");
|
|
357
|
+
}
|
|
358
|
+
const { privateKey } = requireWalletAndKeystore();
|
|
359
|
+
const cfg = loadConfig();
|
|
360
|
+
const amount = parseUnits(options.amount, 18);
|
|
361
|
+
if (amount <= 0n) {
|
|
362
|
+
throw new ImmError(ErrorCodes.INVALID_AMOUNT, "Amount must be greater than 0");
|
|
363
|
+
}
|
|
364
|
+
const spin = spinner("Wrapping 0G...");
|
|
365
|
+
spin.start();
|
|
366
|
+
const walletClient = createJaineWalletClient(privateKey);
|
|
367
|
+
try {
|
|
368
|
+
const txHash = await walletClient.writeContract({
|
|
369
|
+
address: cfg.protocol.w0g,
|
|
370
|
+
abi: W0G_ABI,
|
|
371
|
+
functionName: "deposit",
|
|
372
|
+
value: amount,
|
|
373
|
+
});
|
|
374
|
+
spin.succeed("0G wrapped successfully");
|
|
375
|
+
const explorerUrl = `${cfg.chain.explorerUrl}/tx/${txHash}`;
|
|
376
|
+
if (isHeadless()) {
|
|
377
|
+
writeJsonSuccess({
|
|
378
|
+
txHash,
|
|
379
|
+
explorerUrl,
|
|
380
|
+
amount: amount.toString(),
|
|
381
|
+
formatted: options.amount,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
successBox("0G Wrapped", `Amount: ${colors.value(options.amount)} 0G → w0G\n` +
|
|
386
|
+
`Tx: ${colors.info(txHash)}\n` +
|
|
387
|
+
`Explorer: ${colors.muted(explorerUrl)}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
catch (err) {
|
|
391
|
+
spin.fail("Wrap failed");
|
|
392
|
+
throw new ImmError(ErrorCodes.RPC_ERROR, `Wrap failed: ${err instanceof Error ? err.message : err}`);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
w0g
|
|
396
|
+
.command("unwrap")
|
|
397
|
+
.description("Unwrap w0G to native 0G")
|
|
398
|
+
.requiredOption("--amount <w0G>", "Amount of w0G to unwrap")
|
|
399
|
+
.requiredOption("--yes", "Confirm the transaction")
|
|
400
|
+
.action(async (options) => {
|
|
401
|
+
if (!options.yes) {
|
|
402
|
+
throw new ImmError(ErrorCodes.CONFIRMATION_REQUIRED, "Add --yes to confirm");
|
|
403
|
+
}
|
|
404
|
+
const { privateKey } = requireWalletAndKeystore();
|
|
405
|
+
const cfg = loadConfig();
|
|
406
|
+
const amount = parseUnits(options.amount, 18);
|
|
407
|
+
if (amount <= 0n) {
|
|
408
|
+
throw new ImmError(ErrorCodes.INVALID_AMOUNT, "Amount must be greater than 0");
|
|
409
|
+
}
|
|
410
|
+
const spin = spinner("Unwrapping w0G...");
|
|
411
|
+
spin.start();
|
|
412
|
+
const walletClient = createJaineWalletClient(privateKey);
|
|
413
|
+
try {
|
|
414
|
+
const txHash = await walletClient.writeContract({
|
|
415
|
+
address: cfg.protocol.w0g,
|
|
416
|
+
abi: W0G_ABI,
|
|
417
|
+
functionName: "withdraw",
|
|
418
|
+
args: [amount],
|
|
419
|
+
});
|
|
420
|
+
spin.succeed("w0G unwrapped successfully");
|
|
421
|
+
const explorerUrl = `${cfg.chain.explorerUrl}/tx/${txHash}`;
|
|
422
|
+
if (isHeadless()) {
|
|
423
|
+
writeJsonSuccess({
|
|
424
|
+
txHash,
|
|
425
|
+
explorerUrl,
|
|
426
|
+
amount: amount.toString(),
|
|
427
|
+
formatted: options.amount,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
successBox("w0G Unwrapped", `Amount: ${colors.value(options.amount)} w0G → 0G\n` +
|
|
432
|
+
`Tx: ${colors.info(txHash)}\n` +
|
|
433
|
+
`Explorer: ${colors.muted(explorerUrl)}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
catch (err) {
|
|
437
|
+
spin.fail("Unwrap failed");
|
|
438
|
+
throw new ImmError(ErrorCodes.RPC_ERROR, `Unwrap failed: ${err instanceof Error ? err.message : err}`);
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
jaine.addCommand(w0g);
|
|
442
|
+
// ============ ALLOWANCE SUBCOMMAND ============
|
|
443
|
+
const allowance = new Command("allowance")
|
|
444
|
+
.description("Manage token approvals for Jaine contracts")
|
|
445
|
+
.exitOverride();
|
|
446
|
+
allowance
|
|
447
|
+
.command("show <token>")
|
|
448
|
+
.description("Show current allowances for a token")
|
|
449
|
+
.option("--spender <type>", "Spender type (router|nft)", "router")
|
|
450
|
+
.action(async (token, options) => {
|
|
451
|
+
const cfg = loadConfig();
|
|
452
|
+
if (!cfg.wallet.address) {
|
|
453
|
+
throw new ImmError(ErrorCodes.WALLET_NOT_CONFIGURED, "No wallet configured.");
|
|
454
|
+
}
|
|
455
|
+
const userTokens = loadUserTokens();
|
|
456
|
+
const tokenAddr = resolveToken(token, userTokens.aliases);
|
|
457
|
+
const allowances = await getAllAllowances(tokenAddr, cfg.wallet.address);
|
|
458
|
+
const decimals = await getTokenDecimals(tokenAddr);
|
|
459
|
+
const symbol = await getTokenSymbolOnChain(tokenAddr);
|
|
460
|
+
if (isHeadless()) {
|
|
461
|
+
writeJsonSuccess({
|
|
462
|
+
token: tokenAddr,
|
|
463
|
+
symbol,
|
|
464
|
+
allowances: {
|
|
465
|
+
router: allowances.router.toString(),
|
|
466
|
+
nft: allowances.nft.toString(),
|
|
467
|
+
},
|
|
468
|
+
formatted: {
|
|
469
|
+
router: allowances.router === maxUint256 ? "unlimited" : formatUnits(allowances.router, decimals),
|
|
470
|
+
nft: allowances.nft === maxUint256 ? "unlimited" : formatUnits(allowances.nft, decimals),
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
const formatAllowance = (val) => val === maxUint256 ? colors.success("unlimited") : colors.value(formatUnits(val, decimals));
|
|
476
|
+
infoBox(`Allowances for ${symbol}`, `Router: ${formatAllowance(allowances.router)}\n` + `NFT Manager: ${formatAllowance(allowances.nft)}`);
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
allowance
|
|
480
|
+
.command("revoke <token>")
|
|
481
|
+
.description("Revoke approval for a token")
|
|
482
|
+
.option("--spender <type>", "Spender type (router|nft)", "router")
|
|
483
|
+
.requiredOption("--yes", "Confirm the transaction")
|
|
484
|
+
.action(async (token, options) => {
|
|
485
|
+
if (!options.yes) {
|
|
486
|
+
throw new ImmError(ErrorCodes.CONFIRMATION_REQUIRED, "Add --yes to confirm");
|
|
487
|
+
}
|
|
488
|
+
const { privateKey } = requireWalletAndKeystore();
|
|
489
|
+
const userTokens = loadUserTokens();
|
|
490
|
+
const tokenAddr = resolveToken(token, userTokens.aliases);
|
|
491
|
+
const spenderAddr = getSpenderAddress(options.spender);
|
|
492
|
+
const spin = spinner("Revoking approval...");
|
|
493
|
+
spin.start();
|
|
494
|
+
const txHash = await revokeApproval(tokenAddr, spenderAddr, privateKey);
|
|
495
|
+
spin.succeed("Approval revoked");
|
|
496
|
+
const cfg = loadConfig();
|
|
497
|
+
const explorerUrl = `${cfg.chain.explorerUrl}/tx/${txHash}`;
|
|
498
|
+
if (isHeadless()) {
|
|
499
|
+
writeJsonSuccess({ txHash, explorerUrl, token: tokenAddr, spender: spenderAddr });
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
successBox("Approval Revoked", `Token: ${colors.address(tokenAddr)}\n` +
|
|
503
|
+
`Spender: ${options.spender}\n` +
|
|
504
|
+
`Tx: ${colors.info(txHash)}`);
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
jaine.addCommand(allowance);
|
|
508
|
+
// ============ SWAP SUBCOMMAND ============
|
|
509
|
+
const swap = new Command("swap")
|
|
510
|
+
.description("Swap tokens on Jaine DEX")
|
|
511
|
+
.exitOverride();
|
|
512
|
+
swap
|
|
513
|
+
.command("sell <tokenIn> <tokenOut>")
|
|
514
|
+
.description("Sell exact amount of tokenIn for tokenOut")
|
|
515
|
+
.requiredOption("--amount-in <amount>", "Amount of tokenIn to sell")
|
|
516
|
+
.option("--slippage-bps <bps>", "Slippage tolerance in basis points", "50")
|
|
517
|
+
.option("--deadline-sec <sec>", "Transaction deadline in seconds", "90")
|
|
518
|
+
.option("--recipient <address>", "Recipient address (defaults to wallet)")
|
|
519
|
+
.option("--max-hops <n>", "Maximum routing hops", "3")
|
|
520
|
+
.option("--approve-exact", "Approve exact amount instead of unlimited")
|
|
521
|
+
.option("--dry-run", "Show quote without executing")
|
|
522
|
+
.option("--yes", "Confirm the transaction")
|
|
523
|
+
.action(async (tokenIn, tokenOut, options) => {
|
|
524
|
+
const userTokens = loadUserTokens();
|
|
525
|
+
const tokenInAddr = resolveToken(tokenIn, userTokens.aliases);
|
|
526
|
+
const tokenOutAddr = resolveToken(tokenOut, userTokens.aliases);
|
|
527
|
+
const slippageBps = validateSlippage(parseIntSafe(options.slippageBps, "slippageBps"));
|
|
528
|
+
const maxHops = Math.min(Math.max(parseIntSafe(options.maxHops, "maxHops"), 1), 4);
|
|
529
|
+
const deadlineSec = parseIntSafe(options.deadlineSec, "deadlineSec");
|
|
530
|
+
const decimalsIn = await getTokenDecimals(tokenInAddr);
|
|
531
|
+
const decimalsOut = await getTokenDecimals(tokenOutAddr);
|
|
532
|
+
const amountIn = parseUnits(options.amountIn, decimalsIn);
|
|
533
|
+
// Find best route
|
|
534
|
+
const spin = spinner("Finding best route...");
|
|
535
|
+
spin.start();
|
|
536
|
+
const route = await findBestRouteExactInput(tokenInAddr, tokenOutAddr, amountIn, {
|
|
537
|
+
maxHops,
|
|
538
|
+
});
|
|
539
|
+
if (!route) {
|
|
540
|
+
spin.fail("No route found");
|
|
541
|
+
throw new ImmError(ErrorCodes.NO_ROUTE_FOUND, "No route found for this swap");
|
|
542
|
+
}
|
|
543
|
+
spin.succeed("Route found");
|
|
544
|
+
// Calculate minimum output with slippage
|
|
545
|
+
const amountOutMinimum = (route.amountOut * BigInt(10000 - slippageBps)) / 10000n;
|
|
546
|
+
const routeStr = formatRoute(route, userTokens.aliases);
|
|
547
|
+
// Dry run output
|
|
548
|
+
if (options.dryRun) {
|
|
549
|
+
if (isHeadless()) {
|
|
550
|
+
writeJsonSuccess({
|
|
551
|
+
dryRun: true,
|
|
552
|
+
tokenIn: tokenInAddr,
|
|
553
|
+
tokenOut: tokenOutAddr,
|
|
554
|
+
amountIn: amountIn.toString(),
|
|
555
|
+
amountOut: route.amountOut.toString(),
|
|
556
|
+
amountOutMinimum: amountOutMinimum.toString(),
|
|
557
|
+
route: routeStr,
|
|
558
|
+
hops: route.tokens.length - 1,
|
|
559
|
+
slippageBps,
|
|
560
|
+
formatted: {
|
|
561
|
+
amountIn: options.amountIn,
|
|
562
|
+
amountOut: formatUnits(route.amountOut, decimalsOut),
|
|
563
|
+
amountOutMinimum: formatUnits(amountOutMinimum, decimalsOut),
|
|
564
|
+
},
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
infoBox("Swap Quote (Dry Run)", `Sell: ${colors.value(options.amountIn)} ${getTokenSymbol(tokenInAddr, userTokens.aliases)}\n` +
|
|
569
|
+
`Receive: ~${colors.value(formatUnits(route.amountOut, decimalsOut))} ${getTokenSymbol(tokenOutAddr, userTokens.aliases)}\n` +
|
|
570
|
+
`Min receive: ${colors.value(formatUnits(amountOutMinimum, decimalsOut))}\n` +
|
|
571
|
+
`Route: ${routeStr}\n` +
|
|
572
|
+
`Slippage: ${(slippageBps / 100).toFixed(2)}%`);
|
|
573
|
+
}
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
// Require --yes for actual execution
|
|
577
|
+
if (!options.yes) {
|
|
578
|
+
throw new ImmError(ErrorCodes.CONFIRMATION_REQUIRED, "Add --yes to confirm (or --dry-run to preview)");
|
|
579
|
+
}
|
|
580
|
+
const { address, privateKey } = requireWalletAndKeystore();
|
|
581
|
+
const cfg = loadConfig();
|
|
582
|
+
let recipient = address;
|
|
583
|
+
if (options.recipient) {
|
|
584
|
+
if (!isAddress(options.recipient)) {
|
|
585
|
+
throw new ImmError(ErrorCodes.INVALID_ADDRESS, `Invalid recipient: ${options.recipient}`);
|
|
586
|
+
}
|
|
587
|
+
recipient = getAddress(options.recipient);
|
|
588
|
+
}
|
|
589
|
+
// Check and approve if needed
|
|
590
|
+
const spinApprove = spinner("Checking allowance...");
|
|
591
|
+
spinApprove.start();
|
|
592
|
+
const approvalResult = await ensureAllowance(tokenInAddr, cfg.protocol.jaineRouter, amountIn, privateKey, options.approveExact);
|
|
593
|
+
if (approvalResult && approvalResult.txHash !== "0x0") {
|
|
594
|
+
spinApprove.succeed("Token approved");
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
spinApprove.succeed("Allowance sufficient");
|
|
598
|
+
}
|
|
599
|
+
// Execute swap
|
|
600
|
+
const spinSwap = spinner("Executing swap...");
|
|
601
|
+
spinSwap.start();
|
|
602
|
+
const walletClient = createJaineWalletClient(privateKey);
|
|
603
|
+
const deadline = BigInt(Math.floor(Date.now() / 1000) + deadlineSec);
|
|
604
|
+
try {
|
|
605
|
+
const txHash = await walletClient.writeContract({
|
|
606
|
+
address: cfg.protocol.jaineRouter,
|
|
607
|
+
abi: ROUTER_ABI,
|
|
608
|
+
functionName: "exactInput",
|
|
609
|
+
args: [
|
|
610
|
+
{
|
|
611
|
+
path: route.encodedPath,
|
|
612
|
+
recipient,
|
|
613
|
+
deadline,
|
|
614
|
+
amountIn,
|
|
615
|
+
amountOutMinimum,
|
|
616
|
+
},
|
|
617
|
+
],
|
|
618
|
+
});
|
|
619
|
+
spinSwap.succeed("Swap executed");
|
|
620
|
+
const explorerUrl = `${cfg.chain.explorerUrl}/tx/${txHash}`;
|
|
621
|
+
if (isHeadless()) {
|
|
622
|
+
writeJsonSuccess({
|
|
623
|
+
txHash,
|
|
624
|
+
explorerUrl,
|
|
625
|
+
tokenIn: tokenInAddr,
|
|
626
|
+
tokenOut: tokenOutAddr,
|
|
627
|
+
amountIn: amountIn.toString(),
|
|
628
|
+
amountOutExpected: route.amountOut.toString(),
|
|
629
|
+
amountOutMinimum: amountOutMinimum.toString(),
|
|
630
|
+
route: routeStr,
|
|
631
|
+
recipient,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
successBox("Swap Executed", `Sold: ${colors.value(options.amountIn)} ${getTokenSymbol(tokenInAddr, userTokens.aliases)}\n` +
|
|
636
|
+
`Expected: ~${colors.value(formatUnits(route.amountOut, decimalsOut))} ${getTokenSymbol(tokenOutAddr, userTokens.aliases)}\n` +
|
|
637
|
+
`Route: ${routeStr}\n` +
|
|
638
|
+
`Tx: ${colors.info(txHash)}\n` +
|
|
639
|
+
`Explorer: ${colors.muted(explorerUrl)}`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
catch (err) {
|
|
643
|
+
spinSwap.fail("Swap failed");
|
|
644
|
+
throw new ImmError(ErrorCodes.SWAP_FAILED, `Swap failed: ${err instanceof Error ? err.message : err}`);
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
swap
|
|
648
|
+
.command("buy <tokenIn> <tokenOut>")
|
|
649
|
+
.description("Buy exact amount of tokenOut using tokenIn")
|
|
650
|
+
.requiredOption("--amount-out <amount>", "Amount of tokenOut to buy")
|
|
651
|
+
.option("--slippage-bps <bps>", "Slippage tolerance in basis points", "50")
|
|
652
|
+
.option("--deadline-sec <sec>", "Transaction deadline in seconds", "90")
|
|
653
|
+
.option("--recipient <address>", "Recipient address (defaults to wallet)")
|
|
654
|
+
.option("--max-hops <n>", "Maximum routing hops", "3")
|
|
655
|
+
.option("--approve-exact", "Approve exact amount instead of unlimited")
|
|
656
|
+
.option("--dry-run", "Show quote without executing")
|
|
657
|
+
.option("--yes", "Confirm the transaction")
|
|
658
|
+
.action(async (tokenIn, tokenOut, options) => {
|
|
659
|
+
const userTokens = loadUserTokens();
|
|
660
|
+
const tokenInAddr = resolveToken(tokenIn, userTokens.aliases);
|
|
661
|
+
const tokenOutAddr = resolveToken(tokenOut, userTokens.aliases);
|
|
662
|
+
const slippageBps = validateSlippage(parseIntSafe(options.slippageBps, "slippageBps"));
|
|
663
|
+
const maxHops = Math.min(Math.max(parseIntSafe(options.maxHops, "maxHops"), 1), 4);
|
|
664
|
+
const deadlineSec = parseIntSafe(options.deadlineSec, "deadlineSec");
|
|
665
|
+
const decimalsIn = await getTokenDecimals(tokenInAddr);
|
|
666
|
+
const decimalsOut = await getTokenDecimals(tokenOutAddr);
|
|
667
|
+
const amountOut = parseUnits(options.amountOut, decimalsOut);
|
|
668
|
+
// Find best route
|
|
669
|
+
const spin = spinner("Finding best route...");
|
|
670
|
+
spin.start();
|
|
671
|
+
const route = await findBestRouteExactOutput(tokenInAddr, tokenOutAddr, amountOut, {
|
|
672
|
+
maxHops,
|
|
673
|
+
});
|
|
674
|
+
if (!route) {
|
|
675
|
+
spin.fail("No route found");
|
|
676
|
+
throw new ImmError(ErrorCodes.NO_ROUTE_FOUND, "No route found for this swap");
|
|
677
|
+
}
|
|
678
|
+
spin.succeed("Route found");
|
|
679
|
+
// Calculate maximum input with slippage
|
|
680
|
+
const amountInMaximum = (route.amountIn * BigInt(10000 + slippageBps)) / 10000n;
|
|
681
|
+
const routeStr = formatRoute(route, userTokens.aliases);
|
|
682
|
+
// Dry run output
|
|
683
|
+
if (options.dryRun) {
|
|
684
|
+
if (isHeadless()) {
|
|
685
|
+
writeJsonSuccess({
|
|
686
|
+
dryRun: true,
|
|
687
|
+
tokenIn: tokenInAddr,
|
|
688
|
+
tokenOut: tokenOutAddr,
|
|
689
|
+
amountOut: amountOut.toString(),
|
|
690
|
+
amountIn: route.amountIn.toString(),
|
|
691
|
+
amountInMaximum: amountInMaximum.toString(),
|
|
692
|
+
route: routeStr,
|
|
693
|
+
hops: route.tokens.length - 1,
|
|
694
|
+
slippageBps,
|
|
695
|
+
formatted: {
|
|
696
|
+
amountOut: options.amountOut,
|
|
697
|
+
amountIn: formatUnits(route.amountIn, decimalsIn),
|
|
698
|
+
amountInMaximum: formatUnits(amountInMaximum, decimalsIn),
|
|
699
|
+
},
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
infoBox("Swap Quote (Dry Run)", `Buy: ${colors.value(options.amountOut)} ${getTokenSymbol(tokenOutAddr, userTokens.aliases)}\n` +
|
|
704
|
+
`Cost: ~${colors.value(formatUnits(route.amountIn, decimalsIn))} ${getTokenSymbol(tokenInAddr, userTokens.aliases)}\n` +
|
|
705
|
+
`Max cost: ${colors.value(formatUnits(amountInMaximum, decimalsIn))}\n` +
|
|
706
|
+
`Route: ${routeStr}\n` +
|
|
707
|
+
`Slippage: ${(slippageBps / 100).toFixed(2)}%`);
|
|
708
|
+
}
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
// Require --yes for actual execution
|
|
712
|
+
if (!options.yes) {
|
|
713
|
+
throw new ImmError(ErrorCodes.CONFIRMATION_REQUIRED, "Add --yes to confirm (or --dry-run to preview)");
|
|
714
|
+
}
|
|
715
|
+
const { address, privateKey } = requireWalletAndKeystore();
|
|
716
|
+
const cfg = loadConfig();
|
|
717
|
+
let recipient = address;
|
|
718
|
+
if (options.recipient) {
|
|
719
|
+
if (!isAddress(options.recipient)) {
|
|
720
|
+
throw new ImmError(ErrorCodes.INVALID_ADDRESS, `Invalid recipient: ${options.recipient}`);
|
|
721
|
+
}
|
|
722
|
+
recipient = getAddress(options.recipient);
|
|
723
|
+
}
|
|
724
|
+
// Check and approve if needed
|
|
725
|
+
const spinApprove = spinner("Checking allowance...");
|
|
726
|
+
spinApprove.start();
|
|
727
|
+
const approvalResult = await ensureAllowance(tokenInAddr, cfg.protocol.jaineRouter, amountInMaximum, privateKey, options.approveExact);
|
|
728
|
+
if (approvalResult && approvalResult.txHash !== "0x0") {
|
|
729
|
+
spinApprove.succeed("Token approved");
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
spinApprove.succeed("Allowance sufficient");
|
|
733
|
+
}
|
|
734
|
+
// Execute swap
|
|
735
|
+
const spinSwap = spinner("Executing swap...");
|
|
736
|
+
spinSwap.start();
|
|
737
|
+
const walletClient = createJaineWalletClient(privateKey);
|
|
738
|
+
const deadline = BigInt(Math.floor(Date.now() / 1000) + deadlineSec);
|
|
739
|
+
try {
|
|
740
|
+
const txHash = await walletClient.writeContract({
|
|
741
|
+
address: cfg.protocol.jaineRouter,
|
|
742
|
+
abi: ROUTER_ABI,
|
|
743
|
+
functionName: "exactOutput",
|
|
744
|
+
args: [
|
|
745
|
+
{
|
|
746
|
+
path: route.encodedPath,
|
|
747
|
+
recipient,
|
|
748
|
+
deadline,
|
|
749
|
+
amountOut,
|
|
750
|
+
amountInMaximum,
|
|
751
|
+
},
|
|
752
|
+
],
|
|
753
|
+
});
|
|
754
|
+
spinSwap.succeed("Swap executed");
|
|
755
|
+
const explorerUrl = `${cfg.chain.explorerUrl}/tx/${txHash}`;
|
|
756
|
+
if (isHeadless()) {
|
|
757
|
+
writeJsonSuccess({
|
|
758
|
+
txHash,
|
|
759
|
+
explorerUrl,
|
|
760
|
+
tokenIn: tokenInAddr,
|
|
761
|
+
tokenOut: tokenOutAddr,
|
|
762
|
+
amountOut: amountOut.toString(),
|
|
763
|
+
amountInExpected: route.amountIn.toString(),
|
|
764
|
+
amountInMaximum: amountInMaximum.toString(),
|
|
765
|
+
route: routeStr,
|
|
766
|
+
recipient,
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
else {
|
|
770
|
+
successBox("Swap Executed", `Bought: ${colors.value(options.amountOut)} ${getTokenSymbol(tokenOutAddr, userTokens.aliases)}\n` +
|
|
771
|
+
`Expected cost: ~${colors.value(formatUnits(route.amountIn, decimalsIn))} ${getTokenSymbol(tokenInAddr, userTokens.aliases)}\n` +
|
|
772
|
+
`Route: ${routeStr}\n` +
|
|
773
|
+
`Tx: ${colors.info(txHash)}\n` +
|
|
774
|
+
`Explorer: ${colors.muted(explorerUrl)}`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
catch (err) {
|
|
778
|
+
spinSwap.fail("Swap failed");
|
|
779
|
+
throw new ImmError(ErrorCodes.SWAP_FAILED, `Swap failed: ${err instanceof Error ? err.message : err}`);
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
jaine.addCommand(swap);
|
|
783
|
+
// ============ LP SUBCOMMAND ============
|
|
784
|
+
const lp = new Command("lp")
|
|
785
|
+
.description("Liquidity position management")
|
|
786
|
+
.exitOverride();
|
|
787
|
+
lp.command("list")
|
|
788
|
+
.description("List your LP positions")
|
|
789
|
+
.action(async () => {
|
|
790
|
+
const cfg = loadConfig();
|
|
791
|
+
if (!cfg.wallet.address) {
|
|
792
|
+
throw new ImmError(ErrorCodes.WALLET_NOT_CONFIGURED, "No wallet configured.");
|
|
793
|
+
}
|
|
794
|
+
const client = getPublicClient();
|
|
795
|
+
const nftManager = cfg.protocol.nftPositionManager;
|
|
796
|
+
const spin = spinner("Fetching positions...");
|
|
797
|
+
spin.start();
|
|
798
|
+
const balance = await client.readContract({
|
|
799
|
+
address: nftManager,
|
|
800
|
+
abi: NFT_MANAGER_ABI,
|
|
801
|
+
functionName: "balanceOf",
|
|
802
|
+
args: [cfg.wallet.address],
|
|
803
|
+
});
|
|
804
|
+
if (balance === 0n) {
|
|
805
|
+
spin.succeed("No positions found");
|
|
806
|
+
if (isHeadless()) {
|
|
807
|
+
writeJsonSuccess({ positions: [] });
|
|
808
|
+
}
|
|
809
|
+
else {
|
|
810
|
+
infoBox("LP Positions", "You have no LP positions.");
|
|
811
|
+
}
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
// Fetch all token IDs
|
|
815
|
+
const tokenIds = [];
|
|
816
|
+
for (let i = 0n; i < balance; i++) {
|
|
817
|
+
const tokenId = await client.readContract({
|
|
818
|
+
address: nftManager,
|
|
819
|
+
abi: NFT_MANAGER_ABI,
|
|
820
|
+
functionName: "tokenOfOwnerByIndex",
|
|
821
|
+
args: [cfg.wallet.address, i],
|
|
822
|
+
});
|
|
823
|
+
tokenIds.push(tokenId);
|
|
824
|
+
}
|
|
825
|
+
// Fetch position details
|
|
826
|
+
const userTokens = loadUserTokens();
|
|
827
|
+
const positions = [];
|
|
828
|
+
for (const tokenId of tokenIds) {
|
|
829
|
+
const position = await client.readContract({
|
|
830
|
+
address: nftManager,
|
|
831
|
+
abi: NFT_MANAGER_ABI,
|
|
832
|
+
functionName: "positions",
|
|
833
|
+
args: [tokenId],
|
|
834
|
+
});
|
|
835
|
+
const [, , token0, token1, fee, tickLower, tickUpper, liquidity, , , tokensOwed0, tokensOwed1,] = position;
|
|
836
|
+
positions.push({
|
|
837
|
+
tokenId: tokenId.toString(),
|
|
838
|
+
token0,
|
|
839
|
+
token1,
|
|
840
|
+
fee,
|
|
841
|
+
tickLower,
|
|
842
|
+
tickUpper,
|
|
843
|
+
liquidity: liquidity.toString(),
|
|
844
|
+
tokensOwed0: tokensOwed0.toString(),
|
|
845
|
+
tokensOwed1: tokensOwed1.toString(),
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
spin.succeed(`Found ${positions.length} positions`);
|
|
849
|
+
if (isHeadless()) {
|
|
850
|
+
writeJsonSuccess({ positions });
|
|
851
|
+
}
|
|
852
|
+
else {
|
|
853
|
+
const table = createTable([
|
|
854
|
+
{ header: "ID", width: 8 },
|
|
855
|
+
{ header: "Pair", width: 20 },
|
|
856
|
+
{ header: "Fee", width: 8 },
|
|
857
|
+
{ header: "Liquidity", width: 20 },
|
|
858
|
+
]);
|
|
859
|
+
for (const pos of positions) {
|
|
860
|
+
const symbol0 = getTokenSymbol(pos.token0, userTokens.aliases);
|
|
861
|
+
const symbol1 = getTokenSymbol(pos.token1, userTokens.aliases);
|
|
862
|
+
table.push([
|
|
863
|
+
pos.tokenId,
|
|
864
|
+
`${symbol0}/${symbol1}`,
|
|
865
|
+
`${(pos.fee / 10000).toFixed(2)}%`,
|
|
866
|
+
pos.liquidity === "0" ? colors.muted("0") : pos.liquidity,
|
|
867
|
+
]);
|
|
868
|
+
}
|
|
869
|
+
writeStderr(table.toString());
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
lp.command("show <tokenId>")
|
|
873
|
+
.description("Show details of a specific LP position")
|
|
874
|
+
.action(async (tokenId) => {
|
|
875
|
+
const cfg = loadConfig();
|
|
876
|
+
const client = getPublicClient();
|
|
877
|
+
const nftManager = cfg.protocol.nftPositionManager;
|
|
878
|
+
const spin = spinner("Fetching position...");
|
|
879
|
+
spin.start();
|
|
880
|
+
try {
|
|
881
|
+
const position = await client.readContract({
|
|
882
|
+
address: nftManager,
|
|
883
|
+
abi: NFT_MANAGER_ABI,
|
|
884
|
+
functionName: "positions",
|
|
885
|
+
args: [BigInt(tokenId)],
|
|
886
|
+
});
|
|
887
|
+
const [nonce, operator, token0, token1, fee, tickLower, tickUpper, liquidity, feeGrowthInside0LastX128, feeGrowthInside1LastX128, tokensOwed0, tokensOwed1,] = position;
|
|
888
|
+
spin.succeed("Position loaded");
|
|
889
|
+
const userTokens = loadUserTokens();
|
|
890
|
+
const [decimals0, decimals1] = await Promise.all([
|
|
891
|
+
getTokenDecimals(token0),
|
|
892
|
+
getTokenDecimals(token1),
|
|
893
|
+
]);
|
|
894
|
+
const symbol0 = getTokenSymbol(token0, userTokens.aliases);
|
|
895
|
+
const symbol1 = getTokenSymbol(token1, userTokens.aliases);
|
|
896
|
+
if (isHeadless()) {
|
|
897
|
+
writeJsonSuccess({
|
|
898
|
+
tokenId,
|
|
899
|
+
token0,
|
|
900
|
+
token1,
|
|
901
|
+
fee,
|
|
902
|
+
tickLower,
|
|
903
|
+
tickUpper,
|
|
904
|
+
liquidity: liquidity.toString(),
|
|
905
|
+
tokensOwed0: tokensOwed0.toString(),
|
|
906
|
+
tokensOwed1: tokensOwed1.toString(),
|
|
907
|
+
formatted: {
|
|
908
|
+
pair: `${symbol0}/${symbol1}`,
|
|
909
|
+
fee: `${(fee / 10000).toFixed(2)}%`,
|
|
910
|
+
tokensOwed0: formatUnits(tokensOwed0, decimals0),
|
|
911
|
+
tokensOwed1: formatUnits(tokensOwed1, decimals1),
|
|
912
|
+
},
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
else {
|
|
916
|
+
infoBox(`Position #${tokenId}`, `Pair: ${colors.info(`${symbol0}/${symbol1}`)}\n` +
|
|
917
|
+
`Fee: ${(fee / 10000).toFixed(2)}%\n` +
|
|
918
|
+
`Tick Range: ${tickLower} → ${tickUpper}\n` +
|
|
919
|
+
`Liquidity: ${liquidity.toString()}\n` +
|
|
920
|
+
`\nUncollected Fees:\n` +
|
|
921
|
+
` ${symbol0}: ${colors.value(formatUnits(tokensOwed0, decimals0))}\n` +
|
|
922
|
+
` ${symbol1}: ${colors.value(formatUnits(tokensOwed1, decimals1))}`);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
catch (err) {
|
|
926
|
+
spin.fail("Failed to fetch position");
|
|
927
|
+
throw new ImmError(ErrorCodes.POSITION_NOT_FOUND, `Position not found: ${tokenId}`);
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
lp.command("collect <tokenId>")
|
|
931
|
+
.description("Collect fees from LP position")
|
|
932
|
+
.option("--recipient <address>", "Recipient address")
|
|
933
|
+
.requiredOption("--yes", "Confirm the transaction")
|
|
934
|
+
.action(async (tokenId, options) => {
|
|
935
|
+
if (!options.yes) {
|
|
936
|
+
throw new ImmError(ErrorCodes.CONFIRMATION_REQUIRED, "Add --yes to confirm");
|
|
937
|
+
}
|
|
938
|
+
const { address, privateKey } = requireWalletAndKeystore();
|
|
939
|
+
const cfg = loadConfig();
|
|
940
|
+
const recipient = options.recipient ? getAddress(options.recipient) : address;
|
|
941
|
+
const spin = spinner("Collecting fees...");
|
|
942
|
+
spin.start();
|
|
943
|
+
const walletClient = createJaineWalletClient(privateKey);
|
|
944
|
+
try {
|
|
945
|
+
const txHash = await walletClient.writeContract({
|
|
946
|
+
address: cfg.protocol.nftPositionManager,
|
|
947
|
+
abi: NFT_MANAGER_ABI,
|
|
948
|
+
functionName: "collect",
|
|
949
|
+
args: [
|
|
950
|
+
{
|
|
951
|
+
tokenId: BigInt(tokenId),
|
|
952
|
+
recipient,
|
|
953
|
+
amount0Max: BigInt("0xffffffffffffffffffffffffffffffff"), // uint128 max
|
|
954
|
+
amount1Max: BigInt("0xffffffffffffffffffffffffffffffff"),
|
|
955
|
+
},
|
|
956
|
+
],
|
|
957
|
+
});
|
|
958
|
+
spin.succeed("Fees collected");
|
|
959
|
+
const explorerUrl = `${cfg.chain.explorerUrl}/tx/${txHash}`;
|
|
960
|
+
if (isHeadless()) {
|
|
961
|
+
writeJsonSuccess({ txHash, explorerUrl, tokenId, recipient });
|
|
962
|
+
}
|
|
963
|
+
else {
|
|
964
|
+
successBox("Fees Collected", `Position: #${tokenId}\n` +
|
|
965
|
+
`Recipient: ${colors.address(recipient)}\n` +
|
|
966
|
+
`Tx: ${colors.info(txHash)}`);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
catch (err) {
|
|
970
|
+
spin.fail("Collection failed");
|
|
971
|
+
throw new ImmError(ErrorCodes.LP_OPERATION_FAILED, `Failed to collect: ${err instanceof Error ? err.message : err}`);
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
lp.command("remove <tokenId>")
|
|
975
|
+
.description("Remove liquidity from position")
|
|
976
|
+
.requiredOption("--percent <n>", "Percentage of liquidity to remove (1-100)")
|
|
977
|
+
.option("--burn", "Burn the NFT after removing all liquidity")
|
|
978
|
+
.option("--slippage-bps <bps>", "Slippage tolerance", "50")
|
|
979
|
+
.requiredOption("--yes", "Confirm the transaction")
|
|
980
|
+
.action(async (tokenId, options) => {
|
|
981
|
+
if (!options.yes) {
|
|
982
|
+
throw new ImmError(ErrorCodes.CONFIRMATION_REQUIRED, "Add --yes to confirm");
|
|
983
|
+
}
|
|
984
|
+
const percent = parseIntSafe(options.percent, "percent");
|
|
985
|
+
if (percent < 1 || percent > 100) {
|
|
986
|
+
throw new ImmError(ErrorCodes.INVALID_AMOUNT, "Percent must be between 1 and 100");
|
|
987
|
+
}
|
|
988
|
+
const slippageBps = validateSlippage(parseIntSafe(options.slippageBps, "slippageBps"));
|
|
989
|
+
const { address, privateKey } = requireWalletAndKeystore();
|
|
990
|
+
const cfg = loadConfig();
|
|
991
|
+
const client = getPublicClient();
|
|
992
|
+
// Fetch position to get liquidity
|
|
993
|
+
const spin = spinner("Fetching position...");
|
|
994
|
+
spin.start();
|
|
995
|
+
const position = await client.readContract({
|
|
996
|
+
address: cfg.protocol.nftPositionManager,
|
|
997
|
+
abi: NFT_MANAGER_ABI,
|
|
998
|
+
functionName: "positions",
|
|
999
|
+
args: [BigInt(tokenId)],
|
|
1000
|
+
});
|
|
1001
|
+
const [, , token0, token1, , , , liquidity] = position;
|
|
1002
|
+
// Allow operation even with 0 liquidity - user may want to collect fees and/or burn
|
|
1003
|
+
if (liquidity === 0n && !options.burn) {
|
|
1004
|
+
spin.fail("Position has no liquidity");
|
|
1005
|
+
throw new ImmError(ErrorCodes.LP_OPERATION_FAILED, "Position has no liquidity to remove", "Use --burn to collect any remaining fees and burn the NFT");
|
|
1006
|
+
}
|
|
1007
|
+
const liquidityToRemove = (liquidity * BigInt(percent)) / 100n;
|
|
1008
|
+
spin.text = "Removing liquidity...";
|
|
1009
|
+
const walletClient = createJaineWalletClient(privateKey);
|
|
1010
|
+
const deadline = BigInt(Math.floor(Date.now() / 1000) + 90);
|
|
1011
|
+
// Calculate minimum amounts with slippage (simplified - 0 for now)
|
|
1012
|
+
const amount0Min = 0n;
|
|
1013
|
+
const amount1Min = 0n;
|
|
1014
|
+
try {
|
|
1015
|
+
const MAX_UINT128 = (2n ** 128n) - 1n;
|
|
1016
|
+
const calls = [];
|
|
1017
|
+
// 1) decreaseLiquidity (only if there's liquidity to remove)
|
|
1018
|
+
if (liquidityToRemove > 0n) {
|
|
1019
|
+
calls.push(encodeFunctionData({
|
|
1020
|
+
abi: NFT_MANAGER_ABI,
|
|
1021
|
+
functionName: "decreaseLiquidity",
|
|
1022
|
+
args: [{
|
|
1023
|
+
tokenId: BigInt(tokenId),
|
|
1024
|
+
liquidity: liquidityToRemove,
|
|
1025
|
+
amount0Min,
|
|
1026
|
+
amount1Min,
|
|
1027
|
+
deadline,
|
|
1028
|
+
}],
|
|
1029
|
+
}));
|
|
1030
|
+
}
|
|
1031
|
+
// 2) collect (always - clears tokensOwed)
|
|
1032
|
+
calls.push(encodeFunctionData({
|
|
1033
|
+
abi: NFT_MANAGER_ABI,
|
|
1034
|
+
functionName: "collect",
|
|
1035
|
+
args: [{
|
|
1036
|
+
tokenId: BigInt(tokenId),
|
|
1037
|
+
recipient: address,
|
|
1038
|
+
amount0Max: MAX_UINT128,
|
|
1039
|
+
amount1Max: MAX_UINT128,
|
|
1040
|
+
}],
|
|
1041
|
+
}));
|
|
1042
|
+
// 3) burn (only with --burn and percent=100)
|
|
1043
|
+
const shouldBurn = options.burn && percent === 100;
|
|
1044
|
+
if (shouldBurn) {
|
|
1045
|
+
calls.push(encodeFunctionData({
|
|
1046
|
+
abi: NFT_MANAGER_ABI,
|
|
1047
|
+
functionName: "burn",
|
|
1048
|
+
args: [BigInt(tokenId)],
|
|
1049
|
+
}));
|
|
1050
|
+
}
|
|
1051
|
+
// Single atomic transaction via multicall
|
|
1052
|
+
const txHash = await walletClient.writeContract({
|
|
1053
|
+
address: cfg.protocol.nftPositionManager,
|
|
1054
|
+
abi: NFT_MANAGER_ABI,
|
|
1055
|
+
functionName: "multicall",
|
|
1056
|
+
args: [calls],
|
|
1057
|
+
});
|
|
1058
|
+
spin.succeed(shouldBurn ? "Liquidity removed and NFT burned" : "Liquidity removed");
|
|
1059
|
+
const explorerUrl = `${cfg.chain.explorerUrl}/tx/${txHash}`;
|
|
1060
|
+
if (isHeadless()) {
|
|
1061
|
+
writeJsonSuccess({
|
|
1062
|
+
txHash,
|
|
1063
|
+
explorerUrl,
|
|
1064
|
+
tokenId,
|
|
1065
|
+
percent,
|
|
1066
|
+
liquidityRemoved: liquidityToRemove.toString(),
|
|
1067
|
+
burned: shouldBurn,
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
else {
|
|
1071
|
+
successBox(shouldBurn ? "Liquidity Removed & NFT Burned" : "Liquidity Removed", `Position: #${tokenId}\n` +
|
|
1072
|
+
`Removed: ${percent}%\n` +
|
|
1073
|
+
`Tx: ${colors.info(txHash)}` +
|
|
1074
|
+
(shouldBurn ? `\n${colors.muted("NFT burned")}` : ""));
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
catch (err) {
|
|
1078
|
+
spin.fail("Operation failed");
|
|
1079
|
+
throw new ImmError(ErrorCodes.LP_OPERATION_FAILED, `Failed to remove liquidity: ${err instanceof Error ? err.message : err}`);
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
lp.command("add")
|
|
1083
|
+
.description("Add liquidity to create a new position")
|
|
1084
|
+
.requiredOption("--token0 <token>", "First token")
|
|
1085
|
+
.requiredOption("--token1 <token>", "Second token")
|
|
1086
|
+
.requiredOption("--fee <fee>", "Fee tier (100, 500, 3000, 10000)")
|
|
1087
|
+
.requiredOption("--amount0 <amount>", "Amount of token0")
|
|
1088
|
+
.requiredOption("--amount1 <amount>", "Amount of token1")
|
|
1089
|
+
.option("--range-pct <percent>", "Price range percentage around current price", "10")
|
|
1090
|
+
.option("--tick-lower <tick>", "Lower tick (overrides --range-pct)")
|
|
1091
|
+
.option("--tick-upper <tick>", "Upper tick (overrides --range-pct)")
|
|
1092
|
+
.option("--create-pool", "Create pool if it doesn't exist")
|
|
1093
|
+
.option("--sqrt-price-x96 <uint160>", "Initial sqrtPriceX96 for new pool (as decimal string)")
|
|
1094
|
+
.option("--approve-exact", "Approve exact amounts")
|
|
1095
|
+
.requiredOption("--yes", "Confirm the transaction")
|
|
1096
|
+
.action(async (options) => {
|
|
1097
|
+
if (!options.yes) {
|
|
1098
|
+
throw new ImmError(ErrorCodes.CONFIRMATION_REQUIRED, "Add --yes to confirm");
|
|
1099
|
+
}
|
|
1100
|
+
const userTokens = loadUserTokens();
|
|
1101
|
+
let token0Addr = resolveToken(options.token0, userTokens.aliases);
|
|
1102
|
+
let token1Addr = resolveToken(options.token1, userTokens.aliases);
|
|
1103
|
+
const fee = validateFeeTier(parseIntSafe(options.fee, "fee"));
|
|
1104
|
+
// Sort tokens (token0 < token1)
|
|
1105
|
+
if (token0Addr.toLowerCase() > token1Addr.toLowerCase()) {
|
|
1106
|
+
[token0Addr, token1Addr] = [token1Addr, token0Addr];
|
|
1107
|
+
[options.amount0, options.amount1] = [options.amount1, options.amount0];
|
|
1108
|
+
}
|
|
1109
|
+
const { address, privateKey } = requireWalletAndKeystore();
|
|
1110
|
+
const cfg = loadConfig();
|
|
1111
|
+
const client = getPublicClient();
|
|
1112
|
+
// Fetch decimals
|
|
1113
|
+
const [decimals0, decimals1] = await Promise.all([
|
|
1114
|
+
getTokenDecimals(token0Addr),
|
|
1115
|
+
getTokenDecimals(token1Addr),
|
|
1116
|
+
]);
|
|
1117
|
+
const amount0Desired = parseUnits(options.amount0, decimals0);
|
|
1118
|
+
const amount1Desired = parseUnits(options.amount1, decimals1);
|
|
1119
|
+
// Check if pool exists
|
|
1120
|
+
const spin = spinner("Checking pool...");
|
|
1121
|
+
spin.start();
|
|
1122
|
+
const poolAddress = await client.readContract({
|
|
1123
|
+
address: cfg.protocol.jaineFactory,
|
|
1124
|
+
abi: [
|
|
1125
|
+
{
|
|
1126
|
+
type: "function",
|
|
1127
|
+
name: "getPool",
|
|
1128
|
+
stateMutability: "view",
|
|
1129
|
+
inputs: [
|
|
1130
|
+
{ name: "tokenA", type: "address" },
|
|
1131
|
+
{ name: "tokenB", type: "address" },
|
|
1132
|
+
{ name: "fee", type: "uint24" },
|
|
1133
|
+
],
|
|
1134
|
+
outputs: [{ name: "pool", type: "address" }],
|
|
1135
|
+
},
|
|
1136
|
+
],
|
|
1137
|
+
functionName: "getPool",
|
|
1138
|
+
args: [token0Addr, token1Addr, fee],
|
|
1139
|
+
});
|
|
1140
|
+
const walletClient = createJaineWalletClient(privateKey);
|
|
1141
|
+
if (poolAddress === "0x0000000000000000000000000000000000000000") {
|
|
1142
|
+
if (!options.createPool || !options.sqrtPriceX96) {
|
|
1143
|
+
spin.fail("Pool does not exist");
|
|
1144
|
+
throw new ImmError(ErrorCodes.POOL_NOT_FOUND, "Pool does not exist for this token pair and fee tier", "Use --create-pool --sqrt-price-x96 <uint160> to create it");
|
|
1145
|
+
}
|
|
1146
|
+
// Create pool
|
|
1147
|
+
spin.text = "Creating pool...";
|
|
1148
|
+
// Parse sqrtPriceX96 as BigInt directly (precise, no float conversion)
|
|
1149
|
+
let sqrtPriceX96;
|
|
1150
|
+
try {
|
|
1151
|
+
sqrtPriceX96 = BigInt(options.sqrtPriceX96);
|
|
1152
|
+
}
|
|
1153
|
+
catch {
|
|
1154
|
+
throw new ImmError(ErrorCodes.INVALID_AMOUNT, `Invalid sqrtPriceX96: ${options.sqrtPriceX96}`, "Must be a valid uint160 decimal string");
|
|
1155
|
+
}
|
|
1156
|
+
try {
|
|
1157
|
+
const createTxHash = await walletClient.writeContract({
|
|
1158
|
+
address: cfg.protocol.nftPositionManager,
|
|
1159
|
+
abi: NFT_MANAGER_ABI,
|
|
1160
|
+
functionName: "createAndInitializePoolIfNecessary",
|
|
1161
|
+
args: [token0Addr, token1Addr, fee, sqrtPriceX96],
|
|
1162
|
+
});
|
|
1163
|
+
// Wait for pool creation to confirm
|
|
1164
|
+
await client.waitForTransactionReceipt({ hash: createTxHash });
|
|
1165
|
+
spin.succeed("Pool created");
|
|
1166
|
+
}
|
|
1167
|
+
catch (err) {
|
|
1168
|
+
spin.fail("Failed to create pool");
|
|
1169
|
+
throw new ImmError(ErrorCodes.LP_OPERATION_FAILED, `Failed to create pool: ${err instanceof Error ? err.message : err}`);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
else {
|
|
1173
|
+
spin.succeed("Pool exists");
|
|
1174
|
+
}
|
|
1175
|
+
// Get current tick for range calculation
|
|
1176
|
+
const spinTick = spinner("Fetching pool state...");
|
|
1177
|
+
spinTick.start();
|
|
1178
|
+
let tickLower;
|
|
1179
|
+
let tickUpper;
|
|
1180
|
+
let tickSpacing;
|
|
1181
|
+
const poolAddr = poolAddress !== "0x0000000000000000000000000000000000000000"
|
|
1182
|
+
? poolAddress
|
|
1183
|
+
: await client.readContract({
|
|
1184
|
+
address: cfg.protocol.jaineFactory,
|
|
1185
|
+
abi: [
|
|
1186
|
+
{
|
|
1187
|
+
type: "function",
|
|
1188
|
+
name: "getPool",
|
|
1189
|
+
stateMutability: "view",
|
|
1190
|
+
inputs: [
|
|
1191
|
+
{ name: "tokenA", type: "address" },
|
|
1192
|
+
{ name: "tokenB", type: "address" },
|
|
1193
|
+
{ name: "fee", type: "uint24" },
|
|
1194
|
+
],
|
|
1195
|
+
outputs: [{ name: "pool", type: "address" }],
|
|
1196
|
+
},
|
|
1197
|
+
],
|
|
1198
|
+
functionName: "getPool",
|
|
1199
|
+
args: [token0Addr, token1Addr, fee],
|
|
1200
|
+
});
|
|
1201
|
+
const [slot0, spacing] = await Promise.all([
|
|
1202
|
+
client.readContract({
|
|
1203
|
+
address: poolAddr,
|
|
1204
|
+
abi: POOL_ABI,
|
|
1205
|
+
functionName: "slot0",
|
|
1206
|
+
}),
|
|
1207
|
+
client.readContract({
|
|
1208
|
+
address: poolAddr,
|
|
1209
|
+
abi: POOL_ABI,
|
|
1210
|
+
functionName: "tickSpacing",
|
|
1211
|
+
}),
|
|
1212
|
+
]);
|
|
1213
|
+
const currentTick = slot0[1];
|
|
1214
|
+
tickSpacing = spacing;
|
|
1215
|
+
if (options.tickLower && options.tickUpper) {
|
|
1216
|
+
tickLower = parseIntSafe(options.tickLower, "tickLower");
|
|
1217
|
+
tickUpper = parseIntSafe(options.tickUpper, "tickUpper");
|
|
1218
|
+
}
|
|
1219
|
+
else {
|
|
1220
|
+
// Calculate range based on percentage
|
|
1221
|
+
const rangePct = parseIntSafe(options.rangePct, "rangePct");
|
|
1222
|
+
// Approximate tick range: 1% price change ≈ 100 ticks
|
|
1223
|
+
const tickRange = Math.floor(rangePct * 100);
|
|
1224
|
+
tickLower = currentTick - tickRange;
|
|
1225
|
+
tickUpper = currentTick + tickRange;
|
|
1226
|
+
}
|
|
1227
|
+
// Round to tick spacing
|
|
1228
|
+
tickLower = Math.floor(tickLower / tickSpacing) * tickSpacing;
|
|
1229
|
+
tickUpper = Math.ceil(tickUpper / tickSpacing) * tickSpacing;
|
|
1230
|
+
spinTick.succeed("Pool state fetched");
|
|
1231
|
+
// Approve tokens
|
|
1232
|
+
const spinApprove = spinner("Approving tokens...");
|
|
1233
|
+
spinApprove.start();
|
|
1234
|
+
await ensureAllowance(token0Addr, cfg.protocol.nftPositionManager, amount0Desired, privateKey, options.approveExact);
|
|
1235
|
+
await ensureAllowance(token1Addr, cfg.protocol.nftPositionManager, amount1Desired, privateKey, options.approveExact);
|
|
1236
|
+
spinApprove.succeed("Tokens approved");
|
|
1237
|
+
// Mint position
|
|
1238
|
+
const spinMint = spinner("Minting position...");
|
|
1239
|
+
spinMint.start();
|
|
1240
|
+
const deadline = BigInt(Math.floor(Date.now() / 1000) + 90);
|
|
1241
|
+
try {
|
|
1242
|
+
const txHash = await walletClient.writeContract({
|
|
1243
|
+
address: cfg.protocol.nftPositionManager,
|
|
1244
|
+
abi: NFT_MANAGER_ABI,
|
|
1245
|
+
functionName: "mint",
|
|
1246
|
+
args: [
|
|
1247
|
+
{
|
|
1248
|
+
token0: token0Addr,
|
|
1249
|
+
token1: token1Addr,
|
|
1250
|
+
fee,
|
|
1251
|
+
tickLower,
|
|
1252
|
+
tickUpper,
|
|
1253
|
+
amount0Desired,
|
|
1254
|
+
amount1Desired,
|
|
1255
|
+
amount0Min: 0n,
|
|
1256
|
+
amount1Min: 0n,
|
|
1257
|
+
recipient: address,
|
|
1258
|
+
deadline,
|
|
1259
|
+
},
|
|
1260
|
+
],
|
|
1261
|
+
});
|
|
1262
|
+
spinMint.succeed("Position minted");
|
|
1263
|
+
const explorerUrl = `${cfg.chain.explorerUrl}/tx/${txHash}`;
|
|
1264
|
+
const symbol0 = getTokenSymbol(token0Addr, userTokens.aliases);
|
|
1265
|
+
const symbol1 = getTokenSymbol(token1Addr, userTokens.aliases);
|
|
1266
|
+
if (isHeadless()) {
|
|
1267
|
+
writeJsonSuccess({
|
|
1268
|
+
txHash,
|
|
1269
|
+
explorerUrl,
|
|
1270
|
+
token0: token0Addr,
|
|
1271
|
+
token1: token1Addr,
|
|
1272
|
+
fee,
|
|
1273
|
+
tickLower,
|
|
1274
|
+
tickUpper,
|
|
1275
|
+
amount0Desired: amount0Desired.toString(),
|
|
1276
|
+
amount1Desired: amount1Desired.toString(),
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
else {
|
|
1280
|
+
successBox("Position Created", `Pair: ${colors.info(`${symbol0}/${symbol1}`)}\n` +
|
|
1281
|
+
`Fee: ${(fee / 10000).toFixed(2)}%\n` +
|
|
1282
|
+
`Range: ${tickLower} → ${tickUpper}\n` +
|
|
1283
|
+
`Amounts: ${options.amount0} ${symbol0} + ${options.amount1} ${symbol1}\n` +
|
|
1284
|
+
`Tx: ${colors.info(txHash)}\n` +
|
|
1285
|
+
`Explorer: ${colors.muted(explorerUrl)}`);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
catch (err) {
|
|
1289
|
+
spinMint.fail("Minting failed");
|
|
1290
|
+
throw new ImmError(ErrorCodes.LP_OPERATION_FAILED, `Failed to mint position: ${err instanceof Error ? err.message : err}`);
|
|
1291
|
+
}
|
|
1292
|
+
});
|
|
1293
|
+
lp.command("increase <tokenId>")
|
|
1294
|
+
.description("Add more liquidity to existing position")
|
|
1295
|
+
.requiredOption("--amount0 <amount>", "Amount of token0 to add")
|
|
1296
|
+
.requiredOption("--amount1 <amount>", "Amount of token1 to add")
|
|
1297
|
+
.option("--approve-exact", "Approve exact amounts")
|
|
1298
|
+
.requiredOption("--yes", "Confirm the transaction")
|
|
1299
|
+
.action(async (tokenId, options) => {
|
|
1300
|
+
if (!options.yes) {
|
|
1301
|
+
throw new ImmError(ErrorCodes.CONFIRMATION_REQUIRED, "Add --yes to confirm");
|
|
1302
|
+
}
|
|
1303
|
+
const { address, privateKey } = requireWalletAndKeystore();
|
|
1304
|
+
const cfg = loadConfig();
|
|
1305
|
+
const client = getPublicClient();
|
|
1306
|
+
// Fetch position to get tokens
|
|
1307
|
+
const spin = spinner("Fetching position...");
|
|
1308
|
+
spin.start();
|
|
1309
|
+
const position = await client.readContract({
|
|
1310
|
+
address: cfg.protocol.nftPositionManager,
|
|
1311
|
+
abi: NFT_MANAGER_ABI,
|
|
1312
|
+
functionName: "positions",
|
|
1313
|
+
args: [BigInt(tokenId)],
|
|
1314
|
+
});
|
|
1315
|
+
const [, , token0, token1] = position;
|
|
1316
|
+
const [decimals0, decimals1] = await Promise.all([
|
|
1317
|
+
getTokenDecimals(token0),
|
|
1318
|
+
getTokenDecimals(token1),
|
|
1319
|
+
]);
|
|
1320
|
+
const amount0Desired = parseUnits(options.amount0, decimals0);
|
|
1321
|
+
const amount1Desired = parseUnits(options.amount1, decimals1);
|
|
1322
|
+
spin.text = "Approving tokens...";
|
|
1323
|
+
await ensureAllowance(token0, cfg.protocol.nftPositionManager, amount0Desired, privateKey, options.approveExact);
|
|
1324
|
+
await ensureAllowance(token1, cfg.protocol.nftPositionManager, amount1Desired, privateKey, options.approveExact);
|
|
1325
|
+
spin.text = "Increasing liquidity...";
|
|
1326
|
+
const walletClient = createJaineWalletClient(privateKey);
|
|
1327
|
+
const deadline = BigInt(Math.floor(Date.now() / 1000) + 90);
|
|
1328
|
+
try {
|
|
1329
|
+
const txHash = await walletClient.writeContract({
|
|
1330
|
+
address: cfg.protocol.nftPositionManager,
|
|
1331
|
+
abi: NFT_MANAGER_ABI,
|
|
1332
|
+
functionName: "increaseLiquidity",
|
|
1333
|
+
args: [
|
|
1334
|
+
{
|
|
1335
|
+
tokenId: BigInt(tokenId),
|
|
1336
|
+
amount0Desired,
|
|
1337
|
+
amount1Desired,
|
|
1338
|
+
amount0Min: 0n,
|
|
1339
|
+
amount1Min: 0n,
|
|
1340
|
+
deadline,
|
|
1341
|
+
},
|
|
1342
|
+
],
|
|
1343
|
+
});
|
|
1344
|
+
spin.succeed("Liquidity increased");
|
|
1345
|
+
const explorerUrl = `${cfg.chain.explorerUrl}/tx/${txHash}`;
|
|
1346
|
+
if (isHeadless()) {
|
|
1347
|
+
writeJsonSuccess({
|
|
1348
|
+
txHash,
|
|
1349
|
+
explorerUrl,
|
|
1350
|
+
tokenId,
|
|
1351
|
+
amount0Added: amount0Desired.toString(),
|
|
1352
|
+
amount1Added: amount1Desired.toString(),
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
else {
|
|
1356
|
+
successBox("Liquidity Increased", `Position: #${tokenId}\n` +
|
|
1357
|
+
`Added: ${options.amount0} + ${options.amount1}\n` +
|
|
1358
|
+
`Tx: ${colors.info(txHash)}`);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
catch (err) {
|
|
1362
|
+
spin.fail("Operation failed");
|
|
1363
|
+
throw new ImmError(ErrorCodes.LP_OPERATION_FAILED, `Failed to increase liquidity: ${err instanceof Error ? err.message : err}`);
|
|
1364
|
+
}
|
|
1365
|
+
});
|
|
1366
|
+
lp.command("rebalance <tokenId>")
|
|
1367
|
+
.description("Close position and open new one with different range")
|
|
1368
|
+
.requiredOption("--range-pct <percent>", "New price range percentage")
|
|
1369
|
+
.requiredOption("--yes", "Confirm the transaction")
|
|
1370
|
+
.action(async (tokenId, options) => {
|
|
1371
|
+
if (!options.yes) {
|
|
1372
|
+
throw new ImmError(ErrorCodes.CONFIRMATION_REQUIRED, "Add --yes to confirm");
|
|
1373
|
+
}
|
|
1374
|
+
const instructions = [
|
|
1375
|
+
`imm jaine lp remove ${tokenId} --percent 100 --yes`,
|
|
1376
|
+
`imm jaine lp add --token0 <t0> --token1 <t1> --fee <fee> --amount0 <a0> --amount1 <a1> --range-pct ${options.rangePct} --yes`,
|
|
1377
|
+
];
|
|
1378
|
+
// This is a compound operation: remove 100% + collect + mint new
|
|
1379
|
+
// For simplicity, we guide the user to do it in steps
|
|
1380
|
+
if (isHeadless()) {
|
|
1381
|
+
writeJsonSuccess({
|
|
1382
|
+
tokenId,
|
|
1383
|
+
rangePct: options.rangePct,
|
|
1384
|
+
instructions,
|
|
1385
|
+
note: "Rebalancing requires multiple transactions. Execute the instructions in order.",
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
else {
|
|
1389
|
+
infoBox("Rebalance Instructions", "Rebalancing requires multiple transactions:\n\n" +
|
|
1390
|
+
`1. Remove liquidity: ${instructions[0]}\n` +
|
|
1391
|
+
`2. Add new position: ${instructions[1]}`);
|
|
1392
|
+
}
|
|
1393
|
+
});
|
|
1394
|
+
jaine.addCommand(lp);
|
|
1395
|
+
return jaine;
|
|
1396
|
+
}
|
|
1397
|
+
//# sourceMappingURL=jaine.js.map
|