Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@
"@coinbase/x402": "^2.1.0",
"@metamask/smart-accounts-kit": "^0.4.0-beta.1",
"@uniswap/sdk-core": "^7.12.2",
"@uniswap/v3-sdk": "^3.29.1",
"@uniswap/v4-sdk": "^1.29.1",
"@x402/core": "^2.11.0",
"@x402/evm": "^2.11.0",
Comment thread
gabririgo marked this conversation as resolved.
"@x402/extensions": "^2.11.0",
"hono": "^4.6.0",
"jsbi": "^3.1.4",
"openai": "^4.70.0",
"viem": "^2.21.0"
},
Expand Down
67 changes: 65 additions & 2 deletions src/llm/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ import {
buildRebalancePlan,
chainName as crosschainChainName,
} from "../services/crosschain.js";
import { buildAddLiquidityTx, buildRemoveLiquidityTx, getVaultLPPositions, buildCollectFeesTx, buildBurnPositionTx, getPoolInfoById, getPositionDirect } from "../services/uniswapLP.js";
import { buildAddLiquidityTx, buildRemoveLiquidityTx, buildInitializePoolTx, getVaultLPPositions, buildCollectFeesTx, buildBurnPositionTx, getPoolInfoById, getPositionDirect, POOL_MANAGER } from "../services/uniswapLP.js";
import {
buildStakeCalldata,
buildUndelegateStakeCalldata,
Expand Down Expand Up @@ -105,6 +105,7 @@ export const TOOL_NAME_ALIASES: Record<string, string> = {
trade: "build_vault_swap",
build_lp_add: "add_liquidity",
build_lp_remove: "remove_liquidity",
init_pool: "initialize_pool",
bridge_tokens: "crosschain_transfer",
stake_grg: "grg_stake",
equalize_nav: "crosschain_sync",
Expand Down Expand Up @@ -4063,7 +4064,16 @@ export async function executeToolCall(
`Currency 1: ${info.currency1}`,
`Current Tick: ${info.currentTick}`,
``,
`To add liquidity: use fee=${info.fee}, tickSpacing=${info.tickSpacing}, hooks=${info.hooks}`,
info.initialized
? info.poolKeyKnown
? `To add liquidity: use fee=${info.fee}, tickSpacing=${info.tickSpacing}, hooks=${info.hooks}`
: `⚠️ Pool is initialized but the pool key could not be confirmed from on-chain Initialize event logs (RPC/node limitation). ` +
`The fee/tickSpacing/hooks/currency0/currency1 values shown may be placeholders or estimates. ` +
`Please verify the exact pool key (fee, tickSpacing, hooks) before calling add_liquidity to avoid a pool-key mismatch. ` +
`You can retry get_pool_info with a different RPC, or provide the known pool key directly.`
: info.poolKeyKnown
? `⚠️ Pool is NOT initialized. Call initialize_pool first with fee=${info.fee}, tickSpacing=${info.tickSpacing}, hooks=${info.hooks}`
: `⚠️ Pool is NOT initialized. The pool key (fee, tickSpacing, hooks, token0, token1) could not be determined from the pool ID alone — please provide the full pool key so initialize_pool can be called with the correct parameters.`,
].join("\n");

return { message };
Expand Down Expand Up @@ -4120,6 +4130,59 @@ export async function executeToolCall(
return { message, transaction, chainSwitch: chainSwitched };
}

case "initialize_pool": {
if (!ctx.operatorAddress) {
throw new Error("Wallet not connected. Connect your wallet first.");
}

let chainSwitched: number | undefined;
if (args.chain) {
const match = resolveChainArg((args.chain as string).trim());
if (match.id !== ctx.chainId) {
ctx.chainId = match.id;
chainSwitched = match.id;
}
}

const result = await buildInitializePoolTx(env, {
tokenA: args.tokenA as string,
tokenB: args.tokenB as string,
fee: args.fee as number,
tickSpacing: args.tickSpacing as number | undefined,
hooks: args.hooks as Address | undefined,
sqrtPriceX96: args.sqrtPriceX96 as string | undefined,
amountA: args.amountA as string | undefined,
amountB: args.amountB as string | undefined,
}, ctx.chainId);

const transaction: UnsignedTransaction = {
to: POOL_MANAGER[ctx.chainId],
data: result.calldata,
value: "0x0",
chainId: ctx.chainId,
gas: await estimateGas(
ctx.chainId, POOL_MANAGER[ctx.chainId],
result.calldata, "0x0",
ctx.operatorAddress, env.ALCHEMY_API_KEY, "lp",
),
description: result.description,
operatorOnly: true,
};

const chainName = resolveChainName(ctx.chainId);
const message = [
`✅ Initialize Pool ready`,
`${result.description}`,
`Pool ID: ${result.poolId}`,
`Chain: ${chainName}`,
``,
`⚠️ This transaction must be signed and broadcast from your wallet (not the vault). ` +
`After confirmation, you can add liquidity via add_liquidity.`,
].join("\n");

return { message, transaction, chainSwitch: chainSwitched };
}

case "remove_liquidity": {
if (!ctx.operatorAddress) {
throw new Error("Wallet not connected. Connect your wallet first.");
Expand Down
14 changes: 11 additions & 3 deletions src/llm/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export const DOMAIN_TOOLS: Record<DomainKey, string[]> = {
"gmx_claim_funding_fees", "gmx_get_markets",
],
lp: [
"get_pool_info", "add_liquidity", "remove_liquidity",
"get_pool_info", "initialize_pool", "add_liquidity", "remove_liquidity",
"get_lp_positions", "collect_lp_fees", "burn_position",
],
bridge: [
Expand Down Expand Up @@ -296,8 +296,16 @@ GMX INTENT PARSING:
Ask the user for pool details or a pool ID, then call get_pool_info to discover the exact pool key.

1. Call get_pool_info if you only have the pool ID.
2. Call add_liquidity with ONE token amount — the backend computes the optimal counterpart.
3. tickRange options: "full" (entire range), "wide" (±50%), "narrow" (±5%), or exact "tickLower,tickUpper".
2. If the pool is NOT initialized, call initialize_pool first (provide both token amounts to compute the initial price).
3. Call add_liquidity with ONE token amount — the backend computes the optimal counterpart.
4. tickRange options: "full" (entire range), "wide" (±50%), "narrow" (±5%), or exact "tickLower,tickUpper".

POOL INITIALIZATION:
- Uniswap v4 pools must be initialized before adding liquidity.
- initialize_pool targets the PoolManager directly (not the vault) and sets the initial price.
- Anyone can initialize a pool — it does not require vault ownership.
- After initialization, use add_liquidity to add liquidity through the vault.
- fee=0 does NOT auto-derive tickSpacing=32767. Always pass tickSpacing explicitly for non-standard pools.

LP POSITION LIFECYCLE:
Active → [remove_liquidity] → Closed (0 liquidity, fees may remain, NFT exists)
Expand Down
86 changes: 84 additions & 2 deletions src/llm/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,9 @@ export const TOOL_DEFINITIONS = [
"the backend computes the optimal counterpart amount from the current pool price and tick range. " +
"IMPORTANT: Uniswap v4 supports infinite fee tiers and custom tickSpacings — the fee and " +
"tickSpacing must match the pool exactly or the transaction will fail. " +
"CRITICAL: fee=0 does NOT auto-derive tickSpacing=32767; the default for fee=0 is tickSpacing=1. " +
"Always provide tickSpacing explicitly for non-standard pools (e.g. oracle pools with tickSpacing=32767). " +
"If the pool is not yet initialized, call initialize_pool first. " +
"If you only know the pool ID, call get_pool_info first to discover the exact pool key.",
parameters: {
type: "object",
Expand Down Expand Up @@ -769,8 +772,10 @@ export const TOOL_DEFINITIONS = [
tickSpacing: {
type: "number",
description:
"Tick spacing for the pool — must match the pool exactly for non-standard pools. " +
"Auto-derived from fee if not specified: 100→1, 500→10, 3000→60, 6000→120, 10000→200. " +
"Tick spacing for the pool — REQUIRED when fee does not map to a standard tier. " +
"Auto-derived from fee if omitted: 100→1, 500→10, 3000→60, 6000→120, 10000→200. " +
"WARNING: fee=0 defaults to tickSpacing=1, NOT 32767. " +
"Oracle pools and full-range positions often use tickSpacing=32767 — pass it explicitly. " +
"Use get_pool_info to retrieve the exact value for any pool.",
},
tickRange: {
Expand All @@ -794,6 +799,83 @@ export const TOOL_DEFINITIONS = [
},
},
},
{
type: "function" as const,
function: {
name: "initialize_pool",
description:
"Initialize a Uniswap v4 pool on the PoolManager. " +
"ANYONE can initialize a pool — it does not require vault ownership. " +
"Initialization sets the initial price (sqrtPriceX96) and creates the pool so liquidity can be added. " +
"You must provide either (1) both token amounts, from which the initial price is computed, OR " +
"(2) an explicit sqrtPriceX96 string. " +
"After initialization, use add_liquidity to add liquidity through the vault. " +
"This transaction targets the PoolManager directly (not the vault) and must be signed by the operator.",
parameters: {
type: "object",
properties: {
tokenA: {
type: "string",
description: "First token — symbol (e.g., ETH, WBTC, USDC) or address",
},
tokenB: {
type: "string",
description: "Second token — symbol (e.g., USDT, USDC, WBTC) or address",
},
fee: {
type: "number",
description:
"Pool fee in hundredths of a bip — REQUIRED. " +
"Common values: 0=0%, 100=0.01%, 500=0.05%, 3000=0.30%, 10000=1%.",
},
tickSpacing: {
type: "number",
description:
"Tick spacing for the pool — REQUIRED for non-standard pools. " +
"Auto-derived from fee if omitted: 100→1, 500→10, 3000→60, 6000→120, 10000→200. " +
"WARNING: fee=0 defaults to tickSpacing=1, NOT 32767.",
},
hooks: {
type: "string",
description: "Hook contract address. Default: zero address (no hooks).",
},
sqrtPriceX96: {
type: "string",
description:
"Explicit initial sqrtPriceX96 as a decimal string (Q64.96 fixed-point). " +
"If omitted, amountA and amountB must be provided to compute the price automatically.",
},
amountA: {
type: "string",
description:
"Amount of tokenA to use for price computation (human-readable). " +
"Required if sqrtPriceX96 is omitted.",
},
amountB: {
type: "string",
description:
"Amount of tokenB to use for price computation (human-readable). " +
"Required if sqrtPriceX96 is omitted.",
},
chain: {
type: "string",
description: "Target chain name or ID (e.g., 'ethereum', 'base', '42161')",
},
},
required: ["tokenA", "tokenB", "fee"],
Comment thread
gabririgo marked this conversation as resolved.
oneOf: [
{
description: "Provide an explicit initial sqrtPriceX96",
required: ["sqrtPriceX96"],
},
{
description: "Provide both token amounts to compute the initial price automatically",
required: ["amountA", "amountB"],
},
],
},
},
},
{
type: "function" as const,
function: {
Expand Down
2 changes: 1 addition & 1 deletion src/routes/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function getToolCategory(name: string): string {
if (name.startsWith("get_swap_quote") || name === "build_vault_swap") return "Spot Trading";
if (name.startsWith("get_vault_info") || name === "get_token_balance" || name === "switch_chain") return "Vault Info";
if (name.startsWith("gmx_")) return "GMX Perpetuals";
if (name.startsWith("get_pool_info") || name.startsWith("add_liquidity") || name.startsWith("remove_liquidity") || name.startsWith("collect_lp_fees") || name.startsWith("burn_position") || name.startsWith("get_lp_positions")) return "Uniswap v4 LP";
if (name.startsWith("get_pool_info") || name.startsWith("add_liquidity") || name.startsWith("remove_liquidity") || name.startsWith("collect_lp_fees") || name.startsWith("burn_position") || name.startsWith("get_lp_positions") || name.startsWith("initialize_pool")) return "Uniswap v4 LP";
if (name.startsWith("crosschain_") || name.startsWith("get_aggregated_nav") || name.startsWith("get_rebalance_plan") || name.startsWith("verify_bridge_arrival")) return "Cross-Chain";
if (name.startsWith("grg_")) return "GRG Staking";
if (name === "deploy_smart_pool" || name === "fund_pool") return "Vault Management";
Expand Down
Loading