Disable SideSwap and Depix (PIX) tools by default#82
Merged
Conversation
andycreed0x
added a commit
that referenced
this pull request
May 27, 2026
* progresive disclosure * New smokes * update prompts * Update prompt_test_send_real_changelly.md * Update prompt_test_send_real_sideshift.md * Update prompt_test_send_real_sideswap.md * Update README.md * Update prompt_test_real_pix.md * Update prompt_test_read_only_integrations.md * Disable SideSwap and Depix (PIX) tools by default (#82) * Disable * Update README.md * Update commands.py * Update test_cli.py * Update test_cli_read_only.py
andycreed0x
added a commit
that referenced
this pull request
May 27, 2026
…ing Address, and more (#88) * Add SideSwap integration: BTC \xe2\x86\x94 L-BTC pegs and asset swap quoting (#27) * Add SideSwap integration: BTC ↔ L-BTC pegs and asset swap quoting Mirrors the AQUA Flutter wallet's SideSwap flow over WebSocket JSON-RPC (wss://api.sideswap.io/json-rpc-ws). Lets users move funds between Bitcoin mainchain and Liquid via SideSwap pegs, which charge 0.1% (vs 0.2% on instant swap-market trades) at the cost of waiting for chain confirmations. - New module src/aqua/sideswap.py: async WS JSON-RPC client with sync wrappers, dataclasses (SideSwapPeg, SideSwapServerStatus, etc.), SideSwapPegManager for orchestration, and recommendation logic that surfaces the time-vs-fee trade-off (and warns about the 102-conf cold-wallet path on large peg-ins). - New MCP tools: sideswap_server_status, sideswap_peg_quote, sideswap_peg_in, sideswap_peg_out, sideswap_peg_status, sideswap_recommend, sideswap_list_assets, sideswap_quote. - New prompts: peg_in, peg_out, swap_assets — each guides the agent to quote, recommend, confirm, execute, and track. - Persistence in ~/.aqua/sideswap_pegs/{order_id}.json (mode 0o600, atomic writes; saved before broadcast for crash recovery). - websockets>=12.0 added to dependencies. - 38 new tests cover the WS client, peg flow, recommendation logic, storage round-trip, and quote handling. All 337 tests pass. Asset swap *execution* (legacy start_swap_web + HTTP swap_start/swap_sign) is intentionally not implemented: local PSET output verification is security-critical and needs an audit before live signing. sideswap_quote returns a quote only and directs users to the AQUA mobile wallet or sideswap.io for execution. * Address PR #27 review feedback Prompt-text consistency: - server.py:729 / 1217 — use `>= 0.01 BTC (1,000,000 sats)` consistently in the agent prompts (matches AGENTS.md threshold). Validation tightening: - tools.py:sideswap_peg_quote, sideswap_peg_out — explicit `amount > 0` check before calling into the manager. - sideswap.py:peg_out — validate `btc_address` parses on the matching Bitcoin network via bdkpython BEFORE creating the SideSwap order, so typos / wrong-network addresses don't produce orphaned orders. - sideswap.py:peg_out — decrypt the wallet mnemonic up-front when the wallet is encrypted, so a bad password fails fast instead of after a SideSwap order has already been created. Watch-only / unencrypted wallets are unaffected. - sideswap.py:peg_out — reserve 200 sats for the Liquid network fee in the balance check (Liquid fees are stable and tiny but non-zero, and the previous check could pass for `balance == amount` then fail at broadcast). State machine: - SideSwapPeg gets a new `local_error: Optional[str]` field. peg_out's broadcast-failure branch now writes to `local_error` instead of setting `tx_state = "InsufficientAmount"` — `tx_state` is reserved for SideSwap server enums (`Detected | Processing | Done | InsufficientAmount`) so it always reflects what the server reported. - peg_status: pick the most-progressed entry from the txns list rather than just `txns[-1]`. SideSwap returns one entry per detected deposit on the peg address; if the user reuses the address, a fresh `Detected` deposit can appear after a completed `Done`. The previous code let the persisted state regress and lost the original `payout_txid`. Regression test added. - peg_in: drop the dead `wallet_manager.load_wallet(...)` call. Receiving a peg-in only needs the wallet's next address, never the mnemonic. The `password` kwarg stays for signature symmetry. Misc: - Hoist `import threading` and `import websockets` to the top of sideswap.py — neither is optional. - Let the `update_price_stream` notification timeout propagate in `fetch_swap_quote` instead of swallowing it. Silent timeouts produced a price=0.0 quote that looked legitimate to callers. delete_wallet: - tools.py:delete_wallet now also removes SideSwap peg records owned by the wallet (`storage.delete_sideswap_pegs_for_wallet`). Returns `sideswap_pegs_removed` in the response. New regression test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add SideSwap atomic asset-swap execution via mkt::* (#31) * Add SideSwap atomic asset-swap execution via mkt::* Layers atomic asset swap execution on top of the SideSwap peg integration (#27). Uses SideSwap's modern `mkt::*` WebSocket-only flow with a security-critical local PSET verifier between get_quote and signing. Both directions are supported in one go: L-BTC → asset and asset → L-BTC. Why this PR is structured this way ---------------------------------- This consolidates and supersedes #28, #29, and #30. Those three were incrementally building toward this end state — first the legacy `start_swap_web`+HTTP path, then bidirectional support, then the migration to `mkt::*` — but the legacy path got entirely replaced by the mkt::* migration, so reviewers would have been auditing scaffolding that never runs in production. This PR lands only the code that actually runs. The verifier ------------ `verify_pset_balances` is a pure function that operates on the dict returned by `wollet.pset_details(pset).balance.balances()`. Three rules; any failure raises `PsetVerificationError` and signing is aborted before any tx hits the wire. | Rule | Why it matters | |---|---| | Wallet must gain *exactly* `recv_amount` of `recv_asset` | Blocks the canonical attack: server's PSET takes our funds and pays us nothing | | Wallet must lose at most `send_amount + fee_tolerance_sats` of `send_asset` (only if `send_asset == fee_asset`) | Blocks overcharge attacks; tolerance covers Liquid's tens-of-sats network fee | | No other asset may have a non-zero balance change | Blocks extra-output / siphon attacks | The manager always passes `fee_asset = policy_asset` (L-BTC), so the fee tolerance only ever relaxes constraints on the L-BTC side. On asset → L-BTC the asset side is checked at strict equality — without this pinning, a hostile server could siphon up to `fee_tolerance_sats` of USDt (or any other asset) per swap. Fee model (verified against AQUA Flutter + sideswap_lwk) ------------------------------------------------------- | Direction | Fee paid by | Wallet effect | |---|---|---| | L-BTC → asset | User's L-BTC change | L-BTC `-(send + fee)`, asset `+recv` | | asset → L-BTC | Dealer's L-BTC change | asset `-send` exact, L-BTC `+recv` exact | UTXO selection picks only `send_asset` inputs in both cases — no separate L-BTC fee inputs needed. Mirrors `swap_provider.dart` `executeTransaction()` in aquawallet/aqua-wallet. mkt::* wire format ------------------ Top-level method is `"market"`, params is a single-key object whose key is the snake_case mkt::Request variant. AssetType and TradeDir are PascalCase strings. Per `sideswap_api/src/mkt.rs`. ``` {"id":1, "method":"market", "params":{"list_markets":{}}} {"id":2, "method":"market", "params":{"start_quotes":{ "asset_pair":{"base":"...","quote":"..."}, "asset_type":"Quote", "amount":100000, "trade_dir":"Sell", "utxos":[...], "receive_address":"lq1...", "change_address":"lq1...", "instant_swap":true }}} {"id":3, "method":"market", "params":{"get_quote":{"quote_id":42}}} {"id":4, "method":"market", "params":{"taker_sign":{"quote_id":42, "pset":"..."}}} ``` Notification: `{"method":"market", "params":{"quote":{"status":{"Success":{...}}}}}` What's in this PR ----------------- - `verify_pset_balances` (pure) + `PsetVerificationError` - `SideSwapWSClient.mkt`, `mkt_list_markets`, `mkt_start_quotes`, `mkt_stop_quotes`, `mkt_get_quote`, `mkt_taker_sign`, `next_market_notification` - `resolve_market` — picks the matching market and derives (asset_type, trade_dir) for our taker case (always Sell with asset_type matching the side we're sending) - `parse_quote_status` — raises on LowBalance / Error so callers never proceed with an invalid quote - `select_swap_utxos` — confidential UTXOs of `send_asset` only, largest-first - `SideSwapSwap` dataclass + storage helpers (`~/.aqua/sideswap_swaps/{order_id}.json`, mode 0o600, atomic writes; saved at every step for crash recovery) - `SideSwapSwapManager.execute_swap` — end-to-end orchestrator - MCP tools `sideswap_execute_swap` (with `send_bitcoins` parameter for direction) and `sideswap_swap_status` - Updated `swap_assets` prompt to drive the full quote → confirm → execute → status flow Tests (52 new, all 393 in suite passing) ---------------------------------------- Verifier (19): exact-match passes; canonical attacks rejected (server keeps recv, short delivery, excess delivery, overcharge, undercharge, unrelated asset movement, fee tolerance limits); same send/recv asset rejected; arg validation; reverse direction with `fee_asset=L_BTC` (5 tests including the "documented behavior" test that fixes the asset-siphon vector). Helpers (10): `resolve_market` for forward/reverse/swapped pairs/no- match/malformed; `parse_quote_status` for Success/LowBalance/Error/ missing/unknown. UTXO selector (5): largest-first, accumulation, asset filtering, non-confidential rejection, insufficient funds. Manager forward direction (12): happy path; correct (asset_type, trade_dir, addresses, instant_swap, UTXOs); three malicious-PSET attack classes never sign and never POST taker_sign; unknown wallet; LowBalance / Error quote responses; no-matching- market; dealer offered wrong send_amount; status returns persisted; unknown order raises. Manager reverse direction (7): happy path with `(asset_type=Base, trade_dir=Sell)`; 500-sat asset-side siphon rejected (the security tightening this PR enforces); short L-BTC delivery rejected; unrelated asset movement rejected; picks asset UTXOs even when wallet also holds L-BTC; insufficient asset balance raises; rejects asset_id == policy_asset. Manual testnet smoke test still required before mainnet sign-off. * Address PR #31 review feedback - Drop the dead `price = float(quote_data.get("server_fee", 0)) and 0.0` line in execute_swap — the next statement overwrites `price` from recv/send unconditionally. - Add `flexible_small_amount` (default False) to sideswap_execute_swap and the underlying manager. When True, accept dealer-rounded send amounts within ±3000 sats of the requested value. Plumbed through the MCP tool schema. Why: SideSwap's mkt::* dealer rounds send amounts internally. On small swaps (5k–25k sats) the dealer's quote frequently comes back at e.g. 5_050 sats when 5_000 was requested, and the strict equality check rejects them. The reviewer hit this manually at both 5k and 25k sats; 43k sats worked. The new flag opts callers in to accepting small dealer adjustments while keeping the strict default for larger amounts where a 3k-sat delta would indicate a real price move rather than rounding. Tests: 3 new cases covering within-tolerance accept, outside-tolerance reject, and flag-off strict (preserves existing behavior for non- interactive callers). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Address PR #31 follow-up review feedback Andy flagged two issues after the first round: - parse_quote_status: validate that the unwrapped Success dict actually has integer `quote_id`, `base_amount`, `quote_amount` before returning it. Without this, a malformed payload exploded as KeyError/TypeError inside execute_swap, far from the cause. Now raises SideSwapWSError with a clear message — including which field is missing or malformed. 3 new tests cover missing key, non-integer amount, and non-dict Success. - mkt::* WebSocket session binding: the previous flow opened one WS for start_quotes / get_quote, did the verify+sign sync, and then opened a fresh WS for taker_sign. SideSwap rejects this with `protocol error: wrong client_id` because quote_id is bound to the issuing session. Refactored execute_swap so a single SideSwapWSClient is held across the entire mkt::* flow (start_quotes → get_quote → verify → sign → taker_sign). The verify and sign steps are sync but cheap and run inside the async with, keeping the WS open until taker_sign completes. Reference: scripts/probe_sideswap_session_binding.py Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Gonzalo Coelho <coelhogonza@gmail.com> * Add `aqua sideswap` CLI subcommand group (#32) * Add SideSwap atomic asset-swap execution via mkt::* Layers atomic asset swap execution on top of the SideSwap peg integration (#27). Uses SideSwap's modern `mkt::*` WebSocket-only flow with a security-critical local PSET verifier between get_quote and signing. Both directions are supported in one go: L-BTC → asset and asset → L-BTC. Why this PR is structured this way ---------------------------------- This consolidates and supersedes #28, #29, and #30. Those three were incrementally building toward this end state — first the legacy `start_swap_web`+HTTP path, then bidirectional support, then the migration to `mkt::*` — but the legacy path got entirely replaced by the mkt::* migration, so reviewers would have been auditing scaffolding that never runs in production. This PR lands only the code that actually runs. The verifier ------------ `verify_pset_balances` is a pure function that operates on the dict returned by `wollet.pset_details(pset).balance.balances()`. Three rules; any failure raises `PsetVerificationError` and signing is aborted before any tx hits the wire. | Rule | Why it matters | |---|---| | Wallet must gain *exactly* `recv_amount` of `recv_asset` | Blocks the canonical attack: server's PSET takes our funds and pays us nothing | | Wallet must lose at most `send_amount + fee_tolerance_sats` of `send_asset` (only if `send_asset == fee_asset`) | Blocks overcharge attacks; tolerance covers Liquid's tens-of-sats network fee | | No other asset may have a non-zero balance change | Blocks extra-output / siphon attacks | The manager always passes `fee_asset = policy_asset` (L-BTC), so the fee tolerance only ever relaxes constraints on the L-BTC side. On asset → L-BTC the asset side is checked at strict equality — without this pinning, a hostile server could siphon up to `fee_tolerance_sats` of USDt (or any other asset) per swap. Fee model (verified against AQUA Flutter + sideswap_lwk) ------------------------------------------------------- | Direction | Fee paid by | Wallet effect | |---|---|---| | L-BTC → asset | User's L-BTC change | L-BTC `-(send + fee)`, asset `+recv` | | asset → L-BTC | Dealer's L-BTC change | asset `-send` exact, L-BTC `+recv` exact | UTXO selection picks only `send_asset` inputs in both cases — no separate L-BTC fee inputs needed. Mirrors `swap_provider.dart` `executeTransaction()` in aquawallet/aqua-wallet. mkt::* wire format ------------------ Top-level method is `"market"`, params is a single-key object whose key is the snake_case mkt::Request variant. AssetType and TradeDir are PascalCase strings. Per `sideswap_api/src/mkt.rs`. ``` {"id":1, "method":"market", "params":{"list_markets":{}}} {"id":2, "method":"market", "params":{"start_quotes":{ "asset_pair":{"base":"...","quote":"..."}, "asset_type":"Quote", "amount":100000, "trade_dir":"Sell", "utxos":[...], "receive_address":"lq1...", "change_address":"lq1...", "instant_swap":true }}} {"id":3, "method":"market", "params":{"get_quote":{"quote_id":42}}} {"id":4, "method":"market", "params":{"taker_sign":{"quote_id":42, "pset":"..."}}} ``` Notification: `{"method":"market", "params":{"quote":{"status":{"Success":{...}}}}}` What's in this PR ----------------- - `verify_pset_balances` (pure) + `PsetVerificationError` - `SideSwapWSClient.mkt`, `mkt_list_markets`, `mkt_start_quotes`, `mkt_stop_quotes`, `mkt_get_quote`, `mkt_taker_sign`, `next_market_notification` - `resolve_market` — picks the matching market and derives (asset_type, trade_dir) for our taker case (always Sell with asset_type matching the side we're sending) - `parse_quote_status` — raises on LowBalance / Error so callers never proceed with an invalid quote - `select_swap_utxos` — confidential UTXOs of `send_asset` only, largest-first - `SideSwapSwap` dataclass + storage helpers (`~/.aqua/sideswap_swaps/{order_id}.json`, mode 0o600, atomic writes; saved at every step for crash recovery) - `SideSwapSwapManager.execute_swap` — end-to-end orchestrator - MCP tools `sideswap_execute_swap` (with `send_bitcoins` parameter for direction) and `sideswap_swap_status` - Updated `swap_assets` prompt to drive the full quote → confirm → execute → status flow Tests (52 new, all 393 in suite passing) ---------------------------------------- Verifier (19): exact-match passes; canonical attacks rejected (server keeps recv, short delivery, excess delivery, overcharge, undercharge, unrelated asset movement, fee tolerance limits); same send/recv asset rejected; arg validation; reverse direction with `fee_asset=L_BTC` (5 tests including the "documented behavior" test that fixes the asset-siphon vector). Helpers (10): `resolve_market` for forward/reverse/swapped pairs/no- match/malformed; `parse_quote_status` for Success/LowBalance/Error/ missing/unknown. UTXO selector (5): largest-first, accumulation, asset filtering, non-confidential rejection, insufficient funds. Manager forward direction (12): happy path; correct (asset_type, trade_dir, addresses, instant_swap, UTXOs); three malicious-PSET attack classes never sign and never POST taker_sign; unknown wallet; LowBalance / Error quote responses; no-matching- market; dealer offered wrong send_amount; status returns persisted; unknown order raises. Manager reverse direction (7): happy path with `(asset_type=Base, trade_dir=Sell)`; 500-sat asset-side siphon rejected (the security tightening this PR enforces); short L-BTC delivery rejected; unrelated asset movement rejected; picks asset UTXOs even when wallet also holds L-BTC; insufficient asset balance raises; rejects asset_id == policy_asset. Manual testnet smoke test still required before mainnet sign-off. * Add `aqua sideswap` CLI subcommand group Mirrors the SideSwap MCP tool surface as Click commands so users can drive pegs and atomic asset swaps from a shell without spinning up the MCP server. The security-critical layer (PSET verification, fee_asset pinning) lives in the manager and is unchanged here; this PR is just argument parsing + output, following the patterns already established by `aqua liquid`, `aqua btc`, and `aqua lightning`. Subcommands ----------- ``` aqua sideswap status [--network …] # server_status aqua sideswap recommend --amount … --direction … # peg vs swap helper aqua sideswap peg-quote --amount … [--peg-out] # fee preview aqua sideswap peg-in [--wallet-name …] # returns BTC deposit aqua sideswap peg-out --amount … --btc-address … # broadcasts L-BTC aqua sideswap peg-status --order-id … # poll aqua sideswap assets [--network …] aqua sideswap quote --asset-ticker … --send-amount … [--reverse] aqua sideswap swap --asset-ticker … --amount … [--reverse] [--yes] aqua sideswap swap-status --order-id … ``` Notable details: - `swap` fetches a fresh quote and prompts for explicit confirmation before signing; pass `--yes` to skip the prompt for scripted use - Password resolution follows the same pattern as the rest of the CLI: `--password-stdin` flag → `AQUA_PASSWORD` env var → no password - Asset ID can be specified by hex (`--asset-id`) or by ticker (`--asset-ticker`, e.g. USDt) — same as `aqua liquid send-asset` - The `swap` subcommand resolves tickers against the wallet's network so testnet aliases work correctly - Output formatting (JSON vs pretty) and error handling reuse the existing `run_tool` / `render` infrastructure Tests (23 new, 416 total passing) --------------------------------- The CLI tests inject fake `SideSwapPegManager` and `SideSwapSwapManager` into the global tool layer so we exercise argument parsing, output shape, and proper invocation of the manager methods without ever touching the network. Coverage: - server status: default and explicit network - recommend: forward direction, reverse direction, bad direction errors - peg-quote: default is peg-in, --peg-out switches direction - peg-in: returns deposit address, passes wallet_name through - peg-out: returns lockup_txid, validates positive amount, requires btc-address - peg-status: passes order_id through - assets: uses fetch_assets (patched directly) - quote: requires exactly one of send/recv amount, exactly one of id/ticker; resolves ticker to asset_id; unknown ticker errors - swap: --yes skips prompt; --reverse flips send_bitcoins; rejects amount=0 - swap-status: passes order_id through * Add SIDESWAP_API_KEY to login in SideSwapClient * Address PR #32 review feedback - Drop unused `from ..wallet import WalletManager` in cli/sideswap.py. - Replace the hardcoded "the asset" label in the swap-confirmation prompt with the real ticker from `lookup_asset` (or the user's --asset-ticker override, or a truncated id if unknown). - Use `click.ClickException` for quote-fetch network failures instead of `click.UsageError` — network problems aren't user input errors. - Document SideSwap Peg and Swap file structures in AGENTS.md (placed next to the Lightning swap structure). Plus: fix the preview vs execution drift the reviewer raised. The CLI's preview goes through `subscribe_price_stream` while the executor uses the mkt::* path, so the rate the user confirms can differ from the rate the PSET locks in. Added a `min_recv_amount` floor to `sideswap_execute_swap` (plumbed from CLI → tools → manager). The CLI passes the recv_amount the user just confirmed; if the dealer's mkt::* quote comes back below it the swap is rejected before signing. Default None preserves existing behavior for non-interactive callers. Drive-by: fixed `peg-quote --peg-out` flag (the `flag_value=False, default=True` combo wasn't producing the documented default; switched to a plain `is_flag=True` and inverted in the body). This also unbroke `tests/test_cli.py::TestSideSwapPegQuote::test_peg_quote_default_is_peg_in`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Gonzalo Coelho <coelhogonza@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * :broom: Code clean-up * :book: Update websockets dependency in uv.lock --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Gonzalo Coelho <coelhogonza@gmail.com> * Add SideShift.ai cross-chain swap integration (#33) * Add SideShift.ai cross-chain swap integration Adds cross-chain swap support via sideshift.ai. Complementary to (not redundant with) the SideSwap stack in earlier PRs: - SideSwap: atomic on Liquid (PSET) + Liquid Federation pegs for BTC ↔ L-BTC. Trustless / federation. Liquid-only universe. - SideShift: 30+ chains, 200+ assets. Use when at least one leg is on a non-Liquid chain (Ethereum, Tron, Solana, USDt-on-other-chains). The killer flow this unlocks is USDt-on-expensive-chains ↔ USDt-Liquid: get users out of high-fee networks into cheap Liquid in one tool call. What's in this PR ----------------- - `src/aqua/sideshift.py` — REST client (stdlib urllib, like Boltz/Ankara), data classes (`SideShiftCoin`, `SideShiftPairInfo`, `SideShiftQuote`, `SideShiftShift`), high-level `SideShiftManager` orchestrating send / receive / status flows, plus `recommend_shift_or_swap` to help agents pick between SideSwap and SideShift. - 7 new MCP tools (`sideshift_*` prefix): sideshift_list_coins / sideshift_pair_info / sideshift_quote sideshift_send / sideshift_receive / sideshift_status sideshift_recommend - 2 new prompts: `cross_chain_send`, `cross_chain_receive`. - New CLI subcommand group: `aqua sideshift {coins,pair-info,quote,send, receive,status,recommend}`. Mirrors the MCP tool surface; `send` shows a fresh quote and prompts for confirmation by default (--yes to skip). - Storage at ~/.aqua/sideshift_shifts/{shift_id}.json, mode 0o600, atomic writes; persisted before broadcast on send so a stuck shift is recoverable. Affiliate --------- Affiliate ID is `PVmPh4Mp3` — the same one AQUA Flutter wallet ships with (publicly committed in their `lib/config/constants/api_keys.dart`). Commission accrues to JAN3's SideShift account. Pass an empty string to `SideShiftClient(affiliate_id="")` to disable. Wire-format quirks documented in the module + AGENTS.md: - L-BTC is `coin: "BTC", network: "liquid"` (not lbtc-liquid) - Coin tickers uppercase, networks lowercase on the wire - All amounts are decimal strings; manager converts to integer sats before calling our wallet send methods - Memo networks (TON, Stellar, BNB Beacon) require a memo on the deposit; the manager surfaces it via the persisted shift record Tests (85 new, 400 passing total) --------------------------------- Module tests (test_sideshift.py): - Decimal → sats conversion (5) - Status helpers + state machine (5) - Recommendation (SideSwap vs SideShift) for various pairs (7) - SideShiftShift dataclass + storage round-trip + 0o600 perms (5) - REST client: affiliate id resolution, get_coins, get_pair URL building, request_quote argument validation, create_fixed_shift body shape, create_variable_shift body shape, get_shift, error extraction, unreachable host (11) - Manager send: happy path L-BTC→USDt-Tron, USDt-Liquid asset_id, BTC→external uses BDK manager, rejects non-native deposit chain, rejects unknown wallet, persists before broadcast so failure is recoverable (6) - Manager receive: into Liquid, into Bitcoin, rejects non-native settle (3) - Manager status: refreshes record, unknown raises, warns on remote error (3) CLI tests (added to test_cli.py): 13 covering all 7 subcommands, fake manager injection, argument validation. Tool registry test updated for the 7 new tools. Manual testnet validation still required before mainnet sign-off. * Drop SIDESHIFT_AFFILIATE_ID env override, hardcode AFFILIATE_ID The env-var override added complexity for a knob nobody is going to turn — the affiliate ID is the same for every install, public, and already overridable per-call via SideShiftClient(affiliate_id=...). Rename DEFAULT_AFFILIATE_ID → AFFILIATE_ID since there is no longer a non-default to fall back from. * Deduplicate AGENTS.md SideShift sections The two sections covered the same trust model and memo network warnings. Keep the user-facing section (under Tools) as the canonical source for those, scope the lower section to integration/wire-format detail with a pointer up. * Address PR #33 review feedback - Hoist L-BTC asset id to assets.py (LBTC_ASSET_ID); drop the duplicate _LIQUID_LBTC_POLICY_ASSET constant in sideshift.py. - Drop unused SideShiftClient.set_refund_address and the LIQUID_ASSET_TO_SIDESHIFT_COIN dict (dead code). - "review" is no longer treated as a final SideShift status — per their docs it's a risk-management hold that can still resolve to settled or refunded. - On deposit broadcast failure, always set shift.status = "failed" rather than preserving the prior server status. - Thread quote_id through sideshift_send → SideShiftManager.send_shift so the CLI can pass the just-confirmed preview["id"] and the shift executes at the rate the user actually saw. Without quote_id the manager fetches a fresh quote (existing behavior), which is fine for non-interactive callers but can drift from any earlier preview. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🧹 Code clean-up * Add 'failed' status to _FAILED_STATUSES --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Gonzalo Coelho <coelhogonza@gmail.com> * Add Changelly USDt cross-chain swap integration (#34) * Add Changelly USDt cross-chain swap integration Adds USDt cross-chain swap support via Changelly, routed through AQUA's Ankara backend proxy at https://ankara.aquabtc.com/api/v1/changelly. Scope (mirrors AQUA Flutter's `ChangellyAssetIds` set in lib/features/changelly/models/changelly_models.dart): USDt-Liquid ↔ USDt on the same 6 external chains we allow in SideShift — Ethereum, Tron, BSC, Solana, Polygon, TON. 6 chains × 2 directions = 12 ordered pairs. Override `CHANGELLY_ALLOW_ALL_PAIRS=1` to bypass for testing or power use. What's in this PR ----------------- - `src/aqua/changelly.py` — REST client, dataclasses (`ChangellySwap`, `ChangellyQuote`), high-level `ChangellyManager` orchestrating send / receive / status, plus pair-allowlist enforcement and status helpers. - 5 new MCP tools (`changelly_*` prefix): changelly_list_currencies / changelly_quote changelly_send / changelly_receive / changelly_status - 2 new prompts: `usdt_cross_chain_send`, `usdt_cross_chain_receive`. - New CLI subcommand group: `aqua changelly {currencies,quote,send, receive,status}`. `send` shows a fresh quote and prompts for confirmation by default (--yes to skip). - Storage at ~/.aqua/changelly_swaps/{order_id}.json, mode 0o600, atomic writes; persisted before broadcast on send so a stuck order is recoverable. Tests (62 new, 389 passing total) --------------------------------- Module tests (test_changelly.py): - Decimal → sats conversion (4) - Status helpers + state machine (8) - Network → asset id mapping (9) - Allowlist contract + override semantics (16) - ChangellySwap dataclass + storage round-trip + 0o600 perms (5) - REST client: base URL handling, get_currencies (wrapped + bare), get_pairs lowercasing, fixed/variable quote bodies, transaction creation, status parsing, error extraction, unreachable host (12) - Manager send: happy path, unknown network, unknown wallet, persists before broadcast so failure is recoverable (4) - Manager receive: deposit address, unknown network (2) - Manager status: refreshes record, unknown raises, warns on remote error (3) CLI tests (added to test_cli.py): 13 covering all 5 subcommands, fake manager injection, argument validation. Tool registry test updated for the 5 new tools. Manual testnet validation still required before mainnet sign-off. * Add suggestions to Changelly PR (#35) * 🧹 Fix minor issues and added extra validations --------- Co-authored-by: Gonzalo Coelho <34550772+coelhogonzalo@users.noreply.github.com> Co-authored-by: Gonzalo Coelho <coelhogonza@gmail.com> * Add Pix → DePix on-ramp (Eulen API) (#36) * Add Pix → DePix on-ramp via Eulen API Adds a Brazilian Real on-ramp: agents mint a Pix charge with pix_receive(amount_cents, wallet_name); the user pays it from their banking app's "Pix Copia e Cola" field; Eulen credits DePix to the wallet's next address. Status polling via pix_status — no claim step, since Eulen pushes DePix automatically once the Pix payment settles. The flow mirrors the existing Ankara Lightning-receive architecture: new src/aqua/pix.py module (EulenClient + PixSwap + PixManager), ~/.aqua/pix_swaps/{id}.json persistence with 0o600 permissions, and two MCP tools (pix_receive, pix_status) plus a receive_via_pix prompt. DePix is already in the asset registry. Configuration: EULEN_API_TOKEN (required, from https://depix.info/#partners) and EULEN_API_URL (defaults to https://depix.eulen.app/api). Amount is in BRL cents (100 = R$1.00) — tool description and prompt warn loudly to prevent 100× errors. 30 new tests (tests/test_pix.py); full suite: 343 passed, 2 skipped. * 🧹 Minor fixes * 🧹 Code clean-up and fixes for minor issues --------- Co-authored-by: Gonzalo Coelho <coelhogonza@gmail.com> * Add Lightning Address (LUD-16) support to lightning_send (#37) * Add Lightning Address (LUD-16) support to lightning_send `lightning_send` now accepts either a BOLT11 invoice or a Lightning Address (user@domain). LN addresses are resolved via the LUD-16 well-known endpoint to a BOLT11 invoice, then paid through the existing Boltz submarine-swap flow — identical architecture to the AQUA Flutter wallet, which also routes LN-address payments via Boltz. Validation mirrors AQUA Flutter (lnurl_provider.dart): cross-check the returned BOLT11 amount against the requested amount and reject zero-amount invoices. LUD-06 description-hash verification is intentionally not enforced, matching the Flutter wallet's pragmatic compatibility behaviour. A new `amount_sats` parameter is required for LN addresses and optional for BOLT11 (must match the encoded amount when supplied). * 🧹 Code clean-up + ln amount validation * 🧹 Adjust docs and ValueError msg for Lightning Address --------- Co-authored-by: Gonzalo Coelho <coelhogonza@gmail.com> * Add AQUA ASCII banner to CLI --help and bump to 0.4.0 (#38) * banner in cli * Update banner.py * update version * Update main.py * Add agentic-aqua console script alias for uvx compatibility (#57) Adds `agentic-aqua = "aqua.server:main"` to [project.scripts] so that `uvx agentic-aqua` works as documented in the README. Previously the package only exposed `aqua` and `aqua-mcp`, causing the recommended MCP server setup to fail. * #22 Use fallback BTC explorer when Esplora connection drops (#58) * Add fallback * Update test_bitcoin.py * Limit broadcast fallback to sync_wallet only send() was using _with_client_fallback for broadcast, which could surface a spurious failure when the first Esplora accepted the tx but dropped the connection before returning a response — the second client would then return "already known" and the caller would see an error despite a successful broadcast. Broadcast now uses the primary client only with transient-error retry. _with_client_fallback is retained for sync_wallet where cross-client fallback is safe and valuable (idempotent scan, expensive to restart). * Fix on non-transient * Address issues in the develop branch (#45) * Fix issues on develop branch * 🧹🧪 Test no longer mocks lightinng manager * fix: mock Liquid sync_wallet in unified balance tests (#46) Two tests in TestUnifiedImportAndBalance were failing in network-restricted environments because the Liquid manager's sync_wallet was not being mocked, causing a live Electrum connection attempt. - Added patch.object(get_manager(), 'sync_wallet') alongside the existing BTC patch in test_unified_balance_aggregates_both_networks - Wrapped unified_balance() call in test_unified_balance_wallet_without_btc_descriptors with patch.object(manager, 'sync_wallet') - Added get_manager to imports Co-authored-by: Nathan Leniski <natel@Nates-MacBook-Pro.local> * fix: cast AnyUrl to str in read_resource() to fix silent documentation failure (#47) * fix: cast AnyUrl to str in read_resource() to fix silent documentation failure The MCP framework passes uri as a Pydantic AnyUrl object, not a plain str. Comparing AnyUrl == str always returns False, making all three documentation resources (quickstart, networks, security) silently unreachable via MCP. - Added uri = str(uri) as first line of read_resource() - Added pragma: no cover to stdio entry points (run_server, main, __main__) * fix: cast AnyUrl to str in read_resource() to fix silent documentation failure The MCP framework passes uri as a Pydantic AnyUrl object, not a plain str. Comparing AnyUrl == str always returns False, making all three documentation resources (quickstart, networks, security) silently unreachable via MCP. - Added uri = str(uri) as first line of read_resource() - Added pragma: no cover to stdio entry points (run_server, main, __main__) --------- Co-authored-by: Nathan Leniski <natel@Nates-MacBook-Pro.local> * progresive disclosure (#59) * #54 Changelly fee deducted from received amount — inconsistent with APK version (#62) * progresive disclosure * feat(changelly): add --amount-to for fee-from-sender semantics Closes #54. The APK takes Changelly fees from the sender; the CLI was taking them from the recipient. Adds `--amount-to` as a mutually exclusive alternative to `--amount-from` in both `changelly send` and `changelly receive`. When provided, the user specifies the amount the recipient should receive and the wallet covers the fees on top. Threaded through CLI, tools, the manager (send_swap / receive_swap), and the MCP server tool schema. Backward compatible: `--amount-from` continues to work unchanged. * #50 sideshift send: requires --liquid-asset-id manually for non-L-BTC assets (#60) * progresive disclosure * fix(sideshift): auto-resolve liquid_asset_id from ticker Closes #50. When `aqua sideshift send --deposit-network liquid` is invoked with a non-BTC coin and no explicit `--liquid-asset-id`, the CLI now looks the asset id up in the known-asset registry via `lookup_asset_by_ticker`. Unknown tickers raise a clear `click.UsageError` instead of a cryptic `ValueError` from the manager layer. Explicit `--liquid-asset-id` continues to take precedence. * #55 expose flexible_small_amount as --flexible flag in sideswap swap CLI (#61) * progresive disclosure * feat(sideswap): expose --flexible flag on swap command Closes #55. Adds a `--flexible` flag to `aqua sideswap swap` that maps to `flexible_small_amount=True` in the underlying executor. Required for small swaps (<25k sats) where SideSwap's dealer rounds the send amount and the strict check rejects every quote. Default remains False to preserve the safer strict-equality behavior on larger amounts. * Add ln-address support to lightning CLI * Document develop as default PR target * Add CI test workflow (#63) * Create tests.yml * Update .gitignore * tuna (#64) * v1 mcp name flaged * Pin Python 3.13 for uv installs (#71) * Pin Python 3.13 for uv installs * Restore uv exclude-newer guardrail * Create CONFIG.md * #78 fix Pix/DePix smoke-test findings (nonce format, approved status, error detail, flat fee disclosure) (#79) - Send X-Nonce as dashed UUID (str(uuid.uuid4())); Eulen rejected hex form with HTTP 400. - Recognize Eulen intermediate status "approved" (Pix received, DePix in flight) instead of warning "unknown status". - Extract error detail from Eulen's wrapped {"response": {"errorMessage": ...}} on 4xx so root cause surfaces. - Surface Eulen's flat R$0,99 per-operation fee in pix_receive (fee_cents, fee_brl, net_amount_cents, net_amount_brl + message); MCP tool description and receive_via_pix prompt updated accordingly. - Tests: update nonce regex to dashed UUID; add coverage for response.errorMessage extraction, approved-status persistence, and fee/net reporting in the tool layer. * Filter SideShift and Changelly list endpoints to USDt allowlist (#80) `sideshift_list_coins` returned ~100 KB / 4500 lines, exceeding the MCP token limit and forcing the agent to read the result from a temp file. `changelly_list_currencies` returned 764 unrelated tickers that confuse the agent since our surface only supports USDt cross-chain swaps. Filter both responses at the manager layer using the existing allowlists so the agent only ever sees pairs we actually support: - SideShift: USDT across {ethereum, tron, bsc, solana, polygon, ton, liquid} plus mainchain BTC. `tokenDetails` and `networksWithMemo` are pruned to the kept networks. Bypass with SIDESHIFT_ALLOW_ALL_NETWORKS=1. - Changelly: `lusdt` + the 6 external USDt variants. Bypass with CHANGELLY_ALLOW_ALL_PAIRS=1. * Add swap integrations and manual smoke test prompts (#81) * progresive disclosure * New smokes * update prompts * Update prompt_test_send_real_changelly.md * Update prompt_test_send_real_sideshift.md * Update prompt_test_send_real_sideswap.md * Update README.md * Update prompt_test_real_pix.md * Update prompt_test_read_only_integrations.md * Disable SideSwap and Depix (PIX) tools by default (#82) * Disable * Update README.md * Update commands.py * Update test_cli.py * Update test_cli_read_only.py * Fix lightning send test to neutralize AQUA_PASSWORD from .env (#85) test_send_ln_address_passes_amount_sats_to_tool asserted password=None but the CLI loads repo .env at import, so AQUA_PASSWORD set there (or in the user shell) leaked into resolve_secret and the test failed locally with password='test'. Pass env=_cli_env() like the other runner.invoke calls in this file to neutralize the loaded env. --------- Co-authored-by: Samson <33304104+samsonmow@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Gonzalo Coelho <coelhogonza@gmail.com> Co-authored-by: Gonzalo Coelho <34550772+coelhogonzalo@users.noreply.github.com> Co-authored-by: Nate L <152243278+marinate305@users.noreply.github.com> Co-authored-by: Nathan Leniski <natel@Nates-MacBook-Pro.local> Co-authored-by: Rocket <rocket@tanduwebs.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Purpose
Gate SideSwap and Depix (PIX) MCP tools / CLI commands behind a manual opt-in by shipping them disabled-by-default via the existing feature-flag system. Users can re-enable any of them in
~/.aqua/config.jsonunderenabled_tools.Main Changes
_SHIPPED_DISABLEDfrozenset infeatures.pylisting the 10 SideSwap tools and 2 PIX tools to ship asFalseinSHIPPED_DEFAULTS_ENABLED_TOOLSassertto catch typos/renames in_SHIPPED_DISABLEDat import timetest_sideswap_and_pix_disabled_by_defaultregression test to lock in the disabled-by-default contractChecklist