Skill: tezatezaz/cast (version 0.1.1)

--- name: clawcast description: Skill for managing EVM wallets, transactions, and network helpers via cast; covers onboarding, checks, and operating procedures. --- # Cast Skill [![No high vulnerabilities found by ClawAudit AI analyse. Click to get more info](https://clawaudit.duckdns.org/badges/7737be97-edda-4cd4-9070-fa47547dd44a.png)](https://clawaudit.duckdns.org/audit/7737be97-edda-4cd4-9070-fa47547dd44a) > **Agent note:** When this skill is installed or first discovered, make sure to record a TOOLS.md entry summarizing it so the workspace cheat sheet stays up to date. This skill is focused on crypto/EVM wallet operations and transaction workflows using `cast`. It covers wallet creation, importing or generating keys, checking balances, sending coins or tokens, monitoring tokens, creating and verifying transactions, and keeping `agent` keystores secure so the agent can guide the user through the core crypto operations a wallet handles. ## Resources - **EVM network list** — `assets/evm-networks.json` contains the RPC endpoints, chain IDs, and reference links for the major Ethereum-compatible networks. Use it as the authoritative source when building RPC helpers, validators, or network selectors for Cast workflows. - **Token metadata** — `assets/evm-network-tokens.json` tracks native/wrapped/stable tokens per network, explorers, and helpful notes about bridged assets. Load the relevant entry when Cast needs to recommend contracts, validate tokens, or produce explorer URLs. ## Scripts - **Step scripts** — `scripts/01_install_cast.sh`..`06_finish.sh` cover the onboarding flow described in the README: install Foundry/cast, create or import a key, encrypt the keystore, choose network/RPC/tokens (sourced from the JSON assets), and show the resulting address and balance. Run them in order when the user requests onboarding. Each script already prompts for the necessary inputs (mnemonic/private key, password, RPC URL, token details), so relaying the same questions to the user and then running the next script is the recommended approach. - **Wallet health check** — `scripts/check_wallet.sh` inspects the shared state and reports whether a keystore/address pair already exists; it returns success (0) when a wallet is present and 1 otherwise. - **Network status** — `scripts/show_network.sh` prints the active network name, chainId, and RPC URL from `~/.agent-wallet/state.env`, or warns if the configuration is incomplete. - **Wallet removal** — `scripts/remove_wallet.sh` safely deletes the keystore, password stash, and metadata from `~/.agent-wallet/state.env` after an explicit confirmation. ## Agent guidance ⚠️ При общении с пользователем не упоминай названия скриптов и не погружайся в технические детали — говори простым, понятным языком и не отвлекай на внутренности системы. Онбординг должен идти пошагово, причём пользователь получает только один пункт за раз, не весь план сразу. Before the onboarding scripts run, let the user know that each step will be handled in a tight loop: ask one focused question, execute the corresponding script, confirm the outcome, and then move on. Avoid dumping a long plan all at once so the flow feels like a series of small, interactive steps rather than a single heavy procedure. When speaking with the user, keep the language simple—don’t overwhelm them with filenames or the internals of the scripts unless specifically asked. Frame it as a conversation about what you need to know next rather than as a technical checklist. Always ask the user, right before running each script, exactly the question that script itself will ask (password, network choice, etc.). Do not invent or fill in answers on their behalf—only use the information they explicitly provide. This keeps onboarding faithful to what they chose and avoids pushing the scripts forward with made-up data. 1. **Start with targeted help if stuck.** Pipe `cast --help` through `grep` (e.g., `cast --help | grep balance`) to zero in on the relevant subcommand and avoid scrolling the entire manual; this saves tokens and keeps the answer focused before you proceed or explain anything. 2. **Automatic readiness check.** Run `scripts/check_wallet.sh` automatically each session; do not ask the user to trigger it. If it detects an existing wallet, immediately display the saved address/keystore path and proceed to show the balance/network status (see next step) so the user sees “wallet ready” without extra probes. 3. **Show wallet + network status.** When `check_wallet` finds a wallet, run `scripts/show_network.sh` and query the balance (e.g., `cast balance <ADDRESS> --rpc-url <RPC_URL> --ether`) so the user sees the current native balance, network name, chainId, and RPC URL without being prompted to check anything manually. 4. **Onboarding flow** (automatic when no wallet exists). If the readiness check exits with 1, walk through the scripted steps in order, mirroring their prompts and explicitly asking the user for every required piece of information before running the next script. After the key-material step finishes, share the derived address immediately so the user sees it before we ask them for anything in step 3: 1. Installation — explain that the script will ensure Foundry/cast is installed so every mentioned `cast` command works before proceeding. 2. Key material — before running the wallet step, ask whether they want to create a new hot keypair, import a 12/24-word MetaMask-compatible mnemonic (`m/44'/60'/0'/0/0`), or import a private key. Collect the chosen secret, confirm the resulting address right after the step finishes, and tell the user that address before moving on. When generating a new keypair, capture the mnemonic displayed by `cast wallet new`, save it to `~/.agent-wallet/mnemonic-words-<timestamp>.txt`, and tell the user the exact path plus the fact that a job (via `at now + 1 hour` if available or a background `sleep` fallback) will delete that file after 60 minutes so the seed phrase does not linger. 3. Password — only ask for the keystore password once (there is no confirmation prompt, no save/remember question, and the account name is forced to “agent”). The script saves that password to the local helper file and uses it when creating the keystore, so nothing else is needed from the user for this step. 4. Network — read aloud the default network list derived from `assets/evm-networks.json`, ask which numbered network they want, and note that the script now auto-selects the first RPC URL from that entry (it saves the matching `CHAIN_ID`/`ETH_RPC_URL` and then just shows the RPC so the user can see which endpoint is being used). 5. Tokens — the script now prints the token table derived from `assets/evm-network-tokens.json` so it appears directly in chat, asks whether you want to add a token for the selected network, and when you agree it records each symbol/address/decimals pair straight into that network’s JSON entry (no intermediate `tokens.tsv` file is involved). 6. Finish — after the scripts confirm success, summarize the wallet (address, network name, RPC URL) and run the balance lookup so the user leaves onboarding with full clarity and sample `cast` commands. 5. **Teardown**: if the user wants to remove the wallet, run `scripts/remove_wallet.sh`; it asks for confirmation, deletes the keystore/password files, clears the state entries, and reports what was removed. ### Transaction logging Whenever you mention a transaction (history, hash, or significant transfer) to the user, append a short summary to `logs/tx_mentions.log` in the workspace. Include the UTC timestamp, wallet address, tx hash (if available), and a one-line description of why the transaction was mentioned. This keeps a running record for later reference. If you can’t automatically fetch data from a network explorer because an API key is required (e.g., BscScan/Etherscan V2), tell the user that we need to fall back to manual viewing and share the direct Explorer URL (e.g., `https://bscscan.com/address/<address>` or `https://bscscan.com/tx/<txHash>`) so they can open it themselves. Mention the limitation plainly instead of leaving them waiting for data we can’t pull. ## Operator reference (common cast commands) 1. `cast balance <address>` — check the native coin balance (ETH, etc.). Common flags: `--rpc-url ...`, `--ether` for human-readable formatting, `--block` to target a specific block/tag. 2. `cast send` — the workhorse for native transfers, ERC-20 transfers/approvals, swaps, or any signed contract interaction. Typical flags: `--rpc-url ...`, `--keystore ...`, `--password-file ...`, `--value ...`, `--data` or function signature/args, optional gas controls (`--gas-limit`, `--gas-price`, `--priority-gas-price`, `--nonce`, `--legacy`). 3. `cast call` — perform read-only contract calls (balanceOf, allowance, decimals, totalSupply, etc.). Common flags: `--rpc-url ...`, `--block ...`, or `--data ...` when you already have calldata. 4. `cast receipt <txHash>` — fetch and inspect the transaction receipt (status, gas, logs); use it to confirm success after `cast send`. Optional flags: `--confirmations ...` or requesting a single field by name. 5. `cast tx <txHash>` — fetch a transaction’s details; you can request a specific field or raw RLP with `--raw`. 6. `cast nonce <address>` — get the current nonce to avoid "nonce too low" errors, especially when batching; optionally target a block/tag. 7. `cast rpc <method> [params...]` — make raw JSON-RPC calls for edge cases, debug methods, or custom node features. Use `--raw` when passing a JSON array by string or via stdin. 8. `cast mktx ...` — build and sign a raw transaction without broadcasting (prep for "prepare → review → publish"); same `to`/signature/args or `--data`, plus knobs like `--value`, `--nonce`, `--gas-limit`, `--gas-price`, `--priority-gas-price`. 9. `cast publish <rawTx>` — broadcast a signed raw transaction (pairs with `mktx` or any external signing flow); `--async` is optionally useful. 10. `cast wallet new` / `cast wallet new-mnemonic` — generate keys or a BIP-39 mnemonic. Supply a keystore path and account name if desired; avoid `--unsafe-password` unless you understand the risk. Use `--words`/`--accounts` to control mnemonic length and derived accounts. 11. `cast wallet import <name>` — import a private key or mnemonic into an encrypted keystore; by default it prompts for secrets, but you can pass `--private-key`, `--mnemonic`, `--mnemonic-derivation-path`, `--mnemonic-index`, `--mnemonic-passphrase`, or `--keystore-dir`. 12. `cast wallet list` — show local keystore accounts; `--dir` points to a custom directory, and hardware flags unlock ledger/trezor lists. 13. `cast wallet address ...` — derive the wallet address from a secret source (`--interactive`, `--private-key`, or `--mnemonic`). 14. `cast wallet sign` / `cast wallet verify` — sign or verify messages/typed data. Provide the message and signer plus `--private-key`, `--interactive`, or `--mnemonic`; add `--no-hash` for raw hashes and `--data`/`--from-file` for EIP-712 JSON. 15. `cast parse-units <amount> --decimals <n>` — convert human-readable numbers (e.g., "1.5 USDC") to base units for ERC-20 transfers. 16. `cast format-units` — convert base integers back into decimals given token decimals. 17. `cast to-unit` / `cast to-wei` — ETH unit conversions; specify target unit (wei, gwei, ether, etc.) or use `cast to-wei` as a shortcut. 18. `cast 4byte` and calldata helpers — look up a 4-byte selector and pretty-print/ decode calldata when debugging unknown transactions.

path: assets/evm-network-tokens.json
content: {
  "generated_at": "2026-02-01",
  "schema_version": 1,
  "networks": {
    "ethereum": {
      "name": "Ethereum Mainnet",
      "chain_id": 1,
      "currency_symbol": "ETH",
      "rpc": [
        "https://cloudflare-eth.com",
        "https://rpc.ankr.com/eth"
      ],
      "explorers": [
        "https://etherscan.io"
      ],
      "tokens": {
        "native": {
          "symbol": "ETH",
          "type": "native"
        },
        "wrapped_native": {
          "symbol": "WETH",
          "name": "Wrapped Ether",
          "address": [REDACTED_SECRET],
          "decimals": 18
        },
        "stables": {
          "USDC": {
            "name": "USD Coin",
            "address": [REDACTED_SECRET],
            "decimals": 6,
            "issuer": "Circle"
          },
          "USDT": {
            "name": "Tether USD",
            "address": [REDACTED_SECRET],
            "decimals": 6,
            "issuer": "Tether"
          }
        }
      }
    },
    "arbitrum_one": {
      "name": "Arbitrum One",
      "chain_id": 42161,
      "currency_symbol": "ETH",
      "rpc": [
        "https://arb1.arbitrum.io/rpc"
      ],
      "explorers": [
        "https://arbiscan.io"
      ],
      "tokens": {
        "native": {
          "symbol": "ETH",
          "type": "native"
        },
        "wrapped_native": {
          "symbol": "WETH",
          "name": "Wrapped Ether",
          "address": [REDACTED_SECRET],
          "decimals": 18
        },
        "stables": {
          "USDC": {
            "name": "USD Coin",
            "address": [REDACTED_SECRET],
            "decimals": 6,
            "issuer": "Circle"
          },
          "USDT": {
            "name": "Tether USD",
            "address": [REDACTED_SECRET],
            "decimals": 6,
            "issuer": "Tether"
          }
        }
      }
    },
    "optimism": {
      "name": "OP Mainnet",
      "chain_id": 10,
      "currency_symbol": "ETH",
      "rpc": [
        "https://mainnet.optimism.io"
      ],
      "explorers": [
        "https://optimistic.etherscan.io"
      ],
      "tokens": {
        "native": {
          "symbol": "ETH",
          "type": "native"
        },
        "wrapped_native": {
          "symbol": "WETH",
          "name": "Wrapped Ether",
          "address": "0x4200000000000000000000000000000000000006",
          "decimals": 18
        },
        "stables": {
          "USDC": {
            "name": "USD Coin",
            "address": [REDACTED_SECRET],
            "decimals": 6,
            "issuer": "Circle"
          },
          "USDT": {
            "name": "Tether USD",
            "address": [REDACTED_SECRET],
            "decimals": 6,
            "issuer": "Tether"
          }
        }
      }
    },
    "base": {
      "name": "Base Mainnet",
      "chain_id": 8453,
      "currency_symbol": "ETH",
      "rpc": [
        "https://mainnet.base.org"
      ],
      "explorers": [
        "https://basescan.org"
      ],
      "tokens": {
        "native": {
          "symbol": "ETH",
          "type": "native"
        },
        "wrapped_native": {
          "symbol": "WETH",
          "name": "Wrapped Ether",
          "address": "0x4200000000000000000000000000000000000006",
          "decimals": 18
        },
        "stables": {
          "USDC": {
            "name": "USD Coin",
            "address": [REDACTED_SECRET],
            "decimals": 6,
            "issuer": "Circle"
          },
          "USDT": {
            "name": "Tether USD",
            "address": [REDACTED_SECRET],
            "decimals": 6,
            "issuer": "Tether",
            "bridged": true
          }
        }
      }
    },
    "polygon_pos": {
      "name": "Polygon PoS",
      "chain_id": 137,
      "currency_symbol": "MATIC",
      "rpc": [
        "https://polygon-rpc.com"
      ],
      "explorers": [
        "https://polygonscan.com"
      ],
      "tokens": {
        "native": {
          "symbol": "MATIC",
          "type": "native"
        },
        "wrapped_native": {
          "symbol": "WMATIC",
          "name": "Wrapped MATIC",
          "address": [REDACTED_SECRET],
          "decimals": 18
        },
        "stables": {
          "USDC": {
            "name": "USD Coin",
            "address": [REDACTED_SECRET],
            "decimals": 6,
            "issuer": "Circle"
          },
          "USDT": {
            "name": "Tether USD",
            "address": [REDACTED_SECRET],
            "decimals": 6,
            "issuer": "Tether"
          }
        }
      }
    },
    "bnb_smart_chain": {
      "name": "BNB Smart Chain",
      "chain_id": 56,
      "currency_symbol": "BNB",
      "rpc": [
        "https://bsc-dataseed.bnbchain.org"
      ],
      "explorers": [
        "https://bscscan.com"
      ],
      "tokens": {
        "native": {
          "symbol": "BNB",
          "type": "native"
        },
        "wrapped_native": {
          "symbol": "WBNB",
          "name": "Wrapped BNB",
          "address": [REDACTED_SECRET],
          "decimals": 18
        },
        "stables": {
          "USDT": {
            "name": "Tether USD",
            "address": [REDACTED_SECRET],
            "decimals": 18,
            "issuer": "Binance-Bridged",
            "bridged": true
          }
        }
      }
    },
    "avalanche_c_chain": {
      "name": "Avalanche C-Chain",
      "chain_id": 43114,
      "currency_symbol": "AVAX",
      "rpc": [
        "https://api.avax.network/ext/bc/C/rpc"
      ],
      "explorers": [
        "https://avascan.info",
        "https://snowtrace.io"
      ],
      "tokens": {
        "native": {
          "symbol": "AVAX",
          "type": "native"
        },
        "wrapped_native": {
          "symbol": "WAVAX",
          "name": "Wrapped AVAX",
          "address": [REDACTED_SECRET],
          "decimals": 18
        },
        "stables": {
          "USDC": {
            "name": "USD Coin",
            "address": [REDACTED_SECRET],
            "decimals": 6,
            "issuer": "Circle"
          },
          "USDT": {
            "name": "Tether USD",
            "address": [REDACTED_SECRET],
            "decimals": 6,
            "issuer": "Tether"
          }
        }
      }
    },
    "gnosis": {
      "name": "Gnosis Chain",
      "chain_id": 100,
      "currency_symbol": "XDAI",
      "rpc": [
        "https://rpc.gnosischain.com"
      ],
      "explorers": [
        "https://gnosisscan.io",
        "https://blockscout.com/gnosis"
      ],
      "tokens": {
        "native": {
          "symbol": "XDAI",
          "type": "native"
        },
        "wrapped_native": {
          "symbol": "WXDAI",
          "name": "Wrapped xDai",
          "address": [REDACTED_SECRET],
          "decimals": 18
        },
        "stables": {
          "USDT": {
            "name": "Tether USD",
            "address": [REDACTED_SECRET],
            "decimals": 6,
            "bridged": true
          }
        }
      }
    },
    "fantom": {
      "name": "Fantom Opera",
      "chain_id": 250,
      "currency_symbol": "FTM",
      "rpc": [
        "https://rpcapi.fantom.network"
      ],
      "explorers": [
        "https://ftmscan.com"
      ],
      "tokens": {
        "native": {
          "symbol": "FTM",
          "type": "native"
        },
        "wrapped_native": {
          "symbol": "WFTM",
          "name": "Wrapped Fantom",
          "address": [REDACTED_SECRET],
          "decimals": 18
        },
        "stables": {
          "USDT": {
            "name": "Frapped USDT",
            "symbol": "fUSDT",
            "address": [REDACTED_SECRET],
            "decimals": 6,
            "bridged": true
          }
        }
      }
    },
    "linea": {
      "name": "Linea",
      "chain_id": 59144,
      "currency_symbol": "ETH",
      "rpc": [
        "https://rpc.linea.build"
      ],
      "explorers": [
        "https://lineascan.build",
        "https://explorer.linea.build"
      ],
      "tokens": {
        "native": {
          "symbol": "ETH",
          "type": "native"
        },
        "wrapped_native": {
          "symbol": "WETH",
          "name": "Wrapped Ether",
          "address": [REDACTED_SECRET],
          "decimals": 18
        },
        "stables": {
          "USDC": {
            "name": "USD Coin",
            "address": [REDACTED_SECRET],
            "decimals": 6,
            "issuer": "Circle"
          },
          "USDT": {
            "name": "Tether USD",
            "address": [REDACTED_SECRET],
            "decimals": 6,
            "issuer": "Tether",
            "bridged": true
          }
        }
      }
    },
    "zksync_era": {
      "name": "zkSync Era",
      "chain_id": 324,
      "currency_symbol": "ETH",
      "rpc": [
        "https://mainnet.era.zksync.io"
      ],
      "explorers": [
        "https://explorer.zksync.io",
        "https://era.zksync.network"
      ],
      "tokens": {
        "native": {
          "symbol": "ETH",
          "type": "native"
        },
        "wrapped_native": {
          "symbol": "WETH",
          "name": "Wrapped Ether",
          "address": "0x5aea5775959fbc2557cc8789bc1bf90a239d9a91",
          "decimals": 18
        },
        "stables": {
          "USDC": {
            "name": "USD Coin",
            "address": "0x1d17cbcf0d6d143135ae902365d2e5e2a16538d4",
            "decimals": 6,
            "issuer": "Circle"
          },
          "USDT": {
            "name": "Tether USD",
            "address": "0x493257fd37edb34451f62edf8d2a0c418852ba4c",
            "decimals": 6,
            "issuer": "Tether"
          }
        }
      }
    },
    "scroll": {
      "name": "Scroll",
      "chain_id": 534352,
      "currency_symbol": "ETH",
      "rpc": [
        "https://rpc.scroll.io"
      ],
      "explorers": [
        "https://scrollscan.com",
        "https://blockscout.scroll.io"
      ],
      "tokens": {
        "native": {
          "symbol": "ETH",
          "type": "native"
        },
        "wrapped_native": {
          "symbol": "WETH",
          "name": "Wrapped Ether",
          "address": "0x5300000000000000000000000000000000000004",
          "decimals": 18
        },
        "stables": {
          "USDC": {
            "name": "USD Coin",
            "address": "0x06efdbff2a14a7c8e15944d1f4a48f9f95f663a4",
            "decimals": 6,
            "bridged": true
          },
          "USDT": {
            "name": "Tether USD",
            "address": "0xf55bec9cafdbe8730f096aa55dad6d22d44099df",
            "decimals": 6,
            "issuer": "Tether",
            "bridged": true
          }
        }
      }
    }
  },
  "notes": [
    "Addresses are mainnet contract addresses. Always validate on the chain's explorer before use.",
    "Some stables are bridged representations (see `bridged: true`)."
  ]
}


path: assets/evm-networks.json
content: {
  "version": 1,
  "evmNetworks": [
    {
      "key": "ethereum",
      "name": "Ethereum Mainnet",
      "chainId": 1,
      "rpcUrls": [
        "https://cloudflare-eth.com",
        "https://rpc.ankr.com/eth",
        "https://1rpc.io/eth"
      ],
      "links": {
        "chainlist": "https://chainlist.org/chain/1"
      }
    },
    {
      "key": "arbitrum_one",
      "name": "Arbitrum One",
      "chainId": 42161,
      "rpcUrls": [
        "https://arb1.arbitrum.io/rpc"
      ],
      "links": {
        "docs": "https://docs.arbitrum.io/build-decentralized-apps/reference/node-providers",
        "chainlist": "https://chainlist.org/chain/42161"
      }
    },
    {
      "key": "optimism",
      "name": "OP Mainnet (Optimism)",
      "chainId": 10,
      "rpcUrls": [
        "https://mainnet.optimism.io"
      ],
      "links": {
        "docs": "https://docs.optimism.io/op-mainnet/network-information/connecting-to-op",
        "chainlist": "https://chainlist.org/chain/10"
      }
    },
    {
      "key": "base",
      "name": "Base",
      "chainId": 8453,
      "rpcUrls": [
        "https://mainnet.base.org",
        "https://mainnet-preconf.base.org"
      ],
      "links": {
        "docs": "https://docs.base.org/base-chain/quickstart/connecting-to-base",
        "chainlist": "https://chainlist.org/chain/8453"
      }
    },
    {
      "key": "polygon_pos",
      "name": "Polygon PoS",
      "chainId": 137,
      "rpcUrls": [
        "https://polygon-rpc.com/"
      ],
      "links": {
        "docs": "https://docs.polygon.technology/pos/reference/rpc-endpoints/",
        "chainlist": "https://chainlist.org/chain/137"
      }
    },
    {
      "key": "bsc",
      "name": "BNB Smart Chain",
      "chainId": 56,
      "rpcUrls": [
        "https://bsc-dataseed.bnbchain.org",
        "https://bsc-dataseed.nariox.org",
        "https://bsc-dataseed.defibit.io",
        "https://bsc-dataseed.ninicoin.io",
        "https://bsc.nodereal.io",
        "https://bsc-dataseed-public.bnbchain.org"
      ],
      "links": {
        "docs": "https://docs.bnbchain.org/bnb-smart-chain/developers/json_rpc/json-rpc-endpoint/",
        "chainlist": "https://chainlist.org/chain/56"
      }
    },
    {
      "key": "avalanche_c",
      "name": "Avalanche C-Chain",
      "chainId": 43114,
      "rpcUrls": [
        "https://api.avax.network/ext/bc/C/rpc"
      ],
      "links": {
        "docs": "https://build.avax.network/docs/primary-network",
        "chainlist": "https://chainlist.org/chain/43114"
      }
    },
    {
      "key": "gnosis",
      "name": "Gnosis Chain",
      "chainId": 100,
      "rpcUrls": [
        "https://rpc.gnosis.gateway.fm",
        "https://rpc.gnosischain.com",
        "https://rpc.ankr.com/gnosis"
      ],
      "links": {
        "chainlist": "https://chainlist.org/chain/100"
      }
    },
    {
      "key": "fantom",
      "name": "Fantom Opera",
      "chainId": 250,
      "rpcUrls": [
        "https://rpcapi.fantom.network",
        "https://fantom.rpc.thirdweb.com/"
      ],
      "links": {
        "docs_wallet": "https://docs.fantom.foundation/wallets/metamask",
        "docs_public_endpoints": "https://docs.fantom.foundation/build-on-opera/api/public-endpoints",
        "chainlist": "https://chainlist.org/chain/250"
      }
    },
    {
      "key": "zksync_era",
      "name": "zkSync Era",
      "chainId": 324,
      "rpcUrls": [
        "https://mainnet.era.zksync.io"
      ],
      "wsUrls": [
        "wss://mainnet.era.zksync.io/ws"
      ],
      "links": {
        "docs": "https://docs.zksync.io/zksync-network/zksync-era/network-details",
        "chainlist": "https://chainlist.org/chain/324"
      }
    },
    {
      "key": "linea",
      "name": "Linea",
      "chainId": 59144,
      "rpcUrls": [
        "https://rpc.linea.build"
      ],
      "links": {
        "chainlist": "https://chainlist.org/chain/59144"
      }
    },
    {
      "key": "scroll",
      "name": "Scroll",
      "chainId": 534352,
      "rpcUrls": [
        "https://rpc.scroll.io/"
      ],
      "links": {
        "docs": "https://docs.scroll.io/en/developers/",
        "chainlist": "https://chainlist.org/chain/534352"
      }
    }
  ]
}


path: scripts/00_lib.sh
content: #!/usr/bin/env bash
set -euo pipefail

APP_NAME="agent-wallet"
APP_DIR="${HOME}/.${APP_NAME}"
STATE_FILE="${APP_DIR}/state.env"
FOUNDRY_BIN_DIR="${HOME}/.foundry/bin"
FOUNDRY_KEYSTORE_DIR="${HOME}/.foundry/keystores"
ASSETS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../assets" && pwd)"
NETWORKS_JSON="${ASSETS_DIR}/evm-networks.json"
TOKENS_JSON="${ASSETS_DIR}/evm-network-tokens.json"

bold() { printf "\033[1m%s\033[0m " "$*"; }
dim() { printf "\033[2m%s\033[0m " "$*"; }
ok() { printf "\033[32m✔\033[0m %s " "$*"; }
warn() { printf "\033[33m!\033[0m %s " "$*"; }
err() { printf "\033[31m✖\033[0m %s " "$*"; }
line() { printf "%s " "------------------------------------------------------------"; }

enable_strict_path() {
  if [[ ":$PATH:" != *":${FOUNDRY_BIN_DIR}:"* ]]; then
    export PATH="${FOUNDRY_BIN_DIR}:$PATH"
  fi
}

has_cmd() { command -v "$1" >/dev/null 2>&1; }

ask_default() {
  local q="$1"
  local d="$2"
  local a
  printf "%s [%s]: " "$q" "$d" >&2
  read -r a || true
  if [[ -z "${a:-}" ]]; then
    printf "%s" "$d"
  else
    printf "%s" "$a"
  fi
}

ask_hidden() {
  local prompt="$1"
  local v
  read -rs -p "$prompt" v
  printf "\n"
  printf "%s" "$v"
}

is_yes() {
  local val
  val="$(printf "%s" "$1" | tr '[:upper:]' '[:lower:]')"
  [[ "$val" == "y" || "$val" == "yes" ]]
}

confirm_prompt() {
  local prompt="$1"
  local default="${2:-N}"
  local answer
  local tty="/dev/tty"
  if [[ -t 0 ]]; then
    while true; do
      read -rp "$prompt [$default]: " answer
      answer="${answer:-$default}"
      answer="$(printf "%s" "$answer" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')"
      case "$answer" in
        y|yes) return 0;;
        n|no) return 1;;
        *) printf "Please answer y or n.\n";;
      esac
    done
  else
    while true; do
      if ! read -rp "$prompt [$default]: " answer <"$tty"; then
        return 1
      fi
      answer="${answer:-$default}"
      answer="$(printf "%s" "$answer" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')"
      case "$answer" in
        y|yes) return 0;;
        n|no) return 1;;
        *) printf "Please answer y or n.\n";;
      esac
    done
  fi
}

_escape_squotes() {
  printf "%s" "$1" | sed "s/'/'\"'\"'/g"
}

ensure_app_dir() {
  mkdir -p "${APP_DIR}"
  chmod 700 "${APP_DIR}" || true
}

state_init() {
  ensure_app_dir
  if [[ ! -f "${STATE_FILE}" ]]; then
    cat > "${STATE_FILE}" <<EOF
APP_DIR='${APP_DIR}'
STATE_FILE='${STATE_FILE}'
EOF
    chmod 600 "${STATE_FILE}" || true
  fi
}

state_get() {
  local key="$1"
  state_init
  # shellcheck disable=SC1090
  source "${STATE_FILE}"
  printf '%s' "${!key-}"
}

state_set() {
  local key="$1"
  local val="$2"
  state_init
  local esc
  esc="$(_escape_squotes "$val")"
  if grep -qE "^${key}=" "${STATE_FILE}"; then
    if sed --version >/dev/null 2>&1; then
      sed -i -E "s#^${key}=.*#${key}='${esc}'#" "${STATE_FILE}"
    else
      sed -i '' -E "s#^${key}=.*#${key}=\'${esc}\'#" "${STATE_FILE}"
    fi
  else
    printf "%s='%s'\n" "$key" "$esc" >> "${STATE_FILE}"
  fi
  chmod 600 "${STATE_FILE}" || true
}

state_unset() {
  local key="$1"
  state_init
  local tmp
  tmp="$(mktemp)"
  grep -vE "^${key}=" "${STATE_FILE}" > "$tmp" || true
  mv "$tmp" "${STATE_FILE}"
  chmod 600 "${STATE_FILE}" || true
}

write_default_networks_if_missing() {
  local target="$1"
  if [[ -f "$target" ]]; then
    return
  fi
  python3 - "${NETWORKS_JSON}" <<'PY' > "$target"
import json, sys
networks=json.load(open(sys.argv[1], 'r'))
for net in networks.get('evmNetworks', []):
    name=net.get('name')
    cid=net.get('chainId')
    key=net.get('key', '')
    if name and cid is not None:
        print(f"{name}\t{cid}\t{key}")
PY
  chmod 600 "$target" || true
}

write_default_tokens_if_missing() {
  local target="$1"
  if [[ -f "$target" ]]; then
    return
  fi
  python3 - "${TOKENS_JSON}" <<'PY' > "$target"
import json, sys
networks=json.load(open(sys.argv[1], 'r')).get('networks', {})
seen=set()
print('# symbol\taddress\tdecimals')
for net_key, data in networks.items():
    name=data.get('name', net_key)
    tokens=data.get('tokens', {})
    for label in ('wrapped_native', 'stables'):
        block=tokens.get(label, {})
        if isinstance(block, dict):
            if 'address' in block and isinstance(block.get('symbol'), str):
                addr=block.get('address')
                if addr:
                    symbol=block.get('symbol', label.upper())
                    dec=block.get('decimals', '')
                    key=(symbol.lower(), addr.lower())
                    if key not in seen:
                        seen.add(key)
                        print(f"{symbol}\t{addr}\t{dec}")
            else:
                for symbol, info in block.items():
                    if not isinstance(info, dict):
                        continue
                    addr=info.get('address')
                    if not addr:
                        continue
                    dec=info.get('decimals', '')
                    key=(symbol.lower(), addr.lower())
                    if key in seen:
                        continue
                    seen.add(key)
                    print(f"{symbol}\t{addr}\t{dec}")
PY
  chmod 600 "$target" || true
}

print_tokens_from_json() {
  python3 - "$1" <<'PY'
import json, sys
networks=json.load(open(sys.argv[1], 'r')).get('networks', {})
seen=set()
rows=[]
for net_key, net in sorted(networks.items(), key=lambda kv: kv[1].get('chain_id', 0)):
    name=net.get('name', net_key)
    tokens=net.get('tokens', {}) or {}
    for label in ('wrapped_native', 'stables', 'custom'):
        block=tokens.get(label)
        if not isinstance(block, dict):
            continue
        entries=[]
        if 'address' in block and isinstance(block.get('symbol'), str):
            entries.append((block.get('symbol', label.upper()), block))
        else:
            entries.extend(block.items())
        for symbol, info in entries:
            if not isinstance(info, dict):
                continue
            addr=info.get('address')
            if not addr:
                continue
            key=(symbol.lower(), addr.lower())
            if key in seen:
                continue
            seen.add(key)
            dec=info.get('decimals', '')
            rows.append((name, label, symbol, addr, dec))
if rows:
    print('network	category	symbol	address	decimals')
    for row in rows:
        net, cat, sym, addr, dec=row
        print(f"{net}	{cat}	{sym}	{addr}	{dec}")
PY
}

add_custom_token_to_json() {
  local json_path="${1}"
  local net_key="${2}"
  local symbol="${3}"
  local address="${4}"
  local decimals="${5}"
  python3 - "${json_path}" "${net_key}" "${symbol}" "${address}" "${decimals}" <<'PY'
import json, sys
from pathlib import Path
path = Path(sys.argv[1])
net_key = sys.argv[2]
symbol = sys.argv[3]
address = sys.argv[4]
decimals = sys.argv[5]
data = json.load(open(path))
networks = data.setdefault('networks', {})
net = networks.get(net_key)
if net is None:
    raise SystemExit(f"Network key {net_key} not found")
tokens = net.setdefault('tokens', {})
custom = tokens.setdefault('custom', {})
existing = custom.get(symbol)
if isinstance(existing, dict) and existing.get('address', '').lower() == address.lower():
    print(f"Token {symbol} already exists on {net_key}")
    raise SystemExit(0)
try:
    decimal_value = int(decimals)
except ValueError:
    decimal_value = decimals
custom[symbol] = {
    'address': address,
    'decimals': decimal_value,
    'added_by': 'agent'
}
json.dump(data, open(path, 'w'), indent=2)
print(f"Token {symbol} saved for network {net_key}")
PY
}

derive_pk_from_mnemonic() {
  local mnemonic="$1"
  local hdpath="m/44'/60'/0'/0/0"
  if ! cast wallet >/dev/null 2>&1; then
    printf ""
    return 0
  fi
  local out
  set +e
  out=$(cast wallet derive --mnemonic "$mnemonic" --hd-path "$hdpath" 2>/dev/null || true)
  if [[ -z "$out" ]]; then
    out=$(cast wallet derive "$mnemonic" --hd-path "$hdpath" 2>/dev/null || true)
  fi
  set -e
  if [[ -z "$out" ]]; then
    printf ""
    return 0
  fi
  local pk
  pk=$(printf "%s" "$out" | grep -Eo '0x[0-9a-fA-F]{64}' | head -n 1 || true)
  printf "%s" "${pk:-}"
}


path: scripts/01_install_cast.sh
content: #!/usr/bin/env bash
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "${DIR}/00_lib.sh"
state_init
enable_strict_path
bold "1) Installing cast (Foundry)"
line
if has_cmd cast; then
  ok "cast is already installed: $(cast --version 2>/dev/null || echo cast)"
  exit 0
fi
if ! has_cmd curl; then
  err "curl is required but not found. Install curl and re-run."
  exit 1
fi
warn "cast not found. Installing Foundry..."
curl -L https://foundry.paradigm.xyz | bash
enable_strict_path
if ! has_cmd foundryup; then
  for p in "${HOME}/.bashrc" "${HOME}/.zshrc" "${HOME}/.profile"; do
    [[ -f "$p" ]] && source "$p" >/dev/null 2>&1 || true
  done
fi
if ! has_cmd foundryup; then
  err "foundryup not found after install. Make sure ${FOUNDRY_BIN_DIR} is in PATH, then re-run."
  exit 1
fi
foundryup
enable_strict_path
if ! has_cmd cast; then
  err "cast still not found after Foundry install."
  exit 1
fi
ok "Installed cast: $(cast --version 2>/dev/null || echo cast)"


path: scripts/02_wallet.sh
content: #!/usr/bin/env bash
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "${DIR}/00_lib.sh"
state_init
enable_strict_path
extract_mnemonic_words() {
  local raw_output="$1"
  printf "%s\n" "$raw_output" | python3 -c 'import re, sys; text=sys.stdin.read(); match=re.search(r"([a-z]+(?: [a-z]+){11,23})", text);
if match:
    print(match.group(1))'
}

ask_default_tty() {
  local prompt="$1"
  local default="$2"
  local tty="/dev/tty"
  local answer
  if [[ -t 0 ]]; then
    printf "%s [%s]: " "$prompt" "$default" >&2
    read -r answer
  else
    if [[ -r "$tty" ]]; then
      local tty_fd
      if exec {tty_fd}<>"$tty" 2>/dev/null; then
        read -rp "$prompt [$default]: " answer <&"$tty_fd" || true
        exec {tty_fd}>&-
      else
        read -rp "$prompt [$default]: " answer || true
      fi
    else
      read -rp "$prompt [$default]: " answer || true
    fi
  fi
  if [[ -z "${answer:-}" ]]; then
    printf "%s" "$default"
  else
    printf "%s" "$answer"
  fi
}

ensure_at_available() {
  if has_cmd at; then
    return 0
  fi
  local installer
  if [[ -x "/usr/bin/apt-get" ]]; then
    installer="apt-get install -y at"
  elif [[ -x "/usr/bin/apt" ]]; then
    installer="apt install -y at"
  else
    warn "'at' command missing and no apt-like installer found; mnemonic cleanup must be manual."
    return 1
  fi
  if [[ -x "/usr/bin/sudo" ]]; then
    if ! sudo sh -c "$installer > /tmp/at-install.log 2>&1"; then
      warn "Failed to install 'at' with sudo; mnemonic cleanup will be manual."
      return 1
    fi
  else
    if ! sh -c "$installer > /tmp/at-install.log 2>&1"; then
      warn "Failed to install 'at'; mnemonic cleanup will be manual."
      return 1
    fi
  fi
  hash -r
  if has_cmd at; then
    ok "Installed 'at' so mnemonic cleanup can be scheduled."
    return 0
  fi
  warn "Installation succeeded but 'at' still unavailable; cleanup will be manual."
  return 1
}

schedule_mnemonic_file_cleanup() {
  local target="$1"
  if ! ensure_at_available; then
    warn "Mnemonic file ${target} won't auto-delete (no 'at'). Please remove it after ~1 hour."
    return
  fi
  local job_output rc
  set +e
  job_output="$(printf 'rm -f -- %q\n' "$target" | at now + 1 hour 2>&1)"
  rc=$?
  set -e
  if [[ $rc -eq 0 ]]; then
    ok "Mnemonic file scheduled for deletion (${job_output//$'\n'/; })."
  else
    warn "Scheduling deletion failed (${job_output//$'\n'/; }). Please remove ${target} manually."
  fi
}
bold "2) Create or import wallet"
line
if ! has_cmd cast; then
  err "cast is not installed. Run ./01_install_cast.sh first."
  exit 1
fi
PK_TMP="${APP_DIR}/privatekey.tmp"
state_set "PRIVATE_KEY_FILE" "${PK_TMP}"
echo "Choose an option:"
echo " 1) Create a new wallet (recommended)"
echo " 2) Import 12/24-word mnemonic (MetaMask derivation m/44'/60'/0'/0/0)"
echo " 3) Import a private key (0x...)"
choice="$(ask_default_tty "Select (1/2/3)" "1")"
PRIVATE_KEY=""
MNEMONIC=""
CREATED_NEW_WALLET=0
case "$choice" in
  1)
    bold "Creating a new wallet key"
    line
    set +e
    out="$(cast wallet new-mnemonic --words 12 --accounts 1 --color never)"
    rc=$?
    set -e
    if [[ $rc -ne 0 ]]; then
      warn "cast wallet new-mnemonic failed; falling back to generic key creation"
      set +e
      out="$(cast wallet new)"
      rc=$?
      set -e
      if [[ $rc -ne 0 ]]; then
        err "Unable to generate wallet material."
        exit 1
      fi
    fi
    PRIVATE_KEY="$(printf "%s" "$out" | grep -Eo '0x[0-9a-fA-F]{64}' | head -n 1 || true)"
    MNEMONIC="$(extract_mnemonic_words "$out")"
    CREATED_NEW_WALLET=1
    if [[ -z "$PRIVATE_KEY" ]]; then
      err "Failed to parse private key from cast output."
      exit 1
    fi
    ok "Generated a new key (private key not printed again)."
    ;;
  2)
    bold "Importing mnemonic (MetaMask-compatible)"
    line
    echo "Paste your 12/24-word mnemonic. It will NOT be printed back."
    MNEMONIC="$(ask_hidden "Mnemonic: ")"
    PRIVATE_KEY="$(derive_pk_from_mnemonic "$MNEMONIC")"
    if [[ -z "$PRIVATE_KEY" ]]; then
      err "Could not derive a private key from mnemonic using your cast version."
      err "Fix: update Foundry (foundryup) or choose 'Import a private key'."
      exit 1
    fi
    ok "Derived private key from mnemonic (not printed)."
    ;;
  3)
    bold "Importing private key"
    line
    echo "Paste your private key (0x...). It will NOT be printed back."
    PRIVATE_KEY="$(ask_hidden "Private key: ")"
    ;;
  *)
    warn "Invalid choice. Defaulting to 'Create a new wallet'."
    set +e
    out="$(cast wallet new-mnemonic --words 12 --accounts 1 --color never)"
    rc=$?
    set -e
    if [[ $rc -ne 0 ]]; then
      warn "cast wallet new-mnemonic failed; falling back to generic key creation"
      set +e
      out="$(cast wallet new)"
      rc=$?
      set -e
      if [[ $rc -ne 0 ]]; then
        err "Unable to generate wallet material."
        exit 1
      fi
    fi
    PRIVATE_KEY="$(printf "%s" "$out" | grep -Eo '0x[0-9a-fA-F]{64}' | head -n 1 || true)"
    MNEMONIC="$(extract_mnemonic_words "$out")"
    CREATED_NEW_WALLET=1
    ;;
esac
umask 077
if [[ "$CREATED_NEW_WALLET" == "1" ]]; then
  if [[ -n "$MNEMONIC" ]]; then
    MNEMONIC_FILE="${APP_DIR}/mnemonic-words-$(date +%Y%m%d%H%M%S).txt"
    printf "%s\n" "$MNEMONIC" > "${MNEMONIC_FILE}"
    chmod 600 "${MNEMONIC_FILE}" || true
    ok "Recovery words saved to ${MNEMONIC_FILE}"
    cleanup_note="$(schedule_mnemonic_file_cleanup "${MNEMONIC_FILE}")"
    warn "Mnemonic file will be deleted in ~1 hour (${cleanup_note})."
  else
    warn "Could not automatically capture the mnemonic from cast; please record it securely now."
  fi
fi
if ! [[ "$PRIVATE_KEY" =~ ^0x[0-9a-fA-F]{64}$ ]]; then
  err "Private key format invalid."
  exit 1
fi
printf "%s" "${PRIVATE_KEY}" > "${PK_TMP}"
chmod 600 "${PK_TMP}" || true
ok "Wallet material prepared."
warn "A temporary private key file was created: ${PK_TMP}"
dim "Step 03 will encrypt it into a keystore and then delete this temp file."
ADDR="$(cast wallet address --private-key "${PRIVATE_KEY}" 2>/dev/null || true)"
if [[ -n "$ADDR" ]]; then
  ok "Address: ${ADDR}"
  state_set "ADDRESS" "${ADDR}"
else
  warn "Could not compute address from private key in this cast version."
fi


path: scripts/03_password.sh
content: #!/usr/bin/env bash
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "${DIR}/00_lib.sh"
state_init
enable_strict_path
bold "3) Set password and create encrypted keystore"
line
if ! has_cmd cast; then
  err "cast is not installed. Run ./01_install_cast.sh first."
  exit 1
fi
PK_FILE="$(state_get "PRIVATE_KEY_FILE")"
if [[ -z "$PK_FILE" || ! -f "$PK_FILE" ]]; then
  err "Private key temp file not found. Run ./02_wallet.sh first."
  exit 1
fi
KEYSTORE_FILE="${APP_DIR}/keystore.json"
PASSWORD_FILE="${APP_DIR}/pw.txt"
state_set "KEYSTORE_FILE" "${KEYSTORE_FILE}"
state_set "PASSWORD_FILE" "${PASSWORD_FILE}"
echo "This password encrypts the keystore file."
echo
PW="$(ask_hidden "Enter password: ")"
if [[ -z "$PW" ]]; then
  err "Password cannot be empty."
  exit 1
fi
state_set "SAVE_PASSWORD" "y"
umask 077
printf "%s" "$PW" > "${PASSWORD_FILE}"
chmod 600 "${PASSWORD_FILE}" || true
ok "Password saved to ${PASSWORD_FILE}"
acct_name="agent"
state_set "KEYSTORE_NAME" "${acct_name}"
PRIVATE_KEY="$(cat "${PK_FILE}")"
if ! [[ "$PRIVATE_KEY" =~ ^0x[0-9a-fA-F]{64}$ ]]; then
  err "Temp private key file is invalid."
  exit 1
fi
warn "Creating keystore via cast..."
dim "If cast prompts you, enter the password you just set."
set +e
CAST_UNSAFE_PASSWORD="$PW" \
  cast wallet import "$acct_name" --private-key "$PRIVATE_KEY"
rc=$?
if [[ $rc -ne 0 ]]; then
  warn "Non-interactive import failed; falling back to interactive mode."
  CAST_UNSAFE_PASSWORD="$PW" \
    cast wallet import "$acct_name" --interactive
  rc=$?
fi
set -e
if [[ ${rc:-1} -ne 0 ]]; then
  err "Keystore creation failed."
  exit 1
fi
candidate=""
if [[ -f "${FOUNDRY_KEYSTORE_DIR}/${acct_name}" ]]; then
  candidate="${FOUNDRY_KEYSTORE_DIR}/${acct_name}"
elif [[ -f "${FOUNDRY_KEYSTORE_DIR}/${acct_name}.json" ]]; then
  candidate="${FOUNDRY_KEYSTORE_DIR}/${acct_name}.json"
else
  newest="$(ls -t "${FOUNDRY_KEYSTORE_DIR}" 2>/dev/null | head -n 1 || true)"
  [[ -n "$newest" && -f "${FOUNDRY_KEYSTORE_DIR}/${newest}" ]] && candidate="${FOUNDRY_KEYSTORE_DIR}/${newest}" || candidate=""
fi
if [[ -z "$candidate" ]]; then
  err "Could not locate created keystore file in ${FOUNDRY_KEYSTORE_DIR}."
  exit 1
fi
cp -f "$candidate" "${KEYSTORE_FILE}"
chmod 600 "${KEYSTORE_FILE}" || true
ok "Keystore saved to ${KEYSTORE_FILE}"
rm -f "${PK_FILE}" || true
state_set "PRIVATE_KEY_FILE" ""
ok "Temporary private key file deleted."
ADDRESS="$(cast wallet address --keystore "${KEYSTORE_FILE}" --password-file "${PASSWORD_FILE}")"
state_set "ADDRESS" "${ADDRESS}"
ok "Address: ${ADDRESS}"


path: scripts/04_network.sh
content: #!/usr/bin/env bash
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "${DIR}/00_lib.sh"
state_init
enable_strict_path
bold "4) Select network"
line

NETS_JSON="${ASSETS_DIR}/evm-networks.json"
TMP_NETS="$(mktemp)"
if ! python3 - "${NETS_JSON}" <<'PY' > "${TMP_NETS}"
import json
from pathlib import Path
import sys
path = Path(sys.argv[1])
if not path.is_file():
    raise SystemExit(1)
data = json.load(open(path))
nets = data.get('evmNetworks', [])
for net in nets:
    rpc_urls = net.get('rpcUrls') or []
    if not rpc_urls:
        continue
    name = net.get('name') or net.get('key') or 'Unnamed network'
    cid = net.get('chainId')
    if cid is None:
        continue
    rpc = rpc_urls[0]
    key = net.get('key') or name.lower().replace(' ', '_')
    print(f"{name}\t{cid}\t{rpc}\t{key}")
PY
then
  rm -f "${TMP_NETS}"
  err "Failed to read networks from ${NETS_JSON}."
  exit 1
fi

i=0
names=()
chainIds=()
rpcs=()
keys=()
set +e
while IFS=$'\t' read -r name cid rpc key; do
  [[ -z "$name" ]] && continue
  [[ -z "$cid" ]] && continue
  [[ -z "$rpc" ]] && continue
  ((i++))
  names+=("$name")
  chainIds+=("$cid")
  rpcs+=("$rpc")
  keys+=("${key:-}")
  if [[ $i -eq 1 ]]; then
    printf " %d) %s (chainId=%s) [default]\n" "$i" "$name" "$cid"
  else
    printf " %d) %s (chainId=%s)\n" "$i" "$name" "$cid"
  fi
done < "${TMP_NETS}"
set -e
rm -f "${TMP_NETS}"

if [[ $i -eq 0 ]]; then
  err "No networks could be loaded from ${NETS_JSON}."
  exit 1
fi

sel="$(ask_default "Choose a network number" "1")"
if ! [[ "$sel" =~ ^[0-9]+$ ]] || (( sel < 1 || sel > ${#names[@]} )); then
  warn "Invalid selection. Falling back to the first network."
  sel=1
fi
net="${names[$((sel-1))]}"
cid="${chainIds[$((sel-1))]}"
rpc="${rpcs[$((sel-1))]}"
key="${keys[$((sel-1))]}"
bold "Network selected: ${net} (chainId=${cid})"
state_set "NETWORK" "${net}"
state_set "CHAIN_ID" "${cid}"
state_set "ETH_RPC_URL" "${rpc}"
state_set "NETWORK_KEY" "${key}"
ok "Network config saved."
dim "Using RPC endpoint: ${rpc}"


path: scripts/05_tokens.sh
content: #!/usr/bin/env bash
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "${DIR}/00_lib.sh"
state_init
enable_strict_path
bold "5) Token list"
line
dim "Default tokens (sourced from assets/evm-network-tokens.json):"
print_tokens_from_json "${TOKENS_JSON}"

NET_KEY="$(state_get "NETWORK_KEY")"
NET_NAME="$(state_get "NETWORK")"
if [[ -z "${NET_KEY}" ]]; then
  NET_KEY="$(python3 - "${TOKENS_JSON}" <<'PY'
import json, sys
nets=json.load(open(sys.argv[1], 'r')).get('networks', {})
print(next(iter(nets), ''))
PY
)"
  [[ -n "${NET_KEY}" ]] && state_set "NETWORK_KEY" "${NET_KEY}"
fi
if [[ -z "${NET_NAME}" && -n "${NET_KEY}" ]]; then
  NET_NAME="$(python3 - "${TOKENS_JSON}" "${NET_KEY}" <<'PY'
import json, sys
net=json.load(open(sys.argv[1], 'r')).get('networks', {}).get(sys.argv[2], {})
print(net.get('name', ''))
PY
)"
  [[ -n "${NET_NAME}" ]] && state_set "NETWORK" "${NET_NAME}"
fi
if [[ -z "${NET_NAME}" ]]; then
  NET_NAME="<не указана>"
fi
if [[ -z "${NET_KEY}" ]]; then
  err "Нужно сначала выбрать сеть (шаг 4)."
  exit 1
fi

add_input="$(ask_default "Добавить токен для сети ${NET_NAME}? (y/N)" "N")"
if ! is_yes "${add_input}"; then
  ok "Token setup skipped."
  exit 0
fi

while true; do
  sym="$(ask_default "Token symbol (e.g., LINK)" "")"
  if [[ -z "${sym}" ]]; then
    warn "Empty symbol. Stopping."
    break
  fi
  addr="$(ask_default "Token contract address (0x...)" "0x")"
  dec="$(ask_default "Token decimals" "18")"
  if ! [[ "${addr}" =~ ^0x[0-9a-fA-F]{40}$ ]]; then
    warn "Invalid token address. Try again."
    continue
  fi
  if ! [[ "${dec}" =~ ^[0-9]+$ ]]; then
    warn "Decimals must be a number."
    continue
  fi
  add_custom_token_to_json "${TOKENS_JSON}" "${NET_KEY}" "${sym}" "${addr}" "${dec}"
  more_input="$(ask_default "Add another token? (y/N)" "N")"
  if ! is_yes "${more_input}"; then
    break
  fi

done
ok "Token list saved in ${TOKENS_JSON}."


path: scripts/06_finish.sh
content: #!/usr/bin/env bash
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "${DIR}/00_lib.sh"
state_init
enable_strict_path
bold "6) Setup complete"
line
ADDRESS="$(state_get "ADDRESS")"
NETWORK="$(state_get "NETWORK")"
CHAIN_ID="$(state_get "CHAIN_ID")"
RPC="$(state_get "ETH_RPC_URL")"
KEYSTORE_FILE="$(state_get "KEYSTORE_FILE")"
PASSWORD_FILE="$(state_get "PASSWORD_FILE")"
if [[ -z "$ADDRESS" || -z "$RPC" || -z "$KEYSTORE_FILE" ]]; then
  err "Missing state. Make sure steps 02–04 were completed."
  exit 1
fi
echo "Address: ${ADDRESS}"
echo "Network: ${NETWORK:-unknown} (chainId=${CHAIN_ID:-unknown})"
echo "RPC: ${RPC}"
dim "Fetching native balance..."
bal="$(cast balance "${ADDRESS}" --rpc-url "${RPC}" 2>/dev/null || true)"
if [[ -n "$bal" ]]; then
  ok "Native balance: ${bal}"
else
  warn "Could not fetch balance. Check RPC URL."
fi
dim "Example commands:"
echo " # native transfer"
if [[ -n "${PASSWORD_FILE}" && -f "${PASSWORD_FILE}" ]]; then
  echo " cast send 0xRecipient --value 0.01ether --rpc-url \"${RPC}\" --keystore \"${KEYSTORE_FILE}\" --password-file \"${PASSWORD_FILE}\""
else
  echo " cast send 0xRecipient --value 0.01ether --rpc-url \"${RPC}\" --keystore \"${KEYSTORE_FILE}\""
fi
ok "Onboarding finished."


path: scripts/check_wallet.sh
content: #!/usr/bin/env bash
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "${DIR}/00_lib.sh"
state_init
enable_strict_path
KEYSTORE_FILE="$(state_get "KEYSTORE_FILE")"
ADDRESS="$(state_get "ADDRESS")"
if [[ -n "${KEYSTORE_FILE}" && -f "${KEYSTORE_FILE}" && -n "${ADDRESS}" ]]; then
  ok "Wallet exists"
  printf "Address: %s\n" "${ADDRESS}"
  printf "Keystore: %s\n" "${KEYSTORE_FILE}"
  exit 0
fi
warn "No wallet detected"
if [[ -n "${KEYSTORE_FILE}" ]]; then
  printf "Expected keystore: %s\n" "${KEYSTORE_FILE}"
fi
if [[ -n "${ADDRESS}" ]]; then
  printf "Last known address: %s\n" "${ADDRESS}"
fi
exit 1


path: scripts/remove_wallet.sh
content: #!/usr/bin/env bash
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "${DIR}/00_lib.sh"
state_init
enable_strict_path
if ! confirm_prompt "This will delete your keystore/password/address and all related info. Continue?" "N"; then
  ok "Aborted wallet removal."
  exit 0
fi
KEYSTORE_FILE="$(state_get "KEYSTORE_FILE")"
PASSWORD_FILE="$(state_get "PASSWORD_FILE")"
PRIVATE_KEY_FILE="$(state_get "PRIVATE_KEY_FILE")"
ADDRESS="$(state_get "ADDRESS")"
KEYSTORE_NAME="$(state_get "KEYSTORE_NAME")"
APP_DIR="${APP_DIR:-${HOME}/.agent-wallet}"
FOUNDRY_KEYSTORE_DIR="${FOUNDRY_KEYSTORE_DIR:-${HOME}/.foundry/keystores}" # just in case
WORKSPACE_ROOT="$(cd "${DIR}/../.." && pwd)"
TX_LOG_FILE="${WORKSPACE_ROOT}/logs/tx_mentions.log"
remove_path() {
  local path="$1"
  local label="$2"
  if [[ -n "$path" && -f "$path" ]]; then
    rm -f "$path"
    ok "Removed ${label}: ${path}"
  elif [[ -n "$path" ]]; then
    warn "Expected ${label} at ${path}, but it was already gone."
  fi
}
remove_path "$KEYSTORE_FILE" "keystore"
remove_path "$PASSWORD_FILE" "password stash"
remove_path "$PRIVATE_KEY_FILE" "temporary private key"
cleanup_mnemonics() {
  local pattern="${APP_DIR}/mnemonic-words-*.txt"
  local removed=0
  if compgen -G "$pattern" >/dev/null 2>&1; then
    for file in "${APP_DIR}"/mnemonic-words-*.txt; do
      if [[ -f "$file" ]]; then
        rm -f "$file"
        ok "Removed mnemonic backup: ${file}"
        removed=1
      fi
    done
  fi
  if [[ $removed -eq 0 ]]; then
    dim "No mnemonic backups found."
  fi
}
cleanup_mnemonics

cleanup_tx_mentions() {
  local file="$TX_LOG_FILE"
  if [[ -z "$ADDRESS" || ! -f "$file" ]]; then
    return
  fi
  if ! grep -Fq "$ADDRESS" "$file"; then
    return
  fi
  local tmp
  tmp="$(mktemp)"
  grep -vF "$ADDRESS" "$file" >"$tmp"
  mv "$tmp" "$file"
  ok "Removed wallet mentions from ${file}"
}
cleanup_tx_mentions

remove_foundry_keystore() {
  local name="$1"
  if [[ -z "$name" ]]; then
    warn "Keystore name unknown; skipping Foundry keystore pruning."
    return
  fi
  local removed=0
  for candidate in "${FOUNDRY_KEYSTORE_DIR}/${name}" "${FOUNDRY_KEYSTORE_DIR}/${name}.json"; do
    if [[ -f "$candidate" ]]; then
      rm -f "$candidate"
      ok "Removed Foundry keystore: ${candidate}"
      removed=1
    fi
  done
  if [[ $removed -eq 0 ]]; then
    dim "No Foundry keystore artifacts for '${name}' were present."
  fi
}
remove_foundry_keystore "$KEYSTORE_NAME"
for key in KEYSTORE_FILE PASSWORD_FILE ADDRESS PRIVATE_KEY_FILE SAVE_PASSWORD KEYSTORE_NAME NETWORK CHAIN_ID ETH_RPC_URL NETWORK_KEY; do
  state_unset "$key"
done
ok "Cleared wallet metadata from ${STATE_FILE}."
if [[ -d "${APP_DIR}" ]]; then
  rm -rf "${APP_DIR}"
  ok "Removed ${APP_DIR} directory to wipe remaining artefacts."
fi
ensure_app_dir
state_init
ok "Fresh wallet state directory ready."
if [[ -n "$ADDRESS" ]]; then
  printf "Removed wallet address: %s
" "$ADDRESS"
fi
ok "Wallet removal complete."


path: scripts/run.sh
content: #!/usr/bin/env bash
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
chmod +x "${DIR}/"*.sh || true
"${DIR}/01_install_cast.sh"
"${DIR}/02_wallet.sh"
"${DIR}/03_password.sh"
"${DIR}/04_network.sh"
"${DIR}/05_tokens.sh"
"${DIR}/06_finish.sh"


path: scripts/show_network.sh
content: #!/usr/bin/env bash
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "${DIR}/00_lib.sh"
state_init
enable_strict_path
NETWORK="$(state_get "NETWORK")"
CHAIN_ID="$(state_get "CHAIN_ID")"
RPC_URL="$(state_get "ETH_RPC_URL")"
if [[ -z "$NETWORK" || -z "$CHAIN_ID" || -z "$RPC_URL" ]]; then
  warn "Network configuration is incomplete."
  [[ -n "$NETWORK" ]] && printf "Network: %s\n" "$NETWORK"
  [[ -n "$CHAIN_ID" ]] && printf "Chain ID: %s\n" "$CHAIN_ID"
  [[ -n "$RPC_URL" ]] && printf "RPC: %s\n" "$RPC_URL"
  exit 1
fi
ok "Current network is ${NETWORK} (chainId=${CHAIN_ID})."
echo "RPC URL: ${RPC_URL}"


path: scripts/state.env
content: # state.env — this file seeds ~/.agent-wallet/state.env on first run. Do not commit secrets here.
APP_DIR='${HOME}/.agent-wallet'
STATE_FILE='${HOME}/.agent-wallet/state.env'


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (CRITICAL)