Measured from source · 7 November 2026
Bitcoin wallets on iPhone, compared from their code
How Winnow compares with seven other open-source iPhone wallets that hold your own keys: features, code size, complexity, proofs and what each one depends on. Every number is measured from pinned source and rebuilt by a script anyone can run.
Winnow is measured at winnowwallet/winnow-lean v0.9.10, its Lean 4 app. What the comparison leaves out
At a glance
Winnow is a Bitcoin-only wallet written in Lean 4: the wallet, its peer networking and the app’s model, update and view compile to C, and LeanNative’s generated Objective-C shell draws them with UIKit. It checks the chain on the phone with compact block filters from Bitcoin peers it finds itself, and reaches Tor and I2P through gateways you run, with no client of its own.
Each wallet below holds its own keys on the phone, with the exceptions noted in the fourth column. Four of them (Phoenix, Bitkit, Blixt and Zeus) are Lightning-first wallets with an on-chain side; this page looks only at their Bitcoin wallet. The table shows defaults; most of these wallets let you change the chain source.
| Wallet | Built with | Where the on-chain wallet lives | Chain data | Who else must sign | On iPhone |
|---|---|---|---|---|---|
| Winnow | Lean 4, drawn by LeanNative's generated Objective-C/UIKit shell; no Swift | Its own, in Lean 4 (Lean/Winnow, and Lean/Secp, a secp256k1 whose field and curve arithmetic carry machine-checked proofs), in the same repository: keys, PSBTs, scripts, compact-filter sync, peer networking over Lean's libuv TCP lakefile. Winnow. Transport. Secp256k1. Gate. | BIP157/158 compact filters from Bitcoin peers found through the census peer list shipped with the build (its signature checked when the build is made) and DNS seeds; Advanced mode adds your own peers Pool. Census. import-census Settings. | Nobody for the everyday wallet; multisig accounts need their other signers (phones, Ledgers or keys someone sent), with no co-signing server README. Send. Multisig. | App Store, in the listing the Swift app used (com.btcswift.app); tagged builds upload to TestFlight README. Project. release. release. |
| BlueWallet | React Native (TypeScript) with small native modules | Its own TypeScript (class/wallets) on bitcoinjs-lib 7 and @noble/secp256k1; chain data through a fork of the Electrum client abstract-hd-electrum-wallet. package. package. | Electrum servers: electrum1.bluewallet.io by default, a short hardcoded fallback list; any host and port can be set in Settings > Network BlueElectrum. ElectrumSettings. | Nobody for on-chain funds. The optional LNDhub (custodial) and Ark wallet types depend on their own servers abstract-hd-electrum-wallet. lightning-custodian-wallet. lightning-ark-wallet. | App Store; builds go through TestFlight README. Fastfile |
| Green | Swift (UIKit) over Blockstream's gdk | gdk 0.78.0 (C++ for multisig, Rust for singlesig), linked as a prebuilt static library fetch_gdk_binaries. session_impl. | Singlesig: Blockstream's Electrum server, or your own in Settings. Multisig: Blockstream's backend, not replaceable network_parameters. network_parameters. AppSettingsViewModel. | Nobody for singlesig. Multisig (2-of-2, 2-of-3) needs Blockstream's server to co-sign every spend ga_auth_handlers. ga_auth_handlers. | App Store |
| Muun | Swift (UIKit) with a Go library, libwallet | libwallet, Muun's own Go library in the same repository: keys, 2-of-2 and MuSig2 signing, addresses, fees. Muun's server holds the second key README. V5. | Muun's own server (Houston API): history, spendable coins, fees and block height. The person cannot change it Environment. SettingsPresenter. init. | Muun's server must co-sign every spend. Without it the person needs the Emergency Kit, the Recovery Code and Muun's separate Recovery Tool V4. V5. RecoveryToolViewController. | App Store, per its fastlane release lane |
| Phoenix | Swift (SwiftUI) over Kotlin Multiplatform shared code | lightning-kmp 1.13.2 (Kotlin Multiplatform): Electrum client, swap-in wallet and final (BIP84) wallet build. SwapInWallet. FinalWallet. | An Electrum server picked at random from a built-in list of 15, trusted as given; the person can enter their own server in Settings AppConfigurationManager. ElectrumConfigurationView. | ACINQ co-signs swap-in deposits (MuSig2 2-of-2) until a refund delay of about six months, and is the channel peer for every on-chain send. The final wallet is the person's alone SwapInProtocol. NodeParams. ValidateView. | App Store |
| Bitkit | Swift (SwiftUI) on Rust libraries | LDK Node 0.7.0-rc.78 (Synonym's fork, Rust); its on-chain wallet is BDK Cargo. builder. | Synonym's Electrum server (bitkit.to); the person can change it in Settings > Advanced > Electrum Server Env. ElectrumSettingsScreen. | Nobody LightningService. | App Store |
| Blixt | React Native (TypeScript) with Go (LND) | Embedded LND 0.21.1 (Go); the on-chain wallet is btcwallet, run in-process through react-native-turbo-lnd btcwallet. | Neutrino (BIP157/158) through six default peers, four run by Blixt; the person can change the peers in Settings constants. Settings. | Nobody OnChain. | TestFlight |
| Zeus | React Native (TypeScript) with native modules and prebuilt Go and Rust libraries | Embedded LND 0.21.5 (Go, Zeus's fork); the on-chain wallet is btcwallet. LDK Node (BDK) is an option, and remote nodes can be driven too btcwallet. WalletCreationUtils. WalletCreationUtils. | Neutrino (BIP157/158) through five default peers, two run by Olympus; the person can add peers or allow only their own. The LDK Node option uses an Esplora server SettingsStore. LndMobileUtils. LdkNodeUtils. | Nobody for on-chain funds; they sit in the embedded wallet under one key on the phone EmbeddedLND. btcwallet. | App Store |
Features
What a person can do in each iPhone app today with the wallet’s Bitcoin, checked against the code. Where a wallet has several account types, a cell shows the best one it offers, and its note says which. Hover or focus a cell for the note; every cell links to its evidence. The cells were last checked by hand between 2 October 2026 and 3 November 2026. Since that review, 26 of the 441 cited passages have changed (in BlueWallet, Bitkit, Blixt and Zeus) and 100 have moved within their file in the code measured below.
| Feature | Winnow | BlueWallet | Green | Muun | Phoenix | Bitkit | Blixt | Zeus |
|---|---|---|---|---|---|---|---|---|
| Keys and signing | ||||||||
| Multisig or shared accounts | Yes | Yes | Yes | Partial | Partial | No | No | No |
| Hardware or external signer | Yes | Yes | Yes | No | No | Yes | No | Yes |
| PSBT import and export | Partial | Yes | Yes | No | No | No | No | Yes |
| Watch-only wallets | Partial | Yes | Yes | No | No | Partial | No | Yes |
| BIP 39 passphrase | No | Partial | Yes | No | No | Yes | Partial | No |
| Spending | ||||||||
| Coin control | No | Yes | Yes | No | No | Yes | No | Yes |
| Speed up a stuck payment (RBF or CPFP) | Partial | Partial | Yes | Partial | Partial | Yes | Partial | Yes |
| Taproot receive addresses | Yes | Yes | Partial | Yes | Yes | Yes | Yes | Yes |
| Output descriptors | Partial | Yes | Yes | Partial | Partial | Yes | No | Partial |
| Chain data and privacy | ||||||||
| Use your own node or server | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes |
| Checks chain data on the phone | Yes | No | No | No | No | Partial | Yes | Yes |
| Tor or I2P | Partial | Partial | Yes | No | Partial | No | Yes | No |
| Backup and recovery | ||||||||
| Standard BIP 39 recovery phrase | Yes | Yes | Yes | Partial | Partial | Yes | Partial | Partial |
| Encrypted backup off the phone | Yes | No | Partial | Yes | Yes | Partial | No | Partial |
| License | ||||||||
| Source license | MIT | MIT | GPL-3.0 | MIT | Apache-2.0 | MIT | MIT | AGPL-3.0 |
- Yes
- Partial
- Engine only
- No
- Unknown
Notes and code evidence for every cell
Links open the exact line that was checked, at the revision reviewed between 2 October 2026 and 3 November 2026.
Cited code that has changed since the review, and so is due a fresh look:
- BlueWallet, Speed up a stuck payment (RBF or CPFP):
TransactionStatus.has changedtsx :1128 - BlueWallet, Output descriptors:
xpub.has changedtsx :72 - BlueWallet, Encrypted backup off the phone:
blue-app.has changedts :302 - Bitkit, Hardware or external signer:
HwSendSignView.has changedswift :115 - Bitkit, Watch-only wallets:
HwWalletManager.has changedswift :5 - Bitkit, Taproot receive addresses:
AddressTypePreferenceView.has changedswift :109 - Bitkit, Use your own node or server:
SettingsViewModel+Electrum.has changedswift :46 - Bitkit, Tor or I2P:
electrum.has changedrs :660 - Bitkit, Encrypted backup off the phone:
BackupService.has changedswift :220 - Bitkit, Encrypted backup off the phone:
Env.has changedswift :340 - Blixt, PSBT import and export:
Withdraw.has changedtsx :50 - Blixt, Coin control:
Withdraw.has changedtsx :50 - Zeus, PSBT import and export:
handleAnything.has changedts :1050 - Zeus, Source license:
README.has changedmd :145 - BlueWallet, Where the on-chain wallet lives:
package.has changedjson :132 - BlueWallet, profile.distribution:
Fastfilehas changed:1306 - BlueWallet, Main libraries:
package.has changedjson :93 - BlueWallet, Prebuilt binaries:
Podfile.has changedlock :28 - BlueWallet, Prebuilt binaries:
Podfile.has changedlock :97 - Bitkit, Who else must sign:
LightningService.has changedswift :796 - Bitkit, Main libraries:
project.has changedpbxproj :1176 - Bitkit, Prebuilt binaries:
Package.has changedswift :29 - Zeus, Who else must sign:
EmbeddedLND.has changedts :100 - Zeus, Main libraries:
package.has changedjson :53 - Zeus, Main libraries:
package.has changedjson :117 - Zeus, Prebuilt binaries:
fetch-libraries-versions.has changedjson :2
Winnow
- Multisig or shared accounts: Yes. k-of-n Taproot accounts in Advanced mode, members from this phone, paired Ledgers and keys others sent; MuSig2 removed.
Multisig.lean :475 Multisig.lean :580 Signers.lean :123 - Hardware or external signer: Yes. Pairs a Ledger over Bluetooth as a multisig member, registers the account on it and has it sign; the everyday wallet signs in-app; this build has paired only a simulated Ledger so far.
LedgerTransport.lean :102 Main.lean :138 Signers.lean :152 Multisig.lean :709 - PSBT import and export: Partial. Multisig accounts only: copy a BIP174 v0 PSBT to share and paste co-signers' PSBTs back; the everyday wallet signs in-app.
Multisig.lean :716 Multisig.lean :654 Send.lean :63 - Watch-only wallets: Partial. Descriptor-only backup JSON restores watch-only; no xpub or address entry; joined accounts must include this phone's key.
View.lean :13 Wallet.lean :954 Multisig.lean :207 - BIP 39 passphrase: No. Seed is derived with BIP39's empty default passphrase on create and import; no passphrase field anywhere in the app.
BIP39.lean :71 Wallet.lean :226 Wallet.lean :938 - Coin control: No. Send picks coins automatically, largest first, for the wallet and multisig accounts; an account's coin list is read-only.
CoinSelection.lean :118 Wallet.lean :685 MultisigAccount.lean :1185 Multisig.lean :661 - Speed up a stuck payment (RBF or CPFP): Partial. RBF Bump fee in Advanced mode for own pending sends with unspent change; accounts only rebroadcast or give up; no CPFP.
Home.lean :317 Wallet.lean :304 Wallet.lean :809 Home.lean :279 - Taproot receive addresses: Yes. Receives to BIP86 bc1p addresses by default; multisig accounts are Taproot too (script path, NUMS internal key).
Wallet.lean :226 Wallet.lean :166 MultisigAccount.lean :10 - Output descriptors: Partial. Multisig accounts join from and share a tr() descriptor; the everyday wallet's is only inside its backup file.
Multisig.lean :517 Multisig.lean :666 Multisig.lean :577 Wallet.lean :894 - Use your own node or server: Yes. Manual host:port peers under Advanced mode, tried first; peer must serve compact filters; pool still fills from others.
Settings.lean :161 Pure.lean :463 Pool.lean :215 - Checks chain data on the phone: Yes. Checks header work, cross-checks filter headers across peers, matches filters locally; merkle-checks fetched blocks.
HeaderChain.lean :12 Filters.lean :5 Stack.lean :307 - Tor or I2P: Partial. No bundled Tor or I2P client; routes peers through SOCKS gateways you run, found on Tailscale or set by hand.
README.md :54 Settings.lean :130 Gateways.lean :6 - Standard BIP 39 recovery phrase: Yes. 12-word BIP39 phrase on BIP86 paths, shown in Advanced mode; the app restores only from its backup file, not the words alone.
Wallet.lean :83 Wallet.lean :224 Backup.lean :160 - Encrypted backup off the phone: Yes. On by default: AES-256-GCM copy in the person's private CloudKit database, key in iCloud Keychain; backup file too.
CloudBackup.lean :104 CloudWalletBackup.lean :7 CloudBackup.lean :397 CloudBackup.lean :460
BlueWallet
- Multisig or shared accounts: Yes. Vault wallet type: m-of-n (default 2-of-3); person can generate or import several keys in the app.
Add.tsx :294 WalletsAddMultisig.tsx :54 addMultisigStep2.tsx :226 - Hardware or external signer: Yes. Imports Coldcard/Cobo/Keystone-style keys by QR or file and signs via PSBT QR or file; no USB, Bluetooth or NFC link.
abstract-wallet.ts :321 abstract-wallet.ts :339 ScanQRCode.tsx :165 WalletDetails.tsx :813 - PSBT import and export: Yes. Unsigned PSBT shows as animated QR, file or clipboard; signed PSBT is scanned or opened from a file.
psbtWithHardwareWallet.tsx :259 psbtWithHardwareWallet.tsx :270 psbtWithHardwareWallet.tsx :314 - Watch-only wallets: Yes. Imports xpub/ypub/zpub, single-key descriptors or one address as watch-only; shows balance and history.
wallet-import.ts :502 watch-only-wallet.ts :64 - BIP 39 passphrase: Partial. Passphrase prompt only when importing a seed or adding a multisig seed key; creating a new wallet offers none.
AddWalletStack.tsx :246 wallet-import.ts :146 abstract-hd-wallet.ts :79 - Coin control: Yes. Coin Control screen selects UTXOs and hands them to Send; coins can also be frozen.
SendDetails.tsx :1056 CoinControl.tsx :289 CoinControl.tsx :301 - Speed up a stuck payment (RBF or CPFP): Partial. RBF bump, RBF cancel and CPFP exist, but allowRBF() is true only for native SegWit (and aezeed) wallets.
abstract-wallet.ts :170 hd-segwit-bech32-wallet.ts :22 TransactionStatus.tsx :1128 - Taproot receive addresses: Yes. Taproot (BIP86) is a wallet-type choice in Add Wallet; receive addresses are bc1p; default type is native SegWit.
AddWalletStack.tsx :155 Add.tsx :250 hd-taproot-wallet.ts :47 - Output descriptors: Yes. Imports wpkh/sh/pkh/tr and sortedmulti descriptors; exports a descriptor (tr()) only for Taproot wallets.
abstract-wallet.ts :238 multisig-hd-wallet.ts :634 xpub.tsx :72 - Use your own node or server: Yes. Electrum server setting takes any host and TCP/SSL port after a connection test; defaults are a hardcoded peer list.
ElectrumSettings.tsx :193 ElectrumSettings.tsx :210 BlueElectrum.ts :90 - Checks chain data on the phone: No. Balances and history are taken straight from Electrum replies; headers are fetched only for timestamps, no proofs.
BlueElectrum.ts :1034 BlueElectrum.ts :1128 BlueElectrum.ts :1609 - Tor or I2P: Partial. .onion Electrum hosts are accepted; no Tor in the app, the error text asks the person to connect the Orbot app.
ElectrumSettings.tsx :199 en.json :253 BlueElectrum.ts :1453 - Standard BIP 39 recovery phrase: Yes. New on-chain wallets get a 12-word BIP39 phrase (16 bytes of entropy), shown on a backup screen.
abstract-hd-electrum-wallet.ts :168 PleaseBackup.tsx :71 Add.tsx :284 - Encrypted backup off the phone: No. Wallet data saved in a this-device-only keychain item and Realm excluded from iCloud backup; only manual seed display.
blue-app.ts :133 blue-app.ts :302 blue-app.ts :709
Green
- Multisig or shared accounts: Yes. 2-of-3 account: your key, a recovery key you hold (phrase or xpub) and Blockstream's key; 2-of-2 with its server also offered.
CreateAccountViewModel.swift :59 AccountCreateRecoveryKeyViewController.swift :89 - Hardware or external signer: Yes. Jade over Bluetooth or QR and Ledger Nano X over Bluetooth sign on-chain transactions.
ConnectViewModel.swift :62 Jade.swift :154 BleLedger.swift :33 QrJadeConnection.swift :11 - PSBT import and export: Yes. Singlesig watch-only: export unsigned PSBT (QR, file, copy) and import signed (QR, file, paste); not multisig.
SendTxConfirmViewModel.swift :131 QRPsbtShowViewController.swift :102 QRPsbtAquireViewController.swift :105 - Watch-only wallets: Yes. Singlesig from xpub/ypub/zpub or descriptors; multisig by username and password; no address lists.
WOViewModel.swift :42 WOViewModel.swift :71 - BIP 39 passphrase: Yes. Wallet menu 'Log in with BIP39 passphrase' opens a different wallet from the same phrase; set at login.
LoginViewController.swift :485 LoginViewController.swift :579 signer.cpp :32 - Coin control: Yes. Select coins on the amount screen lists UTXOs with filter and sort; the picked coins fund the payment.
SendCoordinator.swift :296 SendAmountViewControllerLegacy.swift :396 SendAmountViewControllerLegacy.swift :684 - Speed up a stuck payment (RBF or CPFP): Yes. RBF only: Speed up Transaction on unconfirmed RBF sends; the app never uses gdk's can_cpfp.
TxDetailsViewModel.swift :208 TxDetailsViewModel.swift :255 account.rs :380 - Taproot receive addresses: Partial. Taproot account creation is commented out; a p2tr account shows only if restore finds BIP86 history.
AccountTypeOption.swift :10 AccountTypeCellModel.swift :25 ga_auth_handlers.cpp :1257 - Output descriptors: Yes. Singlesig accounts show, copy and QR their output descriptors; watch-only import takes descriptors; multisig none.
ManageAssetViewController.swift :600 AccountDescriptorCellModel.swift :7 WOViewModel.swift :92 - Use your own node or server: Yes. Settings: Personal Electrum server, a URL per network with TLS option; singlesig only, multisig uses Blockstream's backend.
AppSettingsViewModel.swift :52 AppSettingsViewController.swift :73 WalletManager.swift :423 - Checks chain data on the phone: No. gdk removed SPV in 0.75 and reports spv_verified as disabled; balances and history come from the server unchecked.
CHANGELOG.md :174 ga_session.cpp :2313 account.rs :589 - Tor or I2P: Yes. Settings: Connect with Tor starts gdk's built-in Tor client; a SOCKS5 proxy option is also offered.
AppSettingsViewModel.swift :40 AppSettingsCell.swift :45 session_impl.cpp :101 - Standard BIP 39 recovery phrase: Yes. New wallets get a 12-word BIP39 phrase on a singlesig BIP84 account; multisig funds also need Blockstream's co-signer.
OnboardViewModel.swift :134 OnboardViewModel.swift :161 account.rs :781 - Encrypted backup off the phone: Partial. No iCloud or file backup; multisig account names and tx notes sync encrypted to Blockstream's server; phrase is manual.
client_blob.cpp :19 ga_session.cpp :1048 gaiosRelease.entitlements :5
Muun
- Multisig or shared accounts: Partial. Fixed 2-of-2 with Muun's server (P2WSH or MuSig2 P2TR); the person holds one key, cannot add keys or choose cosigners.
AddressActions.swift :81 V4.go :47 - Hardware or external signer: No. Keys live in the app. A flag-gated NFC Security Card only signs a 2FA challenge (mock backend in libwallet), not transactions.
NewOperationPresenter.swift :646 sign_message_security_card_action_v2.go :65 init.go :170 - PSBT import and export: No. Server returns a partly signed tx in Muun's own API format, the app signs and pushes it back; no PSBT import or export.
OperationActions.swift :265 OperationActions.swift :293 - Watch-only wallets: No. Get Started offers only create new wallet or recover existing (email or recovery code); every wallet holds its own key.
GetStartedViewController.swift :50 CreateFirstSessionAction.swift :49 - BIP 39 passphrase: No. No BIP 39 phrase: the key is a random 32-byte seed; the optional password only encrypts the key backup, it is not a wallet variant.
WalletPrivateKey.swift :49 BuildChallengeSetupAction.swift :33 - Coin control: No. The server supplies the ordered UTXO list (next transaction size) and a send spends in that order; no coin picker.
HoustonService.swift :487 Operation.swift :37 - Speed up a stuck payment (RBF or CPFP): Partial. No speed-up button or RBF; a new send adds a CPFP bump for unconfirmed parent coins, only while the backend flag is on.
fees.go :84 NewOpLoadingPresenter.swift :121 - Taproot receive addresses: Yes. P2TR (MuSig2) offered in the Receive address picker and as default in Settings once active; default stays SegWit.
ReceiveAddressTypeSelectPresenter.swift :70 OnchainSettingsPresenter.swift :72 V5.go :16 - Output descriptors: Partial. Emergency Kit PDF prints wsh(multi) and tr(musig) descriptors with key fingerprints, not xpubs; no descriptor import.
descriptors.go :15 emergencykit.go :39 - Use your own node or server: No. Fixed to Muun's Houston API; no server setting. libwallet's Electrum client serves only a scan RPC the app never calls.
Environment.swift :42 SettingsPresenter.swift :154 init.go :160 - Checks chain data on the phone: No. History, UTXOs and block height come from Muun's server; the app has no header, filter or block checks.
HoustonService.swift :436 RealTimeDataAction.swift :58 - Tor or I2P: No. No Tor, I2P or proxy setting; the app talks to Muun's Houston HTTPS endpoint and Firebase/Google services.
HoustonService.swift :38 SettingsPresenter.swift :154 - Standard BIP 39 recovery phrase: Partial. Recovery Code (not BIP 39) plus Emergency Kit; in-app restore asks Muun's server, outside restore needs Muun's Recovery Tool.
recoverycode.go :37 LogInWithRCAction.swift :31 WalletPrivateKey.swift :49 - Encrypted backup off the phone: Yes. Emergency Kit PDF with keys encrypted to the Recovery Code is saved to iCloud Drive or Google Drive when the person exports it.
ShareEmergencyKitPresenter.swift :96 ICloudHelper.swift :36 EmergencyKitDataSelector.swift :25
Phoenix
- Multisig or shared accounts: Partial. Swap-in address is a MuSig2 2-of-2 of the user key and ACINQ's key; the person holds one key, never several.
SwapInProtocol.kt :11 SwapInProtocol.kt :19 SwapInView.swift :336 - Hardware or external signer: No. Keys come from the on-phone seed and spends are signed in the app; 'Swap-in signer' is a support tool, not a device link.
WalletManager.kt :47 SpendOnChainFunds.swift :583 SwapInSigner.swift :87 - PSBT import and export: No. No PSBT code; the Swap-in signer tool takes a raw transaction plus a server nonce, and spends are built, signed and sent in-app.
SwapInSignerHelper.kt :73 SpendOnChainFunds.swift :583 - Watch-only wallets: No. Wallets are created or restored from a 12-word phrase only; no xpub, descriptor or address import.
WalletManager.kt :47 RestoreWalletController.kt :56 - BIP 39 passphrase: No. The shared code can take a passphrase, but every app call passes an empty string and no screen asks for one.
BusinessManager.swift :464 RestoreWalletController.kt :59 - Coin control: No. On-chain spends are send-all (expired swap-ins, final wallet) or splice-outs; the swap-in UTXO list is view-only.
SpendOnChainFunds.swift :173 ElectrumMiniWallet.kt :99 SwapInWalletDetails.swift :223 - Speed up a stuck payment (RBF or CPFP): Partial. 'Accelerate transaction' does a CPFP splice, only for unconfirmed splice payments; no RBF and no bump for other sends.
SummaryView.swift :334 SummaryView.swift :884 CpfpView.swift :744 - Taproot receive addresses: Yes. The on-chain receive tab shows a P2TR swap-in address (MuSig2 plus refund script); a legacy P2WSH address is an option.
SwapInView.swift :28 SwapInView.swift :336 SwapInProtocol.kt :25 - Output descriptors: Partial. Swap-in wallet shows a copyable tr() descriptor (and a legacy wsh()); final wallet shows only an xpub; nothing imports.
WalletInfoView.swift :221 WalletInfoView.swift :344 KeyManager.kt :268 - Use your own node or server: Yes. Electrum server screen takes a custom host and port in place of the random list; the Lightning peer stays ACINQ's.
ElectrumConfigurationView.swift :59 AppConnectionsDaemon.kt :265 - Checks chain data on the phone: No. Tip, UTXOs and history come from the Electrum server; a merkle root is computed but no header or proof check is called.
ElectrumClient.kt :144 ElectrumMiniWallet.kt :197 ElectrumDataTypes.kt :142 - Tor or I2P: Partial. Use Tor switches to .onion Electrum and ACINQ endpoints but needs a third-party Tor VPN app such as Orbot; none is bundled.
TorConfigurationView.swift :66 PeerManager.kt :172 AppConfigurationManager.kt :66 - Standard BIP 39 recovery phrase: Partial. 12-word BIP39; the BIP84 final wallet restores elsewhere, but channel and swap-in funds need Phoenix and ACINQ.
RecoveryPhraseReveal.swift :104 LocalKeyManager.kt :42 NodeParams.kt :158 - Encrypted backup off the phone: Yes. iCloud switch saves the phrase (CloudKit encrypted field, end-to-end only with Apple ADP) and seed-encrypted history.
CloudBackupView.swift :105 SyncSeedManager.swift :380 PaymentsBackupView.swift :78
Bitkit
- Multisig or shared accounts: No. Single-key accounts only (BIP 44/49/84/86 templates); no multisig or shared-account flow.
builder.rs :1385 LDKNode+AddressType.swift :7 - Hardware or external signer: Yes. Trezor over Bluetooth; on-chain sends signed on the device. Only Trezor; bitkit-core's Jade code is never called.
HardwareWalletsSettingsScreen.swift :31 HwSendSignView.swift :115 TrezorTransport.swift :37 - PSBT import and export: No. PSBT is internal to Trezor signing; only a hidden dev screen copies an unsigned one; no import, file, QR or share.
HwWalletManager.swift :1177 DevSettingsView.swift :59 TrezorSendTransactionView.swift :269 - Watch-only wallets: Partial. Paired Trezor accounts are tracked by xpub (balance, history); pasting an xpub or address works only in hidden dev settings.
HwWalletManager.swift :5 HardwareWalletScreen.swift :4 TrezorWatcherView.swift :16 - BIP 39 passphrase: Yes. Advanced setup adds a passphrase to a new wallet; restore takes an optional one; both feed the BIP 39 seed.
OnboardingSlider.swift :11 CreateWalletWithPassphraseView.swift :65 RestoreWalletView.swift :198 - Coin control: Yes. Manual coin selection setting; the send flow then lists UTXOs to tick and spends exactly those.
CoinSelectionSettingsView.swift :128 SendAmountView.swift :302 SendUtxoSelectionView.swift :74 - Speed up a stuck payment (RBF or CPFP): Yes. Boost button on an unconfirmed on-chain activity: RBF for sent, CPFP for received; off for hardware wallet activity.
CoreService.swift :1648 ActivityItemView.swift :562 mod.rs :827 - Taproot receive addresses: Yes. Advanced > Address Type offers Taproot (BIP 86, bc1p); new wallets default to native SegWit.
AddressTypePreferenceView.swift :109 SettingsViewModel.swift :576 builder.rs :1399 - Output descriptors: Yes. Scanning a BTCPay SamRock QR sends the account's output descriptor to the store; no descriptor import or copy screen.
SamRockService.swift :273 SamRockService.swift :333 implementation.rs :194 - Use your own node or server: Yes. Settings > Advanced > Electrum Server takes host, port and TCP or TLS; default is Synonym's bitkit.to.
AdvancedSettingsView.swift :100 ElectrumSettingsScreen.swift :56 SettingsViewModel+Electrum.swift :46 - Checks chain data on the phone: Partial. Electrum sync through BDK, which checks merkle proofs against headers the server sends; no PoW, filter or block check.
electrum.rs :671 electrum.rs :785 Cargo.toml :50 - Tor or I2P: No. Electrum client is built with no proxy; settings offer only host, port and TCP/TLS; no Tor or I2P code in the app.
electrum.rs :660 ElectrumSettingsScreen.swift :83 - Standard BIP 39 recovery phrase: Yes. 12-word BIP 39 phrase shown for backup; accounts use standard BIP 44/49/84/86 paths; restore takes 12 or 24 words.
StartupHandler.swift :13 BackupMnemonic.swift :114 builder.rs :1385 - Encrypted backup off the phone: Partial. Settings, tags, activity and transfer records go encrypted to Synonym's fixed VSS server; no keys, no iCloud.
BackupService.swift :220 Env.swift :340 implementation.rs :248
Blixt
- Multisig or shared accounts: No. One wallet from one 24-word seed; the app has no m-of-n or MuSig2 account (LND signing RPCs are never called).
index.ts :795 OnChain.ts :165 - Hardware or external signer: No. Keys come from the on-phone seed; the generated lnd.conf has no remote signer and no device code exists in the app.
index.ts :795 index.ts :674 - PSBT import and export: No. Withdraw signs and broadcasts through LND sendCoins; there is no PSBT import, export or share.
Withdraw.tsx :50 OnChain.ts :217 - Watch-only wallets: No. Restore accepts a 24-word seed only; no xpub, descriptor or address import in the app.
Restore.tsx :69 index.ts :778 - BIP 39 passphrase: Partial. Optional aezeed passphrase on create and restore; not BIP 39: a wrong one is rejected, it does not open another wallet.
Start.tsx :63 Restore.tsx :310 errors.go :10 - Coin control: No. sendCoins gets address, amount and fee rate only, no outpoints; no coin list in the app (LND would accept outpoints).
OnChain.ts :217 Withdraw.tsx :50 - Speed up a stuck payment (RBF or CPFP): Partial. Bump Fee does CPFP on the change of a pending channel-open tx only; ordinary on-chain sends have no bump.
PendingChannelCard.tsx :117 PendingChannelCard.tsx :176 OnChain.ts :239 - Taproot receive addresses: Yes. Default receive address is P2TR (bc1p); long-pressing New Address gives P2WPKH.
OnChain.ts :164 OnChainInfo.tsx :62 - Output descriptors: No. On-chain screens are address, history, details and withdraw; no descriptor or xpub export or import.
index.tsx :19 OnChainInfo.tsx :129 - Use your own node or server: Yes. Settings > Bitcoin Node takes any BIP 157 peer. A bitcoind mode sits behind a hidden debug gate; the fee URL is fixed.
Settings.tsx :1670 Settings.tsx :750 PowerUserTools.tsx :696 - Checks chain data on the phone: Yes. Embedded LND runs neutrino (BIP 157/158): headers and filters are checked on the phone, not a server's balances.
index.ts :695 config_builder.go :1872 - Tor or I2P: Yes. Experimental toggle starts embedded Tor; Bitcoin and LN peers use it, rates and mempool.space calls stay clearnet.
Settings.tsx :1867 index.ts :295 en.json :958 - Standard BIP 39 recovery phrase: Partial. 24-word aezeed phrase (LND), not BIP 39; BIP 39 wallets cannot restore it and the app cannot import a BIP 39 phrase.
index.ts :628 Restore.tsx :70 cipherseed.go :15 - Encrypted backup off the phone: No. iCloud backup holds encrypted channel state only (off-chain funds); on-chain funds restore from the written phrase.
ICloudBackup.ts :102 en.json :1241
Zeus
- Multisig or shared accounts: No. Single-key accounts only; account import takes one xpub or a wpkh/sh(wpkh) descriptor; no m-of-n or MuSig2 code in the app.
ImportAccount.tsx :34 AddressUtils.ts :73 - Hardware or external signer: Yes. Imports signer xpub and fingerprint by QR (BC-UR, JSON export) and signs by PSBT QR; no named-device pairing; labelled experimental.
HandleAnythingQRScanner.tsx :114 PSBT.tsx :201 en.json :426 - PSBT import and export: Yes. Unsigned PSBT shown as animated QR (BC-UR, BBQr) or copied; signed PSBT scanned back and broadcast; for imported accounts only.
PSBT.tsx :201 handleAnything.ts :1050 HandleAnythingQRScanner.tsx :161 - Watch-only wallets: Yes. Imports an xpub or wpkh/sh(wpkh) descriptor as a watch-only LND account with its own balance; embedded LND rescans from a chosen height.
ImportingAccount.tsx :164 index.tsx :496 handleAnything.ts :1122 - BIP 39 passphrase: No. initWallet accepts a passphrase argument but no create or restore screen collects one; same for LDK Node.
LndMobileUtils.ts :1316 SeedRecovery.tsx :924 WalletCreationUtils.ts :126 - Coin control: Yes. On-chain Send has a UTXO picker to tick coins; each account also has a Coins list; LDK Node has it too.
Send.tsx :1015 UTXOPicker.tsx :208 - Speed up a stuck payment (RBF or CPFP): Yes. Speed up button on unconfirmed txs calls LND BumpFee on a wallet output (CPFP); LDK Node has no bump.
Transaction.tsx :404 walletkit.proto :316 LdkNode.ts :2177 - Taproot receive addresses: Yes. Receive settings offer Taproot (P2TR) beside P2WPKH and nested SegWit; P2WPKH is the default; LDK Node issues P2WPKH only.
Receive.tsx :1127 EmbeddedLND.ts :586 LdkNode.ts :2176 - Output descriptors: Partial. Imports single-key wpkh() and sh(wpkh()) descriptors as an account; no descriptor export; no tr(), pkh or multi-key forms.
AddressUtils.ts :73 handleAnything.ts :1094 - Use your own node or server: Yes. Add Neutrino peers by host and switch on 'don't allow other peers' to use only yours; LDK Node takes a custom Esplora URL.
LndMobileUtils.ts :222 NeutrinoPeers.tsx :213 NeutrinoPeers.tsx :258 - Checks chain data on the phone: Yes. Embedded LND runs Neutrino: headers and BIP 157/158 filters checked on the phone; LDK Node and remote nodes trust Esplora or the node.
LndMobileUtils.ts :270 config_builder.go :1876 - Tor or I2P: No. No Tor toggle for the embedded node (embeddedTor stays false); the built-in Tor client is offered only for remote nodes.
WalletConfiguration.tsx :1462 SettingsStore.ts :1608 Advanced.tsx :66 - Standard BIP 39 recovery phrase: Partial. Embedded LND shows a 24-word aezeed (LND format, not BIP 39); the LDK Node option shows a BIP 39 phrase.
LndMobileUtils.ts :1295 README.md :5 Seed.tsx :319 - Encrypted backup off the phone: Partial. Embedded LND uploads only encrypted channel state to Olympus, no on-chain data or keys; LDK Node keeps node state on a VSS server.
ChannelBackupStore.ts :63 ChannelBackupStore.ts :95 LdkNodeUtils.ts :47
Code size
Winnow counts 24,808 lines of code: 9,322 for the app and 15,486 for its own Bitcoin library, which lives in the same repository and includes 872 lines of proofs. The other wallets count 1.5 to 7.8 times as many lines. That is the app, plus a Bitcoin library of its own for Muun.
The count leaves out the engines each wallet is built on. Green, Phoenix, Bitkit, Blixt and Zeus embed gdk, lightning-kmp, LDK Node and LND; four of the five are Lightning nodes. Winnow is built on LeanNative, a 6,051-line framework that runs a Lean app on iOS; it plays the part SwiftUI or React Native play for the others, which are not counted either. Each engine is shown in the table, not in the totals. BlueWallet takes its Bitcoin primitives from npm packages, which the dependency tables count.
Winnow’s lines are denser, at about 44 characters each against 20–29 for the others. Counted in characters, the gap is 0.9 to 3.5 times.
- App
- Own Bitcoin library
| Wallet | App | Own Bitcoin library | Total lines | Characters | vs Winnow | Embedded engine, not in the total |
|---|---|---|---|---|---|---|
| Winnow | 9,322 | 15,486 | 24,808 | 1.09M | — | 6,051 LeanNative/leannative@700b388 |
| BlueWallet | 59,121 | — | 59,121 | 1.69M | 2.4× · 1.6× | None embedded |
| Green | 70,474 | — | 70,474 | 2.03M | 2.8× · 1.9× | 34,374 Blockstream/gdk release_0.78.0 |
| Muun | 57,144 | 28,469 | 85,613 | 2.33M | 3.5× · 2.1× | None embedded |
| Phoenix | 72,881 | — | 72,881 | 1.90M | 2.9× · 1.8× | 28,779 ACINQ/lightning-kmp v1.13.2 |
| Bitkit | 114,604 | — | 114,604 | 3.35M | 4.6× · 3.1× | 128,396 synonymdev/ldk-node 0.7.0-rc.78, lightningdevkit/rust-lightning v0.2.7 |
| Blixt | 36,740 | — | 36,740 | 0.96M | 1.5× · 0.9× | 238,947 hsjoberg/lnd@ae185f2 (0.21.1) |
| Zeus | 194,646 | — | 194,646 | 3.82M | 7.8× · 3.5× | 243,921 ZeusLN/lnd v0.21.5-beta-zeus |
“vs Winnow” divides each total by Winnow’s, first in lines, then in characters. The engine column shows each engine at the version the app pins, so its size can be read beside the wallet’s.
Languages
Share of each wallet’s lines, app, own Bitcoin library and embedded engine together, since all of them ship in the app. Winnow and LeanNative are Lean, apart from LeanNative’s small C shim; most of the others combine a user-interface language with a library or engine in another.
| Wallet | Lean | Swift | Kotlin | Rust | Go | TypeScript/JS | C family |
|---|---|---|---|---|---|---|---|
| Winnow | 100% | — | — | — | — | — | <1% |
| BlueWallet | — | 6% | — | — | — | 94% | <1% |
| Green | — | 68% | — | 9% | — | — | 23% |
| Muun | — | 67% | — | <1% | 33% | — | <1% |
| Phoenix | — | 60% | 40% | — | — | — | 1% |
| Bitkit | — | 47% | — | 53% | — | — | — |
| Blixt | — | <1% | — | — | 87% | 13% | <1% |
| Zeus | — | 1% | — | — | 56% | 43% | <1% |
Complexity and CRAP
CRAP scores each function by its cyclomatic complexity (CC, the number of paths through it) and how much of it the tests run: CRAP = CC² × (1 − coverage)³ + CC. Coverage needs each project’s test suite run with instrumentation, so it is not measured here. Testing can only bring CRAP down to CC, so complexity alone sets a floor: a function above 12 scores above 12 however well it is tested, and one above 30 fails crap4j’s default threshold whatever its tests. Complexity is measured on each wallet’s counted code, not on embedded engines.
4.3% of Winnow’s 3,001 functions are above 12, against 0.8–7.9% for the others. Its mean is 3.7; the others’ range from 1.9 to 5.3. Its most complex function, parseDouble, scores 44, and 11 of its functions are above 30. Winnow’s app follows the Elm architecture, and its update dispatchers, split by screen, come next: match expressions with an arm for each message, which this rule counts as paths.
| Wallet | Functions | Mean CC | 90th pct | Max | CRAP above 12 at any coverage | Share above 12, relative to the highest | CRAP above 30 at any coverage |
|---|---|---|---|---|---|---|---|
| Winnow | 3,001 | 3.7 | 8 | 44 | 130 (4.3%) | 11 (0.37%) | |
| BlueWallet | 1,821 | 5.3 | 10 | 262 | 143 (7.9%) | 45 (2.47%) | |
| Green | 6,905 | 2.2 | 4 | 45 | 85 (1.2%) | 7 (0.10%) | |
| Muun | 6,572 | 1.9 | 4 | 26 | 52 (0.8%) | 0 (0.00%) | |
| Phoenix | 4,703 | 2.5 | 5 | 155 | 77 (1.6%) | 9 (0.19%) | |
| Bitkit | 6,900 | 3.2 | 6 | 111 | 168 (2.4%) | 26 (0.38%) | |
| Blixt | 1,067 | 3.6 | 7 | 139 | 58 (5.4%) | 19 (1.78%) | |
| Zeus | 5,074 | 4.7 | 10 | 246 | 372 (7.3%) | 78 (1.54%) |
By part: app and own Bitcoin library
| Part | Functions | Mean CC | 90th pct | Max | CC > 12 | CC > 30 | Most complex |
|---|---|---|---|---|---|---|---|
| Winnow · app | 982 | 4.2 | 10 | 44 | 68 | 7 | parseDouble |
| Winnow · own bitcoin library | 2,019 | 3.4 | 8 | 34 | 62 | 4 | replenish |
| BlueWallet · app | 1,821 | 5.3 | 10 | 262 | 143 | 45 | (arrow_function) |
| Green · app | 6,905 | 2.2 | 4 | 45 | 85 | 7 | validateTransaction |
| Muun · app | 5,094 | 1.6 | 3 | 22 | 19 | 0 | getKindOfError |
| Muun · own bitcoin library | 1,478 | 2.8 | 6 | 26 | 33 | 0 | do |
| Phoenix · app | 4,703 | 2.5 | 5 | 155 | 77 | 9 | (computed_property) |
| Bitkit · app | 6,900 | 3.2 | 6 | 111 | 168 | 26 | (computed_property) |
| Blixt · app | 1,067 | 3.6 | 7 | 139 | 58 | 19 | Settings |
| Zeus · app | 5,074 | 4.7 | 10 | 246 | 372 | 78 | render |
Machine-checked proofs
Lean is also a proof assistant, so part of Winnow’s code comes with theorems that Lean’s kernel checks on every build. Tests check examples; a theorem covers every input. The other wallets are written in languages without proofs, so this section has nothing to compare them on.
Theorems in Winnow’s code
872 lines, all in its Bitcoin library’s secp256k1: limb and field arithmetic, the curve and its Jacobian formulas. Secp/Proofs/Gate. lists them, and the build fails if one rests on sorry or an extra axiom.
Theorems in LeanNative
303 lines, most of them about how the runtime steps an app: each step runs update on one message and asks for exactly that message’s commands, whatever the order of events. The Objective-C renderer and platform effects are not proved; they are trusted and tested.
What the theorems leave out
The wallet logic, networking and app have tests, not proofs. Signing runs on the proved arithmetic but is not constant-time: its timing depends on the key (Keys/Secp256k1.).
Proofs are erased when Lean compiles, so they add nothing to the app. They are counted as lines in the code size above, and are not functions in the complexity table.
Dependencies and vendored code
Winnow resolves no third-party package: its Lake manifest pins only LeanNative, the framework counted above as its engine. Like every Lean program it links Lean’s runtime and standard library from the toolchain, which bring libuv and GMP; they are under prebuilt binaries below. The others depend on 58 to 1,447 packages, counting what their embedded engines pull in, because that code ships in the app too. Package counts from different ecosystems are not equal units: an npm package is often a few hundred lines, a Rust crate or Go module often more. Read them per row.
| Kind | Winnow | BlueWallet | Green | Muun | Phoenix | Bitkit | Blixt | Zeus |
|---|---|---|---|---|---|---|---|---|
| Swift packages | — | — | 31 | — | 19 | 5 | — | — |
| Lean packages (Lake) | — | — | — | — | — | — | — | — |
| Kotlin libraries | — | — | — | — | 39 | — | — | — |
| Rust crates, inside prebuilt libraries | — | — | 126 | — | — | 715 | — | 411 |
| Go modules, inside the Go library | — | — | — | 111 | — | — | 120 | 136 |
| npm packages | — | 901 | — | — | — | — | 552 | 897 |
| CocoaPods from the trunk | — | 2 | — | 28 | — | — | 8 | 3 |
| Third-party packages | 0 | 903 | 157 | 139 | 58 | 720 | 680 | 1,447 |
| Wallet | Main libraries | Bitcoin code not counted above | Prebuilt binaries | Third-party code copied into the repository | Patched dependencies |
|---|---|---|---|---|---|
| Winnow | LeanNative 700b388 (its Lean framework, C shim and printed Objective-C shell) and Lean 4.35.0's runtime and standard library (libLean, libStd, libInit, libleanrt) with the toolchain's libuv and GMP; no package from a registry lakefile. lean-toolchain lake-manifest. ios_lean_sources. Manifest. | — | None in the repository; the build links the Lean toolchain's static libraries (libLean, libStd, libInit, libleanrt, libuv, libgmp), macOS arm64 objects retargeted to iOS by LeanNative's retarget_ar.py ios_lean_sources. ios_lean_sources. ios_lean_sources. | None | No patch files; the build rewrites the platform (LC_BUILD_VERSION) of Lean's macOS static libraries to iOS, which Lean does not support |
| BlueWallet | bitcoinjs-lib, bip32, bip39, ecpair, @noble/secp256k1, BlueWallet forks of coinselect, rn-electrum-client, bip47, SilentPayments and slip39-js, aezeed, bolt11, payjoin-client, bc-ur (Keystone and NGRAVE), realm, react-native-keychain, @arkade-os/sdk and boltz-swap, Bugsnag. package. | bitcoinjs-lib, bip32 and @noble/secp256k1, from npm | No binaries are checked in. The build links React Native's prebuilt Hermes and React-Core-prebuilt pods, and the patched react-native-background-fetch pod vendors the npm package's TSBackgroundFetch.xcframework. Podfile. Podfile. react-native-background-fetch+4. | 5,136 lines in blue_modules: pako, bbqr, uint8array-extras, bc-ur, bc-bech32 | Four patch-package patches on npm packages: react-native-background-fetch (iOS xcframework with Catalyst), react-native-notifications (Android token), react-native-context-menu-view (Android menus), react-native-screens (iOS accessibility id; patch is for 4.27.0 while package.json pins 4.28.0). Several dependencies also come from BlueWallet forks pinned to git commits. |
| Green | gdk (libwally-core, rust-bitcoin, Tor), Liquid Wallet Kit, Greenlight SDK, Firebase, Countly, Zendesk, Rive, Charts, SwiftCBOR, AsyncBluetooth Package. Package. Package. | libwally-core, rust-bitcoin, rust-elements (inside the prebuilt gdk) | gdk static library (libgreen_gdk_full.a) from Blockstream's release tarballs, checked against pinned SHA-256 sums; Google's grpc-binary and abseil-cpp-binary packages fetch_gdk_binaries. | None | Countly's iOS SDK comes from a fork (angelix/countly-sdk-ios 23.6.1-ave). gdk's own build patches libevent and bc-ur, outside the app. |
| Muun | Swift: Firebase (Analytics, Crashlytics, Messaging), GoogleSignIn and Google Drive API, GRDB.swift, RxSwift, Dip, grpc-swift, Lottie. Go: btcd, lnd (lnwire, zpay32), lightning-onion, go-lnurl, gofpdf, pdfcpu, gorm with SQLite, gRPC. Podfile Package. go. | btcd and lnd, as Go modules. Copies inside libwallet (btcsuitew, musig2v040) are counted | Rust zero-knowledge proof verifier (plonky2) linked as the static library multi-apple-ios-librs.a, built by libwallet/librs/makelibs.sh and not in the tree; FirebaseAnalytics and GoogleAppMeasurement xcframeworks from dl.google.com librs. pods_dependency_registry. pods_dependency_registry. | 101,694 lines in falcon/app/Pods: GRDB.swift, FirebaseCrashlytics, RxSwift, GTMSessionFetcher, FirebaseMessaging, GoogleAPIClientForREST and 19 more | Go 1.24.7 toolchain is patched for cgo struct alignment (falcon/0001-align-to-ptrsize.patch); golang.org/x/mobile is replaced by the muun/mobile fork (libwallet/go.mod:200) |
| Phoenix | bitcoin-kmp with secp256k1-kmp, Ktor, kotlinx-serialization, SQLDelight (Kotlin side); Firebase Messaging and a few small SwiftUI packages (Swift side) build. build. project. | bitcoin-kmp | Kotlin libraries (lightning-kmp, bitcoin-kmp, secp256k1-kmp native code, Ktor, SQLDelight) as compiled klibs from Maven Central; Firebase through Swift Package Manager build. build. | None | None |
| Bitkit | LDK Node (with BDK), bitkit-core, Paykit, VSS client, Lottie, swift-secp256k1 project. | BDK, rust-bitcoin | LDK Node, bitkit-core, Paykit and the VSS client, as XCFrameworks Package. Package. Package. Package. | None | None |
| Blixt | React Native, react-native-turbo-lnd, @bufbuild/protobuf, secp256k1, bech32, react-native-nitro-tor, react-native-keychain, easy-peasy package. package. package. package. package. | btcwallet (with btcd, neutrino) under LND | Lndmobile.xcframework (LND), downloaded from the react-native-turbo-lnd release for the pinned LND commit react-native-turbo-lnd. | None | None in the app (no patch files); LND is Blixt's own fork (hsjoberg/lnd) with an added gossip speedloader |
| Zeus | React Native, bitcoinjs-lib, noble and scure cryptography, bc-ur registries (Keystone, NGRAVE), Nostr (NDK), react-native-nitro-tor, Cashu CDK package. package. package. package. package. package. package. package. | btcwallet (with btcd, neutrino) under LND; bitcoinjs-lib; BDK in the optional LDK Node | Lndmobile.xcframework (LND), LDKNodeFFI.xcframework (LDK Node), and the Cashu CDK and restore xcframeworks, downloaded from release archives pinned by SHA-256 fetch-libraries-versions. fetch-libraries. | 4,043 lines in zeus_modules, zeus_modules/@lightninglabs: lnc-core, lnc-rn, noble_ecc | Six postinstall scripts rewrite installed npm packages (two are Android-only: jcenter and notifications; four reach iOS: NativeEventEmitter, keychain cloudSync, noble-hashes exports, reanimated C++); rn-nodeify shims crypto, stream and dgram |
- Lean packages are the git packages in the app’s
lake-manifest.json, less the framework counted as its engine. - Swift packages are every package Swift Package Manager resolves. Phoenix links Firebase Messaging, which brings in Google’s package graph.
- Kotlin libraries are the 39 libraries Phoenix’s iOS framework links besides lightning-kmp, resolved for the iPhone target.
- Rust crates are compiled into prebuilt libraries. Bitkit’s four libraries each carry their own copies: 715 distinct crates, 1,122 counting each library’s copy. Green’s gdk adds 126. gdk’s C++ dependencies are not counted.
- Go modules are linked into Muun’s libwallet (111), and into LND in Blixt and Zeus.
- npm packages are everything the app’s runtime dependencies pull in (98 declared by BlueWallet, 86 by Blixt, 109 by Zeus), not build tools. The bundler includes only what the app imports.
- CocoaPods counts pods from the CocoaPods trunk. Another 106 pods in BlueWallet, 98 in Blixt and 114 in Zeus are the native halves of npm packages and React Native itself.
- Muun commits the sources of its CocoaPods into its repository: 101,694 lines of third-party code that build into the app, with a registry of each pod’s upstream and checksum.
Over time
Wallet repositories are read at the head of their main branch, except Phoenix and Winnow, which are read at their latest release (ios-v2.8.4, v0.9.10); Phoenix’s default branch was configured for testnet when we checked. Every engine and library is read at the version its wallet pins. A snapshot is recorded whenever any of them moves: 14 since 2 October 2026, when this page began counting Bitcoin wallets only. Changes are shown against that first snapshot. On 30 October 2026 Winnow’s measured source moved from its Swift app to its Lean 4 rewrite, so its line jumps there. Hover or focus a line for each snapshot. The 4 earlier snapshots (27 September 2026 to 1 October 2026) compared Lightning wallets and counted their Lightning engines; they are in data.json.
| Wallet | Lines of code | Third-party packages | Functions above CC 12 |
|---|---|---|---|
| Winnow | 24,808+2,811 (+12.8%) | 0−1 (−100.0%) | 130+106 (+441.7%) |
| BlueWallet | 59,121+1,496 (+2.6%) | 903−27 (−2.9%) | 143+4 (+2.9%) |
| Green | 70,474no change | 157no change | 85no change |
| Muun | 85,613no change | 139no change | 52no change |
| Phoenix | 72,881no change | 58no change | 77no change |
| Bitkit | 114,604+20,915 (+22.3%) | 720+29 (+4.2%) | 168+76 (+82.6%) |
| Blixt | 36,740+380 (+1.0%) | 680−1 (−0.1%) | 58+2 (+3.6%) |
| Zeus | 194,646+4,662 (+2.5%) | 1,447−20 (−1.4%) | 372+27 (+7.8%) |
Every snapshot
| Snapshot | Winnow | BlueWallet | Green | Muun | Phoenix | Bitkit | Blixt | Zeus | Sources that moved |
|---|---|---|---|---|---|---|---|---|---|
| 7 November 2026 | 24,808 | 59,121 | 70,474 | 85,613 | 72,881 | 114,604 | 36,740 | 194,646 | zeus, bluewallet |
| 6 November 2026 | 24,808 | 59,069 | 70,474 | 85,613 | 72,881 | 114,604 | 36,740 | 194,447 | bitkit, zeus, bluewallet |
| 4 November 2026 | 24,808 | 59,067 | 70,474 | 85,613 | 72,881 | 113,066 | 36,740 | 194,004 | bitkit, zeus, bluewallet |
| 3 November 2026 | 24,808 | 59,075 | 70,474 | 85,613 | 72,881 | 112,906 | 36,740 | 193,516 | winnow, leannative, bitkit, synonym-ldk-node, rust-lightning, bitkit-core, paykit-rs, vss-rust-client-ffi, blixt, zeus, lnd-zeus, zeus-ldk-node, cdk, bluewallet |
| 30 October 2026 | 24,683 | 57,858 | 70,474 | 85,613 | 72,881 | 100,640 | 36,360 | 191,851 | winnow, leannative |
| 10 October 2026 | 21,997 | 57,858 | 70,474 | 85,613 | 72,881 | 100,640 | 36,360 | 191,851 | bitkit, paykit-rs, zeus |
| 9 October 2026 | 21,997 | 57,858 | 70,474 | 85,613 | 72,881 | 100,239 | 36,360 | 190,159 | bitkit, zeus, lnd-zeus |
| 8 October 2026 | 21,997 | 57,858 | 70,474 | 85,613 | 72,881 | 99,671 | 36,360 | 190,500 | bitkit, paykit-rs, zeus, bluewallet |
| 7 October 2026 | 21,997 | 57,858 | 70,474 | 85,613 | 72,881 | 98,068 | 36,360 | 190,456 | bitkit, zeus, bluewallet |
| 6 October 2026 | 21,997 | 57,816 | 70,474 | 85,613 | 72,881 | 97,921 | 36,360 | 190,382 | bitkit, zeus |
| 5 October 2026 | 21,997 | 57,816 | 70,474 | 85,613 | 72,881 | 94,774 | 36,360 | 190,363 | bitkit, zeus, zeus-ldk-node, bluewallet |
| 4 October 2026 | 21,997 | 57,816 | 70,474 | 85,613 | 72,881 | 93,689 | 36,360 | 190,001 | zeus, bluewallet |
| 3 October 2026 | 21,997 | 57,625 | 70,474 | 85,613 | 72,881 | 93,689 | 36,360 | 190,005 | winnow, zeus, lnd-zeus |
| 2 October 2026 | 21,997 | 57,625 | 70,474 | 85,613 | 72,881 | 93,689 | 36,360 | 189,984 | First snapshot |
| Snapshot | Winnow | BlueWallet | Green | Muun | Phoenix | Bitkit | Blixt | Zeus |
|---|---|---|---|---|---|---|---|---|
| 7 November 2026 | 0 | 903 | 157 | 139 | 58 | 720 | 680 | 1,447 |
| 6 November 2026 | 0 | 903 | 157 | 139 | 58 | 720 | 680 | 1,447 |
| 4 November 2026 | 0 | 903 | 157 | 139 | 58 | 720 | 680 | 1,452 |
| 3 November 2026 | 0 | 904 | 157 | 139 | 58 | 720 | 680 | 1,452 |
| 30 October 2026 | 0 | 924 | 157 | 139 | 58 | 698 | 681 | 1,457 |
| 10 October 2026 | 1 | 924 | 157 | 139 | 58 | 698 | 681 | 1,457 |
| 9 October 2026 | 1 | 924 | 157 | 139 | 58 | 698 | 681 | 1,457 |
| 8 October 2026 | 1 | 924 | 157 | 139 | 58 | 698 | 681 | 1,457 |
| 7 October 2026 | 1 | 930 | 157 | 139 | 58 | 691 | 681 | 1,457 |
| 6 October 2026 | 1 | 930 | 157 | 139 | 58 | 691 | 681 | 1,466 |
| 5 October 2026 | 1 | 930 | 157 | 139 | 58 | 691 | 681 | 1,466 |
| 4 October 2026 | 1 | 930 | 157 | 139 | 58 | 691 | 681 | 1,466 |
| 3 October 2026 | 1 | 930 | 157 | 139 | 58 | 691 | 681 | 1,466 |
| 2 October 2026 | 1 | 930 | 157 | 139 | 58 | 691 | 681 | 1,467 |
| Snapshot | Winnow | BlueWallet | Green | Muun | Phoenix | Bitkit | Blixt | Zeus |
|---|---|---|---|---|---|---|---|---|
| 7 November 2026 | 130 | 143 | 85 | 52 | 77 | 168 | 58 | 372 |
| 6 November 2026 | 130 | 143 | 85 | 52 | 77 | 168 | 58 | 372 |
| 4 November 2026 | 130 | 143 | 85 | 52 | 77 | 166 | 58 | 370 |
| 3 November 2026 | 130 | 143 | 85 | 52 | 77 | 164 | 58 | 368 |
| 30 October 2026 | 117 | 141 | 85 | 52 | 77 | 109 | 56 | 350 |
| 10 October 2026 | 24 | 141 | 85 | 52 | 77 | 109 | 56 | 350 |
| 9 October 2026 | 24 | 141 | 85 | 52 | 77 | 105 | 56 | 345 |
| 8 October 2026 | 24 | 141 | 85 | 52 | 77 | 101 | 56 | 347 |
| 7 October 2026 | 24 | 141 | 85 | 52 | 77 | 96 | 56 | 347 |
| 6 October 2026 | 24 | 141 | 85 | 52 | 77 | 96 | 56 | 347 |
| 5 October 2026 | 24 | 141 | 85 | 52 | 77 | 93 | 56 | 347 |
| 4 October 2026 | 24 | 141 | 85 | 52 | 77 | 92 | 56 | 347 |
| 3 October 2026 | 24 | 139 | 85 | 52 | 77 | 92 | 56 | 346 |
| 2 October 2026 | 24 | 139 | 85 | 52 | 77 | 92 | 56 | 345 |
What the numbers leave out
Less code is not proof of safety
A smaller codebase is quicker to read and review. It does not show the code is correct, and it says nothing about whether anyone independent has reviewed it. Check each project’s own security page.
The wallets do different amounts of work
Each count is the whole app, not only its on-chain wallet. Zeus also manages remote LND and Core Lightning nodes and includes Cashu, Nostr Wallet Connect and a point of sale. Bitkit includes Pubky contacts, Paykit and Trezor support. Winnow includes k-of-n multisig accounts and a Ledger signer. BlueWallet includes Lightning wallet types (an LNDHub custodian and an Ark one). Green’s count includes its lightning folder. Muun includes Lightning payments through swaps.
Engines are not counted
Phoenix, Bitkit, Blixt, Zeus and Green build their Bitcoin wallet on an engine that is a separate project: lightning-kmp, LDK Node with BDK, LND and gdk, so they look smaller than everything they ship. Winnow’s engine is a UI framework, LeanNative; its Bitcoin code is its own and counted, as Muun’s is. BlueWallet has no engine.
Who can move on-chain funds
Where a service has to cosign, the wallet cannot spend without it, or only after a delay. The fourth column of the table at the top says which wallets that applies to, and the notes under each cell give the account type.
How we measured
- Lines: cloc 2.06 counted non-blank, non-comment lines in each app’s programming languages, for code that builds into the iPhone app. Characters were counted after removing comments and all whitespace. Excluded: tests; Android, desktop and web code; build tools; files marked as generated (protobuf, sqlc and UniFFI bindings); and vendored third-party code. Rust test modules inside source files were removed. Winnow’s test oracles, test vectors and UI tests are left out by name; Lean proofs are counted as lines.
- Complexity: each file was parsed with tree-sitter, and one rule applied to every language. CC is 1, plus 1 for each
iforguard, loop,catch, ternary,&&,||,??or elvis, and non-defaultcase,whenormatcharm. Closures, lambdas and nested functions count toward the function that contains them, so a React screen written as one function component counts as one function. Tree-sitter could not fully parse at most 5% of any wallet’s files; it recovers locally, so their functions are still counted. No tree-sitter grammar parses current Lean, so Lean is read lexically under the same rule: eachdef,instance,abbrevoropaqueis a function, and theorems are not. - Dependencies: read from each app’s own lockfiles (Swift Package Manager, Lake, CocoaPods, npm, yarn and bun). Kotlin libraries were resolved from Maven Central’s metadata for the iPhone target. Rust crates come from
cargo treeforaarch64-apple-iosat each prebuilt library’s pinned source. Go modules come fromgo list -depsfor iOS, with the build tags LND’s own iOS build uses or, for Muun, none. - Vendored code: each repository was searched for third-party code committed into it: vendor directories, committed CocoaPods, copied npm packages, license files below the root, committed binaries and dependency patches.
- Features: each cell was checked by hand in the app’s and the engine’s source between 2 October 2026 and 3 November 2026, at the revisions in
reviewed.json. The build compares every cited passage with today’s code and lists any that have changed. The rows and what each value means are infeatures/rows.json. - Embedded engines: counted at the version each app pins, as the named library only, and kept out of every total. Winnow’s LeanNative (
LeanNative/leannative@700b388) is the commit itslake-manifest.jsonpins: its Lean framework, the Lean that prints the shell’s Objective-C, and its C shim. Phoenix’s lightning-kmp comes from its Gradle catalog. Bitkit’s LDK Node and libraries come from its Package.resolved, and rust-lightning from LDK Node’s lockfile. Blixt’s LND is the fork its react-native-turbo-lnd release builds. Zeus’s LND, LDK Node and Cashu versions come from itsfetch-libraries-versions.json, and its count uses its default embedded LND. Green’s gdk is the release itsfetch_gdk_binaries.shdownloads.
Exact sources
| Wallet | App source | Embedded engines | Prebuilt libraries resolved for dependencies |
|---|---|---|---|
| Winnow | winnowwallet/winnow-lean v0.9.10 | LeanNative/leannative@700b388 | — |
| BlueWallet | BlueWallet/BlueWallet@3c50040 | In the same repository | — |
| Green | Blockstream/green_ios@131f820 | Blockstream/gdk release_0.78.0 | — |
| Muun | muun/falcon@d6719c0 | In the same repository | — |
| Phoenix | ACINQ/phoenix ios-v2.8.4 | ACINQ/lightning-kmp v1.13.2 | — |
| Bitkit | synonymdev/bitkit-ios@90949e4 | synonymdev/ldk-node 0.7.0-rc.78, lightningdevkit/rust-lightning v0.2.7 | synonymdev/bitkit-core 0.8.0-rc11, pubky/paykit-rs 0.1.1-rc2, synonymdev/vss-rust-client-ffi 0.5.26-rc1 |
| Blixt | hsjoberg/blixt-wallet@b0408dd | hsjoberg/lnd@ae185f2 (0.21.1) | — |
| Zeus | ZeusLN/zeus@eb27332 | ZeusLN/lnd v0.21.5-beta-zeus | ZeusLN/ldk-node v0.7.1-zeus, cashubtc/cdk v0.18.2 |
Each project changes often. These numbers are measured again whenever a pinned source moves, and each snapshot is dated. They are a measurement, not a ranking.
Reproduce it
Everything on this page is generated from the source repository. It pins every project to an exact commit, and data.json holds every measurement, including the full package lists. To rebuild it:
git clone https://github.com/winnowwallet/compare
cd compare
npm ci && pip install -r requirements.txt
./build # fetch the pinned sources, measure, render
git diff # nothing changes if the measurement reproduces
It needs Python 3.11, Node, Go and Cargo, plus network access to GitHub, crates.io, the Go module proxy and Maven Central; no Lean toolchain is needed. Feature cells and profile facts are reviewed by hand. The build checks that every citation resolves at the revision it was reviewed against, and reports which cited code has changed since.
Not compared
This comparison needs the iPhone app’s source, so closed-source and custodial wallets cannot be measured and are left out, however widely they are used. Other open-source wallets could be added.
- Exodus, Cash App and Strike are widely used iPhone wallets whose apps are closed source, so there is no code to measure or cite.
- Envoy, Bull Bitcoin, Bitcoin Keeper, Cake Wallet and Edge publish source code and may be added. Each needs a measured iPhone build and a hand review of its features.