Skip to content

Commit 51a6b0b

Browse files
andycreed0xsamsonmowclaudecoelhogonzalomarinate305
authored
Release develop → main: swap integrations, feature flags, Pix, Lightning 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>
1 parent c9a3852 commit 51a6b0b

54 files changed

Lines changed: 15909 additions & 701 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/tests.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Tests
2+
3+
on:
4+
pull_request:
5+
branches: [main, develop]
6+
7+
jobs:
8+
test:
9+
name: Run tests
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
14+
- name: Install uv
15+
uses: astral-sh/setup-uv@v4
16+
with:
17+
version: "latest"
18+
19+
- name: Set up Python
20+
uses: actions/setup-python@v5
21+
with:
22+
python-version: "3.13"
23+
24+
- name: Install dependencies
25+
run: uv sync --all-extras
26+
27+
- name: Run tests
28+
run: uv run pytest

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ venv/
2525
ENV/
2626
env/
2727
.venv/
28+
.github/worktree/
2829

2930
# IDE
3031
.idea/

0 commit comments

Comments
 (0)