@spectratools/aborean-cli 0.9.0 → 0.10.1

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 (2) hide show
  1. package/dist/cli.js +1170 -20
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -1991,12 +1991,496 @@ cl.command("swap", {
1991
1991
  });
1992
1992
  }
1993
1993
  });
1994
+ var nfpmMintAbi = [
1995
+ {
1996
+ type: "function",
1997
+ name: "mint",
1998
+ stateMutability: "payable",
1999
+ inputs: [
2000
+ {
2001
+ name: "params",
2002
+ type: "tuple",
2003
+ components: [
2004
+ { name: "token0", type: "address" },
2005
+ { name: "token1", type: "address" },
2006
+ { name: "tickSpacing", type: "int24" },
2007
+ { name: "tickLower", type: "int24" },
2008
+ { name: "tickUpper", type: "int24" },
2009
+ { name: "amount0Desired", type: "uint256" },
2010
+ { name: "amount1Desired", type: "uint256" },
2011
+ { name: "amount0Min", type: "uint256" },
2012
+ { name: "amount1Min", type: "uint256" },
2013
+ { name: "recipient", type: "address" },
2014
+ { name: "deadline", type: "uint256" },
2015
+ { name: "sqrtPriceX96", type: "uint160" }
2016
+ ]
2017
+ }
2018
+ ],
2019
+ outputs: [
2020
+ { name: "tokenId", type: "uint256" },
2021
+ { name: "liquidity", type: "uint128" },
2022
+ { name: "amount0", type: "uint256" },
2023
+ { name: "amount1", type: "uint256" }
2024
+ ]
2025
+ }
2026
+ ];
2027
+ var nfpmDecreaseLiquidityAbi = [
2028
+ {
2029
+ type: "function",
2030
+ name: "decreaseLiquidity",
2031
+ stateMutability: "payable",
2032
+ inputs: [
2033
+ {
2034
+ name: "params",
2035
+ type: "tuple",
2036
+ components: [
2037
+ { name: "tokenId", type: "uint256" },
2038
+ { name: "liquidity", type: "uint128" },
2039
+ { name: "amount0Min", type: "uint256" },
2040
+ { name: "amount1Min", type: "uint256" },
2041
+ { name: "deadline", type: "uint256" }
2042
+ ]
2043
+ }
2044
+ ],
2045
+ outputs: [
2046
+ { name: "amount0", type: "uint256" },
2047
+ { name: "amount1", type: "uint256" }
2048
+ ]
2049
+ }
2050
+ ];
2051
+ var nfpmCollectAbi = [
2052
+ {
2053
+ type: "function",
2054
+ name: "collect",
2055
+ stateMutability: "payable",
2056
+ inputs: [
2057
+ {
2058
+ name: "params",
2059
+ type: "tuple",
2060
+ components: [
2061
+ { name: "tokenId", type: "uint256" },
2062
+ { name: "recipient", type: "address" },
2063
+ { name: "amount0Max", type: "uint128" },
2064
+ { name: "amount1Max", type: "uint128" }
2065
+ ]
2066
+ }
2067
+ ],
2068
+ outputs: [
2069
+ { name: "amount0", type: "uint256" },
2070
+ { name: "amount1", type: "uint256" }
2071
+ ]
2072
+ }
2073
+ ];
2074
+ var nfpmBurnAbi = [
2075
+ {
2076
+ type: "function",
2077
+ name: "burn",
2078
+ stateMutability: "payable",
2079
+ inputs: [{ name: "tokenId", type: "uint256" }],
2080
+ outputs: []
2081
+ }
2082
+ ];
2083
+ var erc20ApproveAbi = [
2084
+ {
2085
+ type: "function",
2086
+ name: "approve",
2087
+ stateMutability: "nonpayable",
2088
+ inputs: [
2089
+ { name: "spender", type: "address" },
2090
+ { name: "amount", type: "uint256" }
2091
+ ],
2092
+ outputs: [{ name: "", type: "bool" }]
2093
+ },
2094
+ {
2095
+ type: "function",
2096
+ name: "allowance",
2097
+ stateMutability: "view",
2098
+ inputs: [
2099
+ { name: "owner", type: "address" },
2100
+ { name: "account", type: "address" }
2101
+ ],
2102
+ outputs: [{ name: "", type: "uint256" }]
2103
+ }
2104
+ ];
2105
+ var amountSchema = z2.object({
2106
+ raw: z2.string(),
2107
+ decimal: z2.string()
2108
+ });
2109
+ cl.command("add-position", {
2110
+ description: "Mint a new concentrated liquidity position via the NonfungiblePositionManager. Approves both tokens if needed. Supports --dry-run.",
2111
+ options: z2.object({
2112
+ "token-a": z2.string().describe("First token address"),
2113
+ "token-b": z2.string().describe("Second token address"),
2114
+ "tick-spacing": z2.coerce.number().int().describe("Pool tick spacing"),
2115
+ "tick-lower": z2.coerce.number().int().describe("Lower tick boundary"),
2116
+ "tick-upper": z2.coerce.number().int().describe("Upper tick boundary"),
2117
+ "amount-0": z2.string().describe("Desired amount of token0 in wei"),
2118
+ "amount-1": z2.string().describe("Desired amount of token1 in wei"),
2119
+ slippage: z2.coerce.number().default(0.5).describe("Slippage tolerance in percent (default: 0.5)"),
2120
+ deadline: z2.coerce.number().int().default(300).describe("Transaction deadline in seconds from now (default: 300)"),
2121
+ ...writeOptions.shape
2122
+ }),
2123
+ env: writeEnv,
2124
+ output: z2.object({
2125
+ pool: z2.string(),
2126
+ token0: tokenSchema,
2127
+ token1: tokenSchema,
2128
+ tickSpacing: z2.number(),
2129
+ tickLower: z2.number(),
2130
+ tickUpper: z2.number(),
2131
+ amount0Desired: amountSchema,
2132
+ amount1Desired: amountSchema,
2133
+ amount0Min: amountSchema,
2134
+ amount1Min: amountSchema,
2135
+ slippagePercent: z2.number(),
2136
+ tx: z2.union([
2137
+ z2.object({
2138
+ txHash: z2.string(),
2139
+ blockNumber: z2.number(),
2140
+ gasUsed: z2.string()
2141
+ }),
2142
+ z2.object({
2143
+ dryRun: z2.literal(true),
2144
+ estimatedGas: z2.string(),
2145
+ simulationResult: z2.unknown()
2146
+ })
2147
+ ])
2148
+ }),
2149
+ async run(c) {
2150
+ const tokenARaw = c.options["token-a"];
2151
+ const tokenBRaw = c.options["token-b"];
2152
+ if (!isAddress(tokenARaw) || !isAddress(tokenBRaw)) {
2153
+ return c.error({
2154
+ code: "INVALID_ADDRESS",
2155
+ message: "token-a and token-b must both be valid 0x-prefixed 20-byte addresses."
2156
+ });
2157
+ }
2158
+ const addrA = checksumAddress2(tokenARaw);
2159
+ const addrB = checksumAddress2(tokenBRaw);
2160
+ const [token0, token1] = addrA.toLowerCase() < addrB.toLowerCase() ? [addrA, addrB] : [addrB, addrA];
2161
+ let amount0Desired;
2162
+ let amount1Desired;
2163
+ try {
2164
+ const amountForA = BigInt(c.options["amount-0"]);
2165
+ const amountForB = BigInt(c.options["amount-1"]);
2166
+ if (addrA.toLowerCase() < addrB.toLowerCase()) {
2167
+ amount0Desired = amountForA;
2168
+ amount1Desired = amountForB;
2169
+ } else {
2170
+ amount0Desired = amountForB;
2171
+ amount1Desired = amountForA;
2172
+ }
2173
+ } catch {
2174
+ return c.error({
2175
+ code: "INVALID_AMOUNT",
2176
+ message: "amount-0 and amount-1 must be valid integers in wei."
2177
+ });
2178
+ }
2179
+ if (amount0Desired <= 0n && amount1Desired <= 0n) {
2180
+ return c.error({
2181
+ code: "INVALID_AMOUNT",
2182
+ message: "At least one of amount-0 or amount-1 must be positive."
2183
+ });
2184
+ }
2185
+ const client = createAboreanPublicClient(c.env.ABSTRACT_RPC_URL);
2186
+ const allPools = await listPoolAddresses(client);
2187
+ const poolStates = await readPoolStates(client, allPools);
2188
+ const matchingPool = poolStates.find(
2189
+ (pool) => normalizeAddress(pool.token0) === normalizeAddress(token0) && normalizeAddress(pool.token1) === normalizeAddress(token1) && pool.tickSpacing === c.options["tick-spacing"]
2190
+ );
2191
+ if (!matchingPool) {
2192
+ return c.error({
2193
+ code: "POOL_NOT_FOUND",
2194
+ message: `No Slipstream pool found for ${checksumAddress2(token0)}/${checksumAddress2(token1)} with tick spacing ${c.options["tick-spacing"]}.`
2195
+ });
2196
+ }
2197
+ const tokenMeta = await readTokenMetadata(client, [token0, token1]);
2198
+ const meta0 = tokenMeta.get(token0) ?? toTokenMetaFallback(token0);
2199
+ const meta1 = tokenMeta.get(token1) ?? toTokenMetaFallback(token1);
2200
+ const slippageBps = BigInt(Math.round(c.options.slippage * 100));
2201
+ const amount0Min = amount0Desired - amount0Desired * slippageBps / 10000n;
2202
+ const amount1Min = amount1Desired - amount1Desired * slippageBps / 10000n;
2203
+ const deadlineTimestamp = BigInt(Math.floor(Date.now() / 1e3) + c.options.deadline);
2204
+ const account = resolveAccount(c.env);
2205
+ const nfpmAddress = ABOREAN_CL_ADDRESSES.nonfungiblePositionManager;
2206
+ if (!c.options["dry-run"]) {
2207
+ for (const [token, amount] of [
2208
+ [token0, amount0Desired],
2209
+ [token1, amount1Desired]
2210
+ ]) {
2211
+ if (amount <= 0n) continue;
2212
+ const currentAllowance = await client.readContract({
2213
+ abi: erc20ApproveAbi,
2214
+ address: token,
2215
+ functionName: "allowance",
2216
+ args: [account.address, nfpmAddress]
2217
+ });
2218
+ if (currentAllowance < amount) {
2219
+ await aboreanWriteTx({
2220
+ env: c.env,
2221
+ options: { ...c.options, "dry-run": false },
2222
+ address: token,
2223
+ abi: erc20ApproveAbi,
2224
+ functionName: "approve",
2225
+ args: [nfpmAddress, amount]
2226
+ });
2227
+ }
2228
+ }
2229
+ }
2230
+ const txResult = await aboreanWriteTx({
2231
+ env: c.env,
2232
+ options: {
2233
+ "dry-run": c.options["dry-run"],
2234
+ "gas-limit": c.options["gas-limit"],
2235
+ "max-fee": c.options["max-fee"],
2236
+ nonce: c.options.nonce
2237
+ },
2238
+ address: nfpmAddress,
2239
+ abi: nfpmMintAbi,
2240
+ functionName: "mint",
2241
+ args: [
2242
+ {
2243
+ token0,
2244
+ token1,
2245
+ tickSpacing: c.options["tick-spacing"],
2246
+ tickLower: c.options["tick-lower"],
2247
+ tickUpper: c.options["tick-upper"],
2248
+ amount0Desired,
2249
+ amount1Desired,
2250
+ amount0Min,
2251
+ amount1Min,
2252
+ recipient: account.address,
2253
+ deadline: deadlineTimestamp,
2254
+ sqrtPriceX96: 0n
2255
+ }
2256
+ ]
2257
+ });
2258
+ return c.ok({
2259
+ pool: checksumAddress2(matchingPool.pool),
2260
+ token0: meta0,
2261
+ token1: meta1,
2262
+ tickSpacing: c.options["tick-spacing"],
2263
+ tickLower: c.options["tick-lower"],
2264
+ tickUpper: c.options["tick-upper"],
2265
+ amount0Desired: {
2266
+ raw: amount0Desired.toString(),
2267
+ decimal: formatUnits(amount0Desired, meta0.decimals)
2268
+ },
2269
+ amount1Desired: {
2270
+ raw: amount1Desired.toString(),
2271
+ decimal: formatUnits(amount1Desired, meta1.decimals)
2272
+ },
2273
+ amount0Min: {
2274
+ raw: amount0Min.toString(),
2275
+ decimal: formatUnits(amount0Min, meta0.decimals)
2276
+ },
2277
+ amount1Min: {
2278
+ raw: amount1Min.toString(),
2279
+ decimal: formatUnits(amount1Min, meta1.decimals)
2280
+ },
2281
+ slippagePercent: c.options.slippage,
2282
+ tx: txResult
2283
+ });
2284
+ }
2285
+ });
2286
+ cl.command("remove-position", {
2287
+ description: "Remove (close) a concentrated liquidity position. Decreases liquidity to zero, collects all tokens, and burns the NFT. Supports --dry-run.",
2288
+ options: z2.object({
2289
+ "token-id": z2.string().describe("Position NFT token ID"),
2290
+ slippage: z2.coerce.number().default(0.5).describe("Slippage tolerance in percent (default: 0.5)"),
2291
+ deadline: z2.coerce.number().int().default(300).describe("Transaction deadline in seconds from now (default: 300)"),
2292
+ ...writeOptions.shape
2293
+ }),
2294
+ env: writeEnv,
2295
+ output: z2.object({
2296
+ tokenId: z2.string(),
2297
+ pair: z2.string(),
2298
+ token0: tokenSchema,
2299
+ token1: tokenSchema,
2300
+ tickLower: z2.number(),
2301
+ tickUpper: z2.number(),
2302
+ liquidity: z2.string(),
2303
+ slippagePercent: z2.number(),
2304
+ tx: z2.union([
2305
+ z2.object({
2306
+ txHash: z2.string(),
2307
+ blockNumber: z2.number(),
2308
+ gasUsed: z2.string()
2309
+ }),
2310
+ z2.object({
2311
+ dryRun: z2.literal(true),
2312
+ estimatedGas: z2.string(),
2313
+ simulationResult: z2.unknown()
2314
+ })
2315
+ ])
2316
+ }),
2317
+ async run(c) {
2318
+ let tokenId;
2319
+ try {
2320
+ tokenId = BigInt(c.options["token-id"]);
2321
+ } catch {
2322
+ return c.error({
2323
+ code: "INVALID_TOKEN_ID",
2324
+ message: `Invalid token-id: "${c.options["token-id"]}". Provide a valid integer.`
2325
+ });
2326
+ }
2327
+ if (tokenId <= 0n) {
2328
+ return c.error({
2329
+ code: "INVALID_TOKEN_ID",
2330
+ message: "token-id must be a positive integer."
2331
+ });
2332
+ }
2333
+ const client = createAboreanPublicClient(c.env.ABSTRACT_RPC_URL);
2334
+ const nfpmAddress = ABOREAN_CL_ADDRESSES.nonfungiblePositionManager;
2335
+ let positionData;
2336
+ try {
2337
+ positionData = await client.readContract({
2338
+ abi: nonfungiblePositionManagerAbi,
2339
+ address: nfpmAddress,
2340
+ functionName: "positions",
2341
+ args: [tokenId]
2342
+ });
2343
+ } catch {
2344
+ return c.error({
2345
+ code: "POSITION_NOT_FOUND",
2346
+ message: `Position with tokenId ${tokenId.toString()} not found.`
2347
+ });
2348
+ }
2349
+ const token0 = positionData[2];
2350
+ const token1 = positionData[3];
2351
+ const tickLower = positionData[5];
2352
+ const tickUpper = positionData[6];
2353
+ const liquidity = positionData[7];
2354
+ if (liquidity === 0n) {
2355
+ return c.error({
2356
+ code: "ZERO_LIQUIDITY",
2357
+ message: `Position ${tokenId.toString()} has zero liquidity. Nothing to remove.`
2358
+ });
2359
+ }
2360
+ const tokenMeta = await readTokenMetadata(client, [token0, token1]);
2361
+ const meta0 = tokenMeta.get(token0) ?? toTokenMetaFallback(token0);
2362
+ const meta1 = tokenMeta.get(token1) ?? toTokenMetaFallback(token1);
2363
+ const account = resolveAccount(c.env);
2364
+ const deadlineTimestamp = BigInt(Math.floor(Date.now() / 1e3) + c.options.deadline);
2365
+ const txResult = await aboreanWriteTx({
2366
+ env: c.env,
2367
+ options: {
2368
+ "dry-run": c.options["dry-run"],
2369
+ "gas-limit": c.options["gas-limit"],
2370
+ "max-fee": c.options["max-fee"],
2371
+ nonce: c.options.nonce
2372
+ },
2373
+ address: nfpmAddress,
2374
+ abi: nfpmDecreaseLiquidityAbi,
2375
+ functionName: "decreaseLiquidity",
2376
+ args: [
2377
+ {
2378
+ tokenId,
2379
+ liquidity,
2380
+ amount0Min: 0n,
2381
+ amount1Min: 0n,
2382
+ deadline: deadlineTimestamp
2383
+ }
2384
+ ]
2385
+ });
2386
+ if (!c.options["dry-run"]) {
2387
+ const maxUint128 = (1n << 128n) - 1n;
2388
+ await aboreanWriteTx({
2389
+ env: c.env,
2390
+ options: { ...c.options, "dry-run": false },
2391
+ address: nfpmAddress,
2392
+ abi: nfpmCollectAbi,
2393
+ functionName: "collect",
2394
+ args: [
2395
+ {
2396
+ tokenId,
2397
+ recipient: account.address,
2398
+ amount0Max: maxUint128,
2399
+ amount1Max: maxUint128
2400
+ }
2401
+ ]
2402
+ });
2403
+ await aboreanWriteTx({
2404
+ env: c.env,
2405
+ options: { ...c.options, "dry-run": false },
2406
+ address: nfpmAddress,
2407
+ abi: nfpmBurnAbi,
2408
+ functionName: "burn",
2409
+ args: [tokenId]
2410
+ });
2411
+ }
2412
+ return c.ok({
2413
+ tokenId: tokenId.toString(),
2414
+ pair: `${meta0.symbol}/${meta1.symbol}`,
2415
+ token0: meta0,
2416
+ token1: meta1,
2417
+ tickLower,
2418
+ tickUpper,
2419
+ liquidity: liquidity.toString(),
2420
+ slippagePercent: c.options.slippage,
2421
+ tx: txResult
2422
+ });
2423
+ }
2424
+ });
1994
2425
 
1995
2426
  // src/commands/gauges.ts
2427
+ import { isAddress as isAddress2 } from "@spectratools/cli-shared";
1996
2428
  import { Cli as Cli2, z as z3 } from "incur";
1997
2429
  var env2 = z3.object({
1998
2430
  ABSTRACT_RPC_URL: z3.string().optional().describe("Abstract RPC URL override")
1999
2431
  });
2432
+ var erc20ApproveAbi2 = [
2433
+ {
2434
+ type: "function",
2435
+ name: "approve",
2436
+ stateMutability: "nonpayable",
2437
+ inputs: [
2438
+ { name: "spender", type: "address" },
2439
+ { name: "amount", type: "uint256" }
2440
+ ],
2441
+ outputs: [{ name: "", type: "bool" }]
2442
+ },
2443
+ {
2444
+ type: "function",
2445
+ name: "allowance",
2446
+ stateMutability: "view",
2447
+ inputs: [
2448
+ { name: "owner", type: "address" },
2449
+ { name: "account", type: "address" }
2450
+ ],
2451
+ outputs: [{ name: "", type: "uint256" }]
2452
+ }
2453
+ ];
2454
+ var gaugeDepositAbi = [
2455
+ {
2456
+ type: "function",
2457
+ name: "deposit",
2458
+ stateMutability: "nonpayable",
2459
+ inputs: [{ name: "amount", type: "uint256" }],
2460
+ outputs: []
2461
+ }
2462
+ ];
2463
+ var gaugeDepositWithTokenIdAbi = [
2464
+ {
2465
+ type: "function",
2466
+ name: "deposit",
2467
+ stateMutability: "nonpayable",
2468
+ inputs: [
2469
+ { name: "amount", type: "uint256" },
2470
+ { name: "tokenId", type: "uint256" }
2471
+ ],
2472
+ outputs: []
2473
+ }
2474
+ ];
2475
+ var gaugeWithdrawAbi = [
2476
+ {
2477
+ type: "function",
2478
+ name: "withdraw",
2479
+ stateMutability: "nonpayable",
2480
+ inputs: [{ name: "amount", type: "uint256" }],
2481
+ outputs: []
2482
+ }
2483
+ ];
2000
2484
  async function discoverGaugePools(client) {
2001
2485
  const [v2PoolCount, clPoolCount] = await Promise.all([
2002
2486
  client.readContract({
@@ -2414,9 +2898,189 @@ gauges.command("staked", {
2414
2898
  );
2415
2899
  }
2416
2900
  });
2901
+ gauges.command("deposit", {
2902
+ description: "Deposit LP tokens into a gauge for staking rewards. Optionally attach a veNFT tokenId for boosted emissions. Approves the gauge to spend LP tokens if needed.",
2903
+ options: z3.object({
2904
+ gauge: z3.string().describe("Gauge contract address"),
2905
+ amount: z3.string().describe("Amount of LP tokens to deposit (in wei)"),
2906
+ "token-id": z3.coerce.number().int().nonnegative().optional().describe("veNFT token id for boosted emissions")
2907
+ }).merge(writeOptions),
2908
+ env: writeEnv,
2909
+ output: z3.object({
2910
+ gauge: z3.string(),
2911
+ stakingToken: z3.string(),
2912
+ amount: z3.string(),
2913
+ tokenId: z3.number().nullable(),
2914
+ tx: z3.union([
2915
+ z3.object({
2916
+ txHash: z3.string(),
2917
+ blockNumber: z3.number(),
2918
+ gasUsed: z3.string()
2919
+ }),
2920
+ z3.object({
2921
+ dryRun: z3.literal(true),
2922
+ estimatedGas: z3.string(),
2923
+ simulationResult: z3.unknown()
2924
+ })
2925
+ ])
2926
+ }),
2927
+ examples: [
2928
+ {
2929
+ options: {
2930
+ gauge: "0x0000000000000000000000000000000000000001",
2931
+ amount: "1000000000000000000",
2932
+ "dry-run": true
2933
+ },
2934
+ description: "Dry-run deposit 1e18 LP tokens into a gauge"
2935
+ },
2936
+ {
2937
+ options: {
2938
+ gauge: "0x0000000000000000000000000000000000000001",
2939
+ amount: "1000000000000000000",
2940
+ "token-id": 42
2941
+ },
2942
+ description: "Deposit with veNFT boost"
2943
+ }
2944
+ ],
2945
+ async run(c) {
2946
+ const gaugeAddress = c.options.gauge;
2947
+ if (!isAddress2(gaugeAddress)) {
2948
+ return c.error({
2949
+ code: "INVALID_ADDRESS",
2950
+ message: `Invalid gauge address: "${gaugeAddress}". Use a valid 0x-prefixed 20-byte hex address.`
2951
+ });
2952
+ }
2953
+ const amount = BigInt(c.options.amount);
2954
+ if (amount <= 0n) {
2955
+ return c.error({
2956
+ code: "INVALID_AMOUNT",
2957
+ message: "amount must be a positive integer in wei."
2958
+ });
2959
+ }
2960
+ const gauge = gaugeAddress;
2961
+ const client = createAboreanPublicClient(c.env.ABSTRACT_RPC_URL);
2962
+ const isAlive = await client.readContract({
2963
+ abi: voterAbi,
2964
+ address: ABOREAN_V2_ADDRESSES.voter,
2965
+ functionName: "isAlive",
2966
+ args: [gauge]
2967
+ });
2968
+ if (!isAlive) {
2969
+ return c.error({
2970
+ code: "GAUGE_NOT_ALIVE",
2971
+ message: `Gauge ${toChecksum(gauge)} is not alive.`,
2972
+ retryable: false
2973
+ });
2974
+ }
2975
+ const stakingToken = await client.readContract({
2976
+ abi: gaugeAbi,
2977
+ address: gauge,
2978
+ functionName: "stakingToken"
2979
+ });
2980
+ const account = resolveAccount(c.env);
2981
+ if (!c.options["dry-run"]) {
2982
+ const currentAllowance = await client.readContract({
2983
+ abi: erc20ApproveAbi2,
2984
+ address: stakingToken,
2985
+ functionName: "allowance",
2986
+ args: [account.address, gauge]
2987
+ });
2988
+ if (currentAllowance < amount) {
2989
+ await aboreanWriteTx({
2990
+ env: c.env,
2991
+ options: { ...c.options, "dry-run": false },
2992
+ address: stakingToken,
2993
+ abi: erc20ApproveAbi2,
2994
+ functionName: "approve",
2995
+ args: [gauge, amount]
2996
+ });
2997
+ }
2998
+ }
2999
+ const tokenId = c.options["token-id"];
3000
+ const tx = await aboreanWriteTx({
3001
+ env: c.env,
3002
+ options: c.options,
3003
+ address: gauge,
3004
+ abi: tokenId !== void 0 ? gaugeDepositWithTokenIdAbi : gaugeDepositAbi,
3005
+ functionName: "deposit",
3006
+ args: tokenId !== void 0 ? [amount, BigInt(tokenId)] : [amount]
3007
+ });
3008
+ return c.ok({
3009
+ gauge: toChecksum(gauge),
3010
+ stakingToken: toChecksum(stakingToken),
3011
+ amount: amount.toString(),
3012
+ tokenId: tokenId !== void 0 ? tokenId : null,
3013
+ tx
3014
+ });
3015
+ }
3016
+ });
3017
+ gauges.command("withdraw", {
3018
+ description: "Withdraw LP tokens from a gauge.",
3019
+ options: z3.object({
3020
+ gauge: z3.string().describe("Gauge contract address"),
3021
+ amount: z3.string().describe("Amount of LP tokens to withdraw (in wei)")
3022
+ }).merge(writeOptions),
3023
+ env: writeEnv,
3024
+ output: z3.object({
3025
+ gauge: z3.string(),
3026
+ amount: z3.string(),
3027
+ tx: z3.union([
3028
+ z3.object({
3029
+ txHash: z3.string(),
3030
+ blockNumber: z3.number(),
3031
+ gasUsed: z3.string()
3032
+ }),
3033
+ z3.object({
3034
+ dryRun: z3.literal(true),
3035
+ estimatedGas: z3.string(),
3036
+ simulationResult: z3.unknown()
3037
+ })
3038
+ ])
3039
+ }),
3040
+ examples: [
3041
+ {
3042
+ options: {
3043
+ gauge: "0x0000000000000000000000000000000000000001",
3044
+ amount: "1000000000000000000",
3045
+ "dry-run": true
3046
+ },
3047
+ description: "Dry-run withdraw 1e18 LP tokens from a gauge"
3048
+ }
3049
+ ],
3050
+ async run(c) {
3051
+ const gaugeAddress = c.options.gauge;
3052
+ if (!isAddress2(gaugeAddress)) {
3053
+ return c.error({
3054
+ code: "INVALID_ADDRESS",
3055
+ message: `Invalid gauge address: "${gaugeAddress}". Use a valid 0x-prefixed 20-byte hex address.`
3056
+ });
3057
+ }
3058
+ const amount = BigInt(c.options.amount);
3059
+ if (amount <= 0n) {
3060
+ return c.error({
3061
+ code: "INVALID_AMOUNT",
3062
+ message: "amount must be a positive integer in wei."
3063
+ });
3064
+ }
3065
+ const gauge = gaugeAddress;
3066
+ const tx = await aboreanWriteTx({
3067
+ env: c.env,
3068
+ options: c.options,
3069
+ address: gauge,
3070
+ abi: gaugeWithdrawAbi,
3071
+ functionName: "withdraw",
3072
+ args: [amount]
3073
+ });
3074
+ return c.ok({
3075
+ gauge: toChecksum(gauge),
3076
+ amount: amount.toString(),
3077
+ tx
3078
+ });
3079
+ }
3080
+ });
2417
3081
 
2418
3082
  // src/commands/lending.ts
2419
- import { checksumAddress as checksumAddress3, isAddress as isAddress2 } from "@spectratools/cli-shared";
3083
+ import { checksumAddress as checksumAddress3, isAddress as isAddress3 } from "@spectratools/cli-shared";
2420
3084
  import { Cli as Cli3, z as z4 } from "incur";
2421
3085
  import { formatUnits as formatUnits2 } from "viem";
2422
3086
  var MORPHO_DEPLOY_BLOCK = 13947713n;
@@ -2921,7 +3585,7 @@ lending.command("position", {
2921
3585
  message: "marketId must be a 32-byte hex string (0x + 64 hex chars)"
2922
3586
  });
2923
3587
  }
2924
- if (!isAddress2(c.args.user)) {
3588
+ if (!isAddress3(c.args.user)) {
2925
3589
  return c.error({
2926
3590
  code: "INVALID_ARGUMENT",
2927
3591
  message: "user must be a valid address"
@@ -3032,13 +3696,13 @@ lending.command("position", {
3032
3696
  });
3033
3697
 
3034
3698
  // src/commands/pools.ts
3035
- import { checksumAddress as checksumAddress4, isAddress as isAddress3 } from "@spectratools/cli-shared";
3699
+ import { checksumAddress as checksumAddress4, isAddress as isAddress4 } from "@spectratools/cli-shared";
3036
3700
  import { Cli as Cli4, z as z5 } from "incur";
3037
3701
  import { formatUnits as formatUnits3, parseUnits as parseUnits2 } from "viem";
3038
3702
  var MULTICALL_BATCH_SIZE2 = 100;
3039
3703
  var DEFAULT_SLIPPAGE_PERCENT2 = 0.5;
3040
3704
  var DEFAULT_DEADLINE_SECONDS2 = 300;
3041
- var erc20ApproveAbi = [
3705
+ var erc20ApproveAbi3 = [
3042
3706
  {
3043
3707
  type: "function",
3044
3708
  name: "approve",
@@ -3092,7 +3756,7 @@ var tokenSchema2 = z5.object({
3092
3756
  symbol: z5.string(),
3093
3757
  decimals: z5.number()
3094
3758
  });
3095
- var amountSchema = z5.object({
3759
+ var amountSchema2 = z5.object({
3096
3760
  raw: z5.string(),
3097
3761
  decimal: z5.string()
3098
3762
  });
@@ -3108,8 +3772,8 @@ var poolSummarySchema = z5.object({
3108
3772
  token0: tokenSchema2,
3109
3773
  token1: tokenSchema2,
3110
3774
  reserves: z5.object({
3111
- token0: amountSchema,
3112
- token1: amountSchema,
3775
+ token0: amountSchema2,
3776
+ token1: amountSchema2,
3113
3777
  blockTimestampLast: z5.number()
3114
3778
  }),
3115
3779
  totalSupply: z5.string(),
@@ -3416,7 +4080,7 @@ pools.command("pool", {
3416
4080
  })
3417
4081
  }),
3418
4082
  async run(c) {
3419
- if (!isAddress3(c.args.address)) {
4083
+ if (!isAddress4(c.args.address)) {
3420
4084
  return c.error({
3421
4085
  code: "INVALID_ADDRESS",
3422
4086
  message: `Invalid pool address: "${c.args.address}". Use a valid 0x-prefixed 20-byte hex address.`
@@ -3525,12 +4189,12 @@ pools.command("quote", {
3525
4189
  stable: z5.boolean(),
3526
4190
  tokenIn: tokenSchema2,
3527
4191
  tokenOut: tokenSchema2,
3528
- amountIn: amountSchema,
3529
- amountOut: amountSchema,
4192
+ amountIn: amountSchema2,
4193
+ amountOut: amountSchema2,
3530
4194
  priceOutPerIn: z5.number().nullable()
3531
4195
  }),
3532
4196
  async run(c) {
3533
- if (!isAddress3(c.args.tokenIn) || !isAddress3(c.args.tokenOut)) {
4197
+ if (!isAddress4(c.args.tokenIn) || !isAddress4(c.args.tokenOut)) {
3534
4198
  return c.error({
3535
4199
  code: "INVALID_ADDRESS",
3536
4200
  message: "tokenIn and tokenOut must both be valid 0x-prefixed 20-byte addresses."
@@ -3640,7 +4304,7 @@ pools.command("fees", {
3640
4304
  volatileFee: feeSchema
3641
4305
  }),
3642
4306
  async run(c) {
3643
- if (!isAddress3(c.args.pool)) {
4307
+ if (!isAddress4(c.args.pool)) {
3644
4308
  return c.error({
3645
4309
  code: "INVALID_ADDRESS",
3646
4310
  message: `Invalid pool address: "${c.args.pool}". Use a valid 0x-prefixed 20-byte hex address.`
@@ -3723,9 +4387,9 @@ pools.command("swap", {
3723
4387
  stable: z5.boolean(),
3724
4388
  tokenIn: tokenSchema2,
3725
4389
  tokenOut: tokenSchema2,
3726
- amountIn: amountSchema,
3727
- expectedAmountOut: amountSchema,
3728
- minAmountOut: amountSchema,
4390
+ amountIn: amountSchema2,
4391
+ expectedAmountOut: amountSchema2,
4392
+ minAmountOut: amountSchema2,
3729
4393
  slippagePercent: z5.number(),
3730
4394
  effectivePrice: z5.number().nullable(),
3731
4395
  tx: z5.union([
@@ -3744,7 +4408,7 @@ pools.command("swap", {
3744
4408
  async run(c) {
3745
4409
  const tokenInRaw = c.options["token-in"];
3746
4410
  const tokenOutRaw = c.options["token-out"];
3747
- if (!isAddress3(tokenInRaw) || !isAddress3(tokenOutRaw)) {
4411
+ if (!isAddress4(tokenInRaw) || !isAddress4(tokenOutRaw)) {
3748
4412
  return c.error({
3749
4413
  code: "INVALID_ADDRESS",
3750
4414
  message: "token-in and token-out must both be valid 0x-prefixed 20-byte addresses."
@@ -3804,7 +4468,7 @@ pools.command("swap", {
3804
4468
  const account = resolveAccount(c.env);
3805
4469
  if (!c.options["dry-run"]) {
3806
4470
  const currentAllowance = await client.readContract({
3807
- abi: erc20ApproveAbi,
4471
+ abi: erc20ApproveAbi3,
3808
4472
  address: tokenIn,
3809
4473
  functionName: "allowance",
3810
4474
  args: [account.address, ABOREAN_V2_ADDRESSES.router]
@@ -3814,7 +4478,7 @@ pools.command("swap", {
3814
4478
  env: c.env,
3815
4479
  options: { ...c.options, "dry-run": false },
3816
4480
  address: tokenIn,
3817
- abi: erc20ApproveAbi,
4481
+ abi: erc20ApproveAbi3,
3818
4482
  functionName: "approve",
3819
4483
  args: [ABOREAN_V2_ADDRESSES.router, amountInRaw]
3820
4484
  });
@@ -3868,9 +4532,348 @@ pools.command("swap", {
3868
4532
  });
3869
4533
  }
3870
4534
  });
4535
+ var v2RouterAddLiquidityAbi = [
4536
+ {
4537
+ type: "function",
4538
+ name: "addLiquidity",
4539
+ stateMutability: "nonpayable",
4540
+ inputs: [
4541
+ { name: "tokenA", type: "address" },
4542
+ { name: "tokenB", type: "address" },
4543
+ { name: "stable", type: "bool" },
4544
+ { name: "amountADesired", type: "uint256" },
4545
+ { name: "amountBDesired", type: "uint256" },
4546
+ { name: "amountAMin", type: "uint256" },
4547
+ { name: "amountBMin", type: "uint256" },
4548
+ { name: "to", type: "address" },
4549
+ { name: "deadline", type: "uint256" }
4550
+ ],
4551
+ outputs: [
4552
+ { name: "amountA", type: "uint256" },
4553
+ { name: "amountB", type: "uint256" },
4554
+ { name: "liquidity", type: "uint256" }
4555
+ ]
4556
+ }
4557
+ ];
4558
+ var v2RouterRemoveLiquidityAbi = [
4559
+ {
4560
+ type: "function",
4561
+ name: "removeLiquidity",
4562
+ stateMutability: "nonpayable",
4563
+ inputs: [
4564
+ { name: "tokenA", type: "address" },
4565
+ { name: "tokenB", type: "address" },
4566
+ { name: "stable", type: "bool" },
4567
+ { name: "liquidity", type: "uint256" },
4568
+ { name: "amountAMin", type: "uint256" },
4569
+ { name: "amountBMin", type: "uint256" },
4570
+ { name: "to", type: "address" },
4571
+ { name: "deadline", type: "uint256" }
4572
+ ],
4573
+ outputs: [
4574
+ { name: "amountA", type: "uint256" },
4575
+ { name: "amountB", type: "uint256" }
4576
+ ]
4577
+ }
4578
+ ];
4579
+ pools.command("add-liquidity", {
4580
+ description: "Add liquidity to a V2 pool. Approves both tokens to the router if needed, then calls addLiquidity. Supports --dry-run.",
4581
+ options: z5.object({
4582
+ "token-a": z5.string().describe("First token address"),
4583
+ "token-b": z5.string().describe("Second token address"),
4584
+ "amount-a": z5.string().describe("Desired amount of token A in wei"),
4585
+ "amount-b": z5.string().describe("Desired amount of token B in wei"),
4586
+ slippage: z5.coerce.number().min(0).max(100).default(DEFAULT_SLIPPAGE_PERCENT2).describe("Slippage tolerance in percent (default: 0.5)"),
4587
+ deadline: z5.coerce.number().int().positive().default(DEFAULT_DEADLINE_SECONDS2).describe("Transaction deadline in seconds from now (default: 300)"),
4588
+ stable: z5.boolean().default(false).describe("Target stable pool (default: volatile)")
4589
+ }).merge(writeOptions),
4590
+ env: writeEnv,
4591
+ output: z5.object({
4592
+ pool: z5.string(),
4593
+ stable: z5.boolean(),
4594
+ tokenA: tokenSchema2,
4595
+ tokenB: tokenSchema2,
4596
+ amountADesired: amountSchema2,
4597
+ amountBDesired: amountSchema2,
4598
+ amountAMin: amountSchema2,
4599
+ amountBMin: amountSchema2,
4600
+ slippagePercent: z5.number(),
4601
+ tx: z5.union([
4602
+ z5.object({
4603
+ txHash: z5.string(),
4604
+ blockNumber: z5.number(),
4605
+ gasUsed: z5.string()
4606
+ }),
4607
+ z5.object({
4608
+ dryRun: z5.literal(true),
4609
+ estimatedGas: z5.string(),
4610
+ simulationResult: z5.unknown()
4611
+ })
4612
+ ])
4613
+ }),
4614
+ async run(c) {
4615
+ const tokenARaw = c.options["token-a"];
4616
+ const tokenBRaw = c.options["token-b"];
4617
+ if (!isAddress4(tokenARaw) || !isAddress4(tokenBRaw)) {
4618
+ return c.error({
4619
+ code: "INVALID_ADDRESS",
4620
+ message: "token-a and token-b must both be valid 0x-prefixed 20-byte addresses."
4621
+ });
4622
+ }
4623
+ const tokenA = toAddress(tokenARaw);
4624
+ const tokenB = toAddress(tokenBRaw);
4625
+ let amountADesired;
4626
+ let amountBDesired;
4627
+ try {
4628
+ amountADesired = BigInt(c.options["amount-a"]);
4629
+ amountBDesired = BigInt(c.options["amount-b"]);
4630
+ } catch {
4631
+ return c.error({
4632
+ code: "INVALID_AMOUNT",
4633
+ message: "amount-a and amount-b must be valid integers in wei."
4634
+ });
4635
+ }
4636
+ if (amountADesired <= 0n || amountBDesired <= 0n) {
4637
+ return c.error({
4638
+ code: "INVALID_AMOUNT",
4639
+ message: "amount-a and amount-b must be positive integers."
4640
+ });
4641
+ }
4642
+ const client = createAboreanPublicClient(c.env.ABSTRACT_RPC_URL);
4643
+ const poolAddress = await client.readContract({
4644
+ abi: poolFactoryAbi,
4645
+ address: ABOREAN_V2_ADDRESSES.poolFactory,
4646
+ functionName: "getPool",
4647
+ args: [tokenA, tokenB, c.options.stable]
4648
+ });
4649
+ if (poolAddress.toLowerCase() === ZERO_ADDRESS.toLowerCase()) {
4650
+ return c.error({
4651
+ code: "POOL_NOT_FOUND",
4652
+ message: `No ${c.options.stable ? "stable" : "volatile"} V2 pool found for pair ${checksumAddress4(tokenA)}/${checksumAddress4(tokenB)}.`
4653
+ });
4654
+ }
4655
+ const tokenMeta = await readTokenMetadata3(client, [tokenA, tokenB]);
4656
+ const aMeta = tokenMeta.get(tokenA.toLowerCase()) ?? fallbackTokenMeta(tokenA);
4657
+ const bMeta = tokenMeta.get(tokenB.toLowerCase()) ?? fallbackTokenMeta(tokenB);
4658
+ const slippageBps = Math.round(c.options.slippage * 100);
4659
+ const amountAMin = amountADesired * BigInt(1e4 - slippageBps) / 10000n;
4660
+ const amountBMin = amountBDesired * BigInt(1e4 - slippageBps) / 10000n;
4661
+ const deadlineTimestamp = BigInt(Math.floor(Date.now() / 1e3) + c.options.deadline);
4662
+ const account = resolveAccount(c.env);
4663
+ if (!c.options["dry-run"]) {
4664
+ for (const [token, amount] of [
4665
+ [tokenA, amountADesired],
4666
+ [tokenB, amountBDesired]
4667
+ ]) {
4668
+ const currentAllowance = await client.readContract({
4669
+ abi: erc20ApproveAbi3,
4670
+ address: token,
4671
+ functionName: "allowance",
4672
+ args: [account.address, ABOREAN_V2_ADDRESSES.router]
4673
+ });
4674
+ if (currentAllowance < amount) {
4675
+ await aboreanWriteTx({
4676
+ env: c.env,
4677
+ options: { ...c.options, "dry-run": false },
4678
+ address: token,
4679
+ abi: erc20ApproveAbi3,
4680
+ functionName: "approve",
4681
+ args: [ABOREAN_V2_ADDRESSES.router, amount]
4682
+ });
4683
+ }
4684
+ }
4685
+ }
4686
+ const tx = await aboreanWriteTx({
4687
+ env: c.env,
4688
+ options: c.options,
4689
+ address: ABOREAN_V2_ADDRESSES.router,
4690
+ abi: v2RouterAddLiquidityAbi,
4691
+ functionName: "addLiquidity",
4692
+ args: [
4693
+ tokenA,
4694
+ tokenB,
4695
+ c.options.stable,
4696
+ amountADesired,
4697
+ amountBDesired,
4698
+ amountAMin,
4699
+ amountBMin,
4700
+ account.address,
4701
+ deadlineTimestamp
4702
+ ]
4703
+ });
4704
+ return c.ok({
4705
+ pool: checksumAddress4(poolAddress),
4706
+ stable: c.options.stable,
4707
+ tokenA: aMeta,
4708
+ tokenB: bMeta,
4709
+ amountADesired: {
4710
+ raw: amountADesired.toString(),
4711
+ decimal: formatUnits3(amountADesired, aMeta.decimals)
4712
+ },
4713
+ amountBDesired: {
4714
+ raw: amountBDesired.toString(),
4715
+ decimal: formatUnits3(amountBDesired, bMeta.decimals)
4716
+ },
4717
+ amountAMin: {
4718
+ raw: amountAMin.toString(),
4719
+ decimal: formatUnits3(amountAMin, aMeta.decimals)
4720
+ },
4721
+ amountBMin: {
4722
+ raw: amountBMin.toString(),
4723
+ decimal: formatUnits3(amountBMin, bMeta.decimals)
4724
+ },
4725
+ slippagePercent: c.options.slippage,
4726
+ tx
4727
+ });
4728
+ }
4729
+ });
4730
+ pools.command("remove-liquidity", {
4731
+ description: "Remove liquidity from a V2 pool. Approves the LP token to the router if needed, then calls removeLiquidity. Supports --dry-run.",
4732
+ options: z5.object({
4733
+ "token-a": z5.string().describe("First token address of the pair"),
4734
+ "token-b": z5.string().describe("Second token address of the pair"),
4735
+ liquidity: z5.string().describe("Amount of LP tokens to burn (in wei)"),
4736
+ slippage: z5.coerce.number().min(0).max(100).default(DEFAULT_SLIPPAGE_PERCENT2).describe("Slippage tolerance in percent (default: 0.5)"),
4737
+ deadline: z5.coerce.number().int().positive().default(DEFAULT_DEADLINE_SECONDS2).describe("Transaction deadline in seconds from now (default: 300)"),
4738
+ stable: z5.boolean().default(false).describe("Target stable pool (default: volatile)")
4739
+ }).merge(writeOptions),
4740
+ env: writeEnv,
4741
+ output: z5.object({
4742
+ pool: z5.string(),
4743
+ stable: z5.boolean(),
4744
+ tokenA: tokenSchema2,
4745
+ tokenB: tokenSchema2,
4746
+ liquidity: amountSchema2,
4747
+ amountAMin: amountSchema2,
4748
+ amountBMin: amountSchema2,
4749
+ slippagePercent: z5.number(),
4750
+ tx: z5.union([
4751
+ z5.object({
4752
+ txHash: z5.string(),
4753
+ blockNumber: z5.number(),
4754
+ gasUsed: z5.string()
4755
+ }),
4756
+ z5.object({
4757
+ dryRun: z5.literal(true),
4758
+ estimatedGas: z5.string(),
4759
+ simulationResult: z5.unknown()
4760
+ })
4761
+ ])
4762
+ }),
4763
+ async run(c) {
4764
+ const tokenARaw = c.options["token-a"];
4765
+ const tokenBRaw = c.options["token-b"];
4766
+ if (!isAddress4(tokenARaw) || !isAddress4(tokenBRaw)) {
4767
+ return c.error({
4768
+ code: "INVALID_ADDRESS",
4769
+ message: "token-a and token-b must both be valid 0x-prefixed 20-byte addresses."
4770
+ });
4771
+ }
4772
+ const tokenA = toAddress(tokenARaw);
4773
+ const tokenB = toAddress(tokenBRaw);
4774
+ let liquidityAmount;
4775
+ try {
4776
+ liquidityAmount = BigInt(c.options.liquidity);
4777
+ } catch {
4778
+ return c.error({
4779
+ code: "INVALID_AMOUNT",
4780
+ message: "liquidity must be a valid integer in wei."
4781
+ });
4782
+ }
4783
+ if (liquidityAmount <= 0n) {
4784
+ return c.error({
4785
+ code: "INVALID_AMOUNT",
4786
+ message: "liquidity must be a positive integer."
4787
+ });
4788
+ }
4789
+ const client = createAboreanPublicClient(c.env.ABSTRACT_RPC_URL);
4790
+ const poolAddress = await client.readContract({
4791
+ abi: poolFactoryAbi,
4792
+ address: ABOREAN_V2_ADDRESSES.poolFactory,
4793
+ functionName: "getPool",
4794
+ args: [tokenA, tokenB, c.options.stable]
4795
+ });
4796
+ if (poolAddress.toLowerCase() === ZERO_ADDRESS.toLowerCase()) {
4797
+ return c.error({
4798
+ code: "POOL_NOT_FOUND",
4799
+ message: `No ${c.options.stable ? "stable" : "volatile"} V2 pool found for pair ${checksumAddress4(tokenA)}/${checksumAddress4(tokenB)}.`
4800
+ });
4801
+ }
4802
+ const [poolStates] = await readPoolStates2(client, [poolAddress]);
4803
+ const tokenMeta = await readTokenMetadata3(client, [tokenA, tokenB]);
4804
+ const aMeta = tokenMeta.get(tokenA.toLowerCase()) ?? fallbackTokenMeta(tokenA);
4805
+ const bMeta = tokenMeta.get(tokenB.toLowerCase()) ?? fallbackTokenMeta(tokenB);
4806
+ const totalSupply = poolStates.totalSupply;
4807
+ const isTokenAToken0 = tokenA.toLowerCase() === poolStates.token0.toLowerCase();
4808
+ const reserveA = isTokenAToken0 ? poolStates.reserve0 : poolStates.reserve1;
4809
+ const reserveB = isTokenAToken0 ? poolStates.reserve1 : poolStates.reserve0;
4810
+ const expectedAmountA = totalSupply > 0n ? liquidityAmount * reserveA / totalSupply : 0n;
4811
+ const expectedAmountB = totalSupply > 0n ? liquidityAmount * reserveB / totalSupply : 0n;
4812
+ const slippageBps = Math.round(c.options.slippage * 100);
4813
+ const amountAMin = expectedAmountA * BigInt(1e4 - slippageBps) / 10000n;
4814
+ const amountBMin = expectedAmountB * BigInt(1e4 - slippageBps) / 10000n;
4815
+ const deadlineTimestamp = BigInt(Math.floor(Date.now() / 1e3) + c.options.deadline);
4816
+ const account = resolveAccount(c.env);
4817
+ if (!c.options["dry-run"]) {
4818
+ const currentAllowance = await client.readContract({
4819
+ abi: erc20ApproveAbi3,
4820
+ address: poolAddress,
4821
+ functionName: "allowance",
4822
+ args: [account.address, ABOREAN_V2_ADDRESSES.router]
4823
+ });
4824
+ if (currentAllowance < liquidityAmount) {
4825
+ await aboreanWriteTx({
4826
+ env: c.env,
4827
+ options: { ...c.options, "dry-run": false },
4828
+ address: poolAddress,
4829
+ abi: erc20ApproveAbi3,
4830
+ functionName: "approve",
4831
+ args: [ABOREAN_V2_ADDRESSES.router, liquidityAmount]
4832
+ });
4833
+ }
4834
+ }
4835
+ const tx = await aboreanWriteTx({
4836
+ env: c.env,
4837
+ options: c.options,
4838
+ address: ABOREAN_V2_ADDRESSES.router,
4839
+ abi: v2RouterRemoveLiquidityAbi,
4840
+ functionName: "removeLiquidity",
4841
+ args: [
4842
+ tokenA,
4843
+ tokenB,
4844
+ c.options.stable,
4845
+ liquidityAmount,
4846
+ amountAMin,
4847
+ amountBMin,
4848
+ account.address,
4849
+ deadlineTimestamp
4850
+ ]
4851
+ });
4852
+ return c.ok({
4853
+ pool: checksumAddress4(poolAddress),
4854
+ stable: c.options.stable,
4855
+ tokenA: aMeta,
4856
+ tokenB: bMeta,
4857
+ liquidity: {
4858
+ raw: liquidityAmount.toString(),
4859
+ decimal: formatUnits3(liquidityAmount, 18)
4860
+ },
4861
+ amountAMin: {
4862
+ raw: amountAMin.toString(),
4863
+ decimal: formatUnits3(amountAMin, aMeta.decimals)
4864
+ },
4865
+ amountBMin: {
4866
+ raw: amountBMin.toString(),
4867
+ decimal: formatUnits3(amountBMin, bMeta.decimals)
4868
+ },
4869
+ slippagePercent: c.options.slippage,
4870
+ tx
4871
+ });
4872
+ }
4873
+ });
3871
4874
 
3872
4875
  // src/commands/vaults.ts
3873
- import { checksumAddress as checksumAddress5, isAddress as isAddress4 } from "@spectratools/cli-shared";
4876
+ import { checksumAddress as checksumAddress5, isAddress as isAddress5 } from "@spectratools/cli-shared";
3874
4877
  import { Cli as Cli5, z as z6 } from "incur";
3875
4878
  var env5 = z6.object({
3876
4879
  ABSTRACT_RPC_URL: z6.string().optional().describe("Abstract RPC URL override")
@@ -4139,7 +5142,7 @@ vaults.command("relay", {
4139
5142
  }
4140
5143
  ],
4141
5144
  async run(c) {
4142
- if (!isAddress4(c.args.relay)) {
5145
+ if (!isAddress5(c.args.relay)) {
4143
5146
  return c.error({
4144
5147
  code: "INVALID_ARGUMENT",
4145
5148
  message: "relay must be a valid address"
@@ -4488,7 +5491,21 @@ ve.command("voting-power", {
4488
5491
  });
4489
5492
 
4490
5493
  // src/commands/voter.ts
5494
+ import { isAddress as isAddress6 } from "@spectratools/cli-shared";
4491
5495
  import { Cli as Cli7, z as z8 } from "incur";
5496
+ var voterVoteAbi = [
5497
+ {
5498
+ type: "function",
5499
+ name: "vote",
5500
+ stateMutability: "nonpayable",
5501
+ inputs: [
5502
+ { name: "tokenId", type: "uint256" },
5503
+ { name: "_poolVote", type: "address[]" },
5504
+ { name: "_weights", type: "uint256[]" }
5505
+ ],
5506
+ outputs: []
5507
+ }
5508
+ ];
4492
5509
  var env7 = z8.object({
4493
5510
  ABSTRACT_RPC_URL: z8.string().optional().describe("Abstract RPC URL override")
4494
5511
  });
@@ -4917,6 +5934,139 @@ voter.command("bribes", {
4917
5934
  );
4918
5935
  }
4919
5936
  });
5937
+ voter.command("vote", {
5938
+ description: "Cast votes for gauge(s) using a veNFT. Weights are relative and will be normalized by the Voter contract. Each pool address must have an active gauge.",
5939
+ options: z8.object({
5940
+ "token-id": z8.coerce.number().int().nonnegative().describe("veNFT token id to vote with"),
5941
+ pools: z8.string().describe("Comma-separated pool addresses to vote for"),
5942
+ weights: z8.string().describe("Comma-separated relative weights (matching pool order)")
5943
+ }).merge(writeOptions),
5944
+ env: writeEnv,
5945
+ output: z8.object({
5946
+ tokenId: z8.number(),
5947
+ pools: z8.array(z8.string()),
5948
+ weights: z8.array(z8.string()),
5949
+ tx: z8.union([
5950
+ z8.object({
5951
+ txHash: z8.string(),
5952
+ blockNumber: z8.number(),
5953
+ gasUsed: z8.string()
5954
+ }),
5955
+ z8.object({
5956
+ dryRun: z8.literal(true),
5957
+ estimatedGas: z8.string(),
5958
+ simulationResult: z8.unknown()
5959
+ })
5960
+ ])
5961
+ }),
5962
+ examples: [
5963
+ {
5964
+ options: {
5965
+ "token-id": 1,
5966
+ pools: "0x0000000000000000000000000000000000000001",
5967
+ weights: "100",
5968
+ "dry-run": true
5969
+ },
5970
+ description: "Dry-run vote for a single pool with weight 100"
5971
+ },
5972
+ {
5973
+ options: {
5974
+ "token-id": 1,
5975
+ pools: "0x0000000000000000000000000000000000000001,0x0000000000000000000000000000000000000002",
5976
+ weights: "60,40",
5977
+ "dry-run": true
5978
+ },
5979
+ description: "Dry-run vote for two pools with relative weights"
5980
+ }
5981
+ ],
5982
+ async run(c) {
5983
+ const poolAddresses = c.options.pools.split(",").map((s) => s.trim());
5984
+ const weightValues = c.options.weights.split(",").map((s) => s.trim());
5985
+ if (poolAddresses.length === 0 || poolAddresses[0] === "") {
5986
+ return c.error({
5987
+ code: "INVALID_INPUT",
5988
+ message: "At least one pool address is required."
5989
+ });
5990
+ }
5991
+ if (poolAddresses.length !== weightValues.length) {
5992
+ return c.error({
5993
+ code: "INVALID_INPUT",
5994
+ message: `Pool count (${poolAddresses.length}) and weight count (${weightValues.length}) must match.`
5995
+ });
5996
+ }
5997
+ for (const addr of poolAddresses) {
5998
+ if (!isAddress6(addr)) {
5999
+ return c.error({
6000
+ code: "INVALID_ADDRESS",
6001
+ message: `Invalid pool address: "${addr}". Use valid 0x-prefixed 20-byte hex addresses.`
6002
+ });
6003
+ }
6004
+ }
6005
+ const parsedWeights = [];
6006
+ for (const w of weightValues) {
6007
+ const parsed = BigInt(w);
6008
+ if (parsed <= 0n) {
6009
+ return c.error({
6010
+ code: "INVALID_INPUT",
6011
+ message: `Weight must be a positive integer, got: "${w}".`
6012
+ });
6013
+ }
6014
+ parsedWeights.push(parsed);
6015
+ }
6016
+ const client = createAboreanPublicClient(c.env.ABSTRACT_RPC_URL);
6017
+ const gaugeResults = await client.multicall({
6018
+ allowFailure: false,
6019
+ contracts: poolAddresses.map((pool) => ({
6020
+ abi: voterAbi,
6021
+ address: ABOREAN_V2_ADDRESSES.voter,
6022
+ functionName: "gauges",
6023
+ args: [pool]
6024
+ }))
6025
+ });
6026
+ for (let i = 0; i < gaugeResults.length; i++) {
6027
+ if (gaugeResults[i].toLowerCase() === ZERO_ADDRESS.toLowerCase()) {
6028
+ return c.error({
6029
+ code: "GAUGE_NOT_FOUND",
6030
+ message: `No gauge exists for pool ${poolAddresses[i]}.`,
6031
+ retryable: false
6032
+ });
6033
+ }
6034
+ }
6035
+ const aliveResults = await client.multicall({
6036
+ allowFailure: false,
6037
+ contracts: gaugeResults.map((gauge) => ({
6038
+ abi: voterAbi,
6039
+ address: ABOREAN_V2_ADDRESSES.voter,
6040
+ functionName: "isAlive",
6041
+ args: [gauge]
6042
+ }))
6043
+ });
6044
+ for (let i = 0; i < aliveResults.length; i++) {
6045
+ if (!aliveResults[i]) {
6046
+ return c.error({
6047
+ code: "GAUGE_NOT_ALIVE",
6048
+ message: `Gauge for pool ${poolAddresses[i]} is not alive.`,
6049
+ retryable: false
6050
+ });
6051
+ }
6052
+ }
6053
+ const tokenId = BigInt(c.options["token-id"]);
6054
+ const tx = await aboreanWriteTx({
6055
+ env: c.env,
6056
+ options: c.options,
6057
+ address: ABOREAN_V2_ADDRESSES.voter,
6058
+ abi: voterVoteAbi,
6059
+ functionName: "vote",
6060
+ args: [tokenId, poolAddresses, parsedWeights]
6061
+ });
6062
+ return c.ok({
6063
+ tokenId: c.options["token-id"],
6064
+ pools: poolAddresses.map((p) => toChecksum(p)),
6065
+ weights: parsedWeights.map((w) => w.toString()),
6066
+ tx
6067
+ });
6068
+ }
6069
+ });
4920
6070
 
4921
6071
  // src/error-handling.ts
4922
6072
  import { AsyncLocalStorage } from "async_hooks";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spectratools/aborean-cli",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "CLI for Aborean Finance DEX on Abstract (pools, swaps, gauges, voting escrow).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,7 +33,7 @@
33
33
  "incur": "^0.3.0",
34
34
  "ox": "^0.14.0",
35
35
  "viem": "^2.47.0",
36
- "@spectratools/cli-shared": "0.1.1",
36
+ "@spectratools/cli-shared": "0.1.2",
37
37
  "@spectratools/tx-shared": "0.5.3"
38
38
  },
39
39
  "devDependencies": {