@clawhub-agentswapx-4ce914e93b
Manage ATX on BSC with wallet creation, price and balance queries, PancakeSwap V3 swaps, liquidity operations, and BNB/ERC20 transfers. Use when the user men...
--- name: atxswap description: >- Manage ATX on BSC with wallet creation, price and balance queries, PancakeSwap V3 swaps, liquidity operations, and BNB/ERC20 transfers. Use when the user mentions ATX, BSC, PancakeSwap V3, wallet creation, price checks, buying, selling, liquidity, fees, or token transfers. version: "0.0.19" compatibility: Requires Node.js 18+ and npm. Network access to BSC RPC required. inject: - bash: echo "-$(cd "$(dirname "$0")/.." && pwd)" as: SKILL_DIR metadata: author: agentswapx openclaw: requires: bins: - node - npm homepage: https://github.com/agentswapx/skills/tree/main/atxswap os: - linux - macos --- # ATXSwap Skill Execute ATX trading and wallet workflows on BSC. This skill is designed for agents that need safe, repeatable commands for wallet management, ATX/USDT quotes, swaps, V3 liquidity actions, and transfers. - **SDK**: [`atxswap-sdk`](https://www.npmjs.com/package/atxswap-sdk) on npm ([source](https://github.com/agentswapx/atxswap-sdk)) - **Docs (team / project)**: [Team introduction](https://docs.atxswap.com/guide/team) · [团队介绍](https://docs.atxswap.com/zh/guide/team) - **Keystore dir**: `~/.config/atxswap/keystore` (fixed, not configurable) - **Secrets dir**: `~/.config/atxswap/` (master.key + secrets.json) ## Use This Skill For - Create the single wallet used by this skill instance (importing an existing private key is not supported) - Query ATX price, balances, LP positions, quotes, and arbitrary ERC20 token info - Buy or sell ATX against USDT on PancakeSwap V3 - Add liquidity (full range or a custom **price range in USDT per ATX** or **tick** bounds), remove liquidity, collect fees, or burn empty LP NFTs - Transfer BNB, ATX, USDT, or arbitrary ERC20 tokens ## Before First Use This skill ships its own Node scripts and depends on `atxswap-sdk`. 1. Open the skill directory where this `SKILL.md` is installed. 2. Run `npm install` there before using any script. 3. If `npm install` fails, stop and report the dependency error instead of guessing. If the skill is installed via ClawHub or OpenClaw CLI, the install location is typically `~/.clawhub/skills/atxswap/` (or the equivalent client-managed path). If you cloned this repository directly, the location is `skills/atxswap/`. ## Script Location Use the skill directory path to locate scripts. If `SKILL_DIR` is available (injected by skills.sh-compatible runtimes), use it; otherwise use the absolute path to this skill's installed directory. Example: ```bash cd skills/atxswap && npm install cd "SKILL_DIR" && node scripts/wallet.js list ``` All examples below use `cd "SKILL_DIR" &&` for clarity. If your runtime does not inject `SKILL_DIR`, replace it with the absolute path of the installed skill directory. ## Runtime Notes - `BSC_RPC_URL` is optional and supports comma-separated values for fallback, e.g. `BSC_RPC_URL="https://primary,https://backup1,https://backup2"`. When unset, scripts use a built-in fallback list of 6 BSC public RPC endpoints and viem will retry them in order. - Wallet files live under `~/.config/atxswap/keystore`. - Secure secrets live under `~/.config/atxswap/` (master.key + secrets.json). - Only **one wallet** is allowed per skill installation. If a wallet already exists, `wallet.js create` fails. - Use `wallet.js list` before creating a wallet. - Importing an existing private key via this skill is **not supported**. If the user asks to import a private key, refuse and tell them to use a dedicated wallet tool of their choice. - Scripts write JSON output. `wallet.js export` prints the address's encrypted **MetaMask-compatible keystore V3 JSON** to stdout (or writes it to a file via `--out <file>`); it never prints the raw private key. - `query.js quote` can return a JSON error if the configured Quoter or RPC rejects the simulation. Surface the error and do not proceed to a write. - For custom-range liquidity, do **not** guess the second token amount from chat. First run `liquidity.js quote-add` or use `liquidity.js add --base-token ... --amount ...` so the script computes the counter-asset from the live pool price and range. ## Password Rules When the user asks to **create** a wallet: 1. Ask the user for a password first (do NOT generate one). 2. Pass it via `--password <pwd>` to the script when running non-interactively. 3. The password is auto-saved to secure storage after creation. 4. Never print the password back to the chat. For **swap**, **transfer**, and **liquidity** operations, rely on auto-unlock first. Only ask for the password if auto-unlock fails. If the user says they forgot the wallet password or asks to recover it, first explain that saved wallet passwords are encrypted at rest in the local SecretStore (for example Keychain, Secret Service, or the file backend under `~/.config/atxswap/`) and are not stored by the agent in chat memory. Even if the user confirms, do **not** print the password in chat; guide them to use a trusted local workflow instead. ## Hard Safety Rules 1. Treat all BSC writes as real-asset operations. 2. **NEVER** output private keys or passwords in chat. 3. **ALWAYS** run a preview before write actions: query price, quote, balance, or positions as appropriate. 4. **ALWAYS** show the preview to the user and wait for explicit confirmation before swap, transfer, or liquidity writes. 5. **NEVER** execute large trades without the user saying "yes" or "confirm". 6. `wallet.js export` only emits the **encrypted MetaMask-compatible keystore JSON**, never the raw private key. There is no command that prints the unencrypted private key, and the agent must not attempt to derive or display one. 7. Prefer `wallet.js export <address> --out <file>` and tell the user the file path. Avoid pasting the keystore JSON itself into chat unless the user explicitly asks for it. 8. Before deleting a wallet, keystore file, or any private-key-bearing wallet material, **ALWAYS** remind the user to export and back up the encrypted keystore first. Do not delete anything until the user explicitly confirms that the keystore backup has been completed. 9. Wallet deletion requires a second explicit confirmation: after backup is confirmed, require the user to send the exact phrase `force delete wallet` before running any delete command. 10. If the user asks to recover or reveal a saved wallet password, remind them that the password is encrypted in local secure storage and must not be disclosed in chat. Do not attempt to print, derive, or expose the password even after user confirmation. 11. If the user asks to recover, reveal, print, or paste the wallet private key, refuse. Offer `wallet.js export <address> --out <file>` as the only supported backup path, because it exports an encrypted keystore instead of exposing the raw private key. ## Required Preview Flow Before every write action: 1. Query the price, quote, balance, or positions that match the requested action. 2. Summarize the preview in plain language. 3. Ask the user to confirm. 4. Execute the write command only after confirmation. 5. Return the transaction hash and the key result fields. ## High-Value Workflows ### Check market state ```bash cd "SKILL_DIR" && node scripts/query.js price cd "SKILL_DIR" && node scripts/query.js balance <address> cd "SKILL_DIR" && node scripts/query.js positions <address> cd "SKILL_DIR" && node scripts/query.js positions <address> <tokenId> ``` ### Preview before swap ```bash cd "SKILL_DIR" && node scripts/query.js quote <buy|sell> <amount> ``` ### Execute after confirmation ```bash cd "SKILL_DIR" && node scripts/swap.js buy <usdtAmount> [--from address] [--slippage bps] [--password <pwd>] cd "SKILL_DIR" && node scripts/liquidity.js add <atxAmount> <usdtAmount> [range opts] [--from address] [--slippage-bps n] [--password <pwd>] cd "SKILL_DIR" && node scripts/liquidity.js add --base-token <atx|usdt> --amount <n> [range opts] [--from address] [--slippage-bps n] [--password <pwd>] cd "SKILL_DIR" && node scripts/transfer.js atx <to> <amount> [--from address] [--password <pwd>] ``` ## Command Reference ### `wallet.js` ```bash cd "SKILL_DIR" && node scripts/wallet.js create [name] --password <pwd> cd "SKILL_DIR" && node scripts/wallet.js list cd "SKILL_DIR" && node scripts/wallet.js export <address> [--out <file>] cd "SKILL_DIR" && node scripts/wallet.js has-password <address> cd "SKILL_DIR" && node scripts/wallet.js forget-password <address> cd "SKILL_DIR" && node scripts/wallet.js delete <address> --backup-confirmed yes --force-phrase "force delete wallet" ``` Before `wallet.js delete`: 1. Require the user to export and back up the encrypted keystore first. 2. Require the user to explicitly confirm that the backup is complete. 3. Require the user to send the exact phrase `force delete wallet`. 4. Only then run `wallet.js delete <address> --backup-confirmed yes --force-phrase "force delete wallet"`. ### `query.js` ```bash cd "SKILL_DIR" && node scripts/query.js price cd "SKILL_DIR" && node scripts/query.js balance <address> cd "SKILL_DIR" && node scripts/query.js quote <buy|sell> <amount> cd "SKILL_DIR" && node scripts/query.js positions <address> cd "SKILL_DIR" && node scripts/query.js positions <address> <tokenId> cd "SKILL_DIR" && node scripts/query.js token-info <tokenAddress> ``` `query.js positions` now includes both the raw `tokensOwed0/1` fields from `positions()` and `collectable0/1`, `collectableAtx`, `collectableUsdt` computed from a simulated `collect()` call. Use the `collectable*` fields to decide whether a fee harvest is worth executing. ### `swap.js` ```bash cd "SKILL_DIR" && node scripts/swap.js buy <usdtAmount> [--from address] [--slippage bps] [--password <pwd>] cd "SKILL_DIR" && node scripts/swap.js sell <atxAmount> [--from address] [--slippage bps] [--password <pwd>] ``` ### `liquidity.js` **`add` — price / tick 区间**(与前端「USDT/ATX」一致;默认全价格区间=全宽流动性): - 默认: 不附加参数即 **全范围**(与原先行为相同)。 - `--full-range`:显式全范围(勿与下面两类同时使用)。 - `--min-price` / `--max-price`:按 **1 ATX 多少 USDT** 的区间,脚本会读池子 `token0`、用与前端相同的 `token1/token0`+`tickSpacing` 换算为 `tickLower` / `tickUpper`(需**同时**提供两个价格)。 - `--range-percent`:以**当前 ATX 价格**为中心,按百分比展开上下界;例如 `20` 表示当前价的 `-20% ~ +20%` 区间。 - `--tick-lower` / `--tick-upper`:直接指定 V3 `tick`(需**同时**提供;脚本会取二者较小者为下界、较大者为上界,并限制在 V3 允许范围内)。 - `quote-add <atx|usdt> <amount>`:按实时池价和所选区间,计算另一边需要多少代币,适合作为写入前预估。 - `add --base-token <atx|usdt> --amount <n>`:只给一边金额,由脚本自动配平另一边,然后直接执行写入。 可选:`--slippage-bps`(0–10000,默认由 SDK 决定)。 ```bash cd "SKILL_DIR" && node scripts/liquidity.js quote-add <atx|usdt> <amount> [range opts] cd "SKILL_DIR" && node scripts/liquidity.js add <atxAmount> <usdtAmount> [range opts] [--from address] [--slippage-bps n] [--password <pwd>] cd "SKILL_DIR" && node scripts/liquidity.js add --base-token <atx|usdt> --amount <n> [range opts] [--from address] [--slippage-bps n] [--password <pwd>] cd "SKILL_DIR" && node scripts/liquidity.js remove <tokenId> <percent> [--from address] [--password <pwd>] cd "SKILL_DIR" && node scripts/liquidity.js collect <tokenId> [--from address] [--password <pwd>] cd "SKILL_DIR" && node scripts/liquidity.js burn <tokenId> [--from address] [--password <pwd>] ``` Before `collect`, preview the target position with: ```bash cd "SKILL_DIR" && node scripts/query.js positions <address> <tokenId> ``` Prefer `collectableAtx` / `collectableUsdt` over `tokensOwed0/1` when deciding whether fees are available, because the raw `tokensOwed` fields may stay at zero while `collect()` can still succeed. 示例(非全宽;区间请先用 `query.js price` 与用户对齐后再执行写入): ```bash cd "SKILL_DIR" && node scripts/liquidity.js quote-add usdt 0.1 --range-percent 20 cd "SKILL_DIR" && node scripts/liquidity.js add --base-token usdt --amount 0.1 --range-percent 20 --from <address> cd "SKILL_DIR" && node scripts/liquidity.js add 10 1 --min-price 0.05 --max-price 0.15 cd "SKILL_DIR" && node scripts/liquidity.js add 10 1 --tick-lower -20000 --tick-upper 1000 ``` 对话映射建议: - 用户说“添加 `0.1u` 的流动性,价格区间 `20%`”时,先执行 `quote-add usdt 0.1 --range-percent 20` - 把返回的 `estimatedAmounts` 展示给用户确认 - 得到确认后,再执行 `add --base-token usdt --amount 0.1 --range-percent 20 --from <address>` ### `transfer.js` ```bash cd "SKILL_DIR" && node scripts/transfer.js bnb <to> <amount> [--from address] [--password <pwd>] cd "SKILL_DIR" && node scripts/transfer.js atx <to> <amount> [--from address] [--password <pwd>] cd "SKILL_DIR" && node scripts/transfer.js usdt <to> <amount> [--from address] [--password <pwd>] cd "SKILL_DIR" && node scripts/transfer.js token <tokenAddress> <to> <amount> [--from address] [--password <pwd>] ``` ## When To Refuse Or Pause - Missing wallet but the user requests a write action - Missing confirmation for swap, transfer, or liquidity writes - User asks to delete a wallet, keystore file, or private-key-bearing wallet material before confirming that the encrypted keystore has been backed up - User asks to delete a wallet but has not explicitly sent `force delete wallet` - User asks to recover or reveal a saved wallet password in chat - User asks to recover, reveal, print, or paste a wallet private key in chat - `npm install` has not been run successfully in the skill directory - RPC, dependency, or wallet-unlock errors that make the state unclear ## Standard Workflow For any write action: 1. Query current price, quote, balance, or positions as needed. 2. Summarize the preview for the user. 3. Wait for explicit confirmation. 4. Execute the write command. 5. Report the transaction hash and result. FILE:CHANGELOG.md # Changelog ## 0.0.19 - ClawHub publish: registry tarball bundles `atxswap-sdk` `^0.0.14` (npm 0.0.14). ## 0.0.18 - ClawHub publish: registry tarball bundles `atxswap-sdk` `^0.0.13` (npm 0.0.13). ## 0.0.17 - ClawHub publish: registry tarball bundles `atxswap-sdk` `^0.0.12` (npm 0.0.12). ## 0.0.16 - ClawHub registry publish (no functional change from 0.0.15 bundle). ## 0.0.14 - Bumped bundled `atxswap-sdk` to `^0.0.11` (BSC `DEFAULT_RPC_URLS`: 6 endpoints, `bsc-dataseed.bnbchain.org` first). ## 0.0.13 - Bumped bundled `atxswap-sdk` to `^0.0.10` (default slippage when omitted is now **1%** / `100` bps, was 3%). ## 0.0.12 - Bumped bundled `atxswap-sdk` to `^0.0.9` (npm README / docs links). - `README` / `README.zh` / `SKILL.md`: link to ATXSwap documentation [team introduction](https://docs.atxswap.com/guide/team) pages. ## 0.0.11 - Bumped bundled `atxswap-sdk` to `^0.0.8` (npm maintenance release). ## 0.0.9 - Bumped `atxswap-sdk` to `^0.0.7` so `wallet.js export` emits MetaMask-compatible encrypted keystore V3 JSON. SDK 0.0.7 uses the standard Web3 Secret Storage MAC (`keccak256`) and can re-export legacy SDK keystores without exposing raw private keys. ## 0.0.8 - Tightened `SKILL.md` safety guidance: before deleting a wallet/keystore, remind the user to back up the encrypted keystore and wait for explicit confirmation of backup. On forgotten-password and recovery requests, explain local encrypted storage and **never** print passwords in chat. Refuse to reveal or paste private keys; point users to `wallet.js export` (encrypted keystore) only. ## 0.0.7 - Bumped the bundled `atxswap-sdk` dependency to `^0.0.6` so the skill uses the cron/headless SecretStore fixes from SDK 0.0.6. Cron, SSH, and other non-desktop Linux environments now fall back to the encrypted file backend instead of failing on `secret-tool store`. - `wallet.js create` now includes `passwordSaved` and optional `passwordSaveError` in its JSON output, making password persistence failures visible without treating them as wallet-creation failures. ## 0.0.6 - Bumped the bundled `atxswap-sdk` dependency to `^0.0.5` so the skill picks up the corrected `DEFAULT_CONTRACTS` (production ATX token + ATX/USDT pool addresses on BSC mainnet). Required because npm semver treats `^0.0.x` as pinned to that exact patch, so older dependency ranges would never have resolved to `0.0.5`. - Version `0.0.5` was intentionally skipped to keep the skill release line distinct from the SDK release line going forward. ## 0.0.4 - Bumped the bundled `atxswap-sdk` dependency to `^0.0.3` (required for the new `WalletManager.exportKeystore()` API used by `wallet.js export`). - Removed `wallet.js import <privateKey>` and the public `WalletManager.importPrivateKey()` SDK method. Importing an existing private key is no longer supported through this skill or the underlying SDK; the only way to provision a wallet for this skill instance is `wallet.js create`. - Replaced `wallet.js export <address>` raw private-key output with **keystore V3 JSON** export. The `WalletManager.exportPrivateKey()` SDK method has been removed and superseded by `WalletManager.exportKeystore(address)`, which returns the on-disk encrypted keystore. `wallet.js export` now also supports `--out <file>` to write the keystore to disk instead of printing to stdout. The skill no longer has any path that exposes the unencrypted private key. ## 0.0.1 - Initial OpenClaw and ClawHub skill bundle for ATX trading on BSC - Added self-contained scripts for wallet, query, swap, liquidity, and transfer flows - Added OpenClaw-oriented `SKILL.md`, publish notes, and localized README files - Normalized runtime failures to compact JSON errors for cleaner agent output FILE:PUBLISH.md # Publish Notes This directory is published to **ClawHub** as a single skill bundle that also works as a standalone `skills.sh`-compatible package (Claude / Cursor / Codex CLI). The same `SKILL.md` frontmatter declares both conventions, so a single source of truth covers all clients. After publish, the skill is installable via both `clawhub install atxswap` and `openclaw skills install atxswap` (OpenClaw pulls from the same ClawHub registry). > Heads up: the OpenClaw CLI itself does **not** have `skills publish` or > `skills validate` subcommands. All publishing flows through the dedicated > `clawhub` CLI (`npm install -g clawhub`). ## Pre-publish checklist 1. Bump versions consistently: - `SKILL.md` frontmatter `version` - `package.json` `version` - These two MUST match — `clawhub publish --version` overrides them at upload time but mismatched local values confuse `skills.sh` consumers. 2. Install dependencies inside this directory (`npm install`) so the SDK builds cleanly. 3. Run the local read-only checks: - `node scripts/wallet.js list` - `node scripts/query.js price` - `node scripts/query.js quote buy 1` 4. Confirm the folder does not include secrets, keystore files, or `node_modules/`. (`.clawhubignore` already excludes `node_modules/`, `.clawhub/`, `.clawdhub/`, `.DS_Store`, `*.log`.) 5. Make sure the parent submodule (`agentswapx/skills`) is committed and pushed — the published `homepage` URL points at GitHub. ## Authenticate with ClawHub ```bash # Browser flow (opens a token-grant page) clawhub login # Or token flow (no browser) clawhub login --token <YOUR_TOKEN> --no-browser # Verify clawhub whoami ``` ## Publish ```bash clawhub publish ./skills/atxswap \ --slug atxswap \ --name "ATXSwap" \ --version 0.0.1 \ --tags latest,atxswap,atx,bsc,trading ``` After upload there is a brief security-scan window during which `clawhub inspect atxswap` returns "Skill is hidden while security scan is pending". Once the scan completes, ClawHub will flag this skill as "suspicious" because VirusTotal Code Insight detects crypto-key / external-API patterns — that is expected for any wallet SDK and not a real warning. Users must pass `--force` in non-interactive contexts: ```bash clawhub install atxswap --force ``` ## Verify the published skill ```bash # Registry round-trip clawhub inspect atxswap # End-user simulation in a clean directory TEST=$(mktemp -d) cd "$TEST" && clawhub install atxswap --force cd skills/atxswap && npm install # pulls atxswap-sdk from npm (~15s) node scripts/query.js # should print usage ``` ## Suggested changelog ```text Initial ClawHub release for ATX wallet, query, swap, liquidity, and transfer workflows on BSC. Compatible with both ClawHub/OpenClaw clients and the standalone skills.sh runtime. ``` FILE:README.md # ATXSwap Skill A skill bundle for the ATXSwap decentralized agent exchange protocol on BSC. A single `SKILL.md` works across clients represented by **Claude Code** and **OpenClaw**, so you do not need separate directories for different clients. [**中文文档**](./README.zh.md) - **GitHub**: https://github.com/agentswapx/skills - **SDK on npm**: [`atxswap-sdk`](https://www.npmjs.com/package/atxswap-sdk) - **SDK source / docs**: [agentswapx/atxswap-sdk](https://github.com/agentswapx/atxswap-sdk) For project background and a short [team introduction](https://docs.atxswap.com/guide/team) ([中文](https://docs.atxswap.com/zh/guide/team)), see the ATXSwap documentation site. This README describes the skill’s scope and scripts. ## What This Skill Covers - Create the single wallet used by the skill (importing an existing private key is not supported) - Query ATX price, balances, LP positions, and ERC20 token info - Buy or sell ATX against USDT on PancakeSwap V3 - Preview custom-range liquidity, add liquidity, remove liquidity, collect fees, and burn empty LP NFTs - Transfer BNB, ATX, USDT, or arbitrary ERC20 tokens ## Directory Layout ```text atxswap/ ├── SKILL.md ├── README.md ├── README.zh.md ├── PUBLISH.md ├── CHANGELOG.md ├── .clawhubignore ├── .gitignore ├── package.json └── scripts/ ├── _helpers.js ├── wallet.js ├── query.js ├── swap.js ├── liquidity.js └── transfer.js ``` ## Install ### OpenClaw Install ```bash openclaw skills install atxswap ``` ### Claude Code Install ```bash git clone https://github.com/agentswapx/skills.git cd skills/atxswap && npm install ``` By default the skill uses a built-in fallback list of 6 BSC public RPC endpoints. To override, set `BSC_RPC_URL` to a single URL or to a comma-separated list (priority left to right): ```bash export BSC_RPC_URL="https://my-private-rpc.example.com,https://bsc-dataseed.bnbchain.org" ``` ## Common Commands ```bash cd skills/atxswap && node scripts/wallet.js list cd skills/atxswap && node scripts/query.js price cd skills/atxswap && node scripts/query.js quote buy 1 cd skills/atxswap && node scripts/query.js positions <address> <tokenId> cd skills/atxswap && node scripts/liquidity.js quote-add usdt 0.1 --range-percent 20 ``` When invoked through a `SKILL_DIR`-aware runtime, `cd "SKILL_DIR"` is preferred so the skill works regardless of where the client installed it. ## Liquidity Preview For custom-range liquidity, do not guess the second token amount from chat. Preview first, then write: ```bash cd "SKILL_DIR" && node scripts/liquidity.js quote-add usdt 0.1 --range-percent 20 cd "SKILL_DIR" && node scripts/liquidity.js add --base-token usdt --amount 0.1 --range-percent 20 --from <address> ``` Supported custom range modes: - `--range-percent <n>`: expands around the current ATX price, e.g. `20` means `-20% ~ +20%` - `--min-price <p> --max-price <p>`: explicit `USDT per 1 ATX` - `--tick-lower <n> --tick-upper <n>`: raw V3 ticks Recommended flow: 1. Run `query.js price` or `liquidity.js quote-add` 2. Show the returned `estimatedAmounts` to the user 3. Wait for confirmation 4. Execute `liquidity.js add` ## Fee Harvest Preview Before collecting fees, preview the position first: ```bash cd "SKILL_DIR" && node scripts/query.js positions <address> <tokenId> cd "SKILL_DIR" && node scripts/liquidity.js collect <tokenId> --from <address> ``` `query.js positions` now returns both the raw `tokensOwed0/1` fields from `positions()` and simulated `collectable0/1`, `collectableAtx`, `collectableUsdt` values. Use the `collectable*` fields to decide whether a position has harvestable fees. ## Security Rules 1. Never expose private keys or passwords in chat output. 2. Always preview price, quote, balance, or positions before write actions. 3. Always wait for explicit user confirmation before swap, transfer, or liquidity writes. 4. Treat all write actions as mainnet asset operations. 5. Before deleting a wallet, require the user to export and back up the encrypted keystore first. 6. Wallet deletion requires a second confirmation: the user must explicitly send `force delete wallet`. ## Wallet Deletion Delete a wallet only after both confirmations are complete: 1. The user confirms the encrypted keystore backup is done 2. The user explicitly sends `force delete wallet` Then run: ```bash cd "SKILL_DIR" && node scripts/wallet.js delete <address> --backup-confirmed yes --force-phrase "force delete wallet" ``` FILE:README.zh.md # ATXSwap 技能 BSC 上 **ATXSwap** 智能体去中心化交换协议的技能包。同一份 `SKILL.md` 同时兼容以 **Claude Code** 和 **OpenClaw** 为代表的客户端,无需为不同客户端维护多份目录。 [**English**](./README.md) - **GitHub**: https://github.com/agentswapx/skills - **SDK (npm)**: [`atxswap-sdk`](https://www.npmjs.com/package/atxswap-sdk) - **SDK 源码 / 文档**: [agentswapx/atxswap-sdk](https://github.com/agentswapx/atxswap-sdk) 项目背景与简要[团队介绍](https://docs.atxswap.com/zh/guide/team)见 ATXSwap 文档站([English](https://docs.atxswap.com/guide/team))。本文档说明技能能力与脚本用法。 ## 能力范围 - 为当前技能实例创建单个钱包(**不支持导入已有私钥**) - 查询 ATX 价格、余额、LP 仓位和 ERC20 代币信息 - 在 PancakeSwap V3 上买卖 ATX/USDT - 预估自定义区间流动性、添加流动性、减仓、收手续费、销毁空仓位 NFT - 转账 BNB、ATX、USDT 或任意 ERC20 代币 ## 目录结构 ```text atxswap/ ├── SKILL.md ├── README.md ├── README.zh.md ├── PUBLISH.md ├── CHANGELOG.md ├── .clawhubignore ├── .gitignore ├── package.json └── scripts/ ├── _helpers.js ├── wallet.js ├── query.js ├── swap.js ├── liquidity.js └── transfer.js ``` ## 安装 ### OpenClaw 安装 ```bash openclaw skills install atxswap ``` ### Claude Code 安装 ```bash git clone https://github.com/agentswapx/skills.git cd skills/atxswap && npm install ``` 默认会使用内置的 6 个 BSC 公共 RPC 端点做 fallback。如需覆盖,可将 `BSC_RPC_URL` 设为单个地址或逗号分隔的多个地址(按从左到右的优先级回退): ```bash export BSC_RPC_URL="https://my-private-rpc.example.com,https://bsc-dataseed.bnbchain.org" ``` ## 常用命令 ```bash cd skills/atxswap && node scripts/wallet.js list cd skills/atxswap && node scripts/query.js price cd skills/atxswap && node scripts/query.js quote buy 1 cd skills/atxswap && node scripts/query.js positions <address> <tokenId> cd skills/atxswap && node scripts/liquidity.js quote-add usdt 0.1 --range-percent 20 ``` 在支持 `SKILL_DIR` 注入的运行时中,建议使用 `cd "SKILL_DIR"`,以便技能 能在客户端管理的任意安装目录下正常运行。 ## 流动性预估 做自定义区间流动性时,不要在对话里直接猜另一边代币数量。建议先预估,再执行写入: ```bash cd "SKILL_DIR" && node scripts/liquidity.js quote-add usdt 0.1 --range-percent 20 cd "SKILL_DIR" && node scripts/liquidity.js add --base-token usdt --amount 0.1 --range-percent 20 --from <address> ``` 支持的区间模式: - `--range-percent <n>`:以当前 ATX 价格为中心展开,例如 `20` 表示 `-20% ~ +20%` - `--min-price <p> --max-price <p>`:显式指定 `1 ATX = 多少 USDT` - `--tick-lower <n> --tick-upper <n>`:直接指定 V3 tick 推荐流程: 1. 先执行 `query.js price` 或 `liquidity.js quote-add` 2. 把返回的 `estimatedAmounts` 展示给用户 3. 等用户确认 4. 再执行 `liquidity.js add` ## 手续费预览 收手续费前,先预览目标仓位: ```bash cd "SKILL_DIR" && node scripts/query.js positions <address> <tokenId> cd "SKILL_DIR" && node scripts/liquidity.js collect <tokenId> --from <address> ``` `query.js positions` 现在会同时返回 `positions()` 原始的 `tokensOwed0/1` 以及通过模拟 `collect()` 得到的 `collectable0/1`、`collectableAtx`、 `collectableUsdt`。判断是否值得收割时,应优先看 `collectable*` 字段。 ## 安全规则 1. 不要在聊天输出中暴露私钥或密码。 2. 所有写操作前,必须先预览价格、报价、余额或仓位。 3. 交换、转账、流动性操作前,必须等待用户明确确认。 4. 所有写操作都按主网真实资产处理。 5. 删除钱包前,必须先要求用户导出并备份加密 keystore。 6. 删除钱包还需要第二次确认:用户必须明确发送 `force delete wallet`。 ## 删除钱包 只有在以下两个条件都满足后,才能删除钱包: 1. 用户已明确确认加密 keystore 备份完成 2. 用户已明确发送 `force delete wallet` 然后再执行: ```bash cd "SKILL_DIR" && node scripts/wallet.js delete <address> --backup-confirmed yes --force-phrase "force delete wallet" ``` FILE:package-lock.json { "name": "atxswap", "version": "0.0.19", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "atxswap", "version": "0.0.19", "dependencies": { "atxswap-sdk": "^0.0.14" } }, "node_modules/@adraffy/ens-normalize": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", "license": "MIT" }, "node_modules/@noble/ciphers": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "license": "MIT", "engines": { "node": "^14.21.3 || >=16" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@noble/curves": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", "license": "MIT", "dependencies": { "@noble/hashes": "1.8.0" }, "engines": { "node": "^14.21.3 || >=16" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@noble/hashes": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", "license": "MIT", "engines": { "node": "^14.21.3 || >=16" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@scure/base": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", "license": "MIT", "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@scure/bip32": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", "license": "MIT", "dependencies": { "@noble/curves": "~1.9.0", "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@scure/bip39": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", "license": "MIT", "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/abitype": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/wevm" }, "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "peerDependenciesMeta": { "typescript": { "optional": true }, "zod": { "optional": true } } }, "node_modules/atxswap-sdk": { "version": "0.0.14", "resolved": "https://registry.npmjs.org/atxswap-sdk/-/atxswap-sdk-0.0.14.tgz", "integrity": "sha512-/6Utda/rO9kgrpJdncsYwDVlP8EU5zqiEAH7piBdVXh1aiXZkJWZQHsxJ37GgJhn5j+S2Nh5E6PzeqBzFosQFQ==", "license": "MIT", "dependencies": { "viem": "^2.31.3" }, "engines": { "node": ">=18.0.0" } }, "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "license": "MIT" }, "node_modules/isows": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/wevm" } ], "license": "MIT", "peerDependencies": { "ws": "*" } }, "node_modules/ox": { "version": "0.14.20", "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.20.tgz", "integrity": "sha512-rby38C3nDn8eQkf29Zgw4hkCZJ64Qqi0zRPWL8ENUQ7JVuoITqrVtwWQgM/He19SCMUEc7hS/Sjw0jIOSLJhOw==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/wevm" } ], "license": "MIT", "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "peerDependenciesMeta": { "typescript": { "optional": true } } }, "node_modules/viem": { "version": "2.48.4", "resolved": "https://registry.npmjs.org/viem/-/viem-2.48.4.tgz", "integrity": "sha512-mReP/rgY2P+WeeRSG4sUvccCLKfyAW1C73Y3KkobAqgzYmVna9qyUMNE44xIUkDtfvRuC33r24UhF4baBYovsg==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/wevm" } ], "license": "MIT", "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", "ox": "0.14.20", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "peerDependenciesMeta": { "typescript": { "optional": true } } }, "node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "license": "MIT", "peer": true, "engines": { "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { "optional": true }, "utf-8-validate": { "optional": true } } } } } FILE:package.json { "name": "atxswap", "private": true, "type": "module", "version": "0.0.19", "description": "Standalone skills.sh-oriented ATX trading skill for BSC wallet, query, swap, liquidity, and transfer workflows", "scripts": { "bootstrap": "npm install", "check:price": "node scripts/query.js price", "check:wallets": "node scripts/wallet.js list" }, "dependencies": { "atxswap-sdk": "^0.0.14" } } FILE:scripts/_helpers.js import { AtxClient } from "atxswap-sdk"; import { homedir } from "node:os"; import { join } from "node:path"; import readline from "node:readline"; const DEFAULT_KEYSTORE_PATH = join(homedir(), ".config", "atxswap", "keystore"); function parseRpcUrls(raw) { if (!raw) return undefined; const urls = raw .split(",") .map((url) => url.trim()) .filter((url) => url.length > 0); return urls.length > 0 ? urls : undefined; } export async function createClient() { const rpcUrls = parseRpcUrls(process.env.BSC_RPC_URL); const client = new AtxClient({ ...(rpcUrls && { rpcUrls }), keystorePath: DEFAULT_KEYSTORE_PATH, }); await client.ready(); return client; } export function getDefaultKeystorePath() { return DEFAULT_KEYSTORE_PATH; } export function parseArgs(args) { const parsed = { _: [] }; for (let i = 0; i < args.length; i++) { if (args[i].startsWith("--")) { const key = args[i].slice(2); parsed[key] = args[i + 1] || true; i++; } else { parsed._.push(args[i]); } } return parsed; } export function fmt(wei, decimals = 18) { const n = Number(wei) / 10 ** decimals; if (n === 0) return "0"; if (n < 0.000001) return n.toExponential(4); return n.toLocaleString("en-US", { maximumFractionDigits: 6 }); } export function exitError(message, code = 1) { console.error(JSON.stringify({ error: message })); process.exit(code); } export function getErrorMessage(error) { if (typeof error === "string") return error; if (error?.shortMessage) return error.shortMessage; if (error?.reason) return error.reason; if (error?.message) return error.message.split("\n")[0]; return "Unknown error"; } export async function runMain(fn) { try { await fn(); } catch (error) { exitError(getErrorMessage(error)); } } async function promptHidden(promptText) { if (!process.stdin.isTTY || !process.stdout.isTTY) { return null; } return await new Promise((resolve, reject) => { let value = ""; const wasRaw = process.stdin.isRaw; readline.emitKeypressEvents(process.stdin); process.stdin.setRawMode?.(true); process.stdin.resume(); process.stdout.write(promptText); const onKeypress = (char, key) => { if (key?.ctrl && key.name === "c") { cleanup(); process.stdout.write("\n"); reject(new Error("Password input cancelled")); return; } if (key?.name === "return" || key?.name === "enter") { cleanup(); process.stdout.write("\n"); resolve(value); return; } if (key?.name === "backspace") { value = value.slice(0, -1); return; } if (char) { value += char; } }; const cleanup = () => { process.stdin.off("keypress", onKeypress); process.stdin.setRawMode?.(Boolean(wasRaw)); process.stdin.pause(); }; process.stdin.on("keypress", onKeypress); }); } export async function resolvePassword(args, promptText = "Enter wallet password: ") { if (args.password) return args.password; const ttyPassword = await promptHidden(promptText); if (ttyPassword) return ttyPassword; exitError("Password required: use --password <pwd> or run in an interactive terminal"); } export async function resolveNewPassword(args) { if (args.password) return args.password; if (process.stdin.isTTY && process.stdout.isTTY) { while (true) { const password = await promptHidden("Create wallet password: "); if (!password) exitError("Password cannot be empty"); const confirm = await promptHidden("Confirm wallet password: "); if (password !== confirm) { console.error("Passwords do not match, please try again."); continue; } return password; } } exitError("Password required: use --password <pwd> or run in an interactive terminal"); } export async function loadWallet(client, address, args) { try { return await client.wallet.load(address); } catch { if (args.password) { return await client.wallet.load(address, args.password); } const ttyPassword = await promptHidden(`Password for address: `); if (ttyPassword) { return await client.wallet.load(address, ttyPassword); } exitError(`Password required for address: use --password <pwd> or run in an interactive terminal`); } } export async function exportKeystore(client, address, args = {}) { if (typeof client.wallet.exportMetaMaskKeystore === "function") { return await client.wallet.exportMetaMaskKeystore(address, args.password); } return client.wallet.exportKeystore(address); } FILE:scripts/liquidity.js #!/usr/bin/env node import { createClient, loadWallet, parseArgs, exitError, runMain, fmt } from "./_helpers.js"; import { formatUnits, parseEther } from "atxswap-sdk"; const POOL_TOKEN0_ABI = [ { type: "function", name: "token0", stateMutability: "view", inputs: [], outputs: [{ type: "address" }], }, ]; const POOL_SLOT0_ABI = [ { type: "function", name: "slot0", stateMutability: "view", inputs: [], outputs: [ { type: "uint160" }, { type: "int24" }, { type: "uint16" }, { type: "uint16" }, { type: "uint16" }, { type: "uint8" }, { type: "bool" }, ], }, ]; const V3_MIN_TICK = -887200; const V3_MAX_TICK = 887200; const LOG_BASE = Math.log(1.0001); const Q96 = 2n ** 96n; function priceToTick(price) { if (price <= 0) return 0; return Math.round(Math.log(price) / LOG_BASE); } function nearestUsableTick(tick, tickSpacing) { return Math.round(tick / tickSpacing) * tickSpacing; } function feeToTickSpacing(fee) { switch (fee) { case 100: return 1; case 500: return 10; case 2500: return 50; case 10000: return 200; default: return 50; } } function tickToSqrtPriceX96(tick) { const sqrtPrice = Math.sqrt(1.0001 ** tick); return BigInt(Math.round(sqrtPrice * Number(Q96))); } function neededTokens(sqrtPriceX96, tickLower, tickUpper) { const sqrtA = tickToSqrtPriceX96(tickLower); const sqrtB = tickToSqrtPriceX96(tickUpper); if (sqrtPriceX96 <= sqrtA) return { need0: true, need1: false }; if (sqrtPriceX96 >= sqrtB) return { need0: false, need1: true }; return { need0: true, need1: true }; } function calcOtherAmount(sqrtPriceX96, tickLower, tickUpper, amount, isAmount0) { if (amount === 0n) return 0n; const sqrtA = tickToSqrtPriceX96(tickLower); const sqrtB = tickToSqrtPriceX96(tickUpper); const sqrtP = sqrtPriceX96; if (sqrtP <= sqrtA || sqrtP >= sqrtB) { return 0n; } if (isAmount0) { const numeratorL = amount * sqrtP * sqrtB; const denominatorL = (sqrtB - sqrtP) * Q96; if (denominatorL === 0n) return 0n; const L = numeratorL / denominatorL; const amount1 = (L * (sqrtP - sqrtA)) / Q96; return amount1 > 0n ? amount1 : 0n; } const diffPA = sqrtP - sqrtA; if (diffPA === 0n) return 0n; const L = (amount * Q96) / diffPA; const amount0 = (L * (sqrtB - sqrtP) * Q96) / (sqrtP * sqrtB); return amount0 > 0n ? amount0 : 0n; } function sqrtPriceX96ToRawPrice(sqrtPriceX96) { const sqrtP = Number(sqrtPriceX96) / Number(Q96); return sqrtP * sqrtP; } function parsePercentValue(raw, label) { const normalized = String(raw).trim().replace(/%$/, ""); const value = parseFloat(normalized); if (!Number.isFinite(value) || value <= 0) { exitError(`label must be a positive number`); } return value; } function parseBaseToken(raw) { const value = String(raw || "").trim().toLowerCase(); if (value !== "atx" && value !== "usdt") { exitError('base token must be either "atx" or "usdt"'); } return value; } function normalizeRangeMode(args) { const hasTick = args["tick-lower"] !== undefined && args["tick-lower"] !== true && args["tick-upper"] !== undefined && args["tick-upper"] !== true; const hasPrice = args["min-price"] !== undefined && args["min-price"] !== true && args["max-price"] !== undefined && args["max-price"] !== true; const hasRangePercent = args["range-percent"] !== undefined && args["range-percent"] !== true; const wantFull = args["full-range"] === true || args["full-range"] === "true"; if (wantFull && (hasTick || hasPrice || hasRangePercent)) { exitError("Do not combine --full-range with other range options"); } if ([hasTick, hasPrice, hasRangePercent].filter(Boolean).length > 1) { exitError("Use only one of --tick-lower/--tick-upper, --min-price/--max-price, or --range-percent"); } if ((args["tick-lower"] !== undefined && !hasTick) || (args["tick-upper"] !== undefined && !hasTick)) { exitError("When using ticks, pass both --tick-lower and --tick-upper"); } if ((args["min-price"] !== undefined && !hasPrice) || (args["max-price"] !== undefined && !hasPrice)) { exitError("When using price range, pass both --min-price and --max-price (USDT per 1 ATX)"); } return { hasTick, hasPrice, hasRangePercent, wantFull }; } /** * @param {number} human price: USDT per 1 ATX (18-dec pool, same as frontend) * @param {boolean} isAtxToken0 from pool */ function humanUsdtPerAtxToToken1OverToken0(h, isAtxToken0) { if (h <= 0) { throw new Error("min-price and max-price must be positive (USDT per 1 ATX)"); } return isAtxToken0 ? h : 1 / h; } /** * @returns {{ tickLower: number, tickUpper: number }} */ function humanPriceRangeToTicks(minHuman, maxHuman, isAtxToken0, tickSpacing) { const rawA = humanUsdtPerAtxToToken1OverToken0(minHuman, isAtxToken0); const rawB = humanUsdtPerAtxToToken1OverToken0(maxHuman, isAtxToken0); let t0 = nearestUsableTick(priceToTick(rawA), tickSpacing); let t1 = nearestUsableTick(priceToTick(rawB), tickSpacing); let tickLower = Math.min(t0, t1); let tickUpper = Math.max(t0, t1); if (tickLower < V3_MIN_TICK) tickLower = V3_MIN_TICK; if (tickUpper > V3_MAX_TICK) tickUpper = V3_MAX_TICK; if (tickLower >= tickUpper) { tickUpper = tickLower + tickSpacing; } if (tickUpper > V3_MAX_TICK) { tickUpper = V3_MAX_TICK; tickLower = tickUpper - tickSpacing; } return { tickLower, tickUpper }; } async function getPoolContext(client) { const [token0, slot0] = await Promise.all([ client.publicClient.readContract({ address: client.contracts.pool, abi: POOL_TOKEN0_ABI, functionName: "token0", }), client.publicClient.readContract({ address: client.contracts.pool, abi: POOL_SLOT0_ABI, functionName: "slot0", }), ]); const atxToken0 = token0.toLowerCase() === client.contracts.atx.toLowerCase(); const sqrtPriceX96 = slot0[0]; const rawPrice = sqrtPriceX96ToRawPrice(sqrtPriceX96); const usdtPerAtx = atxToken0 ? rawPrice : 1 / rawPrice; return { isAtxToken0: atxToken0, sqrtPriceX96, currentTick: Number(slot0[1]), tickSpacing: feeToTickSpacing(client.poolFee), usdtPerAtx, }; } function resolveRangeFromArgs(args, ctx) { const { hasTick, hasPrice, hasRangePercent } = normalizeRangeMode(args); if (hasPrice) { const minP = parseFloat(String(args["min-price"])); const maxP = parseFloat(String(args["max-price"])); if (!Number.isFinite(minP) || !Number.isFinite(maxP) || minP <= 0 || maxP <= 0) { exitError("min-price and max-price must be positive numbers (USDT per 1 ATX)"); } const { tickLower, tickUpper } = humanPriceRangeToTicks( minP, maxP, ctx.isAtxToken0, ctx.tickSpacing, ); return { tickLower, tickUpper, meta: { mode: "price", minPrice: minP, maxPrice: maxP, tickLower, tickUpper, tickSpacing: ctx.tickSpacing, isAtxToken0: ctx.isAtxToken0, }, }; } if (hasRangePercent) { const percent = parsePercentValue(args["range-percent"], "range-percent"); const factor = percent / 100; const minPrice = ctx.usdtPerAtx * (1 - factor); const maxPrice = ctx.usdtPerAtx * (1 + factor); if (!Number.isFinite(minPrice) || !Number.isFinite(maxPrice) || minPrice <= 0 || maxPrice <= 0) { exitError("range-percent is too large for the current price; it would make the lower price non-positive"); } const { tickLower, tickUpper } = humanPriceRangeToTicks( minPrice, maxPrice, ctx.isAtxToken0, ctx.tickSpacing, ); return { tickLower, tickUpper, meta: { mode: "percent", rangePercent: percent, centerPrice: ctx.usdtPerAtx, minPrice, maxPrice, tickLower, tickUpper, tickSpacing: ctx.tickSpacing, isAtxToken0: ctx.isAtxToken0, }, }; } if (hasTick) { const tickLower = parseInt(String(args["tick-lower"]), 10); const tickUpper = parseInt(String(args["tick-upper"]), 10); if (!Number.isFinite(tickLower) || !Number.isFinite(tickUpper)) { exitError("tick-lower and tick-upper must be integers"); } let tl = Math.min(tickLower, tickUpper); let tu = Math.max(tickLower, tickUpper); if (tl < V3_MIN_TICK) tl = V3_MIN_TICK; if (tu > V3_MAX_TICK) tu = V3_MAX_TICK; if (tl >= tu) { exitError("tick-lower and tick-upper must allow tickLower < tickUpper after normalizing to pool limits"); } return { tickLower: tl, tickUpper: tu, meta: { mode: "tick", tickLower: tl, tickUpper: tu, tickSpacing: ctx.tickSpacing, isAtxToken0: ctx.isAtxToken0, }, }; } return { tickLower: V3_MIN_TICK, tickUpper: V3_MAX_TICK, meta: { mode: "full-range", fullRange: true, tickLower: V3_MIN_TICK, tickUpper: V3_MAX_TICK, tickSpacing: ctx.tickSpacing, isAtxToken0: ctx.isAtxToken0, }, }; } function resolveSingleSidedAmounts(ctx, tickLower, tickUpper, baseToken, amountRaw) { const amount = parseEther(amountRaw); if (amount <= 0n) { exitError("amount must be greater than 0"); } const { need0, need1 } = neededTokens(ctx.sqrtPriceX96, tickLower, tickUpper); const needAtx = ctx.isAtxToken0 ? need0 : need1; const needUsdt = ctx.isAtxToken0 ? need1 : need0; if (baseToken === "atx") { if (!needAtx && needUsdt) { exitError("Current price is above the selected range; only USDT is needed for this position"); } return { atxAmount: amount, usdtAmount: needUsdt ? calcOtherAmount(ctx.sqrtPriceX96, tickLower, tickUpper, amount, ctx.isAtxToken0) : 0n, meta: { baseToken, amount: amountRaw, needAtx, needUsdt }, }; } if (!needUsdt && needAtx) { exitError("Current price is below the selected range; only ATX is needed for this position"); } return { atxAmount: needAtx ? calcOtherAmount(ctx.sqrtPriceX96, tickLower, tickUpper, amount, !ctx.isAtxToken0) : 0n, usdtAmount: amount, meta: { baseToken, amount: amountRaw, needAtx, needUsdt }, }; } function buildQuoteResult(ctx, range, amounts, extra = {}) { return { currentPrice: { usdtPerAtx: ctx.usdtPerAtx, atxPerUsdt: ctx.usdtPerAtx === 0 ? 0 : 1 / ctx.usdtPerAtx, }, range: range.meta, estimatedAmounts: { atx: fmt(amounts.atxAmount), usdt: fmt(amounts.usdtAmount), atxWei: amounts.atxAmount.toString(), usdtWei: amounts.usdtAmount.toString(), }, ...extra, }; } function toAmountInput(wei) { const value = formatUnits(wei, 18); return value.includes(".") ? value.replace(/\.?0+$/, "") : value; } function parseSlippageBps(args) { if (args["slippage-bps"] === undefined || args["slippage-bps"] === true) { return undefined; } const value = parseInt(String(args["slippage-bps"]), 10); if (!Number.isFinite(value) || value < 0 || value > 10_000) { exitError("slippage-bps must be between 0 and 10000"); } return value; } function buildSdkRangeArgs(args) { const { hasTick, hasPrice, hasRangePercent } = normalizeRangeMode(args); if (hasPrice) { const minPrice = parseFloat(String(args["min-price"])); const maxPrice = parseFloat(String(args["max-price"])); if (!Number.isFinite(minPrice) || !Number.isFinite(maxPrice) || minPrice <= 0 || maxPrice <= 0) { exitError("min-price and max-price must be positive numbers (USDT per 1 ATX)"); } return { minPrice, maxPrice }; } if (hasRangePercent) { return { rangePercent: parsePercentValue(args["range-percent"], "range-percent") }; } if (hasTick) { const tickLower = parseInt(String(args["tick-lower"]), 10); const tickUpper = parseInt(String(args["tick-upper"]), 10); if (!Number.isFinite(tickLower) || !Number.isFinite(tickUpper)) { exitError("tick-lower and tick-upper must be integers"); } return { tickLower, tickUpper }; } return { fullRange: true }; } async function quoteViaSdk(client, baseToken, amountRaw, args) { if (typeof client.liquidity.quoteAddLiquidity !== "function") { return null; } return await client.liquidity.quoteAddLiquidity({ baseToken, amount: parseEther(amountRaw), range: buildSdkRangeArgs(args), slippageBps: parseSlippageBps(args), }); } function sdkQuoteToJson(result, extra = {}) { return { currentPrice: result.currentPrice, range: { ...result.range, tickSpacing: result.pool.tickSpacing, isAtxToken0: result.pool.isAtxToken0, }, estimatedAmounts: { atx: fmt(result.desiredAmounts.atx), usdt: fmt(result.desiredAmounts.usdt), atxWei: result.desiredAmounts.atx.toString(), usdtWei: result.desiredAmounts.usdt.toString(), }, minAmounts: { atx: fmt(result.minAmounts.atx), usdt: fmt(result.minAmounts.usdt), atxWei: result.minAmounts.atx.toString(), usdtWei: result.minAmounts.usdt.toString(), }, ...extra, }; } await runMain(async () => { const client = await createClient(); const args = parseArgs(process.argv.slice(2)); const command = args._[0]; if (!command) { exitError( "Usage: liquidity.js <quote-add|add|remove|collect|burn> [args] [--from address] [--password <pwd>]\n" + " quote-add: <atx|usdt> <amount> with optional range --full-range | --tick-lower <n> --tick-upper <n> | --min-price <p> --max-price <p> | --range-percent <n>\n" + " add: positional <atxAmount> <usdtAmount> OR auto-balance with --base-token <atx|usdt> --amount <n>, plus optional range options and --slippage-bps <n>", ); } switch (command) { case "quote-add": { const baseToken = parseBaseToken(args._[1]); const amount = args._[2]; if (!amount) { exitError( "Usage: liquidity.js quote-add <atx|usdt> <amount> [--full-range] [--tick-lower n --tick-upper n] [--min-price p --max-price p] [--range-percent n]", ); } const sdkQuote = await quoteViaSdk(client, baseToken, amount, args); if (sdkQuote) { console.log( JSON.stringify( sdkQuoteToJson(sdkQuote, { action: "quote add liquidity", input: { baseToken, amount }, }), null, 2, ), ); break; } const ctx = await getPoolContext(client); const range = resolveRangeFromArgs(args, ctx); const amounts = resolveSingleSidedAmounts(ctx, range.tickLower, range.tickUpper, baseToken, amount); console.log( JSON.stringify( buildQuoteResult(ctx, range, amounts, { action: "quote add liquidity", input: { baseToken, amount }, }), null, 2, ), ); break; } case "add": { const positionalAtxAmount = args._[1]; const positionalUsdtAmount = args._[2]; const hasPositionalAmounts = !!positionalAtxAmount && !!positionalUsdtAmount; const hasAutoBalance = args["base-token"] !== undefined && args["base-token"] !== true && args.amount !== undefined && args.amount !== true; if (hasPositionalAmounts && hasAutoBalance) { exitError("Use either positional amounts or --base-token/--amount auto-balance mode, not both"); } if (!hasPositionalAmounts && !hasAutoBalance) { exitError( "Usage: liquidity.js add <atxAmount> <usdtAmount> [--from address] [--full-range] [--tick-lower n --tick-upper n] [--min-price p --max-price p] [--range-percent n] [--slippage-bps n]\n" + " or: liquidity.js add --base-token <atx|usdt> --amount <n> [same range opts]\n" + " Default: full range. Custom: either tick pair, min/max price (USDT per 1 ATX), or range-percent centered on current price.", ); } const ctx = await getPoolContext(client); const fromAddress = args.from || client.wallet.list()[0]?.address; if (!fromAddress) exitError("No wallet found. Create one first."); const wallet = await loadWallet(client, fromAddress, args); const range = resolveRangeFromArgs(args, ctx); let liqOptions; let meta = { ...range.meta }; let atxAmount; let usdtAmount; if (range.meta.fullRange) { liqOptions = { fullRange: true }; } else { liqOptions = { fullRange: false, tickLower: range.tickLower, tickUpper: range.tickUpper, }; } if (hasPositionalAmounts) { atxAmount = positionalAtxAmount; usdtAmount = positionalUsdtAmount; } else { const baseToken = parseBaseToken(args["base-token"]); const sdkQuote = await quoteViaSdk(client, baseToken, String(args.amount), args); if (sdkQuote) { atxAmount = toAmountInput(sdkQuote.desiredAmounts.atx); usdtAmount = toAmountInput(sdkQuote.desiredAmounts.usdt); meta = { ...meta, autoBalanced: true, input: { baseToken, amount: String(args.amount), needAtx: sdkQuote.needs.atx, needUsdt: sdkQuote.needs.usdt, }, estimatedAmounts: { atx: fmt(sdkQuote.desiredAmounts.atx), usdt: fmt(sdkQuote.desiredAmounts.usdt), }, }; } else { const quoted = resolveSingleSidedAmounts( ctx, range.tickLower, range.tickUpper, baseToken, String(args.amount), ); atxAmount = toAmountInput(quoted.atxAmount); usdtAmount = toAmountInput(quoted.usdtAmount); meta = { ...meta, autoBalanced: true, input: quoted.meta, estimatedAmounts: { atx: fmt(quoted.atxAmount), usdt: fmt(quoted.usdtAmount), }, }; } } const b = parseSlippageBps(args); if (b !== undefined) { liqOptions = { ...liqOptions, slippageBps: b }; meta = { ...meta, slippageBps: b }; } const result = await client.liquidity.addLiquidity( wallet, parseEther(atxAmount), parseEther(usdtAmount), liqOptions, ); console.log( JSON.stringify( { action: "add liquidity", txHash: result.txHash, range: meta }, null, 2, ), ); break; } case "remove": { const fromAddress = args.from || client.wallet.list()[0]?.address; if (!fromAddress) exitError("No wallet found. Create one first."); const wallet = await loadWallet(client, fromAddress, args); const tokenId = args._[1]; const percent = args._[2]; if (!tokenId || !percent) { exitError("Usage: liquidity.js remove <tokenId> <percent> [--from address]"); } const result = await client.liquidity.removeLiquidity(wallet, BigInt(tokenId), parseInt(percent)); console.log(JSON.stringify({ action: "remove liquidity", txHash: result.txHash }, null, 2)); break; } case "collect": { const fromAddress = args.from || client.wallet.list()[0]?.address; if (!fromAddress) exitError("No wallet found. Create one first."); const wallet = await loadWallet(client, fromAddress, args); const tokenId = args._[1]; if (!tokenId) exitError("Usage: liquidity.js collect <tokenId> [--from address]"); const result = await client.liquidity.collectFees(wallet, BigInt(tokenId)); console.log(JSON.stringify({ action: "collect fees", txHash: result.txHash }, null, 2)); break; } case "burn": { const fromAddress = args.from || client.wallet.list()[0]?.address; if (!fromAddress) exitError("No wallet found. Create one first."); const wallet = await loadWallet(client, fromAddress, args); const tokenId = args._[1]; if (!tokenId) exitError("Usage: liquidity.js burn <tokenId> [--from address]"); const result = await client.liquidity.burnPosition(wallet, BigInt(tokenId)); console.log(JSON.stringify({ action: "burn position", txHash: result.txHash }, null, 2)); break; } default: exitError("Usage: liquidity.js <quote-add|add|remove|collect|burn> [args]"); } }); FILE:scripts/query.js #!/usr/bin/env node import { createClient, parseArgs, fmt, runMain } from "./_helpers.js"; import { parseEther } from "atxswap-sdk"; await runMain(async () => { const client = await createClient(); const args = parseArgs(process.argv.slice(2)); const command = args._[0]; switch (command) { case "price": { const price = await client.query.getPrice(); console.log(JSON.stringify({ usdtPerAtx: price.usdtPerAtx, atxPerUsdt: price.atxPerUsdt, sqrtPriceX96: price.sqrtPriceX96.toString(), }, null, 2)); break; } case "balance": { const address = args._[1]; if (!address) { console.error("Usage: query.js balance <address>"); process.exit(1); } const bal = await client.query.getBalance(address); console.log(JSON.stringify({ address, bnb: fmt(bal.bnb), atx: fmt(bal.atx), usdt: fmt(bal.usdt), }, null, 2)); break; } case "quote": { const direction = args._[1]; const amount = args._[2]; if (!direction || !amount) { console.error("Usage: query.js quote <buy|sell> <amount>"); process.exit(1); } const amountWei = parseEther(amount); const quote = await client.query.getQuote(direction, amountWei); console.log(JSON.stringify({ direction: quote.direction, amountIn: fmt(quote.amountIn), amountOut: fmt(quote.amountOut), priceImpact: (quote.priceImpact * 100).toFixed(4) + "%", }, null, 2)); break; } case "positions": { const address = args._[1]; const tokenId = args._[2]; if (!address) { console.error("Usage: query.js positions <address> [tokenId]"); process.exit(1); } const queriedPositions = await client.query.getPositions(address, { includeCollectableFees: true, ...(tokenId ? { tokenId: BigInt(tokenId) } : {}), }); const positions = tokenId ? queriedPositions.filter((position) => position.tokenId === BigInt(tokenId)) : queriedPositions; if (positions.length === 0) { console.log("No ATX/USDT positions found."); } else { for (const p of positions) { const isAtxToken0 = p.token0.toLowerCase() === client.contracts.atx.toLowerCase(); const collectable0 = p.collectable0 ?? p.tokensOwed0; const collectable1 = p.collectable1 ?? p.tokensOwed1; console.log(JSON.stringify({ tokenId: p.tokenId.toString(), fee: p.fee, tickLower: p.tickLower, tickUpper: p.tickUpper, liquidity: p.liquidity.toString(), tokensOwed0: fmt(p.tokensOwed0), tokensOwed1: fmt(p.tokensOwed1), collectable0: fmt(collectable0), collectable1: fmt(collectable1), collectableAtx: fmt(isAtxToken0 ? collectable0 : collectable1), collectableUsdt: fmt(isAtxToken0 ? collectable1 : collectable0), }, null, 2)); } } break; } case "token-info": { const tokenAddr = args._[1]; if (!tokenAddr) { console.error("Usage: query.js token-info <tokenAddress>"); process.exit(1); } const info = await client.query.getTokenInfo(tokenAddr); console.log(JSON.stringify({ address: info.address, name: info.name, symbol: info.symbol, decimals: info.decimals, totalSupply: fmt(info.totalSupply, info.decimals), }, null, 2)); break; } default: console.error("Usage: query.js <price|balance|quote|positions|token-info> [args]"); process.exit(1); } }); FILE:scripts/swap.js #!/usr/bin/env node import { createClient, loadWallet, parseArgs, fmt, exitError, runMain } from "./_helpers.js"; import { parseEther } from "atxswap-sdk"; await runMain(async () => { const client = await createClient(); const args = parseArgs(process.argv.slice(2)); const command = args._[0]; const amount = args._[1]; if (!command || !amount || !["buy", "sell"].includes(command)) { exitError("Usage: swap.js <buy|sell> <amount> [--from address] [--slippage bps] [--password <pwd>]"); } const fromAddress = args.from || client.wallet.list()[0]?.address; if (!fromAddress) exitError("No wallet found. Create one first."); const wallet = await loadWallet(client, fromAddress, args); const amountWei = parseEther(amount); const slippage = args.slippage ? parseInt(args.slippage) : undefined; if (command === "buy") { const result = await client.swap.buy(wallet, amountWei, slippage); console.log(JSON.stringify({ action: "buy ATX", txHash: result.txHash, usdtSpent: fmt(result.amountIn), atxReceived: fmt(result.amountOut), }, null, 2)); } else { const result = await client.swap.sell(wallet, amountWei, slippage); console.log(JSON.stringify({ action: "sell ATX", txHash: result.txHash, atxSold: fmt(result.amountIn), usdtReceived: fmt(result.amountOut), }, null, 2)); } }); FILE:scripts/transfer.js #!/usr/bin/env node import { createClient, loadWallet, parseArgs, exitError, runMain } from "./_helpers.js"; import { parseEther } from "atxswap-sdk"; await runMain(async () => { const client = await createClient(); const args = parseArgs(process.argv.slice(2)); const command = args._[0]; if (!command) { exitError("Usage: transfer.js <bnb|atx|usdt|token> <to> <amount> [tokenAddress] [--from address] [--password <pwd>]"); } const fromAddress = args.from || client.wallet.list()[0]?.address; if (!fromAddress) exitError("No wallet found. Create one first."); const wallet = await loadWallet(client, fromAddress, args); switch (command) { case "bnb": { const to = args._[1]; const amount = args._[2]; if (!to || !amount) exitError("Usage: transfer.js bnb <to> <amount> [--from address]"); const result = await client.transfer.sendBnb(wallet, to, parseEther(amount)); console.log(JSON.stringify({ action: "send BNB", to, amount, txHash: result.txHash }, null, 2)); break; } case "atx": { const to = args._[1]; const amount = args._[2]; if (!to || !amount) exitError("Usage: transfer.js atx <to> <amount> [--from address]"); const result = await client.transfer.sendAtx(wallet, to, parseEther(amount)); console.log(JSON.stringify({ action: "send ATX", to, amount, txHash: result.txHash }, null, 2)); break; } case "usdt": { const to = args._[1]; const amount = args._[2]; if (!to || !amount) exitError("Usage: transfer.js usdt <to> <amount> [--from address]"); const result = await client.transfer.sendUsdt(wallet, to, parseEther(amount)); console.log(JSON.stringify({ action: "send USDT", to, amount, txHash: result.txHash }, null, 2)); break; } case "token": { const tokenAddr = args._[1]; const to = args._[2]; const amount = args._[3]; if (!tokenAddr || !to || !amount) { exitError("Usage: transfer.js token <tokenAddress> <to> <amount> [--from address]"); } const result = await client.transfer.sendToken(wallet, tokenAddr, to, parseEther(amount)); console.log(JSON.stringify({ action: "send token", token: tokenAddr, to, amount, txHash: result.txHash }, null, 2)); break; } default: exitError("Usage: transfer.js <bnb|atx|usdt|token> [args]"); } }); FILE:scripts/wallet.js #!/usr/bin/env node import { createClient, exportKeystore, getDefaultKeystorePath, parseArgs, fmt, exitError, runMain, resolveNewPassword, } from "./_helpers.js"; import { writeFileSync } from "node:fs"; import { resolve as resolvePath } from "node:path"; const DELETE_FORCE_PHRASE = "force delete wallet"; await runMain(async () => { const client = await createClient(); const args = parseArgs(process.argv.slice(2)); const command = args._[0]; switch (command) { case "create": { const existing = client.wallet.list(); if (existing.length > 0) { exitError(`Wallet already exists (existing[0].address). Only one wallet is allowed per skill instance.`); } const password = await resolveNewPassword(args); const name = args._[1]; const result = await client.wallet.create(password, name); console.log(JSON.stringify({ action: "create", address: result.address, keystoreFile: result.keystoreFile, keystoreDir: getDefaultKeystorePath(), name: name || null, passwordSaved: result.passwordSaved, ...(result.passwordSaveError ? { passwordSaveError: result.passwordSaveError } : {}), }, null, 2)); break; } case "list": { const wallets = client.wallet.list(); if (wallets.length === 0) { console.log(JSON.stringify({ wallets: [] }, null, 2)); break; } const results = []; for (const w of wallets) { const entry = { address: w.address, name: w.name || null }; try { const bal = await client.query.getBalance(w.address); entry.bnb = fmt(bal.bnb); entry.atx = fmt(bal.atx); entry.usdt = fmt(bal.usdt); } catch (e) { entry.balanceError = e?.shortMessage || e?.message?.split("\n")[0] || String(e); } results.push(entry); } console.log(JSON.stringify({ wallets: results }, null, 2)); break; } case "export": { const address = args._[1]; if (!address) { exitError("Usage: wallet.js export <address> [--out <file>]"); } const { keystore, keystoreFile } = await exportKeystore(client, address, args); const outPath = typeof args.out === "string" ? resolvePath(args.out) : null; const json = JSON.stringify(keystore, null, 2); if (outPath) { writeFileSync(outPath, json); console.log(JSON.stringify({ action: "export", address, format: "keystore-v3", source: keystoreFile, output: outPath, }, null, 2)); } else { console.log(json); } break; } case "forget-password": { const address = args._[1]; if (!address) exitError("Usage: wallet.js forget-password <address>"); await client.wallet.forgetPassword(address); console.log(JSON.stringify({ action: "forget-password", address, success: true }, null, 2)); break; } case "has-password": { const address = args._[1]; if (!address) exitError("Usage: wallet.js has-password <address>"); const saved = await client.wallet.hasSavedPassword(address); console.log(JSON.stringify({ address, hasSavedPassword: saved }, null, 2)); break; } case "delete": { const address = args._[1]; if (!address) { exitError("Usage: wallet.js delete <address> --backup-confirmed yes --force-phrase \"force delete wallet\""); } if (args["backup-confirmed"] !== "yes") { exitError("Refusing to delete wallet: export and back up the keystore first, then rerun with --backup-confirmed yes"); } if (args["force-phrase"] !== DELETE_FORCE_PHRASE) { exitError("Refusing to delete wallet: rerun with --force-phrase \"force delete wallet\" after the user explicitly sends that exact phrase"); } await client.wallet.delete(address); console.log(JSON.stringify({ action: "delete", address, deleted: true, backupConfirmed: true, forcePhrase: DELETE_FORCE_PHRASE, }, null, 2)); break; } default: exitError("Usage: wallet.js <create|list|export|forget-password|has-password|delete> [args] [--password <pwd>]"); } });