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.
Files changed (227) hide show
  1. package/README.md +315 -0
  2. package/dist/cli.d.ts +7 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +112 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/config.d.ts +3 -0
  7. package/dist/commands/config.d.ts.map +1 -0
  8. package/dist/commands/config.js +251 -0
  9. package/dist/commands/config.js.map +1 -0
  10. package/dist/commands/immbook.d.ts +16 -0
  11. package/dist/commands/immbook.d.ts.map +1 -0
  12. package/dist/commands/immbook.js +795 -0
  13. package/dist/commands/immbook.js.map +1 -0
  14. package/dist/commands/jaine.d.ts +3 -0
  15. package/dist/commands/jaine.d.ts.map +1 -0
  16. package/dist/commands/jaine.js +1397 -0
  17. package/dist/commands/jaine.js.map +1 -0
  18. package/dist/commands/send.d.ts +3 -0
  19. package/dist/commands/send.d.ts.map +1 -0
  20. package/dist/commands/send.js +229 -0
  21. package/dist/commands/send.js.map +1 -0
  22. package/dist/commands/setup.d.ts +3 -0
  23. package/dist/commands/setup.d.ts.map +1 -0
  24. package/dist/commands/setup.js +83 -0
  25. package/dist/commands/setup.js.map +1 -0
  26. package/dist/commands/slop-app.d.ts +9 -0
  27. package/dist/commands/slop-app.d.ts.map +1 -0
  28. package/dist/commands/slop-app.js +793 -0
  29. package/dist/commands/slop-app.js.map +1 -0
  30. package/dist/commands/slop.d.ts +3 -0
  31. package/dist/commands/slop.d.ts.map +1 -0
  32. package/dist/commands/slop.js +1053 -0
  33. package/dist/commands/slop.js.map +1 -0
  34. package/dist/commands/wallet.d.ts +3 -0
  35. package/dist/commands/wallet.d.ts.map +1 -0
  36. package/dist/commands/wallet.js +298 -0
  37. package/dist/commands/wallet.js.map +1 -0
  38. package/dist/config/paths.d.ts +6 -0
  39. package/dist/config/paths.d.ts.map +1 -0
  40. package/dist/config/paths.js +24 -0
  41. package/dist/config/paths.js.map +1 -0
  42. package/dist/config/store.d.ts +44 -0
  43. package/dist/config/store.d.ts.map +1 -0
  44. package/dist/config/store.js +109 -0
  45. package/dist/config/store.js.map +1 -0
  46. package/dist/constants/chain.d.ts +56 -0
  47. package/dist/constants/chain.d.ts.map +1 -0
  48. package/dist/constants/chain.js +50 -0
  49. package/dist/constants/chain.js.map +1 -0
  50. package/dist/errors.d.ts +86 -0
  51. package/dist/errors.d.ts.map +1 -0
  52. package/dist/errors.js +100 -0
  53. package/dist/errors.js.map +1 -0
  54. package/dist/immbook/api.d.ts +38 -0
  55. package/dist/immbook/api.d.ts.map +1 -0
  56. package/dist/immbook/api.js +86 -0
  57. package/dist/immbook/api.js.map +1 -0
  58. package/dist/immbook/auth.d.ts +31 -0
  59. package/dist/immbook/auth.d.ts.map +1 -0
  60. package/dist/immbook/auth.js +93 -0
  61. package/dist/immbook/auth.js.map +1 -0
  62. package/dist/immbook/comments.d.ts +26 -0
  63. package/dist/immbook/comments.d.ts.map +1 -0
  64. package/dist/immbook/comments.js +20 -0
  65. package/dist/immbook/comments.js.map +1 -0
  66. package/dist/immbook/follows.d.ts +19 -0
  67. package/dist/immbook/follows.d.ts.map +1 -0
  68. package/dist/immbook/follows.js +21 -0
  69. package/dist/immbook/follows.js.map +1 -0
  70. package/dist/immbook/jwtCache.d.ts +15 -0
  71. package/dist/immbook/jwtCache.d.ts.map +1 -0
  72. package/dist/immbook/jwtCache.js +63 -0
  73. package/dist/immbook/jwtCache.js.map +1 -0
  74. package/dist/immbook/points.d.ts +35 -0
  75. package/dist/immbook/points.d.ts.map +1 -0
  76. package/dist/immbook/points.js +20 -0
  77. package/dist/immbook/points.js.map +1 -0
  78. package/dist/immbook/posts.d.ts +46 -0
  79. package/dist/immbook/posts.d.ts.map +1 -0
  80. package/dist/immbook/posts.js +43 -0
  81. package/dist/immbook/posts.js.map +1 -0
  82. package/dist/immbook/profile.d.ts +29 -0
  83. package/dist/immbook/profile.d.ts.map +1 -0
  84. package/dist/immbook/profile.js +14 -0
  85. package/dist/immbook/profile.js.map +1 -0
  86. package/dist/immbook/submolts.d.ts +22 -0
  87. package/dist/immbook/submolts.d.ts.map +1 -0
  88. package/dist/immbook/submolts.js +24 -0
  89. package/dist/immbook/submolts.js.map +1 -0
  90. package/dist/immbook/tradeProof.d.ts +21 -0
  91. package/dist/immbook/tradeProof.d.ts.map +1 -0
  92. package/dist/immbook/tradeProof.js +14 -0
  93. package/dist/immbook/tradeProof.js.map +1 -0
  94. package/dist/immbook/votes.d.ts +17 -0
  95. package/dist/immbook/votes.d.ts.map +1 -0
  96. package/dist/immbook/votes.js +20 -0
  97. package/dist/immbook/votes.js.map +1 -0
  98. package/dist/intents/store.d.ts +22 -0
  99. package/dist/intents/store.d.ts.map +1 -0
  100. package/dist/intents/store.js +76 -0
  101. package/dist/intents/store.js.map +1 -0
  102. package/dist/intents/types.d.ts +21 -0
  103. package/dist/intents/types.d.ts.map +1 -0
  104. package/dist/intents/types.js +2 -0
  105. package/dist/intents/types.js.map +1 -0
  106. package/dist/jaine/abi/erc20.d.ts +90 -0
  107. package/dist/jaine/abi/erc20.d.ts.map +1 -0
  108. package/dist/jaine/abi/erc20.js +65 -0
  109. package/dist/jaine/abi/erc20.js.map +1 -0
  110. package/dist/jaine/abi/factory.d.ts +38 -0
  111. package/dist/jaine/abi/factory.d.ts.map +1 -0
  112. package/dist/jaine/abi/factory.js +26 -0
  113. package/dist/jaine/abi/factory.js.map +1 -0
  114. package/dist/jaine/abi/index.d.ts +11 -0
  115. package/dist/jaine/abi/index.d.ts.map +1 -0
  116. package/dist/jaine/abi/index.js +11 -0
  117. package/dist/jaine/abi/index.js.map +1 -0
  118. package/dist/jaine/abi/nftManager.d.ts +282 -0
  119. package/dist/jaine/abi/nftManager.d.ts.map +1 -0
  120. package/dist/jaine/abi/nftManager.js +182 -0
  121. package/dist/jaine/abi/nftManager.js.map +1 -0
  122. package/dist/jaine/abi/pool.d.ts +77 -0
  123. package/dist/jaine/abi/pool.d.ts.map +1 -0
  124. package/dist/jaine/abi/pool.js +56 -0
  125. package/dist/jaine/abi/pool.js.map +1 -0
  126. package/dist/jaine/abi/quoter.d.ts +84 -0
  127. package/dist/jaine/abi/quoter.d.ts.map +1 -0
  128. package/dist/jaine/abi/quoter.js +53 -0
  129. package/dist/jaine/abi/quoter.js.map +1 -0
  130. package/dist/jaine/abi/router.d.ts +135 -0
  131. package/dist/jaine/abi/router.d.ts.map +1 -0
  132. package/dist/jaine/abi/router.js +88 -0
  133. package/dist/jaine/abi/router.js.map +1 -0
  134. package/dist/jaine/abi/w0g.d.ts +41 -0
  135. package/dist/jaine/abi/w0g.d.ts.map +1 -0
  136. package/dist/jaine/abi/w0g.js +34 -0
  137. package/dist/jaine/abi/w0g.js.map +1 -0
  138. package/dist/jaine/allowance.d.ts +48 -0
  139. package/dist/jaine/allowance.d.ts.map +1 -0
  140. package/dist/jaine/allowance.js +192 -0
  141. package/dist/jaine/allowance.js.map +1 -0
  142. package/dist/jaine/coreTokens.d.ts +32 -0
  143. package/dist/jaine/coreTokens.d.ts.map +1 -0
  144. package/dist/jaine/coreTokens.js +91 -0
  145. package/dist/jaine/coreTokens.js.map +1 -0
  146. package/dist/jaine/pathEncoding.d.ts +39 -0
  147. package/dist/jaine/pathEncoding.d.ts.map +1 -0
  148. package/dist/jaine/pathEncoding.js +98 -0
  149. package/dist/jaine/pathEncoding.js.map +1 -0
  150. package/dist/jaine/paths.d.ts +11 -0
  151. package/dist/jaine/paths.d.ts.map +1 -0
  152. package/dist/jaine/paths.js +20 -0
  153. package/dist/jaine/paths.js.map +1 -0
  154. package/dist/jaine/poolCache.d.ts +42 -0
  155. package/dist/jaine/poolCache.d.ts.map +1 -0
  156. package/dist/jaine/poolCache.js +164 -0
  157. package/dist/jaine/poolCache.js.map +1 -0
  158. package/dist/jaine/routing.d.ts +41 -0
  159. package/dist/jaine/routing.d.ts.map +1 -0
  160. package/dist/jaine/routing.js +247 -0
  161. package/dist/jaine/routing.js.map +1 -0
  162. package/dist/jaine/userTokens.d.ts +27 -0
  163. package/dist/jaine/userTokens.d.ts.map +1 -0
  164. package/dist/jaine/userTokens.js +89 -0
  165. package/dist/jaine/userTokens.js.map +1 -0
  166. package/dist/slop/abi/factory.d.ts +128 -0
  167. package/dist/slop/abi/factory.d.ts.map +1 -0
  168. package/dist/slop/abi/factory.js +70 -0
  169. package/dist/slop/abi/factory.js.map +1 -0
  170. package/dist/slop/abi/feeCollector.d.ts +95 -0
  171. package/dist/slop/abi/feeCollector.d.ts.map +1 -0
  172. package/dist/slop/abi/feeCollector.js +71 -0
  173. package/dist/slop/abi/feeCollector.js.map +1 -0
  174. package/dist/slop/abi/index.d.ts +5 -0
  175. package/dist/slop/abi/index.d.ts.map +1 -0
  176. package/dist/slop/abi/index.js +5 -0
  177. package/dist/slop/abi/index.js.map +1 -0
  178. package/dist/slop/abi/registry.d.ts +135 -0
  179. package/dist/slop/abi/registry.d.ts.map +1 -0
  180. package/dist/slop/abi/registry.js +90 -0
  181. package/dist/slop/abi/registry.js.map +1 -0
  182. package/dist/slop/abi/token.d.ts +320 -0
  183. package/dist/slop/abi/token.d.ts.map +1 -0
  184. package/dist/slop/abi/token.js +251 -0
  185. package/dist/slop/abi/token.js.map +1 -0
  186. package/dist/slop/quote.d.ts +80 -0
  187. package/dist/slop/quote.d.ts.map +1 -0
  188. package/dist/slop/quote.js +174 -0
  189. package/dist/slop/quote.js.map +1 -0
  190. package/dist/utils/canonicalJson.d.ts +8 -0
  191. package/dist/utils/canonicalJson.d.ts.map +1 -0
  192. package/dist/utils/canonicalJson.js +20 -0
  193. package/dist/utils/canonicalJson.js.map +1 -0
  194. package/dist/utils/env.d.ts +11 -0
  195. package/dist/utils/env.d.ts.map +1 -0
  196. package/dist/utils/env.js +20 -0
  197. package/dist/utils/env.js.map +1 -0
  198. package/dist/utils/http.d.ts +19 -0
  199. package/dist/utils/http.d.ts.map +1 -0
  200. package/dist/utils/http.js +61 -0
  201. package/dist/utils/http.js.map +1 -0
  202. package/dist/utils/logger.d.ts +4 -0
  203. package/dist/utils/logger.d.ts.map +1 -0
  204. package/dist/utils/logger.js +21 -0
  205. package/dist/utils/logger.js.map +1 -0
  206. package/dist/utils/output.d.ts +19 -0
  207. package/dist/utils/output.d.ts.map +1 -0
  208. package/dist/utils/output.js +37 -0
  209. package/dist/utils/output.js.map +1 -0
  210. package/dist/utils/respond.d.ts +19 -0
  211. package/dist/utils/respond.d.ts.map +1 -0
  212. package/dist/utils/respond.js +25 -0
  213. package/dist/utils/respond.js.map +1 -0
  214. package/dist/utils/ui.d.ts +38 -0
  215. package/dist/utils/ui.d.ts.map +1 -0
  216. package/dist/utils/ui.js +126 -0
  217. package/dist/utils/ui.js.map +1 -0
  218. package/dist/wallet/client.d.ts +4 -0
  219. package/dist/wallet/client.d.ts.map +1 -0
  220. package/dist/wallet/client.js +53 -0
  221. package/dist/wallet/client.js.map +1 -0
  222. package/dist/wallet/keystore.d.ts +21 -0
  223. package/dist/wallet/keystore.d.ts.map +1 -0
  224. package/dist/wallet/keystore.js +111 -0
  225. package/dist/wallet/keystore.js.map +1 -0
  226. package/package.json +56 -0
  227. 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