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, Maya and Chainflip; same-chain ETH-token swaps additionally route through CoW Protocol's keyless intent API.
⚠️ 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-10. 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 | ✅ | ||||||
| ATOM | ✅ | ||||||
| XRP | ✅ | ||||||
| ADA | ◑ | ||||||
| ETH-ARB | ✅ | ✅ | ✅ | ◑ | ◑ | ◑ | ◑ |
| USDC-ARB | ✅ | ✅ | ✅ | ◑ | ◑ | ◑ | ◑ |
| USDC-AVAX | ✅ | ||||||
| DASH | ✅ | ✅ | ✅ | ◑ | ◑ | ◑ | ◑ |
| ZEC | ✅ | ✅ | ✅ | ◑ | ◑ | ◑ | ◑ |
| CACAO | ✅ | ✅ | ✅ | ◑ | ◑ | ||
| RUNE | ✅ | ✅ | ✅ | ◑ | ◑ |
Features explained
- Hold — derive an
address, hold a balance, receive funds - Bal — show the
balance: one aligned sheet of native coins, tracked tokens like USDT, and any THORChain/Maya liquidity positions, each row valued and totalled (see below) - To — use as a
swapdestination (for a currency whose address the wallet can't derive yet, give an external one via--dest) - From — use as a
swapsource (the asset you spend). ◑ = the native swap-from for CACAO/RUNE (a CosmosMsgDeposit, no inbound vault) is implemented + gated + unit-tested, and the underlyingMsgDepositbuild/sign/broadcast is now mainnet-proven on Maya (the symmetric LP leg of docs/live-session-2026-08-16.md) — but the swap variant's own memo/destination binding has not itself broadcast, and nothing RUNE has; there is no Maya/THORChain testnet wired up. The same ◑ covers ARB (implemented, never broadcast) and DASH and ZEC (vault + OP_RETURN memo, Maya-only, no testnet; ZEC over its bespoke signer) - Send —
sendto 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; the native CACAO/RUNE CosmosMsgSendis 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; and the DASH legacy send shares the BTC build/gate/sign path and is unit-tested (signatures verified) but its broadcast is likewise unproven on mainnet (no Dash testnet — an opt-in mainnet self-sweep test exists, see docs/testnet.md); ZEC rides a bespoke v4/ZIP-243 signer (bitcoinlib can't sign Zcash) whose sighash is verified against a real mainnet transaction's signature, with the same unproven-broadcast caveat - Sweep —
--amount maxsends 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. ◑ = DASH/ZEC sweeps end at 0 like BTC but ride the mainnet-unproven broadcasts above. Blank = not yet (native TRX). - Liq —
add-liquidityandwithdraw-liquidityprovide/withdraw liquidity, single-sided by default and two-sided with--symmetric(EVM assets only, pairing with your own RUNE/CACAO), now including ERC-20 tokens (e.g. USDT-ETH on Maya, via the router). ◑ = DASH/ZEC LP is Maya-only (--backend maya, pairs with CACAO) and rides their mainnet-unproven broadcasts; ARB LP is likewise Maya-only and mainnet-unproven. Experimental; see below.
Other features:
quote— read-only price preview for any supported assetstatus— track a swap by its inbound txid. For a BTC hash it also prints what the transaction did on-chain (inputs, each output, change, fee in sats and EUR) — so a plainsend, which no swap vault ever observes, still shows something useful instead of an empty stage list.--backend auto— compares THORChain + Maya + CoW + Chainflip (CoW only quotes same-chain ETH-token pairs) and routes to the best price (quote,swap).--backend cowforces it: a same-chain USDT-ETH/USDC-ETH/ETH swap settles via a signed EIP-712 order (no vault, no memo) instead of THORChain/Maya's two-pool-leg route — see docs/backends.md.status <order-uid>tracks a submitted CoW order (auto-detected by its 56-byte uid shape, vs. a chain txid).--backend chainflip— an independent cross-chain protocol, so it keeps working when THORChain and Maya halt together (as they did on 2026-08-18 — see docs/halt-alternatives.md). Swapping from BTC settles as a vault swap: one ordinary Bitcoin transaction paying a protocol vault, with the destination and an on-chain minimum-output floor encoded in its OP_RETURN — no broker, no deposit channel, and nothing registered on your behalf. The gate decodes those bytes itself before signing and checks they pay your address, clear your floor, and carry no broker/boost/affiliate fee; the deposit address must appear in the vault list the chain publishes. Destinations are the EVM assets the gate can re-derive an address for (ETH/ARB native and USDC/USDT); other sources and destinations quote but don't execute there, andautoroutes around them while saying so when they were the cheaper route.--amount maxcan't be a vault swap — Chainflip needs a change output above dust to refund to. See docs/chainflip-effort.md.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 breakdown —
quoteandswapitemise what you lose: the slip/swap (liquidity) fee, the flat outbound fee, and the quoted total (withbps), 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 defaultquote/swapalso 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, which is accepted by every command that prices anything (quote,swap,send,add-liquidity,withdraw-liquidity,status,balance) and suppresses the request itself, not merely its output — the lookup would otherwise tell a third party that you are about to spend that asset.- Streaming swaps —
swap/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 (quoteprints 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.
The balance sheet. balance prints one aligned table once every chain has
answered (progress goes to stderr, so the table on stdout stays a table):
BTC 0.00000000 €0.00 2 used addresses
+LP maya BTC.BTC ~0.00162822 €159.57 deposited ~0.00162545; 0.00001600 via CACAO
ETH 0.05606159 €173.79 0x…
USDC-ETH 40.94348000 €35.21
spendable €217.60
liquidity €10879.47 not spendable; gross of exit fees
total €11097.07
zero: USDT-ETH, ETH-ARB, USDC-ARB, BNB, DASH, RUNE
- Liquidity is totalled apart from spendable funds. An LP position is not
liquid and its redeemable figure is gross of exit fees, so it is never folded
into one number that reads like cash. A
~marks an amount that includes the RUNE/CACAO side repriced at the current pool rate. - A row that cannot be priced is named, never counted as zero — a total that quietly omits something is worse than no total.
- Rows worth nothing collapse into the trailing
zero:line, so nothing vanishes without being named.--zerosgives each its own row. --unitdenominates the value column and the total (EURUSDUSDTUSDCBTCETHSATS; the dollar stablecoins price in USD, as CoinGecko has no stablecoin rate).--no-price-checkmakes no price request at all — one lookup would otherwise tell a third party every asset this wallet holds — and prints the amounts alone. A price feed that fails costs you the value column and nothing else.
Liquidity (experimental). add-liquidity / withdraw-liquidity add or
remove 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 (symmetric) liquidity is preferable to
single-sided: it enters at the current pool ratio and so takes no entry
slip. add-liquidity --symmetric does this, currently for EVM assets
(Ethereum and Arbitrum) — see below for what it costs you in exchange.
Symmetric liquidity (two-sided)
swapsack add-liquidity --asset USDC-ETH --amount 100 --symmetric --backend maya
A symmetric add is two linked deposits: the asset leg goes to the inbound
vault with memo +:POOL:<your maya1/thor1 address>, and the protocol leg is a
native RUNE/CACAO MsgDeposit with memo +:POOL:<your asset-chain address>.
The protocol pairs them by matching each memo's referenced address against the
other leg's observed sender. You supply --amount for the asset side; the
RUNE/CACAO amount is computed from the live pool ratio and must already be in
your own wallet (swap --to CACAO --dest maya1… is how you get it).
What you get and what it costs:
- No entry slip — you enter at the pool's current ratio instead of making the pool rebalance around a one-sided deposit.
- It does not reduce your RUNE/CACAO exposure. A single-sided add ends up ~50% exposed to the settlement asset anyway once the pool rebalances. On a stablecoin pool that means half a nominally dollar-stable position is a small-cap protocol token either way — go in knowing that.
- Two irreversible transactions on two chains. Both legs are built and gated before either is broadcast, and the CLI refuses to send anything if either gate fails. The protocol (cheap, fast, native) leg goes first, so a failure there leaves nothing live. If the asset leg then fails, the position is genuinely half-added and the CLI says so loudly, with the live txid — wait for the protocol to refund the unpaired leg before retrying.
- EVM assets only (Ethereum and Arbitrum). The pairing depends on the asset
leg having one unambiguous sender, which an account-model chain has and a UTXO
transaction does not (the
vin[0]convention is an assumption no testnet exists to verify).--amount maxis refused for the same reason the amount must be definite: the pair leg is derived from it. - THORChain has LP deposits globally paused (
PAUSELP), so symmetric works on Maya (asset + CACAO) today; the CLI aborts with the mimir key if you aim it at a paused pool. On RUNE the protocol leg is unproven — no THORChain native transaction has ever been broadcast. - Exiting works the same way you got in — from the protocol side.
withdraw-liquiditylooks up which kind of position you hold and, for a symmetric one, triggers the withdraw with a dustMsgDepositon Maya/THORChain instead of a transaction on the asset chain (that is where the protocol files the position, so an asset-chain trigger would find nothing). Nothing to pass:swapsack withdraw-liquidity --asset USDC-ETH --backend mayapicks the right one. Both sides come back proportionally — the asset to your asset address, the CACAO to yourmaya1….
See docs/liquidity-symmetric.md for the mechanics and the full safety protocol.
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 | full | |
| TRX | TRON | TRON | partial | send done; sweep pending |
| 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) |
| USDT-SOL | Tether | SPL token | none | Not currently available on THORChain/Maya |
| AVAX | Avalanche C-Chain | EVM | partial | USDC-AVAX is a destination via --dest; native AVAX is not exposed yet. C-Chain (0x…, EIP-55 checked) only — an X-/P-Chain X-avax1… address is refused, since a payout could never credit it |
| BASE | Base (ETH L2) | EVM | none | Blocked: THORChain's BASE.USDC and BASE.ETH pools are Available but trading-halted (checked 2026-08-16), the same shape as the BSC block. Revisit when the halt lifts |
| ARB | Arbitrum (ETH L2) | EVM | partial | Maya-only. Every feature is wired: hold, balance, destination (auto-derived — it is your ETH address), send/sweep, swap-from and liquidity, single- and two-sided. ETH-ARB is native ETH on Arbitrum; the ARB token pool is Staged, not tradeable. USDC-ARB is Circle's native USDC (0xaf88d065…), not the bridged USDC.e. Partial because the spend paths ship mainnet-unproven. Mind the depth: Maya's ARB.USDC pool held ~8.9k USDC on 2026-08-16, so anything above ~€100 slips hard — the Market: line shows it |
| USDC | USD Coin (ETH/BSC/AVAX/BASE/ARB) | ERC-20 token | partial | ETH done (incl. send, via the shared ERC-20 path). AVAX and ARB are destinations (--dest) — receiving needs no adapter; holding or spending them still does. BASE and BSC are blocked by THORChain halts |
| 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) |
| ADA | Cardano | Cardano | partial | Maya-only; destination only (via --dest), Shelley addr1… bech32 (Byron Ae2…/DdzFF… not accepted — no verifiable checksum). Usually reachable only from an account-model source (ETH): the base address wallets normally hand out is 103 chars, which pushes the swap memo past the 80-byte OP_RETURN a BTC/DASH/ZEC source must fit it in — the CLI checks the actual address and refuses up front, so a shorter Shelley form (an enterprise address is 58 chars) does work from BTC |
| DASH | Dash | UTXO | partial | Maya-only (--backend maya/auto). Every feature is wired: hold, balance, destination, send/sweep, swap-from and single-sided LP (Maya, pairs with CACAO) — but all spend paths ship mainnet-unproven (no Dash testnet; opt-in mainnet test in docs/testnet.md), hence partial. See docs/dash.md |
| ZEC | Zcash | UTXO | partial | Maya-only (--backend maya/auto); transparent (t1…) addresses only. Every feature is wired: hold, balance, destination, send/sweep, swap-from and single-sided LP (Maya, pairs with CACAO) — the spend paths ride a bespoke v4/ZIP-243 signer with ZIP-317 fees (bitcoinlib can't sign Zcash), anchored to a real mainnet tx in the tests but shipping mainnet-unproven (no testnet; opt-in test in docs/testnet.md), hence partial. 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 — instead CACAO is the protocol leg of add-liquidity --symmetric, which is wired and mainnet-proven (docs/live-session-2026-08-16.md). The MsgSend path behind a plain send has still never broadcast; see docs/cacao.md |
| ATOM | Cosmos Hub | Cosmos | partial | destination only (via --dest, a cosmos1… address) |
| XRP | XRP Ledger | XRP | partial | destination only (via --dest). Classic r… addresses only — THORChain rejects X-addresses and address:tag, so a payout cannot carry a destination tag; never send to an exchange deposit address that needs one |
| SOL | Solana | Solana | none | Blocked: SOL.SOL exists on THORChain but is halted (a live quote returns "trading is halted"), so there is nothing to swap against. Chainflip would be the other route — see docs/chainflip.md |
| 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, valued in EUR
swapsack balance --unit BTC # …or in BTC / USD / USDT / ETH / SATS
swapsack balance --zeros --no-price-check # every row, no external price request
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 swap --from USDT-ETH --to USDC-ETH --amount 100 --backend cow # DRY RUN (CoW order)
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
swapsack send bitcoin:bc1q...?label=Alice --amount 0.001 # BIP21 URI / QR code
swapsack bump <txid> # unstick a BTC tx (DRY RUN)
swapsack bump <txid> --fee-rate 25 --confirm # …at an explicit sats/vB
Recipients and --dest also accept BIP21-style payment URIs (bitcoin:…,
litecoin:…, ethereum:…, …) as pasted from a wallet or QR code. The scheme
must name the chain being spent — a litecoin: URI in a BTC send is refused —
and on send, an amount= in the URI that contradicts --amount aborts rather
than picking one. A --dest URI's other parameters are ignored: on a swap the
URI cannot know the output amount, so only the address is taken from it.
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 (default: try
blockstream.info, fall back to mempool.space; setting this uses
that endpoint alone), Ethereum
RPC $SWAPSACK_ETH_RPC, TRON API $SWAPSACK_TRON_API, BSC RPC
$SWAPSACK_BSC_RPC, Dash Insight API $SWAPSACK_DASH_API, Zcash
lightwalletd $SWAPSACK_ZEC_LWD (gRPC host:port), THORChain REST
$SWAPSACK_THORNODE.
UTXO fee target. BTC/DASH spends aim for confirmation within N
blocks (lower N = faster & pricier). Default is next-block-ish (2);
override per run with --fee-blocks N, or set a personal default via
$SWAPSACK_FEE_BLOCKS or a config file at
~/.config/swapsack/config.toml ($SWAPSACK_CONFIG to relocate):
[fees]
target_blocks = 4 # cheaper/slower; --fee-blocks and $SWAPSACK_FEE_BLOCKS win
Precedence: --fee-blocks › $SWAPSACK_FEE_BLOCKS › config file › default.
Spending unconfirmed money. By default only confirmed UTXOs are
spendable — an output still in the mempool can be replaced or evicted, and
whatever you built on it dies with it (no funds lost; the spend simply never
happens). --allow-unconfirmed opts into them anyway, on send, swap and
the liquidity commands. The spend then pays each unconfirmed parent's fee
shortfall on top of its own fee, so the parent+child package reaches the
targeted rate — child-pays-for-parent, which is the point: a fee-stuck
incoming transaction gets dragged into a block by the one you are sending
now. Confirmed coins are still selected first, and the extra fee may need a
higher --max-fee before the gate will pass it.
Two things it does not do: it looks one hop back only (a parent that is itself spending unconfirmed money can leave the package short), and it does not make a swap settle sooner — THORChain, Maya and Chainflip all wait for their own confirmation count on the deposit regardless. On DASH the flag spends mempool outputs with no surcharge (no replacement, flat fee rate); on ZEC it has nothing to act on, and says so.
Unsticking a transaction you already sent. Every BTC spend signals BIP125
opt-in replace-by-fee, so swapsack bump <txid> can replace one that is sitting
in the mempool with an identical transaction paying more. Identical is meant
literally: the same inputs, the same recipient (or vault) output for the same
amount, the same OP_RETURN memo byte-for-byte — the extra fee comes out of the
change output and nothing else moves, because a THORChain/Maya deposit is
matched against exactly those bytes. The rebuild goes back through the same
verify gate a first-time spend passes, and like every spend it is a dry run
until --confirm. The replacement gets a new txid; the old one leaves the
mempool.
The fee target is --fee-rate N (sats/vB) or --fee-blocks N as everywhere
else, floored by what BIP125 requires to relay at all (1 sat/vB of the
transaction's size on top of the old fee). If the transaction spends inputs
whose own parents are still unconfirmed, the bump pays their shortfall too —
raising one transaction's rate is no use while an ancestor holds the package
down.
It is BTC-only and deliberately narrow. It refuses, with the reason, a
transaction that is already confirmed, one that does not signal RBF, one with
inputs this wallet cannot sign, one with no change output to take the bump from
(a --amount max sweep), one whose change would drop below dust (it names the
highest rate that would fit), and any shape it did not build itself. Dash
implements no mempool replacement at all — deliberately, for InstantSend — and
Zcash's transparent spends do not signal RBF; on both, child-pays-for-parent
(--allow-unconfirmed, above) is the only lever. Bumping a swap deposit does
not re-quote it: the memo still carries the min-out limit it was quoted at, so a
market that moved on refunds rather than fills.
Shell tab-completion (via argcomplete) for commands, subcommands and
options should need no setup at all: the install ships a completion file into
<prefix>/share/bash-completion/completions/swapsack (and
share/zsh/site-functions/_swapsack), which bash-completion and zsh find on
their own — including from a venv, pipx or uv tool tree. Start a new shell
after installing and press Tab.
If nothing happens, your setup is one of the cases that needs a nudge:
-
bash without the bash-completion package. Install it (
pacman -S bash-completion,apt install bash-completion) — its dynamic loader is what picks the shipped file up. -
fish, which has no such lookup:
register-python-argcomplete --shell fish swapsack > ~/.config/fish/completions/swapsack.fish
-
anything else — register it by hand for the current shell:
eval "$(register-python-argcomplete swapsack)" # add to ~/.bashrc to persist
argcomplete's global completion
(activate-global-python-argcomplete, one hook for every argcomplete-enabled
command) also works: swapsack carries the PYTHON_ARGCOMPLETE_OK marker that
hook requires.
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
.githubprofile repo full of keyword stuffing, with a "GET ParaSwap" button pointing at a third-partygithub.iopage. Avoid. The real ParaSwap/Velora is a same-chain EVM DEX aggregator — see below.
Backend ideas from the survey
Scoped in depth (with live API probes) in docs/backends.md; the short version:
- CoW Protocol — done (
--backend cow/auto): same-chain ETH-token swaps (where THORChain/Maya are at their worst) via a keyless API and an intent model (sign a structured EIP-712 order, solvers settle) that fits this wallet's verify-gate philosophy — unlike calldata-style aggregators (ParaSwap/1inch/0x/LiFi), whose opaque router calldata can't be independently gated. - Chainflip — done for BTC → EVM (
--backend chainflip, and inauto): a second independent non-custodial cross-chain venue that price-competes on BTC/ETH and keeps working when THORChain and Maya halt together. Executes as a vault swap — one Bitcoin transaction paying a protocol vault with the swap parameters in an OP_RETURN — which is the transaction shape the wallet already built for THORChain. Quoting covers more pairs than executing; EVM and Tron sources price but don't settle there yet. It also reaches SOL and DOT, which need wallet keys of their own first. - Instant-exchange APIs (LightningEX, ChangeNOW, SideShift, …) — huge coin coverage 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. Not planned.
- 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)
make 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
The unit suite is offline by design, and enforced: tests/conftest.py
refuses outbound connections (sockets and grpc channels) in any test not marked
network, and names the offending test if one tries. Live I/O belongs behind
-m network — a test that quietly makes a real call does not look broken, it
looks flaky, and only when the remote host happens to be down. The guard cannot
see a client that connects from C without a Python socket, so
unshare -rn -- uv run --no-sync pytest -q (after a uv sync) is the
belt-and-braces 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
# DASH — MAINNET (no Dash testnet path; a self-sweep, fee ~450 duffs)
SWAPSACK_DASH_MNEMONIC=... # a funded mainnet account (keep it tiny)
SWAPSACK_DASH_RECIPIENT=X... # optional; defaults to a self-send
# ZEC — MAINNET (no Zcash testnet path; a self-sweep, ZIP-317 fee 10000 zat)
SWAPSACK_ZEC_MNEMONIC=... # a funded mainnet account (keep it tiny)
SWAPSACK_ZEC_RECIPIENT=t1... # 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"
Documentation
Deeper topic notes live in docs/. Most are also linked inline above
where they're relevant; this is the full index:
- docs/TODO.md — the running backlog and roadmap (the live source of "what's next")
- docs/backends.md — swap backends (THORChain, Maya, CoW, and Chainflip)
- docs/chainflip.md — Chainflip execution notes (deposit channels, broker decision)
- docs/streaming.md — streaming swaps and the streaming-vs-tolerance interaction
- docs/liquidity-symmetric.md — two-sided (symmetric) liquidity mechanics + the two-leg safety protocol
- docs/dash.md · docs/zcash.md — the Maya-only legacy-UTXO chains (their bespoke signing/fee specifics)
- docs/cacao.md — Maya CACAO and the shared Cosmos-SDK adapter (also covers RUNE)
- docs/monero.md — why XMR doesn't fit the model yet, and the open custody choices
- docs/testnet.md — funded testnet/mainnet broadcast tests: addresses to fund and faucets
- docs/live-session-2026-07-24.md · docs/live-session-2026-08-16.md — real-funds mainnet run-throughs. These are what "mainnet-proven" above cites; anything not exercised in one of them has never actually broadcast. The 2026-07-24 run used a throwaway wallet, so it keeps its addresses, amounts and txids; the 2026-08-16 run used a personal wallet and is redacted accordingly
(docs/code-review-*.md are dated review snapshots, not reference docs.)
Metadata
Release files for swapsack 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| swapsack-0.2.0.tar.gz | 508.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| swapsack-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 700.3 kB
Release files / swapsack-0.2.0.tar.gz
| Download URL | swapsack-0.2.0.tar.gz |
|---|---|
| Size | 508.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a3c594ccfdaec6381030731940be3c0d117a974a66c8fd459373e2789a433cca
|
|
BLAKE2b-256 checksum How to use checksums |
48c36f3aa37a7cab315b1bdb0c8dcb56477d81cd011c32048afafa0112d0b86d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 30, 2026.
Transparency logRelease files / swapsack-0.2.0-py3-none-any.whl
| Download URL | swapsack-0.2.0-py3-none-any.whl |
|---|---|
| Size | 191.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
26b75be00f980123138c9c1f60eca2a6450ef24ce42def86267c7d72024da1d0
|
|
BLAKE2b-256 checksum How to use checksums |
71669eea4220a6b514966b32d1998daa72ff95b8e7deed0ad3389917f40f5650
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 30, 2026.
Transparency log