Skip to content

Commit 0439703

Browse files
committed
feat(cli): add --chains comma list for balances
- Parallel Sequence indexer fan-out per chain (max 20) - JSON shape: multiChain + chains[] when 2+ chains; flat unchanged otherwise - Document in README and agent skills (incl. connector-ui copies) Made-with: Cursor
1 parent cf6d5f3 commit 0439703

6 files changed

Lines changed: 191 additions & 51 deletions

File tree

packages/connector-ui/public/SKILL.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ polygon-agent wallet remove [--name <n>]
9797

9898
### Operations
9999
```bash
100-
polygon-agent balances [--wallet <n>] [--chain <chain>]
100+
polygon-agent balances [--wallet <n>] [--chain <chain>] [--chains <csv>]
101101
polygon-agent send --to <addr> --amount <num> [--symbol <SYM>] [--token <addr>] [--decimals <n>] [--broadcast]
102102
polygon-agent send-native --to <addr> --amount <num> [--broadcast] [--direct]
103103
polygon-agent send-token --symbol <SYM> --to <addr> --amount <num> [--token <addr>] [--decimals <n>] [--broadcast]
@@ -125,6 +125,7 @@ polygon-agent agent feedback --agent-id <id> --value <score> [--tag1 <t>] [--tag
125125

126126
- **Dry-run by default** — all write commands require `--broadcast` to execute
127127
- **Smart defaults**`--wallet main`, `--chain polygon`, auto-wait on `wallet create`
128+
- **`balances --chains`** — comma-separated chains (max 20); two or more return JSON with `multiChain: true` and a `chains` array (same wallet address on each)
128129
- **Fee preference** — auto-selects USDC over native POL when both available
129130
- **`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.
130131
- **`deposit`** — picks highest-TVL pool via Trails `getEarnPools`. If session rejects (contract not whitelisted), re-create wallet with `--contract <depositAddress>`

packages/connector-ui/public/skills/SKILL.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ polygon-agent wallet remove [--name <n>]
9797

9898
### Operations
9999
```bash
100-
polygon-agent balances [--wallet <n>] [--chain <chain>]
100+
polygon-agent balances [--wallet <n>] [--chain <chain>] [--chains <csv>]
101101
polygon-agent send --to <addr> --amount <num> [--symbol <SYM>] [--token <addr>] [--decimals <n>] [--broadcast]
102102
polygon-agent send-native --to <addr> --amount <num> [--broadcast] [--direct]
103103
polygon-agent send-token --symbol <SYM> --to <addr> --amount <num> [--token <addr>] [--decimals <n>] [--broadcast]
@@ -125,6 +125,7 @@ polygon-agent agent feedback --agent-id <id> --value <score> [--tag1 <t>] [--tag
125125

126126
- **Dry-run by default** — all write commands require `--broadcast` to execute
127127
- **Smart defaults**`--wallet main`, `--chain polygon`, auto-wait on `wallet create`
128+
- **`balances --chains`** — comma-separated chains (max 20); two or more return JSON with `multiChain: true` and a `chains` array (same wallet address on each)
128129
- **Fee preference** — auto-selects USDC over native POL when both available
129130
- **`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.
130131
- **`deposit`** — picks highest-TVL pool via Trails `getEarnPools`. If session rejects (contract not whitelisted), re-create wallet with `--contract <depositAddress>`

packages/polygon-agent-cli/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,9 @@ polygon-agent fund # Open funding widget
173173
### Token Operations
174174

175175
```bash
176-
polygon-agent balances # Check all balances
176+
polygon-agent balances # Balances on session default chain
177+
polygon-agent balances --chain arbitrum # Single chain override
178+
polygon-agent balances --chains polygon,base,arbitrum # Same wallet, multiple chains (JSON)
177179
polygon-agent send --to 0x... --amount 1.0 # Send POL (dry-run)
178180
polygon-agent send --symbol USDC --to 0x... --amount 10 --broadcast
179181
polygon-agent swap --from USDC --to USDT --amount 5 --broadcast
@@ -194,6 +196,7 @@ polygon-agent agent reviews --agent-id <id>
194196
| ------------- | ---------------------- | -------------------- |
195197
| Wallet name | `main` | `--name <name>` |
196198
| Chain | `polygon` | `--chain <name\|id>` |
199+
| Multi-chain balances || `--chains <csv>` (comma-separated, max 20; overrides `--chain`) |
197200
| Wallet create | Auto-wait for approval | `--no-wait` |
198201
| Broadcast | Dry-run (preview) | `--broadcast` |
199202

packages/polygon-agent-cli/skills/SKILL.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ polygon-agent wallet remove [--name <n>]
9797

9898
### Operations
9999
```bash
100-
polygon-agent balances [--wallet <n>] [--chain <chain>]
100+
polygon-agent balances [--wallet <n>] [--chain <chain>] [--chains <csv>]
101101
polygon-agent send --to <addr> --amount <num> [--symbol <SYM>] [--token <addr>] [--decimals <n>] [--broadcast]
102102
polygon-agent send-native --to <addr> --amount <num> [--broadcast] [--direct]
103103
polygon-agent send-token --symbol <SYM> --to <addr> --amount <num> [--token <addr>] [--decimals <n>] [--broadcast]
@@ -125,6 +125,7 @@ polygon-agent agent feedback --agent-id <id> --value <score> [--tag1 <t>] [--tag
125125

126126
- **Dry-run by default** — all write commands require `--broadcast` to execute
127127
- **Smart defaults**`--wallet main`, `--chain polygon`, auto-wait on `wallet create`
128+
- **`balances --chains`** — comma-separated chains (max 20); two or more return JSON with `multiChain: true` and a `chains` array (same wallet address on each)
128129
- **Fee preference** — auto-selects USDC over native POL when both available
129130
- **`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.
130131
- **`deposit`** — picks highest-TVL pool via Trails `getEarnPools`. If session rejects (contract not whitelisted), re-create wallet with `--contract <depositAddress>`

packages/polygon-agent-cli/src/commands/operations.ts

Lines changed: 179 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -111,16 +111,114 @@ async function getTokenConfig({
111111

112112
const bigintReplacer = (_k: string, v: unknown) => (typeof v === 'bigint' ? v.toString() : v);
113113

114+
const BALANCES_MAX_CHAINS = 20;
115+
116+
function parseCommaChainList(chainsArg: string | undefined): string[] {
117+
if (!chainsArg || typeof chainsArg !== 'string') return [];
118+
return chainsArg
119+
.split(',')
120+
.map((s) => s.trim())
121+
.filter(Boolean);
122+
}
123+
124+
type BalanceRowJson =
125+
| { type: 'native'; symbol: string; balance: string }
126+
| {
127+
type: 'erc20';
128+
symbol: string;
129+
name?: string;
130+
contractAddress: string;
131+
balance: string;
132+
};
133+
134+
async function fetchBalancesRowsForChain(
135+
walletAddress: string,
136+
chainSpec: string,
137+
indexerKey: string
138+
): Promise<{ chainId: number; chain: string; balances: BalanceRowJson[] }> {
139+
const network = resolveNetwork(chainSpec);
140+
const nativeDecimals = network.nativeToken?.decimals ?? 18;
141+
const nativeSymbol = network.nativeToken?.symbol || 'POL';
142+
143+
const { SequenceIndexer } = await import('@0xsequence/indexer');
144+
const indexerUrl = getChainIndexerUrl(network.chainId);
145+
const indexer = new SequenceIndexer(indexerUrl, indexerKey);
146+
147+
const [nativeRes, tokenRes] = await Promise.all([
148+
indexer.getNativeTokenBalance({
149+
accountAddress: walletAddress
150+
}),
151+
indexer.getTokenBalances({
152+
accountAddress: walletAddress,
153+
includeMetadata: true
154+
})
155+
]);
156+
157+
const nativeWei = nativeRes?.balance?.balance || '0';
158+
const native: BalanceRowJson[] = [
159+
{
160+
type: 'native',
161+
symbol: nativeSymbol,
162+
balance: formatUnits(BigInt(nativeWei), nativeDecimals)
163+
}
164+
];
165+
166+
const erc20: BalanceRowJson[] = (tokenRes?.balances || []).map(
167+
(b: {
168+
contractInfo?: { symbol?: string; name?: string; decimals?: number };
169+
contractAddress: string;
170+
balance?: string;
171+
}) => ({
172+
type: 'erc20' as const,
173+
symbol: b.contractInfo?.symbol || 'ERC20',
174+
name: b.contractInfo?.name || undefined,
175+
contractAddress: b.contractAddress,
176+
balance: formatUnits(b.balance || '0', b.contractInfo?.decimals ?? 18)
177+
})
178+
);
179+
180+
return {
181+
chainId: network.chainId,
182+
chain: network.name,
183+
balances: [...native, ...erc20]
184+
};
185+
}
186+
114187
// --- balances ---
115188
export const balancesCommand: CommandModule = {
116189
command: 'balances',
117190
describe: 'Check token balances',
118-
builder: (yargs) => withWalletAndChain(yargs),
191+
builder: (yargs) =>
192+
withWalletAndChain(yargs).option('chains', {
193+
type: 'string',
194+
describe:
195+
'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).'
196+
}),
119197
handler: async (argv) => {
120198
const walletName = argv.wallet as string;
199+
const chainListRaw = parseCommaChainList(argv.chains as string | undefined);
200+
if (chainListRaw.length > BALANCES_MAX_CHAINS) {
201+
console.error(
202+
JSON.stringify(
203+
{
204+
ok: false,
205+
error: `Too many chains in --chains (max ${BALANCES_MAX_CHAINS}).`
206+
},
207+
null,
208+
2
209+
)
210+
);
211+
process.exit(1);
212+
}
213+
const chainList = chainListRaw;
121214

122-
if (!isTTY()) {
123-
// Non-TTY: original JSON output
215+
const preferChainsArg = chainList.length > 0;
216+
const singleChainSpec = preferChainsArg
217+
? chainList[0]
218+
: ((argv.chain as string) || undefined);
219+
const multiChainMode = preferChainsArg && chainList.length > 1;
220+
221+
if (multiChainMode || (preferChainsArg && !isTTY())) {
124222
try {
125223
const session = await loadWalletSession(walletName);
126224
if (!session) {
@@ -135,58 +233,93 @@ export const balancesCommand: CommandModule = {
135233
throw new Error('Missing project access key (not in wallet session or environment)');
136234
}
137235

138-
const network = resolveNetwork((argv.chain as string) || session.chain || 'polygon');
139-
const nativeDecimals = network.nativeToken?.decimals ?? 18;
140-
const nativeSymbol = network.nativeToken?.symbol || 'POL';
141-
142-
const { SequenceIndexer } = await import('@0xsequence/indexer');
143-
const indexerUrl = getChainIndexerUrl(network.chainId);
144-
const indexer = new SequenceIndexer(indexerUrl, indexerKey);
145-
146-
const [nativeRes, tokenRes] = await Promise.all([
147-
indexer.getNativeTokenBalance({
148-
accountAddress: session.walletAddress
149-
}),
150-
indexer.getTokenBalances({
151-
accountAddress: session.walletAddress,
152-
includeMetadata: true
153-
})
154-
]);
236+
if (multiChainMode) {
237+
const chainsOut = await Promise.all(
238+
chainList.map((spec) =>
239+
fetchBalancesRowsForChain(session.walletAddress, spec, indexerKey)
240+
)
241+
);
242+
console.log(
243+
JSON.stringify(
244+
{
245+
ok: true,
246+
walletName,
247+
walletAddress: session.walletAddress,
248+
multiChain: true,
249+
chains: chainsOut
250+
},
251+
bigintReplacer,
252+
2
253+
)
254+
);
255+
} else {
256+
const one = await fetchBalancesRowsForChain(
257+
session.walletAddress,
258+
singleChainSpec!,
259+
indexerKey
260+
);
261+
console.log(
262+
JSON.stringify(
263+
{
264+
ok: true,
265+
walletName,
266+
walletAddress: session.walletAddress,
267+
chainId: one.chainId,
268+
chain: one.chain,
269+
balances: one.balances
270+
},
271+
bigintReplacer,
272+
2
273+
)
274+
);
275+
}
276+
} catch (error) {
277+
console.error(
278+
JSON.stringify(
279+
{
280+
ok: false,
281+
error: (error as Error).message,
282+
stack: (error as Error).stack
283+
},
284+
null,
285+
2
286+
)
287+
);
288+
process.exit(1);
289+
}
290+
return;
291+
}
155292

156-
const nativeWei = nativeRes?.balance?.balance || '0';
157-
const native = [
158-
{
159-
type: 'native',
160-
symbol: nativeSymbol,
161-
balance: formatUnits(BigInt(nativeWei), nativeDecimals)
162-
}
163-
];
293+
if (!isTTY()) {
294+
// Non-TTY: original JSON output (single default / --chain)
295+
try {
296+
const session = await loadWalletSession(walletName);
297+
if (!session) {
298+
throw new Error(`Wallet not found: ${walletName}`);
299+
}
164300

165-
const erc20 = (tokenRes?.balances || []).map(
166-
(b: {
167-
contractInfo?: { symbol?: string; name?: string; decimals?: number };
168-
contractAddress: string;
169-
balance?: string;
170-
}) => ({
171-
type: 'erc20',
172-
symbol: b.contractInfo?.symbol || 'ERC20',
173-
name: b.contractInfo?.name || undefined,
174-
contractAddress: b.contractAddress,
175-
balance: formatUnits(b.balance || '0', b.contractInfo?.decimals ?? 18)
176-
})
177-
);
301+
const indexerKey =
302+
process.env.SEQUENCE_INDEXER_ACCESS_KEY ||
303+
session.projectAccessKey ||
304+
process.env.SEQUENCE_PROJECT_ACCESS_KEY;
305+
if (!indexerKey) {
306+
throw new Error('Missing project access key (not in wallet session or environment)');
307+
}
308+
309+
const chainSpec = (argv.chain as string) || session.chain || 'polygon';
310+
const one = await fetchBalancesRowsForChain(session.walletAddress, chainSpec, indexerKey);
178311

179312
console.log(
180313
JSON.stringify(
181314
{
182315
ok: true,
183316
walletName,
184317
walletAddress: session.walletAddress,
185-
chainId: network.chainId,
186-
chain: network.name,
187-
balances: [...native, ...erc20]
318+
chainId: one.chainId,
319+
chain: one.chain,
320+
balances: one.balances
188321
},
189-
null,
322+
bigintReplacer,
190323
2
191324
)
192325
);
@@ -211,7 +344,7 @@ export const balancesCommand: CommandModule = {
211344
await inkRender(
212345
React.createElement(BalancesUI, {
213346
walletName,
214-
chainOverride: argv.chain as string | undefined
347+
chainOverride: preferChainsArg ? singleChainSpec : (argv.chain as string | undefined)
215348
})
216349
);
217350
} catch {

skills/SKILL.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ polygon-agent wallet remove [--name <n>]
9797

9898
### Operations
9999
```bash
100-
polygon-agent balances [--wallet <n>] [--chain <chain>]
100+
polygon-agent balances [--wallet <n>] [--chain <chain>] [--chains <csv>]
101101
polygon-agent send --to <addr> --amount <num> [--symbol <SYM>] [--token <addr>] [--decimals <n>] [--broadcast]
102102
polygon-agent send-native --to <addr> --amount <num> [--broadcast] [--direct]
103103
polygon-agent send-token --symbol <SYM> --to <addr> --amount <num> [--token <addr>] [--decimals <n>] [--broadcast]
@@ -125,6 +125,7 @@ polygon-agent agent feedback --agent-id <id> --value <score> [--tag1 <t>] [--tag
125125

126126
- **Dry-run by default** — all write commands require `--broadcast` to execute
127127
- **Smart defaults**`--wallet main`, `--chain polygon`, auto-wait on `wallet create`
128+
- **`balances --chains`** — comma-separated chains (max 20); two or more return JSON with `multiChain: true` and a `chains` array (same wallet address on each)
128129
- **Fee preference** — auto-selects USDC over native POL when both available
129130
- **`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.
130131
- **`deposit`** — picks highest-TVL pool via Trails `getEarnPools`. If session rejects (contract not whitelisted), re-create wallet with `--contract <depositAddress>`

0 commit comments

Comments
 (0)