diff --git a/packages/connector-ui/public/SKILL.md b/packages/connector-ui/public/SKILL.md index 91625da..1d373b2 100644 --- a/packages/connector-ui/public/SKILL.md +++ b/packages/connector-ui/public/SKILL.md @@ -1,6 +1,6 @@ --- name: Polygon Agent -description: "Complete Polygon agent toolkit for on-chain operations on Polygon. Use this skill whenever helping an agent set up a wallet, check balances, send or swap tokens, bridge assets, deposit to earn yield, register on-chain identity, submit or query reputation/feedback, or make x402 micropayments. Covers the full lifecycle: Sequence smart contract wallets, Trails DeFi actions, ERC-8004 identity + reputation, x402 payments. Single CLI entry point (`polygon-agent`), AES-256-GCM encrypted storage." +description: "Complete Polygon agent toolkit for on-chain operations on Polygon. Use this skill whenever helping an agent set up a wallet, check balances, send or swap tokens, bridge assets, deposit or withdraw from yield (Aave aTokens, ERC-4626 vaults), register on-chain identity, submit or query reputation/feedback, or make x402 micropayments. Covers the full lifecycle: Sequence smart contract wallets, Trails DeFi actions, ERC-8004 identity + reputation, x402 payments. Single CLI entry point (`polygon-agent`), AES-256-GCM encrypted storage." --- # Polygon Agentic CLI @@ -99,12 +99,13 @@ 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] polygon-agent swap --from --to --amount [--to-chain ] [--slippage ] [--broadcast] polygon-agent deposit --asset --amount [--protocol aave|morpho] [--broadcast] +polygon-agent withdraw --position --amount [--chain ] [--broadcast] polygon-agent fund [--wallet ] [--token ] polygon-agent x402-pay --url --wallet [--method GET] [--body ] [--header Key:Value] ``` @@ -127,9 +128,11 @@ 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 ` +- **`withdraw`** — `--position` = aToken or ERC-4626 vault; `--amount` = `max` or underlying units (Aave / vault). Dry-run JSON includes `poolAddress` / `vault`. Broadcast needs session on the **same chain** as `--chain`, with pool/vault + underlying token whitelisted where the relayer touches them - **`x402-pay`** — probes endpoint for 402, smart wallet funds builder EOA with exact token amount, EOA signs EIP-3009 payment. Chain auto-detected from 402 response - **`send-native --direct`** — bypasses ValueForwarder contract for direct EOA transfer - **Session permissions** — without `--usdc-limit` etc., session gets bare-bones defaults and may not transact @@ -162,6 +165,7 @@ CLI commands output JSON (non-TTY). After running a command, always render the r | `send` / `send-token` / `send-native` | One-liner summary: amount, symbol, recipient. If broadcast, show tx hash as a code span and explorer URL as a link. | | `swap` | Summary: `X FROM → Y TO` with chain. If broadcast, show deposit tx hash + explorer link. | | `deposit` | Summary: amount, asset, protocol, pool address. If broadcast, show tx hash + explorer link. | +| `withdraw` | Summary: `kind` (aave / erc4626), position, amount, pool or vault. If broadcast, show tx hash + explorer link. | | `fund` | Show the `fundingUrl` as a clickable link with a brief instruction to open it. | | `wallet create` / `wallet list` | Wallet name, truncated address, chain in a small table or bullet list. | | `agent register` | Show agent name and tx hash as a code span with Polygonscan link. Remind user to retrieve `agentId` from the Registered event on the Logs tab. | @@ -182,7 +186,7 @@ For specific workflows, fetch and load the relevant sub-skill: | Use Case | Skill URL | |----------|-----------| | Polymarket prediction market trading | https://agentconnect.polygon.technology/polygon-polymarket/SKILL.md | -| DeFi — swap, deposit, yield | https://agentconnect.polygon.technology/polygon-defi/SKILL.md | +| DeFi — swap, deposit, withdraw, yield | https://agentconnect.polygon.technology/polygon-defi/SKILL.md | | x402 discovery & pay-per-call APIs | https://agentconnect.polygon.technology/polygon-discovery/SKILL.md | --- @@ -200,6 +204,8 @@ For specific workflows, fetch and load the relevant sub-skill: | `Invalid code: hash mismatch` | Wrong 6-digit code entered — retry (3 attempts allowed) | | `Relay request not found` | Session expired or already used — re-run `wallet create` (or `wallet create --print-url`) | | Deposit session rejected | Re-create wallet with `--contract ` | +| `withdraw` / broadcast: wrong chain or session rejects | Use `wallet create --chain ` and `--contract` for pool/vault + underlying ERC-20 on that chain; omit tight `--usdc-limit` if it blocks fee transfers | +| `Stored explicit session is missing pk` | Re-link: `wallet import --code …` after `wallet create` | | Wrong recipient in Trails widget | Run `polygon-agent fund` (do not construct the URL manually) | | `x402-pay`: no 402 response | Endpoint doesn't require x402 payment, or URL is wrong | | `x402-pay`: payment token mismatch | Chain/token in the 402 response differs from wallet — check `--wallet` points to the right chain | diff --git a/packages/connector-ui/public/skills/SKILL.md b/packages/connector-ui/public/skills/SKILL.md index 91625da..1d373b2 100644 --- a/packages/connector-ui/public/skills/SKILL.md +++ b/packages/connector-ui/public/skills/SKILL.md @@ -1,6 +1,6 @@ --- name: Polygon Agent -description: "Complete Polygon agent toolkit for on-chain operations on Polygon. Use this skill whenever helping an agent set up a wallet, check balances, send or swap tokens, bridge assets, deposit to earn yield, register on-chain identity, submit or query reputation/feedback, or make x402 micropayments. Covers the full lifecycle: Sequence smart contract wallets, Trails DeFi actions, ERC-8004 identity + reputation, x402 payments. Single CLI entry point (`polygon-agent`), AES-256-GCM encrypted storage." +description: "Complete Polygon agent toolkit for on-chain operations on Polygon. Use this skill whenever helping an agent set up a wallet, check balances, send or swap tokens, bridge assets, deposit or withdraw from yield (Aave aTokens, ERC-4626 vaults), register on-chain identity, submit or query reputation/feedback, or make x402 micropayments. Covers the full lifecycle: Sequence smart contract wallets, Trails DeFi actions, ERC-8004 identity + reputation, x402 payments. Single CLI entry point (`polygon-agent`), AES-256-GCM encrypted storage." --- # Polygon Agentic CLI @@ -99,12 +99,13 @@ 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] polygon-agent swap --from --to --amount [--to-chain ] [--slippage ] [--broadcast] polygon-agent deposit --asset --amount [--protocol aave|morpho] [--broadcast] +polygon-agent withdraw --position --amount [--chain ] [--broadcast] polygon-agent fund [--wallet ] [--token ] polygon-agent x402-pay --url --wallet [--method GET] [--body ] [--header Key:Value] ``` @@ -127,9 +128,11 @@ 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 ` +- **`withdraw`** — `--position` = aToken or ERC-4626 vault; `--amount` = `max` or underlying units (Aave / vault). Dry-run JSON includes `poolAddress` / `vault`. Broadcast needs session on the **same chain** as `--chain`, with pool/vault + underlying token whitelisted where the relayer touches them - **`x402-pay`** — probes endpoint for 402, smart wallet funds builder EOA with exact token amount, EOA signs EIP-3009 payment. Chain auto-detected from 402 response - **`send-native --direct`** — bypasses ValueForwarder contract for direct EOA transfer - **Session permissions** — without `--usdc-limit` etc., session gets bare-bones defaults and may not transact @@ -162,6 +165,7 @@ CLI commands output JSON (non-TTY). After running a command, always render the r | `send` / `send-token` / `send-native` | One-liner summary: amount, symbol, recipient. If broadcast, show tx hash as a code span and explorer URL as a link. | | `swap` | Summary: `X FROM → Y TO` with chain. If broadcast, show deposit tx hash + explorer link. | | `deposit` | Summary: amount, asset, protocol, pool address. If broadcast, show tx hash + explorer link. | +| `withdraw` | Summary: `kind` (aave / erc4626), position, amount, pool or vault. If broadcast, show tx hash + explorer link. | | `fund` | Show the `fundingUrl` as a clickable link with a brief instruction to open it. | | `wallet create` / `wallet list` | Wallet name, truncated address, chain in a small table or bullet list. | | `agent register` | Show agent name and tx hash as a code span with Polygonscan link. Remind user to retrieve `agentId` from the Registered event on the Logs tab. | @@ -182,7 +186,7 @@ For specific workflows, fetch and load the relevant sub-skill: | Use Case | Skill URL | |----------|-----------| | Polymarket prediction market trading | https://agentconnect.polygon.technology/polygon-polymarket/SKILL.md | -| DeFi — swap, deposit, yield | https://agentconnect.polygon.technology/polygon-defi/SKILL.md | +| DeFi — swap, deposit, withdraw, yield | https://agentconnect.polygon.technology/polygon-defi/SKILL.md | | x402 discovery & pay-per-call APIs | https://agentconnect.polygon.technology/polygon-discovery/SKILL.md | --- @@ -200,6 +204,8 @@ For specific workflows, fetch and load the relevant sub-skill: | `Invalid code: hash mismatch` | Wrong 6-digit code entered — retry (3 attempts allowed) | | `Relay request not found` | Session expired or already used — re-run `wallet create` (or `wallet create --print-url`) | | Deposit session rejected | Re-create wallet with `--contract ` | +| `withdraw` / broadcast: wrong chain or session rejects | Use `wallet create --chain ` and `--contract` for pool/vault + underlying ERC-20 on that chain; omit tight `--usdc-limit` if it blocks fee transfers | +| `Stored explicit session is missing pk` | Re-link: `wallet import --code …` after `wallet create` | | Wrong recipient in Trails widget | Run `polygon-agent fund` (do not construct the URL manually) | | `x402-pay`: no 402 response | Endpoint doesn't require x402 payment, or URL is wrong | | `x402-pay`: payment token mismatch | Chain/token in the 402 response differs from wallet — check `--wallet` points to the right chain | diff --git a/packages/connector-ui/public/skills/polygon-defi/SKILL.md b/packages/connector-ui/public/skills/polygon-defi/SKILL.md index 3751d99..b2edf43 100644 --- a/packages/connector-ui/public/skills/polygon-defi/SKILL.md +++ b/packages/connector-ui/public/skills/polygon-defi/SKILL.md @@ -108,16 +108,19 @@ interface PoolTokenInfo { Pool discovery uses `TrailsApi.getEarnPools` — picks the most liquid pool (highest TVL) for the asset on the current chain. No hardcoded addresses — the pool is resolved at runtime. +**Note on Morpho Vaults:** +Trails categorizes Morpho vaults by their *receipt* token symbol (e.g., `STEAKUSDC` or `gtUSDCp`), rather than the underlying token. To deposit into a Morpho vault, you must provide the vault's exact receipt token symbol as the `--asset`. +If you try to deposit using `--asset USDC`, it may fail to find the pool. In those cases, you can use the `swap` command to swap `USDC` directly for the vault's receipt token address. + ```bash # Dry-run — shows pool name, APY, TVL, and deposit address before committing -polygon-agent deposit --asset USDC --amount 0.3 +polygon-agent deposit --asset STEAKUSDC --amount 0.3 --protocol morpho # Execute — deposits into the highest-TVL active pool -polygon-agent deposit --asset USDC --amount 0.3 --broadcast +polygon-agent deposit --asset STEAKUSDC --amount 0.3 --protocol morpho --broadcast -# Filter by protocol +# Filter by protocol (Aave uses the underlying asset name) polygon-agent deposit --asset USDC --amount 0.3 --protocol aave --broadcast -polygon-agent deposit --asset USDC --amount 0.3 --protocol morpho --broadcast ``` ### Supported Protocols @@ -129,6 +132,25 @@ polygon-agent deposit --asset USDC --amount 0.3 --protocol morpho --broadcast Vault/pool addresses are resolved dynamically from Trails — they are not hardcoded. The dry-run output includes `depositAddress` so you can inspect the exact contract before broadcasting. +## Withdraw (Aave aToken or ERC-4626 vault) + +Pass the **position token** you hold: an **Aave aToken** address, or a **Morpho / ERC-4626 vault** (share) address. The CLI resolves the Aave **Pool** via `POOL()` on the aToken, or uses `redeem` on the vault. Dry-run by default. + +```bash +# Full exit from an Aave position (aToken from balances output) +polygon-agent withdraw --position 0x68215b6533c47ff9f7125ac95adf00fe4a62f79e --amount max --chain mainnet + +# Partial Aave withdraw (underlying units, e.g. USDC) +polygon-agent withdraw --position --amount 0.5 --chain mainnet --broadcast + +# ERC-4626: max redeems all shares; partial amount is underlying units (convertToShares) +polygon-agent withdraw --position --amount max --chain polygon --broadcast +``` + +Whitelist the **pool** (Aave) or **vault** contract on the session if the wallet rejects the call (`polygon-agent wallet create --contract `). + +**Same chain as the transaction:** if you use `withdraw --chain mainnet`, create or refresh the session with **`wallet create --chain mainnet`** (not only Polygon defaults). Include **`--contract`** for the **pool** and for the **underlying ERC-20** on that chain (e.g. mainnet USDC `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48`) so fee / helper transfers are allowed. Tight **`--usdc-limit`** can block those — omit or relax for yield exits. + ### Session Whitelisting If the deposit is rejected with a session permission error, the pool's contract address needs to be whitelisted when creating the wallet session: diff --git a/packages/connector-ui/src/App.tsx b/packages/connector-ui/src/App.tsx index 2eeee8c..1b3163a 100644 --- a/packages/connector-ui/src/App.tsx +++ b/packages/connector-ui/src/App.tsx @@ -412,12 +412,12 @@ function App() { deadline: BigInt(Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 183), permissions: [ ...basePermissions, - ...contractWhitelistPermissions, ...oneOffErc20Permissions, ...openTokenPermissions, ...dynamicTokenPermissions, ...nativeFeePermission, - ...feePermissions + ...feePermissions, + ...contractWhitelistPermissions ] }; diff --git a/packages/polygon-agent-cli/CHANGELOG.md b/packages/polygon-agent-cli/CHANGELOG.md index e392d43..091a24c 100644 --- a/packages/polygon-agent-cli/CHANGELOG.md +++ b/packages/polygon-agent-cli/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [Unreleased] + +### Features + +* **cli:** `withdraw` command — exit Aave v3 via aToken (`Pool.withdraw`) or ERC-4626 vaults (`redeem`); dry-run shows pool/vault and calldata ([docs](README.md#cli-reference)) + +### Bug Fixes + +* **cli:** avoid double slash in transaction explorer URLs when `blockExplorer.rootUrl` ends with `/` + ## [0.7.2](https://github.com/0xPolygon/polygon-agent-cli/compare/@polygonlabs/agent-cli@0.7.1...@polygonlabs/agent-cli@0.7.2) (2026-04-14) diff --git a/packages/polygon-agent-cli/README.md b/packages/polygon-agent-cli/README.md index 3c84957..283a297 100644 --- a/packages/polygon-agent-cli/README.md +++ b/packages/polygon-agent-cli/README.md @@ -124,7 +124,7 @@ Wallet sessions are created through a secure handshake between the CLI, the Conn | ------------ | -------------------------------------------------------------------------------------------- | ------------------------------- | | **Bridging** | Move assets cross-chain into your Polygon wallet and fund the initial flows to your wallet | `fund` | | **Swapping** | Token swaps with configurable slippage seamlessly built in | `swap` | -| **Actions** | Composable onchain operations (deposit into a DeFi vault, stake with your favorite protocol) | `send`, `deposit`, `send-token` | +| **Actions** | Composable onchain operations (deposit / withdraw from yield, send tokens) | `send`, `deposit`, `withdraw`, `send-token` | ### Onchain Agentic Identity @@ -173,12 +173,20 @@ 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 +polygon-agent withdraw --position --amount max [--chain ] # dry-run; add --broadcast +polygon-agent withdraw --position --amount 0.5 --chain mainnet --broadcast # partial (underlying units) ``` +**`withdraw`** exits **Aave v3** positions using your **aToken** address (`POOL()` + `UNDERLYING_ASSET_ADDRESS()` → `Pool.withdraw`), or **ERC-4626** vaults (e.g. Morpho) via `redeem`. Dry-run prints `poolAddress` / `vault` and calldata. + +For **`--broadcast`**, the session must allow the **pool or vault** and (on that chain) **fee / underlying ERC-20** touches — use `wallet create --chain --contract --contract `. If your default session is Polygon but you transact on **mainnet**, create or extend a **mainnet** session for that chain. + ### Agent Registry (ERC-8004) ```bash @@ -194,6 +202,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..1d373b2 100644 --- a/packages/polygon-agent-cli/skills/SKILL.md +++ b/packages/polygon-agent-cli/skills/SKILL.md @@ -1,6 +1,6 @@ --- name: Polygon Agent -description: "Complete Polygon agent toolkit for on-chain operations on Polygon. Use this skill whenever helping an agent set up a wallet, check balances, send or swap tokens, bridge assets, deposit to earn yield, register on-chain identity, submit or query reputation/feedback, or make x402 micropayments. Covers the full lifecycle: Sequence smart contract wallets, Trails DeFi actions, ERC-8004 identity + reputation, x402 payments. Single CLI entry point (`polygon-agent`), AES-256-GCM encrypted storage." +description: "Complete Polygon agent toolkit for on-chain operations on Polygon. Use this skill whenever helping an agent set up a wallet, check balances, send or swap tokens, bridge assets, deposit or withdraw from yield (Aave aTokens, ERC-4626 vaults), register on-chain identity, submit or query reputation/feedback, or make x402 micropayments. Covers the full lifecycle: Sequence smart contract wallets, Trails DeFi actions, ERC-8004 identity + reputation, x402 payments. Single CLI entry point (`polygon-agent`), AES-256-GCM encrypted storage." --- # Polygon Agentic CLI @@ -99,12 +99,13 @@ 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] polygon-agent swap --from --to --amount [--to-chain ] [--slippage ] [--broadcast] polygon-agent deposit --asset --amount [--protocol aave|morpho] [--broadcast] +polygon-agent withdraw --position --amount [--chain ] [--broadcast] polygon-agent fund [--wallet ] [--token ] polygon-agent x402-pay --url --wallet [--method GET] [--body ] [--header Key:Value] ``` @@ -127,9 +128,11 @@ 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 ` +- **`withdraw`** — `--position` = aToken or ERC-4626 vault; `--amount` = `max` or underlying units (Aave / vault). Dry-run JSON includes `poolAddress` / `vault`. Broadcast needs session on the **same chain** as `--chain`, with pool/vault + underlying token whitelisted where the relayer touches them - **`x402-pay`** — probes endpoint for 402, smart wallet funds builder EOA with exact token amount, EOA signs EIP-3009 payment. Chain auto-detected from 402 response - **`send-native --direct`** — bypasses ValueForwarder contract for direct EOA transfer - **Session permissions** — without `--usdc-limit` etc., session gets bare-bones defaults and may not transact @@ -162,6 +165,7 @@ CLI commands output JSON (non-TTY). After running a command, always render the r | `send` / `send-token` / `send-native` | One-liner summary: amount, symbol, recipient. If broadcast, show tx hash as a code span and explorer URL as a link. | | `swap` | Summary: `X FROM → Y TO` with chain. If broadcast, show deposit tx hash + explorer link. | | `deposit` | Summary: amount, asset, protocol, pool address. If broadcast, show tx hash + explorer link. | +| `withdraw` | Summary: `kind` (aave / erc4626), position, amount, pool or vault. If broadcast, show tx hash + explorer link. | | `fund` | Show the `fundingUrl` as a clickable link with a brief instruction to open it. | | `wallet create` / `wallet list` | Wallet name, truncated address, chain in a small table or bullet list. | | `agent register` | Show agent name and tx hash as a code span with Polygonscan link. Remind user to retrieve `agentId` from the Registered event on the Logs tab. | @@ -182,7 +186,7 @@ For specific workflows, fetch and load the relevant sub-skill: | Use Case | Skill URL | |----------|-----------| | Polymarket prediction market trading | https://agentconnect.polygon.technology/polygon-polymarket/SKILL.md | -| DeFi — swap, deposit, yield | https://agentconnect.polygon.technology/polygon-defi/SKILL.md | +| DeFi — swap, deposit, withdraw, yield | https://agentconnect.polygon.technology/polygon-defi/SKILL.md | | x402 discovery & pay-per-call APIs | https://agentconnect.polygon.technology/polygon-discovery/SKILL.md | --- @@ -200,6 +204,8 @@ For specific workflows, fetch and load the relevant sub-skill: | `Invalid code: hash mismatch` | Wrong 6-digit code entered — retry (3 attempts allowed) | | `Relay request not found` | Session expired or already used — re-run `wallet create` (or `wallet create --print-url`) | | Deposit session rejected | Re-create wallet with `--contract ` | +| `withdraw` / broadcast: wrong chain or session rejects | Use `wallet create --chain ` and `--contract` for pool/vault + underlying ERC-20 on that chain; omit tight `--usdc-limit` if it blocks fee transfers | +| `Stored explicit session is missing pk` | Re-link: `wallet import --code …` after `wallet create` | | Wrong recipient in Trails widget | Run `polygon-agent fund` (do not construct the URL manually) | | `x402-pay`: no 402 response | Endpoint doesn't require x402 payment, or URL is wrong | | `x402-pay`: payment token mismatch | Chain/token in the 402 response differs from wallet — check `--wallet` points to the right chain | diff --git a/packages/polygon-agent-cli/skills/polygon-defi/SKILL.md b/packages/polygon-agent-cli/skills/polygon-defi/SKILL.md index 3751d99..2ab310c 100644 --- a/packages/polygon-agent-cli/skills/polygon-defi/SKILL.md +++ b/packages/polygon-agent-cli/skills/polygon-defi/SKILL.md @@ -108,16 +108,19 @@ interface PoolTokenInfo { Pool discovery uses `TrailsApi.getEarnPools` — picks the most liquid pool (highest TVL) for the asset on the current chain. No hardcoded addresses — the pool is resolved at runtime. +**Note on Morpho Vaults:** +Trails categorizes Morpho vaults by their *receipt* token symbol (e.g., `STEAKUSDC` or `gtUSDCp`), rather than the underlying token. To deposit into a Morpho vault, you must provide the vault's exact receipt token symbol as the `--asset`. +If you try to deposit using `--asset USDC`, it may fail to find the pool. In those cases, you can use the `swap` command to swap `USDC` directly for the vault's receipt token address. + ```bash # Dry-run — shows pool name, APY, TVL, and deposit address before committing -polygon-agent deposit --asset USDC --amount 0.3 +polygon-agent deposit --asset STEAKUSDC --amount 0.3 --protocol morpho # Execute — deposits into the highest-TVL active pool -polygon-agent deposit --asset USDC --amount 0.3 --broadcast +polygon-agent deposit --asset STEAKUSDC --amount 0.3 --protocol morpho --broadcast -# Filter by protocol +# Filter by protocol (Aave uses the underlying asset name) polygon-agent deposit --asset USDC --amount 0.3 --protocol aave --broadcast -polygon-agent deposit --asset USDC --amount 0.3 --protocol morpho --broadcast ``` ### Supported Protocols diff --git a/packages/polygon-agent-cli/src/commands/operations.ts b/packages/polygon-agent-cli/src/commands/operations.ts index 7e84c07..f49ea22 100644 --- a/packages/polygon-agent-cli/src/commands/operations.ts +++ b/packages/polygon-agent-cli/src/commands/operations.ts @@ -10,6 +10,7 @@ import { formatUnits, parseUnits, getExplorerUrl, + getRpcUrl, fileCoerce } from '../lib/utils.ts'; import { isTTY, inkRender } from '../ui/render.js'; @@ -111,16 +112,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 +234,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 +316,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 +345,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 { @@ -1189,6 +1323,473 @@ export const depositCommand: CommandModule = { } }; +const AAVE_ATOKEN_META_ABI = [ + { + name: 'POOL', + type: 'function', + stateMutability: 'view', + inputs: [], + outputs: [{ type: 'address' }] + }, + { + name: 'UNDERLYING_ASSET_ADDRESS', + type: 'function', + stateMutability: 'view', + inputs: [], + outputs: [{ type: 'address' }] + } +] as const; + +const ERC20_DECIMALS_ABI = [ + { + name: 'decimals', + type: 'function', + stateMutability: 'view', + inputs: [], + outputs: [{ type: 'uint8' }] + } +] as const; + +const ERC20_BALANCE_OF_ABI = [ + { + name: 'balanceOf', + type: 'function', + stateMutability: 'view', + inputs: [{ name: 'account', type: 'address' }], + outputs: [{ type: 'uint256' }] + } +] as const; + +const ERC4626_ASSET_ABI = [ + { + name: 'asset', + type: 'function', + stateMutability: 'view', + inputs: [], + outputs: [{ type: 'address' }] + } +] as const; + +const ERC4626_CONVERT_TO_SHARES_ABI = [ + { + name: 'convertToShares', + type: 'function', + stateMutability: 'view', + inputs: [{ name: 'assets', type: 'uint256' }], + outputs: [{ type: 'uint256' }] + } +] as const; + +const AAVE_POOL_WITHDRAW_ABI = [ + { + name: 'withdraw', + type: 'function', + stateMutability: 'nonpayable', + inputs: [ + { name: 'asset', type: 'address' }, + { name: 'amount', type: 'uint256' }, + { name: 'to', type: 'address' } + ], + outputs: [{ type: 'uint256' }] + } +] as const; + +const ERC4626_REDEEM_ABI = [ + { + name: 'redeem', + type: 'function', + stateMutability: 'nonpayable', + inputs: [ + { name: 'shares', type: 'uint256' }, + { name: 'receiver', type: 'address' }, + { name: 'owner', type: 'address' } + ], + outputs: [{ type: 'uint256' }] + } +] as const; + +async function viemChainForWithdraw(chainId: number) { + const { + mainnet, + polygon, + arbitrum, + optimism, + base, + avalanche, + bsc, + gnosis, + polygonAmoy + } = await import('viem/chains'); + const map = { + 1: mainnet, + 137: polygon, + 42161: arbitrum, + 10: optimism, + 8453: base, + 43114: avalanche, + 56: bsc, + 100: gnosis, + 80002: polygonAmoy + } as const; + const c = map[chainId as keyof typeof map]; + if (!c) { + throw new Error( + `withdraw: chainId ${chainId} has no bundled viem chain config. Extend viemChainForWithdraw or use a supported chain.` + ); + } + return c; +} + +// --- withdraw --- +export const withdrawCommand: CommandModule = { + command: 'withdraw', + describe: 'Withdraw from an Aave v3 aToken position or ERC-4626 vault (dry-run by default)', + builder: (yargs) => + withBroadcast( + withWalletAndChain(yargs) + .option('position', { + type: 'string', + describe: 'Position token: Aave aToken address, or ERC-4626 vault (share token) address. (Optional if --asset and --protocol are used)', + coerce: fileCoerce + }) + .option('asset', { + type: 'string', + describe: 'Asset symbol (e.g. USDC). Used with --protocol to auto-discover the position address.' + }) + .option('protocol', { + type: 'string', + describe: 'Filter by protocol (e.g. aave, morpho). Used with --asset to auto-discover the position address.' + }) + .option('amount', { + type: 'string', + demandOption: true, + describe: 'Underlying amount to withdraw (Aave), or max | partial underlying (ERC-4626). Use max for full exit.', + coerce: fileCoerce + }) + ), + handler: async (argv) => { + const walletName = (argv.wallet as string) || 'main'; + const amountArg = String(argv.amount || '').trim().toLowerCase(); + const broadcast = argv.broadcast as boolean; + const protocolFilter = (argv.protocol as string)?.toLowerCase(); + const assetSymbol = (argv.asset as string)?.toUpperCase(); + + try { + const session = await loadWalletSession(walletName); + if (!session) throw new Error(`Wallet not found: ${walletName}`); + + const network = resolveNetwork((argv.chain as string) || session.chain || 'polygon'); + const { chainId } = network; + const walletAddress = session.walletAddress as `0x${string}`; + + const { createPublicClient, http, encodeFunctionData, maxUint256, parseUnits } = await import('viem'); + const viemChain = await viemChainForWithdraw(chainId); + const publicClient = createPublicClient({ + chain: viemChain, + transport: http(getRpcUrl(network)) + }); + + let positionAddr = String(argv.position || '').trim().toLowerCase() as `0x${string}`; + + if (!positionAddr && assetSymbol && protocolFilter) { + const asset = await getTokenConfig({ + chainId, + symbol: assetSymbol, + nativeSymbol: network.nativeToken?.symbol || 'POL' + }); + + const { TrailsApi } = await import('@0xtrails/api'); + const trailsApiKey = + process.env.TRAILS_API_KEY || + session.projectAccessKey || + process.env.SEQUENCE_PROJECT_ACCESS_KEY || + ''; + const trails = new TrailsApi(trailsApiKey, { + hostname: process.env.TRAILS_API_HOSTNAME + }); + + const earnRes = await trails.getEarnPools({ chainIds: [chainId] }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let pools = ((earnRes as any)?.pools || []).filter( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (p: any) => + p.isActive && + p.chainId === chainId && + (p.token?.symbol?.toUpperCase() === assetSymbol || + p.token?.address?.toLowerCase() === asset.address.toLowerCase()) + ); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + pools = pools.filter((p: any) => + p.protocol?.toLowerCase().includes(protocolFilter) + ); + + if (pools.length === 0) { + throw new Error( + `No active earn pools found for ${assetSymbol} on ${network.name} (protocol filter: ${protocolFilter}). Try passing --position explicitly.` + ); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + pools.sort((a: any, b: any) => b.tvl - a.tvl); + const pool = pools[0]; + + if (protocolFilter.includes('aave')) { + const AAVE_POOL_RESERVE_DATA_ABI = [{ + "inputs": [{"internalType": "address","name": "asset","type": "address"}], + "name": "getReserveData", + "outputs": [ + {"components": [ + {"components": [{"internalType": "uint256","name": "data","type": "uint256"}],"internalType": "struct DataTypes.ReserveConfigurationMap","name": "configuration","type": "tuple"}, + {"internalType": "uint128","name": "liquidityIndex","type": "uint128"}, + {"internalType": "uint128","name": "currentLiquidityRate","type": "uint128"}, + {"internalType": "uint128","name": "variableBorrowIndex","type": "uint128"}, + {"internalType": "uint128","name": "currentVariableBorrowRate","type": "uint128"}, + {"internalType": "uint128","name": "currentStableBorrowRate","type": "uint128"}, + {"internalType": "uint40","name": "lastUpdateTimestamp","type": "uint40"}, + {"internalType": "uint16","name": "id","type": "uint16"}, + {"internalType": "address","name": "aTokenAddress","type": "address"}, + {"internalType": "address","name": "stableDebtTokenAddress","type": "address"}, + {"internalType": "address","name": "variableDebtTokenAddress","type": "address"}, + {"internalType": "address","name": "interestRateStrategyAddress","type": "address"}, + {"internalType": "uint128","name": "accruedToTreasury","type": "uint128"}, + {"internalType": "uint128","name": "unbacked","type": "uint128"}, + {"internalType": "uint128","name": "isolationModeTotalDebt","type": "uint128"} + ],"internalType": "struct DataTypes.ReserveData","name": "","type": "tuple"} + ], + "stateMutability": "view", + "type": "function" + }]; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const reserveData: any = await publicClient.readContract({ + address: pool.depositAddress as `0x${string}`, + abi: AAVE_POOL_RESERVE_DATA_ABI, + functionName: 'getReserveData', + args: [asset.address as `0x${string}`] + }).catch(() => null); + + if (!reserveData || !reserveData.aTokenAddress) { + throw new Error(`Failed to resolve aToken address for ${assetSymbol} on Aave pool ${pool.depositAddress}. Try passing --position explicitly.`); + } + positionAddr = reserveData.aTokenAddress.toLowerCase() as `0x${string}`; + } else { + positionAddr = pool.depositAddress.toLowerCase() as `0x${string}`; + } + } + + if (!positionAddr.startsWith('0x') || positionAddr.length !== 42) { + throw new Error('Invalid or missing --position address (expected 0x + 40 hex chars). Provide --position or both --asset and --protocol.'); + } + + const aaveMeta = await publicClient + .readContract({ + address: positionAddr, + abi: AAVE_ATOKEN_META_ABI, + functionName: 'POOL' + }) + .then(async (pool) => { + const underlying = await publicClient.readContract({ + address: positionAddr, + abi: AAVE_ATOKEN_META_ABI, + functionName: 'UNDERLYING_ASSET_ADDRESS' + }); + return { pool: pool as `0x${string}`, underlying: underlying as `0x${string}` }; + }) + .catch(() => null); + + let transactions: { to: `0x${string}`; value: bigint; data: `0x${string}` }[]; + let kind: 'aave' | 'erc4626'; + let summary: Record; + + if (aaveMeta) { + kind = 'aave'; + const underlyingDec = Number( + await publicClient.readContract({ + address: aaveMeta.underlying, + abi: ERC20_DECIMALS_ABI, + functionName: 'decimals' + }) + ); + const amountWei = + amountArg === 'max' || amountArg === 'all' + ? maxUint256 + : parseUnits(amountArg, underlyingDec); + + transactions = [ + { + to: aaveMeta.pool, + value: 0n, + data: encodeFunctionData({ + abi: AAVE_POOL_WITHDRAW_ABI, + functionName: 'withdraw', + args: [aaveMeta.underlying, amountWei, walletAddress] + }) + } + ]; + summary = { + protocol: 'aave', + poolAddress: aaveMeta.pool, + underlyingAsset: aaveMeta.underlying, + aToken: positionAddr, + amount: amountArg === 'max' || amountArg === 'all' ? 'max' : amountArg, + underlyingDecimals: underlyingDec + }; + } else { + const underlying = await publicClient + .readContract({ + address: positionAddr, + abi: ERC4626_ASSET_ABI, + functionName: 'asset' + }) + .catch(() => null); + + if (!underlying) { + throw new Error( + `Could not treat position (${positionAddr}) as Aave aToken (POOL / UNDERLYING_ASSET_ADDRESS) or ERC-4626 vault (asset()). ` + + 'Pass the aToken or vault share contract you hold.' + ); + } + + kind = 'erc4626'; + const underlyingAddr = underlying as `0x${string}`; + const underlyingDec = Number( + await publicClient.readContract({ + address: underlyingAddr, + abi: ERC20_DECIMALS_ABI, + functionName: 'decimals' + }) + ); + + const shareBal = await publicClient.readContract({ + address: positionAddr, + abi: ERC20_BALANCE_OF_ABI, + functionName: 'balanceOf', + args: [walletAddress] + }); + + if (shareBal === 0n) { + throw new Error('ERC-4626 share balance is zero for this wallet on this chain.'); + } + + let sharesOut: bigint; + if (amountArg === 'max' || amountArg === 'all') { + sharesOut = shareBal; + } else { + const assetsWei = parseUnits(amountArg, underlyingDec); + sharesOut = await publicClient.readContract({ + address: positionAddr, + abi: ERC4626_CONVERT_TO_SHARES_ABI, + functionName: 'convertToShares', + args: [assetsWei] + }); + if (sharesOut > shareBal) { + throw new Error( + `Requested underlying withdraw exceeds vault shares (need ${sharesOut.toString()} shares, have ${shareBal.toString()}). Try --amount max.` + ); + } + } + + transactions = [ + { + to: positionAddr, + value: 0n, + data: encodeFunctionData({ + abi: ERC4626_REDEEM_ABI, + functionName: 'redeem', + args: [sharesOut, walletAddress, walletAddress] + }) + } + ]; + summary = { + protocol: 'erc4626', + vault: positionAddr, + underlyingAsset: underlyingAddr, + sharesRedeemed: sharesOut.toString(), + shareBalance: shareBal.toString(), + underlyingDecimals: underlyingDec + }; + } + + const targetContract = kind === 'aave' ? (summary.poolAddress as string) : positionAddr; + + if (!broadcast) { + console.log( + JSON.stringify( + { + ok: true, + dryRun: true, + walletName, + walletAddress, + chainId, + chain: network.name, + kind, + ...summary, + transactions, + note: + `Re-run with --broadcast to submit. If the session rejects the call, re-create the wallet with the pool/vault whitelisted: polygon-agent wallet create --contract ${targetContract}` + }, + bigintReplacer, + 2 + ) + ); + return; + } + + let result; + try { + result = await runDappClientTx({ + walletName, + chainId, + transactions, + broadcast, + preferNativeFee: false + }); + } catch (txErr) { + if ((txErr as Error).message?.includes('No signer supported')) { + throw new Error( + `Session does not permit calls to ${targetContract}. ` + + `Re-create the wallet session with: polygon-agent wallet create --contract ${targetContract}\n` + + `Original error: ${(txErr as Error).message}` + ); + } + throw txErr; + } + + console.log( + JSON.stringify( + { + ok: true, + walletName, + walletAddress, + chainId, + chain: network.name, + kind, + ...summary, + txHash: result.txHash, + explorerUrl: getExplorerUrl(network, result.txHash ?? '') + }, + 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); + } + } +}; + // --- x402-pay --- export const x402PayCommand: CommandModule = { command: 'x402-pay', diff --git a/packages/polygon-agent-cli/src/commands/wallet.ts b/packages/polygon-agent-cli/src/commands/wallet.ts index 31e684a..ba89bbe 100644 --- a/packages/polygon-agent-cli/src/commands/wallet.ts +++ b/packages/polygon-agent-cli/src/commands/wallet.ts @@ -129,10 +129,10 @@ function applySessionPermissionParams(url: URL, argv: SessionPermissionArgs): vo } const nativeLimit = argv['native-limit']; - const usdcLimit = argv['usdc-limit'] || '50'; + const usdcLimit = argv['usdc-limit']; const usdtLimit = argv['usdt-limit']; if (nativeLimit) url.searchParams.set('nativeLimit', nativeLimit); - url.searchParams.set('usdcLimit', usdcLimit); + if (usdcLimit) url.searchParams.set('usdcLimit', usdcLimit); if (usdtLimit) url.searchParams.set('usdtLimit', usdtLimit); const tokenLimits = (argv['token-limit'] || []) diff --git a/packages/polygon-agent-cli/src/index.ts b/packages/polygon-agent-cli/src/index.ts index ade2f7b..476e8d4 100644 --- a/packages/polygon-agent-cli/src/index.ts +++ b/packages/polygon-agent-cli/src/index.ts @@ -16,6 +16,7 @@ import { sendNativeCommand, sendTokenCommand, swapCommand, + withdrawCommand, x402PayCommand } from './commands/operations.ts'; import { polymarketCommand } from './commands/polymarket.ts'; @@ -93,6 +94,7 @@ const parser = yargs(hideBin(process.argv)) .command(sendTokenCommand) .command(swapCommand) .command(depositCommand) + .command(withdrawCommand) .command(x402PayCommand) .command(agentCommand) .command(polymarketCommand); diff --git a/packages/polygon-agent-cli/src/lib/utils.ts b/packages/polygon-agent-cli/src/lib/utils.ts index 9807ea7..bf4c936 100644 --- a/packages/polygon-agent-cli/src/lib/utils.ts +++ b/packages/polygon-agent-cli/src/lib/utils.ts @@ -87,7 +87,8 @@ export function getRpcUrl(network: NetworkMetadata): string { /** Explorer URL for transaction */ export function getExplorerUrl(network: NetworkMetadata, txHash: string): string { - const base = network.blockExplorer?.rootUrl || `https://polygonscan.com`; + const raw = network.blockExplorer?.rootUrl || `https://polygonscan.com`; + const base = raw.replace(/\/+$/, ''); return `${base}/tx/${txHash}`; } diff --git a/skills/SKILL.md b/skills/SKILL.md index 91625da..1d373b2 100644 --- a/skills/SKILL.md +++ b/skills/SKILL.md @@ -1,6 +1,6 @@ --- name: Polygon Agent -description: "Complete Polygon agent toolkit for on-chain operations on Polygon. Use this skill whenever helping an agent set up a wallet, check balances, send or swap tokens, bridge assets, deposit to earn yield, register on-chain identity, submit or query reputation/feedback, or make x402 micropayments. Covers the full lifecycle: Sequence smart contract wallets, Trails DeFi actions, ERC-8004 identity + reputation, x402 payments. Single CLI entry point (`polygon-agent`), AES-256-GCM encrypted storage." +description: "Complete Polygon agent toolkit for on-chain operations on Polygon. Use this skill whenever helping an agent set up a wallet, check balances, send or swap tokens, bridge assets, deposit or withdraw from yield (Aave aTokens, ERC-4626 vaults), register on-chain identity, submit or query reputation/feedback, or make x402 micropayments. Covers the full lifecycle: Sequence smart contract wallets, Trails DeFi actions, ERC-8004 identity + reputation, x402 payments. Single CLI entry point (`polygon-agent`), AES-256-GCM encrypted storage." --- # Polygon Agentic CLI @@ -99,12 +99,13 @@ 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] polygon-agent swap --from --to --amount [--to-chain ] [--slippage ] [--broadcast] polygon-agent deposit --asset --amount [--protocol aave|morpho] [--broadcast] +polygon-agent withdraw --position --amount [--chain ] [--broadcast] polygon-agent fund [--wallet ] [--token ] polygon-agent x402-pay --url --wallet [--method GET] [--body ] [--header Key:Value] ``` @@ -127,9 +128,11 @@ 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 ` +- **`withdraw`** — `--position` = aToken or ERC-4626 vault; `--amount` = `max` or underlying units (Aave / vault). Dry-run JSON includes `poolAddress` / `vault`. Broadcast needs session on the **same chain** as `--chain`, with pool/vault + underlying token whitelisted where the relayer touches them - **`x402-pay`** — probes endpoint for 402, smart wallet funds builder EOA with exact token amount, EOA signs EIP-3009 payment. Chain auto-detected from 402 response - **`send-native --direct`** — bypasses ValueForwarder contract for direct EOA transfer - **Session permissions** — without `--usdc-limit` etc., session gets bare-bones defaults and may not transact @@ -162,6 +165,7 @@ CLI commands output JSON (non-TTY). After running a command, always render the r | `send` / `send-token` / `send-native` | One-liner summary: amount, symbol, recipient. If broadcast, show tx hash as a code span and explorer URL as a link. | | `swap` | Summary: `X FROM → Y TO` with chain. If broadcast, show deposit tx hash + explorer link. | | `deposit` | Summary: amount, asset, protocol, pool address. If broadcast, show tx hash + explorer link. | +| `withdraw` | Summary: `kind` (aave / erc4626), position, amount, pool or vault. If broadcast, show tx hash + explorer link. | | `fund` | Show the `fundingUrl` as a clickable link with a brief instruction to open it. | | `wallet create` / `wallet list` | Wallet name, truncated address, chain in a small table or bullet list. | | `agent register` | Show agent name and tx hash as a code span with Polygonscan link. Remind user to retrieve `agentId` from the Registered event on the Logs tab. | @@ -182,7 +186,7 @@ For specific workflows, fetch and load the relevant sub-skill: | Use Case | Skill URL | |----------|-----------| | Polymarket prediction market trading | https://agentconnect.polygon.technology/polygon-polymarket/SKILL.md | -| DeFi — swap, deposit, yield | https://agentconnect.polygon.technology/polygon-defi/SKILL.md | +| DeFi — swap, deposit, withdraw, yield | https://agentconnect.polygon.technology/polygon-defi/SKILL.md | | x402 discovery & pay-per-call APIs | https://agentconnect.polygon.technology/polygon-discovery/SKILL.md | --- @@ -200,6 +204,8 @@ For specific workflows, fetch and load the relevant sub-skill: | `Invalid code: hash mismatch` | Wrong 6-digit code entered — retry (3 attempts allowed) | | `Relay request not found` | Session expired or already used — re-run `wallet create` (or `wallet create --print-url`) | | Deposit session rejected | Re-create wallet with `--contract ` | +| `withdraw` / broadcast: wrong chain or session rejects | Use `wallet create --chain ` and `--contract` for pool/vault + underlying ERC-20 on that chain; omit tight `--usdc-limit` if it blocks fee transfers | +| `Stored explicit session is missing pk` | Re-link: `wallet import --code …` after `wallet create` | | Wrong recipient in Trails widget | Run `polygon-agent fund` (do not construct the URL manually) | | `x402-pay`: no 402 response | Endpoint doesn't require x402 payment, or URL is wrong | | `x402-pay`: payment token mismatch | Chain/token in the 402 response differs from wallet — check `--wallet` points to the right chain | diff --git a/skills/polygon-defi/SKILL.md b/skills/polygon-defi/SKILL.md index 3751d99..0afcbff 100644 --- a/skills/polygon-defi/SKILL.md +++ b/skills/polygon-defi/SKILL.md @@ -129,6 +129,25 @@ polygon-agent deposit --asset USDC --amount 0.3 --protocol morpho --broadcast Vault/pool addresses are resolved dynamically from Trails — they are not hardcoded. The dry-run output includes `depositAddress` so you can inspect the exact contract before broadcasting. +## Withdraw (Aave aToken or ERC-4626 vault) + +Pass the **position token** you hold: an **Aave aToken** address, or a **Morpho / ERC-4626 vault** (share) address. The CLI resolves the Aave **Pool** via `POOL()` on the aToken, or uses `redeem` on the vault. Dry-run by default. + +```bash +# Full exit from an Aave position (aToken from balances output) +polygon-agent withdraw --position 0x68215b6533c47ff9f7125ac95adf00fe4a62f79e --amount max --chain mainnet + +# Partial Aave withdraw (underlying units, e.g. USDC) +polygon-agent withdraw --position --amount 0.5 --chain mainnet --broadcast + +# ERC-4626: max redeems all shares; partial amount is underlying units (convertToShares) +polygon-agent withdraw --position --amount max --chain polygon --broadcast +``` + +Whitelist the **pool** (Aave) or **vault** contract on the session if the wallet rejects the call (`polygon-agent wallet create --contract `). + +**Same chain as the transaction:** if you use `withdraw --chain mainnet`, create or refresh the session with **`wallet create --chain mainnet`** (not only Polygon defaults). Include **`--contract`** for the **pool** and for the **underlying ERC-20** on that chain (e.g. mainnet USDC `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48`) so fee / helper transfers are allowed. Tight **`--usdc-limit`** can block those — omit or relax for yield exits. + ### Session Whitelisting If the deposit is rejected with a session permission error, the pool's contract address needs to be whitelisted when creating the wallet session: