Skip to main content

CLI multi-currency hot wallet with non-custodial cross-chain swaps via THORChain

Project description

What is this

It's a Python library and a CLI for holding, sending, receiving and swapping multiple cryptocurrencies.

Non-custodial cross-chain swaps are supported via THORChain and Maya.

⚠️ This project is vibed-up ... what could possibly go wrong?

Don't use this wallet for more funds than what you can afford to lose. Bugs in the code may easily cause irreversible loss of funds. Even if all the code is perfect, consider that this is a hot wallet, an attacker that gains a foothold on the computer running this wallet software may potentially manage to drain the funds in the wallet.

The rest of this document is partially AI-generated.

Installation

make install

This auto-detects uv, pipx, or pip and installs the swapsack binary on your PATH. Then run swapsack --help.

Features

The wallet is still under rapid development as of 2026-07-03. Missing features and currency support will be prioritized by personal need and by issues/PRs received. Here is the "current status" of (partially) supported currencies (✅ = working, ◑ = partial, blank = not yet):

Currency Hold Bal To From Send Sweep Liq
BTC
ETH
USDT-ETH
USDC-ETH
TRX
USDT-TRON
BNB (BSC)
LTC
DOGE
BCH
CACAO
RUNE

Features explained

  • Hold — derive an address, hold a balance, receive funds
  • Bal — show the balance (native, tracked tokens like USDT, and any THORChain/Maya liquidity positions)
  • To — use as a swap destination (for a currency whose address the wallet can't derive yet, give an external one via --dest)
  • From — use as a swap source (the asset you spend). ◑ = the native swap-from for CACAO/RUNE (a Cosmos MsgDeposit, no inbound vault) is implemented + gated + unit-tested but its broadcast is unproven on mainnet — there is no Maya/THORChain testnet wired up
  • Sendsend to an external address (a plain transfer, no swap). ✅ = implemented and tested; ◑ = USDC-ETH rides the same ERC-20 send path as USDT-ETH (only the contract/decimals differ) but isn't separately covered by a test, and the native CACAO/RUNE Cosmos MsgSend is implemented + unit-tested (protobuf byte-exact vs cosmpy, signature verified) but its broadcast is unproven on mainnet — there is no Maya/THORChain testnet wired up
  • Sweep--amount max sends the maximum amount. ✅ = works: UTXO and token sweeps end at 0 (a token's gas is paid in the native coin); native account coins (ETH/TRX) intentionally retain a small gas reserve — the fee is only known at send time, and you want some left to move tokens or swap later, so the wallet warns rather than draining you to 0. ◑ = not yet (native TRX).
  • Liqadd-liquidity and withdraw-liquidity provide/withdraw single-sided liquidity, now including ERC-20 tokens (e.g. USDT-ETH on Maya, via the router). Experimental; see below.

Other features:

  • quote — read-only price preview for any supported asset
  • status — track a swap by its inbound txid
  • --backend auto — compares THORChain + Maya and routes to the best price (quote, swap) for currencies supported by both backends. (Other backends may be considered in the future)
  • swap --tolerance-bps N — raise the slippage/fee tolerance (default 300 = 3%). Small or thinly-traded swaps whose fees exceed the default are refused by THORChain; the wallet aborts with a clear message instead of a traceback, and you can opt into a wider tolerance here.
  • cost breakdownquote and swap itemise what you lose: the slip/swap (liquidity) fee, the flat outbound fee, and the quoted total (with bps), plus the inbound (source-chain) tx fee shown separately. On THORChain the liquidity fee is the slippage — the two are one number, not two.
  • Market: block — by default quote/swap also compare the quoted output against a public spot price (CoinGecko), surfacing the total realised cost including the pool-vs-market spread arbitrageurs capture (which the protocol's own fee fields don't include). Three lines: a source header, the per-asset comparison (~X DEST at spot → ~N bps total vs market), and the estimated absolute loss in EUR. Best-effort: silently dropped if the feed is unreachable or the asset isn't mapped (the EUR line is dropped if the feed has no EUR price). Disable with --no-price-check.
  • Streaming swapsswap/quote --stream-interval N [--stream-quantity M] spreads the trade over blocks (sub-swaps) so each hits the pool smaller, sharply cutting slippage on large or thinly-pooled swaps (e.g. a 0.05 BTC→DASH that's refused at the default tolerance clears at ~20 bps when streamed). N = blocks between sub-swaps; M = number of sub-swaps (omit to let the network pick). Streaming manages slippage itself, so it overrides --tolerance-bps (the memo's limit is set to 0). The tradeoff: the swap settles over more blocks (quote prints the estimated duration), during which your funds are in-flight and exposed to price movement. See docs/streaming.md for the mechanics and the streaming-vs-tolerance interaction.
  • Transaction listings are not supported yet.

Liquidity (experimental). add-liquidity / withdraw-liquidity add or remove single-sided liquidity on a THORChain pool. By adding liquidity one will earn a share of that pool's swap fees, but it's not without risks. As of 2026-06-28 THORChain rejects new liquidity for all assets, probably due to a switch to protocol-owned liquidity (POL). It's still possible to use add-liquidity --backend maya. For bigger amounts, double-sided liquidity should be used rather than single-sided liquidity, but this is not supported yet.

Currency roadmap

It's on the roadmap to support the union of the currency sets supported by the available swapping backends. Support: full = every feature working, partial = some features working, none = planned. Listed in recommended implementation order; see the capability grid above for the per-feature detail.

Currency What it is Family Support Notes
BTC Bitcoin UTXO full
ETH Ethereum EVM partial send done
TRX TRON TRON partial send done
BSC / BNB BNB Smart Chain EVM partial Hold + balance work (native BNB and BEP-20 USDC/USDT, 18-decimal). Swaps blocked: BSC trading halted on THORChain (chain_trading_paused), and Maya has no BSC pools — nothing to swap against until THORChain re-enables it
USDT-ETH Tether ERC-20 token full send + single-sided liquidity (Maya, via router) done
USDT-TRON Tether TRC-20 token partial send done
USDT-BSC Tether BEP-20 token none Blocked: halted on THORChain, not on Maya (Maya has no BSC pools)
USBT-SOL Tether ? none Not currently available on THORChain/Maya
AVAX Avalanche C-Chain EVM none
BASE Base (ETH L2) EVM none
ARB Arbitrum (ETH L2) EVM none Maya-only
USDC USD Coin (ETH/BSC/AVAX/BASE/ARB) ERC-20 token partial ETH done (incl. send, via the shared ERC-20 path); AVAX/BASE/ARB need new EVM chain adapters; BSC additionally blocked by the THORChain halt
LTC Litecoin UTXO partial destination only (via --dest)
DOGE Dogecoin UTXO partial destination only (via --dest)
BCH Bitcoin Cash UTXO partial destination only (via --dest)
DASH Dash UTXO partial Maya-only; destination only (via --dest, --backend maya/auto). Wallet side (hold/bal/send/from) is a legacy-UTXO effort — see docs/dash.md
ZEC Zcash UTXO partial Maya-only; destination only (via --dest, --backend maya/auto). Full wallet side is harder than DASH — Zcash's tx format can't be signed by bitcoinlib; see docs/zcash.md
RUNE THORChain native THORChain partial Hold + balance + destination + send (MsgSend) + swap-from (MsgDeposit) done — reuses the shared Cosmos-SDK adapter (RUNE is 1e8). Spend paths ship unproven on mainnet (no testnet); see docs/cacao.md
CACAO Maya native Maya partial Maya-only; 1e10 decimals (not 1e8). Hold + balance + destination + send (MsgSend) + swap-from (MsgDeposit, no vault) done; single-sided liquidity n/a for the settlement asset (it's the RUNE-leg of symmetric LP, TODO #4). Spend paths ship unproven on mainnet (no Maya testnet); see docs/cacao.md
ATOM Cosmos Hub Cosmos none
XRP XRP Ledger XRP none
SOL Solana Solana none
ADA Cardano Cardano none Maya-only
XMR Monero Monero none Coming soon to THORChain pool; doesn't fit the current model — see docs/monero.md
TCY THORChain reward token THORChain token none niche; low priority
MAYA Maya governance token Maya token none Maya-only; niche; low priority

Usage

swapsack --help                              # subcmd --help also works
swapsack init                                # create encrypted keystore
swapsack add-hd --label main                 # import seed (prompted), or:
swapsack add-hd --label test --generate      # generate a fresh seed
swapsack address                             # BTC / ETH / TRON addresses
swapsack balance                             # balances across chains
swapsack quote --from ETH --to USDT-TRON --amount 0.02
swapsack swap  --from ETH --to BTC --amount max          # DRY RUN (sweep)
swapsack swap  --from BTC --to USDT-TRON --amount 0.001 --confirm
swapsack swap  --from BTC --to DASH --dest X... --stream-interval 1  # streamed, low slip
swapsack send  bc1q...recipient --amount 0.001                 # DRY RUN
swapsack send  bc1q...recipient --amount max --confirm         # sweep + send
swapsack send  0x...recipient --asset ETH --amount 0.01        # native ETH
swapsack send  0x...recipient --asset USDT-ETH --amount max    # sweep tokens
swapsack send  T...recipient --asset USDT-TRON --amount 25     # TRC-20

Defaults are --from BTC --to ETH. --confirm prints the freshly-quoted swap and asks before broadcasting (--yes skips the prompt for automation).

Swaps default to a dry run (build + verify + print); --confirm is required to broadcast, and --yes skips the interactive confirmation prompt. Destination addresses auto-derive from the seed; pass --dest to override.

Config via flags or env: keystore $SWAPSACK_KEYSTORE (~/.config/swapsack/keystore.json), passphrase $SWAPSACK_PASSPHRASE, Esplora $SWAPSACK_ESPLORA, Ethereum RPC $SWAPSACK_ETH_RPC, TRON API $SWAPSACK_TRON_API, BSC RPC $SWAPSACK_BSC_RPC.

Shell tab-completion (via argcomplete) — enable for the current shell, e.g. bash:

eval "$(register-python-argcomplete swapsack)"   # add to ~/.bashrc to persist

zsh and fish work too; see the argcomplete docs.

Related projects

This project started out from a personal need. When asking Claude Opus to search for existing products, it found nothing. Later, when searching for the (temporary) name of this package as well as doing research on possible permanent names, different software appeared on the radar. Here is a comparison:

The CLI / library niche for non-custodial cross-chain swaps appears unoccupied — GUI swap-wallets for phones, web and desktop are plentiful, but the closest Python packages on PyPI do something else entirely:

  • pywallet — a BIP32/HD key-and-address generator (BTC, ETH, LTC, DASH, DOGE, …). No balances, no broadcasting, no network I/O and no swaps; last released 2018. It's a key-derivation helper, not a spendable wallet.
  • multiwallet — a PyQt5 desktop GUI for stateless multisig Bitcoin (airgapped seedpicker + PSBT). Bitcoin-only, cold-storage focused, no swaps; last released 2020.

Neither is multi-chain and swap-capable from a terminal or as a library, which is the gap this project fills.

Name-collision neighbours on GitHub (surveyed 2026-07-08)

Several GitHub projects share a name with this project or live in the same "crypto swap" search space; none turned out to compete in this niche:

  • swaponline/MultiCurrencyWallet — the only substantial one (~540 stars, MIT, TypeScript, still active). A client-side web GUI wallet (BTC, ETH/ERC-20, BSC, Polygon + tokens) with a P2P atomic-swap exchange and a 0x orderbook, aimed at white-label / embedded deployment (WordPress plugin, iframe widgets). Same spirit — non-custodial multi-currency wallet with built-in swapping — but a browser app rather than a library/CLI, and its swaps need a live counterparty on their own orderbook instead of an AMM.
  • MatthewShelby/swap — dead and mostly irrelevant.
  • yoyoemily/crypto-swap — a small Node CLI / LLM-skill wrapper around LightningEX (api.lightningex.io), an instant-exchange service. 0 stars, but the closest in shape — swaps driven from a CLI. It holds no keys, though: it's an API client for a custodial exchange service, not a wallet.
  • ParaSwap-Crypto-Swap (GitHub org, deliberately not linked) — SEO spam impersonating the real ParaSwap (whose actual code lives under VeloraDEX): a lone .github profile repo full of keyword stuffing, with a "GET ParaSwap" button pointing at a third-party github.io page. Avoid. The real ParaSwap/Velora is a same-chain EVM DEX aggregator — see below.

Backend ideas from the survey

  • Same-chain EVM DEX aggregators (ParaSwap/Velora, 1inch, 0x) — THORChain/Maya only trade between their pooled assets, and same-chain token↔token swaps (e.g. USDT-ETH↔USDC-ETH) route needlessly through the pools. A DEX aggregator would cover those with one signed on-chain transaction — fully non-custodial, and it would slot naturally into --backend auto. The most promising candidate.
  • Instant-exchange APIs (LightningEX, ChangeNOW, SideShift, …) — huge coin coverage (hundreds of assets) behind a trivial REST API, but the operator holds your funds mid-swap (custodial in flight, occasionally KYC/AML-frozen), which cuts against this project's non-custodial premise. Only worth adding with loud labeling, if ever.
  • P2P atomic swaps (à la MultiCurrencyWallet) — trust-minimized in theory, but they require a counterparty/orderbook network; there is no liquidity pool to route against.

Development

make dev           # set up the environment (uv)
gmake test          # unit tests (live network tests excluded)
make test-network  # opt-in: read-only integration tests vs live THORChain
make lint          # ruff check + format check

Most network tests are read-only (no funds moved); they guard against THORChain API drift and stale hard-coded asset strings, and run in CI (the Integration (network) workflow, on push/PR and a daily schedule) in addition to the release gate.

One opt-in network test broadcasts a real TRC-20 transfer on TRON's Nile testnet (build → sign → broadcast → confirm → read the memo back on-chain) to exercise the USDT-TRON deposit mechanics end to end. It is skipped unless a funded Nile account is provided via env / CI secrets:

SWAPSACK_NILE_MNEMONIC=...  # Nile account holding the token + some TRX
SWAPSACK_NILE_TOKEN=T...    # a TRC-20 contract (base58) the account holds
SWAPSACK_NILE_RECIPIENT=T...  # optional; defaults to a self-transfer

Two more opt-in tests (tests/test_integration_testnet.py) prove the send spending path end to end on public testnets — build → sign → broadcast → confirm a real (valueless) transfer, defaulting to a self-send. They skip unless a funded testnet account our wallet derives is provided. The funding addresses (and faucets) are documented in docs/testnet.md; the seeds live only in CI secrets:

# BTC signet (sweeps the wallet's signet UTXOs to itself; testnet3 is deprecated)
SWAPSACK_BTC_TESTNET_MNEMONIC=...    # a funded account
SWAPSACK_BTC_TESTNET_NETWORK=...     # optional; "signet" (default) / "testnet"
SWAPSACK_BTC_TESTNET_ESPLORA=...     # optional; defaults to blockstream <network>
SWAPSACK_BTC_TESTNET_RECIPIENT=tb1.. # optional; defaults to a self-send

# ETH Sepolia (self-sends 0.001 ETH, chain id 11155111)
SWAPSACK_ETH_SEPOLIA_MNEMONIC=...    # a funded Sepolia account
SWAPSACK_ETH_SEPOLIA_RPC=...         # optional; defaults to a public Sepolia RPC
SWAPSACK_ETH_SEPOLIA_RECIPIENT=0x..  # optional; defaults to a self-send

Releasing

Versioning is automatic from git tags (hatch-vcs). Pushing a v* tag triggers .github/workflows/publish.yml, which runs lint + the full test suite including the live integration tests (pytest -m network) and only then builds and publishes to PyPI via trusted publishing — so a THORChain outage blocks a release. (Configure trusted publishing once at pypi.org.) pre-commit runs ruff plus a Conventional-Commits message check on commit, and the unit tests plus a lychee link check on push. Run make dev once to install the hooks.

Refreshing test fixtures

The fixtures in tests/ are trimmed real responses from the THORChain REST API:

curl -s "https://thornode.thorchain.network/thorchain/quote/swap?from_asset=BTC.BTC&to_asset=ETH.ETH&amount=178100"
curl -s "https://thornode.thorchain.network/thorchain/inbound_addresses"

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

swapsack-0.1.0.tar.gz (255.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

swapsack-0.1.0-py3-none-any.whl (87.2 kB view details)

Uploaded Python 3

File details

Details for the file swapsack-0.1.0.tar.gz.

File metadata

  • Download URL: swapsack-0.1.0.tar.gz
  • Upload date:
  • Size: 255.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for swapsack-0.1.0.tar.gz
Algorithm Hash digest
SHA256 3c7730893dc238755f0b9cd9b300ae8af6d3e213b0eecd48b174ade6a5f09d73
MD5 1d24a0fccb2bdd97339f9cf04a0d444e
BLAKE2b-256 e17c815c278f08985e0ca05da7a0fa3751a1230103a6eeeed5689e84ec638b64

See more details on using hashes here.

Provenance

The following attestation bundles were made for swapsack-0.1.0.tar.gz:

Publisher: publish.yml on tobixen/swapsack

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file swapsack-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: swapsack-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 87.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for swapsack-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c8a6bde2ab8ddb1f2bcbde8da88f5d71d93faa2f517b2b240c5766af971e5fc4
MD5 e4ead3e9a6dde7f4b66a7f0bccbe40f0
BLAKE2b-256 bd9f16ea71226ff460eb7315416685703b578b71bc5151ee4f65e1716df8ec0f

See more details on using hashes here.

Provenance

The following attestation bundles were made for swapsack-0.1.0-py3-none-any.whl:

Publisher: publish.yml on tobixen/swapsack

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page