I build Ye₿ente, an open-source Bitcoin Lightning wallet, here in Kisumu, Kenya. It runs on my own Lightning node, and it can pay any shop in kTown, a small network of local shops that take bitcoin and get paid in M-Pesa.
Today's work was about one thing: when someone taps Send, the money should actually go. Nine pull requests later, here is what changed and what I learned.
1. Sends were refused while the node was healthy
Users saw "Sending is paused while the wallet reconnects." The node was fine: synced, one active channel. So why?
Before every send, the server asks the node how much it can send. That read used the pay key, and the pay key could pay but couldn't read channel balances. The answer came back blank, and a blank meant "node unreachable", so every send was refused.
The fix: reads fall back to the node's read-only key. Neither key can move money through these calls.
// Reads about sending (channels, payments): the pay key first, // then the read-only key. async function readForPay(path, opts = {}) { const keys = [...new Set([process.env.YEBENTE_LND_PAY_MACAROON, READ()].filter(Boolean))]; let last = new Error("No node key can read this."); for (const macaroon of keys) { try { return await lndRequest("GET", path, null, { ...opts, macaroon }); } catch (e) { last = e; } } throw last; } The new test fails on the old code with the exact error users saw, and passes with the fix. A security review checked the change before merge. (PR #495)
Lesson: least privilege is right, but test the reads your code depends on with the exact key it will use in production.
2. Never show "sent" for a payment that didn't go
When the node didn't answer in time, the server replied "pending", and the app still played the sent animation. Now a pending payment shows "Not confirmed yet", no celebration, and if it fails the sats come back. (PR #491)
3. Paying kTown shop QR codes
A kTown QR is an LNURL on our own domain (/.well-known/lnurlp/k-<id>). The wallet saw its own domain and read k-<id> as a wallet name: "No Ye₿ente wallet has that handle."
Now k- means a shop, and the wallet asks kTown for the shop's invoice directly. Going over HTTP to ourselves would have failed anyway: kTown fronts the payout partner, so the invoice commits to the partner's description, not kTown's. New wallets can't take a k- name, so none can pose as a shop. (PR #493)
4. Send and Receive, simpler
- Send opens on the scanner: continuous focus, tap to focus, torch and zoom where the phone has them, and every other frame a sharp close-up of the middle for small or dense codes. (PR #501)
- Clipboard offer: copy an invoice or Lightning address and the app offers to paste it. It's read on the phone only, used only if you tap Paste.
- Approve last: no PIN box on the amount screen. Send opens your fingerprint, or a final PIN pop-up. (PR #503)
- Receive is one big QR with your address under it, one tap to copy, and an invoice that fills the screen until it's paid. (PR #505)
- No empty wallet: while the node reconnects, the buttons stay on screen, greyed out. (PR #489)
How it ships
Every change gets an issue, a pull request, tests that fail on the old code first, CI, and a review before merge. The suite is at 779 tests passing. It's all public: github.com/altradits/altradits.
I'm working toward full-time Bitcoin open-source work, learning to review Bitcoin's code alongside building this.
Not sponsored. No paid posts. Try the wallet: altradits.com/yebente


