diff --git a/packages/connector-ui/public/SKILL.md b/packages/connector-ui/public/SKILL.md index 91625da..c65b57e 100644 --- a/packages/connector-ui/public/SKILL.md +++ b/packages/connector-ui/public/SKILL.md @@ -99,7 +99,7 @@ polygon-agent wallet remove [--name ] ### Operations ```bash -polygon-agent balances [--wallet ] [--chain ] +polygon-agent balances [--wallet ] [--chain ] [--chains ] polygon-agent send --to --amount [--symbol ] [--token ] [--decimals ] [--broadcast] polygon-agent send-native --to --amount [--broadcast] [--direct] polygon-agent send-token --symbol --to --amount [--token ] [--decimals ] [--broadcast] @@ -127,6 +127,7 @@ polygon-agent agent feedback --agent-id --value [--tag1 ] [--tag - **Dry-run by default** — all write commands require `--broadcast` to execute - **Smart defaults** — `--wallet main`, `--chain polygon`, auto-wait on `wallet create` +- **`balances --chains`** — comma-separated chains (max 20); two or more return JSON with `multiChain: true` and a `chains` array (same wallet address on each) - **Fee preference** — auto-selects USDC over native POL when both available - **`fund`** — reads `walletAddress` from the wallet session and sets it as `toAddress` in the Trails widget URL. Always run `polygon-agent fund` to get the correct URL — never construct it manually or hardcode any address. - **`deposit`** — picks highest-TVL pool via Trails `getEarnPools`. If session rejects (contract not whitelisted), re-create wallet with `--contract ` diff --git a/packages/connector-ui/public/skills/SKILL.md b/packages/connector-ui/public/skills/SKILL.md index 91625da..c65b57e 100644 --- a/packages/connector-ui/public/skills/SKILL.md +++ b/packages/connector-ui/public/skills/SKILL.md @@ -99,7 +99,7 @@ polygon-agent wallet remove [--name ] ### Operations ```bash -polygon-agent balances [--wallet ] [--chain ] +polygon-agent balances [--wallet ] [--chain ] [--chains ] polygon-agent send --to --amount [--symbol ] [--token ] [--decimals ] [--broadcast] polygon-agent send-native --to --amount [--broadcast] [--direct] polygon-agent send-token --symbol --to --amount [--token ] [--decimals ] [--broadcast] @@ -127,6 +127,7 @@ polygon-agent agent feedback --agent-id --value [--tag1 ] [--tag - **Dry-run by default** — all write commands require `--broadcast` to execute - **Smart defaults** — `--wallet main`, `--chain polygon`, auto-wait on `wallet create` +- **`balances --chains`** — comma-separated chains (max 20); two or more return JSON with `multiChain: true` and a `chains` array (same wallet address on each) - **Fee preference** — auto-selects USDC over native POL when both available - **`fund`** — reads `walletAddress` from the wallet session and sets it as `toAddress` in the Trails widget URL. Always run `polygon-agent fund` to get the correct URL — never construct it manually or hardcode any address. - **`deposit`** — picks highest-TVL pool via Trails `getEarnPools`. If session rejects (contract not whitelisted), re-create wallet with `--contract ` diff --git a/packages/polygon-agent-cli/README.md b/packages/polygon-agent-cli/README.md index 3c84957..ceae61b 100644 --- a/packages/polygon-agent-cli/README.md +++ b/packages/polygon-agent-cli/README.md @@ -173,7 +173,9 @@ polygon-agent fund # Open funding widget ### Token Operations ```bash -polygon-agent balances # Check all balances +polygon-agent balances # Balances on session default chain +polygon-agent balances --chain arbitrum # Single chain override +polygon-agent balances --chains polygon,base,arbitrum # Same wallet, multiple chains (JSON) polygon-agent send --to 0x... --amount 1.0 # Send POL (dry-run) polygon-agent send --symbol USDC --to 0x... --amount 10 --broadcast polygon-agent swap --from USDC --to USDT --amount 5 --broadcast @@ -194,6 +196,7 @@ polygon-agent agent reviews --agent-id | ------------- | ---------------------- | -------------------- | | Wallet name | `main` | `--name ` | | Chain | `polygon` | `--chain ` | +| Multi-chain balances | — | `--chains ` (comma-separated, max 20; overrides `--chain`) | | Wallet create | Auto-wait for approval | `--no-wait` | | Broadcast | Dry-run (preview) | `--broadcast` | diff --git a/packages/polygon-agent-cli/skills/SKILL.md b/packages/polygon-agent-cli/skills/SKILL.md index 91625da..c65b57e 100644 --- a/packages/polygon-agent-cli/skills/SKILL.md +++ b/packages/polygon-agent-cli/skills/SKILL.md @@ -99,7 +99,7 @@ polygon-agent wallet remove [--name ] ### Operations ```bash -polygon-agent balances [--wallet ] [--chain ] +polygon-agent balances [--wallet ] [--chain ] [--chains ] polygon-agent send --to --amount [--symbol ] [--token ] [--decimals ] [--broadcast] polygon-agent send-native --to --amount [--broadcast] [--direct] polygon-agent send-token --symbol --to --amount [--token ] [--decimals ] [--broadcast] @@ -127,6 +127,7 @@ polygon-agent agent feedback --agent-id --value [--tag1 ] [--tag - **Dry-run by default** — all write commands require `--broadcast` to execute - **Smart defaults** — `--wallet main`, `--chain polygon`, auto-wait on `wallet create` +- **`balances --chains`** — comma-separated chains (max 20); two or more return JSON with `multiChain: true` and a `chains` array (same wallet address on each) - **Fee preference** — auto-selects USDC over native POL when both available - **`fund`** — reads `walletAddress` from the wallet session and sets it as `toAddress` in the Trails widget URL. Always run `polygon-agent fund` to get the correct URL — never construct it manually or hardcode any address. - **`deposit`** — picks highest-TVL pool via Trails `getEarnPools`. If session rejects (contract not whitelisted), re-create wallet with `--contract ` diff --git a/packages/polygon-agent-cli/src/commands/operations.ts b/packages/polygon-agent-cli/src/commands/operations.ts index 7e84c07..df66c17 100644 --- a/packages/polygon-agent-cli/src/commands/operations.ts +++ b/packages/polygon-agent-cli/src/commands/operations.ts @@ -111,16 +111,114 @@ async function getTokenConfig({ const bigintReplacer = (_k: string, v: unknown) => (typeof v === 'bigint' ? v.toString() : v); +const BALANCES_MAX_CHAINS = 20; + +function parseCommaChainList(chainsArg: string | undefined): string[] { + if (!chainsArg || typeof chainsArg !== 'string') return []; + return chainsArg + .split(',') + .map((s) => s.trim()) + .filter(Boolean); +} + +type BalanceRowJson = + | { type: 'native'; symbol: string; balance: string } + | { + type: 'erc20'; + symbol: string; + name?: string; + contractAddress: string; + balance: string; + }; + +async function fetchBalancesRowsForChain( + walletAddress: string, + chainSpec: string, + indexerKey: string +): Promise<{ chainId: number; chain: string; balances: BalanceRowJson[] }> { + const network = resolveNetwork(chainSpec); + const nativeDecimals = network.nativeToken?.decimals ?? 18; + const nativeSymbol = network.nativeToken?.symbol || 'POL'; + + const { SequenceIndexer } = await import('@0xsequence/indexer'); + const indexerUrl = getChainIndexerUrl(network.chainId); + const indexer = new SequenceIndexer(indexerUrl, indexerKey); + + const [nativeRes, tokenRes] = await Promise.all([ + indexer.getNativeTokenBalance({ + accountAddress: walletAddress + }), + indexer.getTokenBalances({ + accountAddress: walletAddress, + includeMetadata: true + }) + ]); + + const nativeWei = nativeRes?.balance?.balance || '0'; + const native: BalanceRowJson[] = [ + { + type: 'native', + symbol: nativeSymbol, + balance: formatUnits(BigInt(nativeWei), nativeDecimals) + } + ]; + + const erc20: BalanceRowJson[] = (tokenRes?.balances || []).map( + (b: { + contractInfo?: { symbol?: string; name?: string; decimals?: number }; + contractAddress: string; + balance?: string; + }) => ({ + type: 'erc20' as const, + symbol: b.contractInfo?.symbol || 'ERC20', + name: b.contractInfo?.name || undefined, + contractAddress: b.contractAddress, + balance: formatUnits(b.balance || '0', b.contractInfo?.decimals ?? 18) + }) + ); + + return { + chainId: network.chainId, + chain: network.name, + balances: [...native, ...erc20] + }; +} + // --- balances --- export const balancesCommand: CommandModule = { command: 'balances', describe: 'Check token balances', - builder: (yargs) => withWalletAndChain(yargs), + builder: (yargs) => + withWalletAndChain(yargs).option('chains', { + type: 'string', + describe: + 'Comma-separated chain names or IDs (e.g. polygon,base,arbitrum). When set, overrides --chain. Two or more chains return multi-chain JSON (TTY included).' + }), handler: async (argv) => { const walletName = argv.wallet as string; + const chainListRaw = parseCommaChainList(argv.chains as string | undefined); + if (chainListRaw.length > BALANCES_MAX_CHAINS) { + console.error( + JSON.stringify( + { + ok: false, + error: `Too many chains in --chains (max ${BALANCES_MAX_CHAINS}).` + }, + null, + 2 + ) + ); + process.exit(1); + } + const chainList = chainListRaw; - if (!isTTY()) { - // Non-TTY: original JSON output + const preferChainsArg = chainList.length > 0; + const singleChainSpec = preferChainsArg + ? chainList[0] + : ((argv.chain as string) || undefined); + const multiChainMode = preferChainsArg && chainList.length > 1; + + if (multiChainMode || (preferChainsArg && !isTTY())) { try { const session = await loadWalletSession(walletName); if (!session) { @@ -135,46 +233,81 @@ export const balancesCommand: CommandModule = { throw new Error('Missing project access key (not in wallet session or environment)'); } - const network = resolveNetwork((argv.chain as string) || session.chain || 'polygon'); - const nativeDecimals = network.nativeToken?.decimals ?? 18; - const nativeSymbol = network.nativeToken?.symbol || 'POL'; - - const { SequenceIndexer } = await import('@0xsequence/indexer'); - const indexerUrl = getChainIndexerUrl(network.chainId); - const indexer = new SequenceIndexer(indexerUrl, indexerKey); - - const [nativeRes, tokenRes] = await Promise.all([ - indexer.getNativeTokenBalance({ - accountAddress: session.walletAddress - }), - indexer.getTokenBalances({ - accountAddress: session.walletAddress, - includeMetadata: true - }) - ]); + if (multiChainMode) { + const chainsOut = await Promise.all( + chainList.map((spec) => + fetchBalancesRowsForChain(session.walletAddress, spec, indexerKey) + ) + ); + console.log( + JSON.stringify( + { + ok: true, + walletName, + walletAddress: session.walletAddress, + multiChain: true, + chains: chainsOut + }, + bigintReplacer, + 2 + ) + ); + } else { + const one = await fetchBalancesRowsForChain( + session.walletAddress, + singleChainSpec!, + indexerKey + ); + console.log( + JSON.stringify( + { + ok: true, + walletName, + walletAddress: session.walletAddress, + chainId: one.chainId, + chain: one.chain, + balances: one.balances + }, + bigintReplacer, + 2 + ) + ); + } + } catch (error) { + console.error( + JSON.stringify( + { + ok: false, + error: (error as Error).message, + stack: (error as Error).stack + }, + null, + 2 + ) + ); + process.exit(1); + } + return; + } - const nativeWei = nativeRes?.balance?.balance || '0'; - const native = [ - { - type: 'native', - symbol: nativeSymbol, - balance: formatUnits(BigInt(nativeWei), nativeDecimals) - } - ]; + if (!isTTY()) { + // Non-TTY: original JSON output (single default / --chain) + try { + const session = await loadWalletSession(walletName); + if (!session) { + throw new Error(`Wallet not found: ${walletName}`); + } - const erc20 = (tokenRes?.balances || []).map( - (b: { - contractInfo?: { symbol?: string; name?: string; decimals?: number }; - contractAddress: string; - balance?: string; - }) => ({ - type: 'erc20', - symbol: b.contractInfo?.symbol || 'ERC20', - name: b.contractInfo?.name || undefined, - contractAddress: b.contractAddress, - balance: formatUnits(b.balance || '0', b.contractInfo?.decimals ?? 18) - }) - ); + const indexerKey = + process.env.SEQUENCE_INDEXER_ACCESS_KEY || + session.projectAccessKey || + process.env.SEQUENCE_PROJECT_ACCESS_KEY; + if (!indexerKey) { + throw new Error('Missing project access key (not in wallet session or environment)'); + } + + const chainSpec = (argv.chain as string) || session.chain || 'polygon'; + const one = await fetchBalancesRowsForChain(session.walletAddress, chainSpec, indexerKey); console.log( JSON.stringify( @@ -182,11 +315,11 @@ export const balancesCommand: CommandModule = { ok: true, walletName, walletAddress: session.walletAddress, - chainId: network.chainId, - chain: network.name, - balances: [...native, ...erc20] + chainId: one.chainId, + chain: one.chain, + balances: one.balances }, - null, + bigintReplacer, 2 ) ); @@ -211,7 +344,7 @@ export const balancesCommand: CommandModule = { await inkRender( React.createElement(BalancesUI, { walletName, - chainOverride: argv.chain as string | undefined + chainOverride: preferChainsArg ? singleChainSpec : (argv.chain as string | undefined) }) ); } catch { diff --git a/skills/SKILL.md b/skills/SKILL.md index 91625da..c65b57e 100644 --- a/skills/SKILL.md +++ b/skills/SKILL.md @@ -99,7 +99,7 @@ polygon-agent wallet remove [--name ] ### Operations ```bash -polygon-agent balances [--wallet ] [--chain ] +polygon-agent balances [--wallet ] [--chain ] [--chains ] polygon-agent send --to --amount [--symbol ] [--token ] [--decimals ] [--broadcast] polygon-agent send-native --to --amount [--broadcast] [--direct] polygon-agent send-token --symbol --to --amount [--token ] [--decimals ] [--broadcast] @@ -127,6 +127,7 @@ polygon-agent agent feedback --agent-id --value [--tag1 ] [--tag - **Dry-run by default** — all write commands require `--broadcast` to execute - **Smart defaults** — `--wallet main`, `--chain polygon`, auto-wait on `wallet create` +- **`balances --chains`** — comma-separated chains (max 20); two or more return JSON with `multiChain: true` and a `chains` array (same wallet address on each) - **Fee preference** — auto-selects USDC over native POL when both available - **`fund`** — reads `walletAddress` from the wallet session and sets it as `toAddress` in the Trails widget URL. Always run `polygon-agent fund` to get the correct URL — never construct it manually or hardcode any address. - **`deposit`** — picks highest-TVL pool via Trails `getEarnPools`. If session rejects (contract not whitelisted), re-create wallet with `--contract `