Winnow compare · Lightning on iPhone
Lightning wallets compared
Winnow is adding Lightning. Here is how it compares with the open-source iPhone wallets that already run a Lightning node on the phone: features, code size, complexity and what each one depends on.
At a glance
Each wallet below holds its own keys and runs a Lightning node inside the app. The table shows defaults; most of these wallets let you change the chain source or the channel partner.
| Wallet | Built with | Lightning engine | Chain data | Channel partner | On iPhone |
|---|---|---|---|---|---|
| Winnow | Swift | Its own, in Swift, in the same repository as the wallet | Compact block filters (BIP157/158) from Bitcoin peers Winnow finds itself README. | Your choice of Olympus, Megalith or LNServer LightningProviders. | Research beta, internal TestFlight |
| Phoenix | Swift on Kotlin Multiplatform | lightning-kmp 1.12 (Kotlin) | An Electrum server picked at random from a built-in list of 15 AppConfigurationManager. | ACINQ NodeParamsManager. | App Store |
| Bitkit | Swift | LDK Node (Synonym’s fork) on rust-lightning 0.2 (Rust) | Synonym’s Electrum server Env. | Blocktank, run by Synonym Env. | App Store |
| Blixt | React Native (TypeScript) | Embedded LND (Go) | Neutrino (BIP157) through six default peers, four run by Blixt constants. | Dunder, run by Blixt, or any node constants. | TestFlight |
| Zeus | React Native (TypeScript) | Embedded LND 0.21 (Go). LDK Node is an option, and Zeus can also drive a remote node. | Neutrino (BIP157) through five default peers, two run by Olympus; the LDK Node option uses an Esplora server SettingsStore. | Olympus, run by Zeus, or any node SettingsStore. | App Store |
Features
What a person can do in each iPhone app today, checked against the code. “Engine only” means the Lightning library supports it but the app does not offer it. Hover or focus a cell for the note; every cell links to its evidence below.
| Feature | Winnow | Phoenix | Bitkit | Blixt | Zeus |
|---|---|---|---|---|---|
| Paying and receiving | |||||
| Pay a BOLT 11 invoice | No | Yes | Yes | Yes | Yes |
| Receive with a BOLT 11 invoice | Yes | Yes | Yes | Yes | Yes |
| Pay a BOLT 12 offer | Partial | Yes | Engine only | No | No |
| Receive with a reusable BOLT 12 offer | Partial | Yes | Engine only | No | No |
| Receive while the app is closed | Partial | Yes | Partial | No | Partial |
| LNURL | No | Yes | Yes | Yes | Yes |
| Pay a Lightning address | No | Yes | Yes | Yes | Yes |
| Your own Lightning address | No | Partial | No | No | Partial |
| Pay a BIP 353 name | No | Yes | No | No | Partial |
| Multipath payments | No | Partial | Yes | Yes | Yes |
| Keysend | No | No | Engine only | Yes | Yes |
| Channels and routing | |||||
| Choose your channel partner | Yes | No | Yes | Yes | Yes |
| Inbound channels from a provider | Partial | Yes | Yes | Yes | Yes |
| Splicing | No | Yes | Engine only | No | No |
| Anchor channels | No | Yes | Yes | Yes | Yes |
| Finds its own routes | No | No | Yes | Yes | Yes |
| Safety and recovery | |||||
| Watchtower | No | Partial | No | Engine only | Yes |
| Channel backup off the phone | No | Yes | Yes | Yes | Yes |
| On-chain and privacy | |||||
| On-chain funds without the channel partner | Yes | Partial | Yes | Yes | Yes |
| Taproot receive addresses | Yes | Yes | Yes | Yes | Yes |
| Multisig or shared accounts | Yes | No | No | No | No |
| Hardware or external signer | Partial | No | Yes | Engine only | Yes |
| Coin control | No | No | Yes | Engine only | Yes |
| Built-in Tor | No | Partial | No | Yes | No |
| License | |||||
| Source license | MIT | Apache-2.0 | MIT | MIT | AGPL-3.0 |
Yes · Partial · Engine only · No
Notes and code evidence for every cell
Links open the exact line at the revision measured.
Winnow
- Pay a BOLT 11 invoice: No. Send accepts only lno offers; setup invoices must be paid elsewhere.
LightningAppPayments.swift :15 LightningLiquidityView.swift :68 - Receive with a BOLT 11 invoice: Yes. Single-use invoice with provider route hint; app must stay open.
LightningReceiveView.swift :34 LightningAppReceiving.swift :89 - Pay a BOLT 12 offer: Partial. Only async static-invoice flow; needs imported profile route, async provider.
LightningAppPayments.swift :22 LightningAsyncSending.swift :14 - Receive with a reusable BOLT 12 offer: Partial. Reusable offer needs imported async-provider profile; mainnet presets lack receive config.
LightningView.swift :117 LightningProviders.swift :16 - Receive while the app is closed: Partial. LDK-style async hold/release via imported profile only; no push.
LightningInputViews.swift :31 swift-lightning.md :68 - LNURL: No. No LNURL code; send input parses BOLT12 offers only.
LightningAppPayments.swift :15 - Pay a Lightning address: No. No Lightning address resolution; only lno offers accepted.
LightningAppPayments.swift :15 - Your own Lightning address: No. Receive offers BOLT11 invoice or BOLT12 offer only.
LightningReceiveView.swift :34 LightningView.swift :117 - Pay a BIP 353 name: No. No DNS/BIP353 resolution code found.
LightningAppPayments.swift :15 - Multipath payments: No. Features explicitly exclude MPP.
LightningFeatures.swift :30 Bolt11Invoice.swift :37 - Keysend: No. Keysend TLV used only internally for async payments; no user keysend.
LightningInputViews.swift :76 AsyncPaymentRoute.swift :62 - Choose your channel partner: Yes. Olympus by ZEUS, Megalith, LNServer Wave, or import any node profile; one channel.
LightningProviders.swift :19 LightningInputViews.swift :30 - Inbound channels from a provider: Partial. LSPS1 inbound purchase (Olympus, Megalith); fee paid externally; no LSPS2.
LightningAppReceiving.swift :34 LightningLiquidityView.swift :68 - Splicing: No. Docs: splicing not implemented or advertised.
README.md :135 - Anchor channels: No. Static-remotekey non-anchor channels only.
README.md :135 ChannelTransactions.swift :4 - Finds its own routes: No. Configured-route client; gossip ignored; no graph routing.
LightningPeerSession.swift :137 README.md :136 - Watchtower: No. No external watchtower; iOS background checks relay pre-signed recovery txs.
swift-lightning.md :237 LightningView.swift :21 - Channel backup off the phone: No. Journal stays on device; iCloud backup disabled in Lightning app.
README.md :129 AppModel.swift :425 - On-chain funds without the channel partner: Yes. Single-key BIP86 Taproot wallet; channels are funded from it, not the reverse.
Wallet.swift :668 README.md :37 - Taproot receive addresses: Yes. Ordinary receive uses BIP86 P2TR.
README.md :8 Wallet.swift :722 - Multisig or shared accounts: Yes. Threshold shared savings (e.g. 2-of-3) and MuSig2 accounts.
README.md :9 VaultsView.swift :72 - Hardware or external signer: Partial. PSBT/MuSig2 cosigning with another device (Core tested); hardware wallets unverified.
VaultsView.swift :72 VaultSpendSession.swift :403 - Coin control: No. Automatic coin selection over all spendable UTXOs; coins list read-only.
AppModel.swift :1842 - Built-in Tor: No. Embedded Tor removed in 0.7.1.
README.md :11 WinnowApp.swift :39
Phoenix
- Pay a BOLT 11 invoice: Yes. payBolt11Invoice sends PayInvoice to peer via trampoline.
SendManager.kt :453 ValidateView.swift :1867 - Receive with a BOLT 11 invoice: Yes. 'Single use' tab generates BOLT11 invoice via peer.createInvoice.
ReceiveController.kt :48 LightningDualView.swift :342 - Pay a BOLT 12 offer: Yes. payBolt12Offer sends PayOffer command.
SendManager.kt :477 ValidateView.swift :1912 - Receive with a reusable BOLT 12 offer: Yes. 'Reusable' tab shows deterministic BOLT12 offer.
LightningDualView.swift :343 LightningDualView.swift :710 - Receive while the app is closed: Yes. Push notification wakes service extension, which connects and receives payment (incl. BOLT12).
NotificationService.swift :423 NodeParams.kt :240 - LNURL: Yes. LNURL-pay, LNURL-withdraw, and LNURL-auth (LoginView) supported.
ValidateView.swift :2094 ValidateView.swift :2141 - Pay a Lightning address: Yes. user@domain resolved via LNURL, with BIP353 tried first for ambiguous input.
SendManager.kt :267 - Your own Lightning address: Partial. Experimental menu: claim BIP353 address for your BOLT12 offer; not an LNURL address.
Experimental.swift :224 Peer.kt :884 - Pay a BIP 353 name: Yes. DNS TXT lookup over DoH, checks DNSSEC AD flag, extracts offer.
SendManager.kt :283 - Multipath payments: Partial. Receives multipart; sends a single part over one channel to the trampoline.
IncomingPaymentHandler.kt :61 OutgoingPaymentHandler.kt :277 - Keysend: No. Only PayInvoice/PayOffer; no keysend code in app or lightning-kmp.
SendManager.kt :13 - Choose your channel partner: No. Only peer is the hardcoded ACINQ trampoline node.
NodeParamsManager.kt :87 - Inbound channels from a provider: Yes. ACINQ opens/splices channels on the fly; manual inbound liquidity purchase via liquidity ads.
NodeParams.kt :241 LiquidityAdsView.swift :926 - Splicing: Yes. On-chain sends are splice-outs; swap-in deposits are spliced in automatically.
ValidateView.swift :1970 Peer.kt :1477 - Anchor channels: Yes. Anchor-outputs format; new channels are simple-taproot, which also have anchors.
Transactions.kt :86 Peer.kt :1560 - Finds its own routes: No. No network graph; all payments routed by ACINQ trampoline node.
OutgoingPaymentHandler.kt :90 - Watchtower: Partial. No remote watchtower client; app wakes in background to check for revoked commitments.
WatchTower.swift :336 - Channel backup off the phone: Yes. Encrypted channel backup kept by ACINQ peer (peer storage); restorable from seed.
NodeParams.kt :158 NodeParams.kt :281 - On-chain funds without the channel partner: Partial. Deposits are 2-of-2 with ACINQ, yours alone after about six months; sends are splice-outs with ACINQ.
SwapInProtocol.kt :11 NodeParams.kt :94 ValidateView.swift :1576 - Taproot receive addresses: Yes. Swap-in receive address is P2TR by default (legacy option available).
SwapInProtocol.kt :25 SwapInView.swift :28 - Multisig or shared accounts: No. Single-seed wallet; 2-of-2 swap-in with ACINQ is internal, not user multisig.
WalletManager.kt :48 - Hardware or external signer: No. Keys come from a LocalKeyManager built from the local seed; no PSBT/hardware wallet code.
WalletManager.kt :48 - Coin control: No. On-chain spends are splice-outs or 'send all'; no UTXO selection.
SpendOnChainFunds.swift :173 - Built-in Tor: Partial. Tor toggle uses .onion endpoints but needs external Orbot VPN; not bundled.
TorConfigurationView.swift :66
Bitkit
- Pay a BOLT 11 invoice: Yes. Scanned/pasted invoices paid via LDK Node bolt11Payment().send.
LightningService.swift :811 WalletViewModel.swift :892 - Receive with a BOLT 11 invoice: Yes. Receive screen shows BIP21 with an embedded bolt11 invoice.
LightningService.swift :712 WalletViewModel.swift :1417 - Pay a BOLT 12 offer: Engine only. The app never calls bolt12Payment; offers fall through to 'Unsupported' toast.
lib.rs :1058 AppViewModel.swift :834 - Receive with a reusable BOLT 12 offer: Engine only. The app creates only bolt11 invoices, never offers.
lib.rs :1058 LightningService.swift :712 - Receive while the app is closed: Partial. Blocktank push asks user to open app; extension doesn't start node.
NotificationService.swift :7 Env.swift :325 - LNURL: Yes. LNURL-pay, withdraw, channel and auth all handled.
AppViewModel.swift :794 LnurlWithdrawConfirm.swift :73 - Pay a Lightning address: Yes. bitkit-core decode turns user@domain into LNURL-pay (source not in scratchpad).
PaymentNavigationHelper.swift :65 AppViewModel.swift :640 - Your own Lightning address: No. Receive offers only on-chain/bolt11; Pubky/Paykit profile keys are a separate system.
WalletViewModel.swift :1417 CopyAddressCard.swift :35 - Pay a BIP 353 name: No. No DNS/HRN resolution in fork or app; user@domain resolves as LNURL.
unified_qr.rs :29 AppViewModel.swift :834 - Multipath payments: Yes. LDK router splits payments automatically, up to 10 paths by default.
router.rs :794 bolt11.rs :122 - Keysend: Engine only. App uses spontaneous only for probes in the hidden dev settings.
spontaneous.rs :55 LightningService.swift :1790 - Choose your channel partner: Yes. Default LSP is Blocktank; manual open to any node URI, or LNURL-channel from other LSPs.
FundAdvancedOptions.swift :37 TransferViewModel.swift :1253 - Inbound channels from a provider: Yes. Blocktank CJIT on receive, plus paid orders for inbound liquidity.
BlocktankViewModel.swift :169 BlocktankViewModel.swift :347 - Splicing: Engine only. App only logs splice events and never calls spliceIn/spliceOut.
lib.rs :1526 LightningService.swift :1528 - Anchor channels: Yes. Anchor channels config is set, with zero reserve for trusted Blocktank peers.
LightningService.swift :107 config.rs :432 - Finds its own routes: Yes. Local LDK routing using Blocktank RGS snapshot and Blocktank-hosted scorer.
LightningService.swift :1116 Env.swift :252 - Watchtower: No. No watchtower client configured in app or offered by LDK Node.
LightningService.swift :120 builder.rs :483 - Channel backup off the phone: Yes. All LDK state stored remotely in Synonym VSS server (bitkit.to).
LightningService.swift :168 Env.swift :337 - On-chain funds without the channel partner: Yes. LDK Node's single-key on-chain wallet; sends do not involve Blocktank.
LightningService.swift :641 LightningService.swift :797 - Taproot receive addresses: Yes. Selectable address type in Advanced settings; default is native segwit.
LDKNode+AddressType.swift :7 AddressTypePreferenceView.swift :109 - Multisig or shared accounts: No. Only single-sig address types; no multisig or shared accounts found.
LDKNode+AddressType.swift :7 - Hardware or external signer: Yes. Trezor pairing (BLE); on-chain sends and transfers signed on device.
TrezorService.swift :157 HardwareWalletsSettingsScreen.swift :35 - Coin control: Yes. Manual coin selection mode lets user pick UTXOs for on-chain sends.
SendUtxoSelectionView.swift :4 LightningService.swift :773 - Built-in Tor: No. No Tor code; UI string says 'Bitkit cannot add Tor nodes'.
Localizable.strings :320
Blixt
- Pay a BOLT 11 invoice: Yes. Decodes lnbc invoice, pays via LND routerSendPaymentV2.
Send.ts :140 Send.ts :408 - Receive with a BOLT 11 invoice: Yes. Receive screen creates invoices via LND addInvoice.
Receive.ts :147 - Pay a BOLT 12 offer: No. Parser only accepts lnurl/lnbc/lntb; LND 0.21.3 lacks BOLT12 offers.
useEvaluateLightningCode.ts :24 Send.ts :130 - Receive with a reusable BOLT 12 offer: No. Only BOLT11 invoices; no offer code in app or LND.
Receive.ts :147 - Receive while the app is closed: No. Lightning Box is Android-only; iOS has only local notifications, no push-woken receive.
Drawer.tsx :221 push-notification.ts :5 - LNURL: Yes. pay, withdraw, channel and auth all supported.
LNURL.ts :30 index.tsx :33 - Pay a Lightning address: Yes. 'Send to Lightning Address' menu item, LUD-16 resolution.
Drawer.tsx :161 LNURL.ts :633 - Your own Lightning address: No. Lightning Box address menu/settings gated to Android only.
Drawer.tsx :221 Settings.tsx :1938 - Pay a BIP 353 name: No. user@domain resolved only via .well-known/lnurlp; no DNS lookup.
useEvaluateLightningCode.ts :71 LNURL.ts :647 - Multipath payments: Yes. MPP on by default (maxParts 16), toggle in settings.
Send.ts :392 app.ts :303 - Keysend: Yes. Keysend screen under drawer 'show more'; accept-keysend=1 for receiving.
Drawer.tsx :210 index.ts :680 - Choose your channel partner: Yes. Open channel to any pubkey@host; also autopilot (Blixt node) and Dunder LSP.
LightningInfo.tsx :216 Channel.ts :323 - Inbound channels from a provider: Yes. Dunder LSP opens channel on receive; on by default (README checkbox outdated).
ReceiveSetupLsp.tsx :90 BlixtLsp.ts :253 - Splicing: No. No splice RPC in LND 0.21.3; nothing in app.
lightning.proto - Anchor channels: Yes. Channel open refuses peers without anchor support (feature bit 23).
Channel.ts :345 - Finds its own routes: Yes. Embedded LND routes from local graph; speedloader gossip sync on by default.
index.ts :406 app.ts :331 - Watchtower: Engine only. LND wtclient exists but app config/UI never enables it.
index.ts :674 wtclient.go :16 - Channel backup off the phone: Yes. SCB exported to iCloud (auto) and restorable at wallet restore.
ICloudBackup.ts :12 Settings.tsx :1568 - On-chain funds without the channel partner: Yes. LND's own single-key wallet; the channel partner is not involved.
OnChain.ts :177 OnChain.ts :218 - Taproot receive addresses: Yes. Default receive address is P2TR; long-press for P2WPKH.
OnChain.ts :173 OnChainInfo.tsx :62 - Multisig or shared accounts: No. Single-sig LND wallet only; no multisig/shared account code.
OnChain.ts :217 - Hardware or external signer: Engine only. LND has PSBT/remote signer; app has no PSBT or signer code.
walletkit.proto :400 remotesigner.go :17 - Coin control: Engine only. App sendCoins passes no outpoints; LND supports outpoint selection.
OnChain.ts :218 lightning.proto :1157 - Built-in Tor: Yes. Experimental toggle on iOS; embedded Tor via react-native-nitro-tor.
Settings.tsx :1867 index.ts :312
Zeus
- Pay a BOLT 11 invoice: Yes. Scanned or pasted BOLT 11 invoices are paid through the embedded LND.
handleAnything.ts :521 PaymentRequest.tsx :360 - Receive with a BOLT 11 invoice: Yes. Receive screen creates invoices, optionally combined with an on-chain address.
Receive.tsx :652 - Pay a BOLT 12 offer: No. Offers are detected but Pay is disabled on LND; works in LDK mode or with remote CLN.
EmbeddedLND.ts :599 Send.tsx :1364 - Receive with a reusable BOLT 12 offer: No. No offers on LND; only available in LDK Node mode or with remote CLN.
EmbeddedLND.ts :599 - Receive while the app is closed: Partial. Only via custodial Cashu-backed ZEUS Pay address; self-custodial Zaplocker being retired.
index.tsx :553 LightningAddressStore.ts :403 - LNURL: Yes. LNURL-pay, withdraw, channel and auth are all handled.
handleAnything.ts :1017 - Pay a Lightning address: Yes. user@domain looked up via LUD-16 .well-known/lnurlp.
handleAnything.ts :675 handleAnything.ts :751 - Your own Lightning address: Partial. New embedded addresses are custodial ecash only; NWC type needs remote node.
index.tsx :553 index.tsx :738 - Pay a BIP 353 name: Partial. Resolves via Cloudflare DNS-over-HTTPS; offers can't be paid on LND, only on-chain/BOLT 11 fallbacks.
handleAnything.ts :707 Send.tsx :1364 - Multipath payments: Yes. MPP toggle with max-parts setting; AMP also offered.
PaymentRequest.tsx :1125 EmbeddedLND.ts :573 - Keysend: Yes. Send to a node pubkey; accept-keysend enabled for receiving.
Send.tsx :683 LndMobileUtils.ts :254 - Choose your channel partner: Yes. Open channels to any node URI; LSP defaults to Olympus, custom host configurable.
OpenChannel.tsx :237 LSP.tsx :235 - Inbound channels from a provider: Yes. Olympus flow LSP wraps invoices with a JIT channel; LSPS1 channel purchase too.
Receive.tsx :325 LSPStore.ts :527 - Splicing: No. No splice capability in Zeus or in the LND RPCs.
EmbeddedLND.ts :555 lightning.proto - Anchor channels: Yes. LND negotiates anchor channels by default; simple taproot channels are optional.
commitment_type_negotiation.go :471 LndMobileUtils.ts :257 - Finds its own routes: Yes. LND routes over its own gossip graph; Express Graph Sync; bimodal/apriori choice.
LndMobileUtils.ts :319 LndMobileUtils.ts :391 - Watchtower: Yes. Client on by default (--wtclient.active); user adds towers under Tools.
Lnd.swift :201 index.tsx :325 - Channel backup off the phone: Yes. Automatic encrypted static channel backups to the Olympus server; restore offered.
ChannelBackupStore.ts :95 DisasterRecovery.tsx :49 - On-chain funds without the channel partner: Yes. Embedded LND's own single-key wallet; the channel partner is not involved.
EmbeddedLND.ts :133 EmbeddedLND.ts :100 - Taproot receive addresses: Yes. Receive screen offers a Taproot (P2TR) address type.
Receive.tsx :1125 EmbeddedLND.ts :586 - Multisig or shared accounts: No. Only single-key account types; no multisig code found.
ImportAccount.tsx :35 - Hardware or external signer: Yes. Import xpub, sign PSBT on another device, scan back via QR (experimental).
ImportAccount.tsx :85 PSBT.tsx :210 - Coin control: Yes. UTXO picker when sending on-chain and when opening channels.
Send.tsx :990 OpenChannel.tsx :1102 - Built-in Tor: No. Built-in Tor only for remote nodes; embedded LND Tor setting has no toggle.
WalletConfiguration.tsx :1462 SettingsStore.ts :1604
Code size
Winnow ships 28,325 lines of code: 10,298 for the app, 13,120 for its Bitcoin library and peer networking, and 4,907 for the Lightning engine. The other wallets ship 3.6 to 15 times as many lines, without counting their Bitcoin libraries.
Winnow’s lines are denser, at about 36 characters each against 22–30 for the others. Counted in characters, the gap is 3.0 to 9.1 times.
- App
- Lightning engine
- Bitcoin library
| Wallet | App | Lightning engine | Bitcoin library | Total lines | Characters | vs Winnow |
|---|---|---|---|---|---|---|
| Winnow | 10,298 | 4,907 | 13,120 | 28,325 | 1.01M | — |
| Phoenix | 72,767 | 28,086 | Not counted (bitcoin-kmp) | 100,853 | 3.07M | 3.6× · 3.0× |
| Bitkit | 91,893 | 126,384 | Not counted (BDK, rust-bitcoin) | 218,277 | 6.33M | 7.7× · 6.3× |
| Blixt | 36,360 | 239,849 | Not counted (btcd, btcwallet, neutrino) | 276,209 | 6.51M | 9.8× · 6.4× |
| Zeus | 189,559 | 239,849 | Not counted (btcd, btcwallet, neutrino) | 429,408 | 9.23M | 15.2× · 9.1× |
“vs Winnow” divides each total by Winnow’s, first in lines, then in characters. The Lightning engines alone: Winnow’s is 4,907 lines, lightning-kmp 28,086, LDK Node with its rust-lightning crates 126,384, and LND 239,849.
Languages
Share of each wallet’s counted lines, app and Lightning engine together. Winnow is written in one language; the others combine a user-interface language with a Lightning engine in another.
| Wallet | Swift | Kotlin | Rust | Go | TypeScript/JS | C family |
|---|---|---|---|---|---|---|
| Winnow | 100% | — | — | — | — | — |
| Phoenix | 60% | 40% | — | — | — | <1% |
| Bitkit | 42% | — | 58% | — | — | — |
| 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, on a Mac for the three Swift apps, so it is not measured here. Testing can only bring CRAP down to CC, so the 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.
1.3% of Winnow’s functions are above 12, against 1.8–6.0% for the others. Winnow’s most complex function scores 31. Its average is 3.3, higher than Phoenix’s and Bitkit’s, because those codebases have more very small functions. Winnow’s CI also rejects any function over 8 as SwiftLint counts complexity, which leaves out case arms, &&, ||, ?? and ternaries.
| Wallet | Functions | Mean CC | 90th pct | Max | CRAP above 12 at any coverage | CRAP above 30 at any coverage | |
|---|---|---|---|---|---|---|---|
| Winnow | 2,164 | 3.3 | 7 | 31 | 28 (1.3%) | 1 (0.05%) | |
| Phoenix | 7,218 | 2.4 | 5 | 168 | 132 (1.8%) | 22 (0.30%) | |
| Bitkit | 10,668 | 3.0 | 6 | 171 | 246 (2.3%) | 46 (0.43%) | |
| Blixt | 11,516 | 3.9 | 8 | 147 | 616 (5.3%) | 85 (0.74%) | |
| Zeus | 15,131 | 4.2 | 9 | 252 | 904 (6.0%) | 141 (0.93%) |
By part: app, Lightning engine and Bitcoin library
| Part | Functions | Mean CC | 90th pct | Max | CC > 12 | CC > 30 | Most complex |
|---|---|---|---|---|---|---|---|
| Winnow · app | 695 | 3.3 | 7 | 31 | 11 | 1 | init |
| Winnow · lightning engine | 464 | 3.3 | 7 | 13 | 3 | 0 | apply |
| Winnow · bitcoin library | 1,005 | 3.3 | 7 | 27 | 14 | 0 | decode |
| Phoenix · app | 4,694 | 2.5 | 5 | 155 | 77 | 9 | (computed_property) |
| Phoenix · lightning engine | 2,524 | 2.3 | 4 | 168 | 55 | 13 | processInternal |
| Bitkit · app | 5,331 | 3.0 | 6 | 127 | 85 | 8 | (computed_property) |
| Bitkit · lightning engine | 5,337 | 3.0 | 6 | 171 | 161 | 38 | read |
| Blixt · app | 1,057 | 3.6 | 7 | 134 | 56 | 19 | Settings |
| Blixt · lightning engine | 10,459 | 3.9 | 9 | 147 | 560 | 66 | ValidateConfig |
| Zeus · app | 4,672 | 4.7 | 10 | 252 | 344 | 75 | render |
| Zeus · lightning engine | 10,459 | 3.9 | 9 | 147 | 560 | 66 | ValidateConfig |
Blixt and Zeus share the same LND, so their engine rows match.
Dependencies and vendored code
Winnow depends on 1 third-party package: swift-secp256k1, which builds Bitcoin Core’s libsecp256k1 from source. Phoenix depends on 59; the others on up to 1,421. 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 | Phoenix | Bitkit | Blixt | Zeus |
|---|---|---|---|---|---|
| Swift packages | 1 | 19 | 5 | — | — |
| Kotlin libraries | — | 40 | — | — | — |
| Rust crates, inside prebuilt libraries | — | — | 686 | — | 407 |
| Go modules, inside LND | — | — | — | 114 | 114 |
| npm packages | — | — | — | 553 | 897 |
| CocoaPods from the trunk | — | — | — | 8 | 3 |
| Third-party packages | 1 | 59 | 691 | 675 | 1,421 |
| Wallet | Main libraries | Prebuilt binaries | Third-party code copied into the repository | Patched dependencies |
|---|---|---|---|---|
| Winnow | libsecp256k1, through swift-secp256k1. Nothing else. Package. | None | None | None |
| Phoenix | bitcoin-kmp, secp256k1-kmp, Ktor, SQLDelight, Firebase Messaging project. | Kotlin libraries, as compiled klibs from Maven Central | None | None |
| Bitkit | BDK (inside LDK Node), bitkit-core, Paykit, VSS client, Lottie | LDK Node, bitkit-core, Paykit and the VSS client, as XCFrameworks Package. Package. Package. Package. | None | None |
| Blixt | React Native, react-native-turbo-lnd, secp256k1, protobuf, Nitro Tor | LND, from react-native-turbo-lnd releases package. | None | None |
| Zeus | React Native, bitcoinjs-lib, noble and scure cryptography, Cashu CDK, Nostr (NDK) | LND, LDK Node and Cashu CDK, pinned by SHA-256 fetch-libraries-versions. | 4,551 lines in zeus_modules: @lightninglabs/lnc-core, @lightninglabs/lnc-rn, bc-bech32, bc-ur, noble_ecc, ur | Five iOS-relevant scripts rewrite installed npm packages (a sixth targets Android); rn-nodeify shims crypto and stream |
- Swift packages are every package Swift Package Manager resolves. Phoenix links Firebase Messaging, which brings in Google’s package graph.
- Kotlin libraries are the 40 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: 686 distinct crates, 1,080 counting each library’s copy. LDK’s own crates are counted as code above, not here.
- npm packages are everything the app’s runtime dependencies pull in (87 declared by Blixt, 111 by Zeus), not build tools. The bundler includes only what the app imports.
- CocoaPods counts pods from the CocoaPods trunk. Another 99 pods in Blixt and 110 in Zeus are the native halves of npm packages and React Native itself.
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. Winnow’s Lightning engine is tested against Core Lightning, LDK and Bitcoin Core on regtest, but it has not had an independent security review.
The engines do different amounts of work
LND and LDK are general-purpose nodes. They keep the network graph, find their own routes and can forward other people’s payments. lightning-kmp supports splicing and dual-funded channels. Winnow’s engine has no general graph routing, pays only BOLT 12 offers, and does not implement multipath payments, anchor channels or splicing. The feature table shows the rest.
The apps do different amounts of work
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 shared accounts with MuSig2 and 2-of-3 signing. Each count is the whole app, not only its Lightning features.
Who can move on-chain funds
Phoenix has no separate on-chain wallet. Its Bitcoin address needs ACINQ’s signature alongside yours; without ACINQ, your key alone can spend it after about six months. Sending to a Bitcoin address takes the amount out of your ACINQ channel, which ACINQ co-signs. Winnow, Bitkit, Blixt and Zeus each keep a single-key on-chain wallet that works without their channel partner.
Recovery differs
Winnow keeps channel state in an encrypted journal on the phone. Keep the phone and its files while a channel is open. The recovery phrase restores the on-chain wallet, not current channel state.
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.
- 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. Tree-sitter could not fully parse at most 5% of any wallet’s files; it recovers locally, so their functions are still counted. - Dependencies: read from each app’s own lockfiles (Swift Package Manager, CocoaPods, 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 -depswith the build tags LND’s own iOS build uses. - Vendored code: each repository was searched for third-party code committed into it: vendor directories, copied npm packages, license files below the root, committed binaries and dependency patches.
- Features: each cell was checked in the app’s and the engine’s source at the revision below.
- Lightning engines: counted at the version each app pins, as the named library only. Zeus’s count uses its default embedded LND and does not add its optional LDK Node.
Exact sources
| Wallet | App source | Lightning engine | Prebuilt libraries resolved for dependencies |
|---|---|---|---|
| Winnow | winnow-lightning@f379b7c | In the same repository | — |
| Phoenix | phoenix@1facad2 | lightning-kmp v1.12.0 | — |
| Bitkit | bitkit-ios@c906d5a | ldk-node@190a17d (0.7.0-rc.66), rust-lightning v0.2 | bitkit-core v0.5.18, paykit-rs v0.1.0-rc55, vss-rust-client-ffi v0.5.23 |
| Blixt | blixt-wallet@baa6ba0 | lnd v0.21.3-beta | — |
| Zeus | zeus@aeb9c10 | lnd v0.21.3-beta | ZeusLN/ldk-node v0.7.0-zeus-rl-0.2.5, cdk v0.17.4 |
Each project changes often. These numbers are a dated 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. Feature cells and profile facts are reviewed by hand. The build checks that every one of their citations still resolves at the pinned revision.
Other approaches
Some iPhone wallets offer Lightning without a channel on the phone. They make different trade-offs and are not counted above.
- Misty Breez holds Liquid funds and swaps to and from Lightning with Breez SDK – Liquid. On 20 September 2026 that SDK’s public repository was replaced with a notice pointing builders to Breez SDK – Spark.
- Aqua is a Bitcoin and Liquid wallet that pays and receives Lightning through Boltz swaps.
- Muun is a 2-of-2 multisig wallet shared with Muun’s server. Its Lightning payments are swaps against that server.
- BlueWallet reaches Lightning through an LNDHub server, which holds the Lightning funds.
- Alby Go controls a wallet that runs elsewhere, such as Alby Hub, over Nostr Wallet Connect.