Repository navigation
Name operations, fees, chain identity: issues #34-#41 and what turned up with them - #42
Merged
Merged
Conversation
transformIgnorePatterns let @react-native and @react-native-community through but not @react-navigation, which ships ES modules. Every test that reached class/index.ts died on `export { ... }` before running a single assertion. On unchanged code this takes the unit suites from 10 passing (51 tests) to 26 passing (186 tests). Three suites now run and fail - watch-only-wallet, segwit-bech32-wallet and deeplink-schema-match, 8 tests. Those failures are pre-existing and were simply invisible; they are not addressed here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
6.1.6 and 6.1.7 are deprecated on npm ("Published by hand from an older
state"), so a fresh install already warns. 6.2.0-6.4.0 bring what the
Doichain fixes need:
6.2.0 restores bitcoinjs-lib's PSBT checks, signs name inputs held by
P2WPKH with BIP143, adds the nameops module
6.3.0 networks.doichain* with the prefixes from chainparams.cpp,
nameops.nameDoiScript and the byte limits
6.4.0 electrum.verifyChain and the checkpoint at block 431,018
Install with npm >= 7: package-lock.json is lockfileVersion 3, and npm 6
re-resolves the whole tree instead of honouring it.
Prerequisite for issues 34, 35, 36, 38 and 40.
Doichain#38
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
network.js carried its own copy of the Doichain network parameters, and the mainnet BIP32 private prefix had drifted to 0x0499ade4 while Doichain Core uses 0x0488ade4 - the same value as Bitcoin and Namecoin. Every standard extended private key was therefore rejected on mainnet. The visible symptom: importing an Electrum multisig wallet file silently dropped a cosigner's private key, because importMultisigWalletFile() only keeps the xprv when isXprvValid() says yes and falls back to the watch-only xpub when it does not. With the wrong prefix it never said yes. Measured on a standard Zprv: isXprvValid() returned false before, true now. The parameters now come from networks.doichain* in doichainjs-lib, which reads them from chainparams.cpp, so they cannot drift apart again. The extra `name` field is kept, it is used elsewhere in the wallet. Doichain#34 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every fee the wallet recommended on Doichain mainnet was far under the mining node's minimum, so a transaction relayed, showed as sent, sat in the mempool and was never mined - with nothing on screen to say so. The chain's estimator has no fee-paying backlog to learn from, so blockchainEstimatefee() answers -1 and mempool_getFeeHistogram() is empty. estimateFee() fell back to 1 sat/vByte and the presets came out at 17/14/2 sat/vByte. Doichain Core's DEFAULT_MIN_RELAY_TX_FEE is COIN/1000 per kvB, i.e. 100 sat/vByte, and the mining node keeps that default. Asking the network does not help: the doi.works fleet runs with minrelaytxfee=0.00000001, so it relays what the miner will never include. The floor is therefore a constant with its source named, clamped in four places - estimateFee(), estimateFees(), the NetworkTransactionFee constructor (which covers the presets and the no-estimate fallback both) and the custom fee field. It is a floor, not a price: a server estimate above it still wins. Verified on the device against mainnet - the presets now read 115/112/100 sat/vByte. Also rounds the estimate up rather than to nearest, as upstream BlueWallet 8.0.2 does. Doichain#39 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A name transferred to a P2SH or Taproot address was lost. The builder took
only the hash out of the recipient's address and wrapped it in a script
shape of its own choosing - a P2PKH script for Base58 addresses, OP_0
<program> for bech32 ones. For a P2SH address that means a P2PKH script
holding a script hash, and for Taproot an OP_0 program where the address
means OP_1. Doichain Core accepts both, and then no key can ever spend the
output: the name is frozen until it expires and the 0.01 DOI locked in it
are gone.
Reproduced on a Doichain Core v31.1.6 regtest node: Core accepted both, and
name_show reported a holder nobody controls.
The builder now comes from nameops.nameDoiScript() with the owner script
from address.toOutputScript(), which checks the network and the address
type, and refuses anything that is not P2PKH or P2WPKH - the two types the
library can sign a name input for. It also:
- counts both limits in bytes, not characters (a name of 200 x "ae" is
400 bytes and Core rejects it; a value over 520 bytes freezes the name)
- normalizes names to NFC, so look-alike spellings are one name
- pushes an empty value as a real push, which Core needs to see a name
- decodes names as UTF-8 rather than ASCII when a PSBT is imported, so a
non-ASCII name is not garbled on the screen where it is confirmed
- prefills the transfer amount with the 0.01 DOI the name locks, instead
of leaving the field empty and failing with "tx-name-greedy"
- moves the builder out of tests/ into utils/, where production code can
import it honestly
The new tests pin its output byte for byte against the fixtures of
doichainjs-lib, whose transactions Doichain Core accepted on regtest.
Doichain#36
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ElectrumX lists a name output under its holder's address right next to that address's coins, so coin selection picked it like any other. A payment that spends one is a non-name transaction with a name input, and Doichain Core rejects it with "Non-name transaction has name input" - the more names a wallet holds, the likelier a payment fails, and the error says nothing about why. getUtxo() now filters them the same way frozen outputs are filtered, so a caller that wants to see everything - coin control - still passes respectFrozen and gets them. That alone would have broken name transfers: createTransaction() used the very same list to find the name input to spend, and would have built a transfer without it, which Core rejects with "NAME_DOI on an existing name must spend its previous output". The lookup therefore goes through getUtxo(true) now, and insists the output it finds actually carries a name rather than being a plain coin that happens to sit at the same address. Coin selection is also told how long a name output really is. It sizes an output from its address, but a name output carries the name and its value on top of the holder's script - some twenty bytes more - and underpaying the fee got the transfer rejected with "min relay fee not met". Covered end to end in tests/regtest/nameops.test.ts. Doichain#37 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
handlePsbtSign() called psbt.setVersion(VERSION) for every name output. A purchase PSBT from names-on-chain reaches the seller already signed by the buyer, and from doichainjs-lib 6.2.0 on setVersion() refuses to touch a transaction that has signatures - so the seller could not sign at all. A name transaction has to carry version 0x7100 before anybody signs it, so there is nothing to set here: the version is checked instead, and only when the PSBT actually has a name output, leaving ordinary multisig and PayJoin imports alone. The fixtures are the purchase PSBTs from doichainjs-lib's own tests, built with names-on-chain and accepted by Doichain Core on regtest after the seller signed. The tests show both halves: they already carry 0x7100, and setVersion() on them throws "signatures exist". combinePSBTs.ts also moves off plain bitcoinjs-lib - a phantom dependency it imported without declaring - onto doichainjs-lib with the network passed. Doichain#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Since 11 September 2026 there are two Doichain chains: the new rules of Core v31 took effect at block 431,017, the old 0.20 nodes kept mining, and their chain ran thousands of blocks ahead - 444,404 against 432,341 on 19 September. A server on that chain answers every request, with balances and confirmations that mean nothing here, and the wallet had no way of telling. connectMain() now asks electrum.verifyChain() before it marks the connection usable: one header at block 431,018, the first block the two chains do not share. Block 431,017 proves nothing - Doichain never enforced nBits, so the old nodes accepted the flag day block too and both chains build on it. A server that fails is dropped like an unreachable one and the next peer is tried. When the server the user chose themselves is the one on the other chain, the alert says so instead of claiming it is unreachable. Note that genesis_hash, which the Electrum protocol offers for exactly this purpose, is useless here: both chains share the same genesis block. Verified on the device, including with the check forced to fail - the wallet cycled through every peer and loaded no data at all. Doichain#40 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
estimateCurrentBlockheight() and calculateBlockTime() still carried BlueWallet's anchors - height 627,179 at 22 April 2020, and 9.93 minutes a block. They are only the fallback for the moment before the first header arrives, but on Doichain they answered roughly 969,000 while the chain stood near 435,000, and a transaction's confirmation count and displayed time are derived from them. Anchored on a real Doichain block instead - 432,000 at 17 September 2026 - with Doichain's ten minute target. Named constants, with a note that they are a fallback and may drift without anything breaking. Doichain#41 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
DoiWallet only knew the names in its own transactions. It could not say who holds a name now, what its current value is, when it expires, or whether it is free - all of which ElectrumX answers with one query by name, and which DoiWallet's own servers support. getNameOperations() asks for every operation on a name over the existing connection, nameShow() reads the state off them: the newest operation holds the name, its outpoint is what a transfer has to spend, and a name expires 36,000 blocks after it. The index script and its hash come from the library, which writes the pushes exactly as ElectrumX does - counting characters instead of bytes yields another hash, and then a name that is taken looks free. The tests pin both against values measured against itchy-jellyfish-89.doi.works. Deciding which operation holds a name needs no connection, so it lives in utils/nameState.ts and is tested on its own. This is the first part of the issue. Wiring transfers to spend the outpoint this returns, instead of picking an input by address, and showing the name's current state in the transfer form are still open. Doichain#35 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
coinselect() corrected the assumed output size only for addresses starting with "bc1", which no Doichain address ever does, so the correction never ran. Fixing the prefix alone would have thrown: toOutputScript() was called without a network and decodes a dc1 address as a bitcoin one. Measured on real derived addresses - coinselect assumes 34 bytes per output, while a taproot (dc1p...) or a multisig (dc1q..., 32-byte program) output really costs 43. Every send to one underpaid by 9 bytes' worth, 900 sat at the chain's floor rate. P2PKH, P2SH-P2WPKH and plain P2WPKH were never short. A length the caller supplied now wins, the same way it already does for inputs: a name output is longer than its holder's address suggests, and the address-based correction would otherwise overwrite it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
TaprootWallet passed the network only in scriptPubKeyToAddress(). Both getAddress() and createTransaction() called ECPair.fromWIF() and payments.p2tr() without it, and the PSBT was built without it too. Without a network, fromWIF() reads a WIF as a bitcoin one: a Doichain WIF (version 180) was rejected outright, so the wallet could not be imported at all, while a bitcoin WIF was accepted and p2tr() handed back a bc1p... address - a Bitcoin address displayed as if it were ours on Doichain. Any send would have failed as well, since a dc1 recipient cannot be decoded under bitcoin parameters. The test asserted that bc1p... address, which is why it passed: it pinned the bug as expected behaviour. It now uses the same private key encoded as a Doichain WIF, expects the dc1p... address, and checks that a WIF of another network is refused rather than silently turned into a foreign address. The transaction case asserts the shape of a key-path taproot spend instead of a hardcoded hex. This also drops the last import of plain bitcoinjs-lib, a dependency the project uses without declaring it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The transaction list built its label as `[${nameOp.name}]` without
checking. A name_new only commits to a hash and carries no name at all, so
the literal string "undefined" appeared in the list on the main screen.
Confirmed against a regtest node: a name_new output decodes to
{"op": "name_new", "hash": "..."} with no name field.
Falls back to naming the operation, so such an entry now reads
"[name_new]".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
For name operations "does it look right" is not a useful answer. Core accepts a name output whatever its owner script says, so a name handed to an address the wallet cannot spend from is accepted, confirmed and lost - that is issue 36, and no unit test would have caught it. The rules around a name input fail the other way, with reject reasons that are hard to predict from the code: tx-name-greedy, tx-namedoi-overwrite-without-input, min relay fee not met. All three turned up while writing these. scripts/regtest-node.sh downloads Core v31.1.6 once with its checksums verified, runs it under .regtest/ with its own datadir and RPC port 19443, and mines spendable coins. ~/.doichain is never touched, so an existing mainnet node and its wallets are safe. The scenarios drive a real HDSegwitBech32Wallet: registering a name, transferring it and watching name_show agree that it moved, and the three refusals the wallet should make itself rather than leaving to the node. The wallet is pointed at regtest by mocking blue_modules/network.js; the BIP32 serialization prefixes stay mainnet on purpose, see the README. Without a running node the suite skips rather than fails, so npm test stays green on a machine that has no node. Covers: Doichain#35 Doichain#36 Doichain#37 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The file was half converted: the first case already used a Doichain WIF and dc1q/N/6 addresses, while the batch send and the other two cases still carried bitcoin ones - recipient 1GX36PGB..., expected bc1q3rl0mky... and the WIF L4vn2Kx... The wallet is right to refuse them, so the suite failed with "has no matching Script", getAddress() returning false and "Invalid network version". The same private key is now encoded as a Doichain WIF, the recipients are the Doichain addresses the file already used elsewhere, and the pinned transaction hex is re-recorded for them. The assertions that carry the weight - fee rate within 0.1, input and output counts, and every address decoding back under DOICHAIN - are unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The three HW wallet cases compared against PSBTs recorded at some earlier point. The wallet builds a 1-input 1-output P2WPKH transaction and pays 112 sat at 1 sat/vByte; such a transaction is about 110 vBytes, so that is right. The recorded expectation implied 192 sat, i.e. 1.75 sat/vByte. The produced PSBTs are byte for byte identical before and after the Doichain fixes in this branch - checked against the commit that only touches jest.config.js - so this is not a regression, just an expectation that drifted while the suite was unable to run at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
'onWalletSelect should work' timed out: it builds a LightningCustodianWallet, which class/index.ts does not export at all. The isBothBitcoinAndLightning assertions further up the same file are commented out for the same reason. It covers choosing between paying on-chain and paying a Lightning invoice, which cannot happen here, so the case is removed rather than repaired, with a note saying why. Worth knowing for whoever brings Lightning back: isBothBitcoinAndLightning() currently returns undefined for every input, because it builds the uri with a "bitcoin:" prefix that isBitcoinAddress() does not strip. Today that is the behaviour this fork wants - a doichain: URI carrying a lightning parameter falls through to the plain address branch and opens SendDetails - but it is an accident, not a decision. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…error class/lnurl.js declared fetchGet(url: string): Promise<any> - TypeScript annotations in a plain .js file. tsc reported two TS8010 errors for them, and because those are *syntactic*, it never got as far as the semantic pass: the rest of the project went unchecked. The annotations are now JSDoc, which tsc reads in a .js file and which keeps the information the author meant to record. With the syntax error gone tsc finally checks the project and reports 129 errors. All of them pre-date this branch - verified by applying this same change to b4a5479 and diffing the two error sets, which come out equal. They are not addressed here; the point of this commit is that they are visible at all. One of them would have been mine: TransactionListItem reads nameOp.op, which class/wallets/types.ts did not declare. ElectrumX really does return {op, hash} for a name_new, with no name and no value, so op and hash are added to the type. name and value stay required: relaxing them cascades into the Transaction/NameOpTransaction typing and pulls in four unrelated errors, which wants sorting out on its own. That leaves a known gap worth its own issue - TransactionDetails renders nameOp.name directly, so a name_new shows "undefined" there the same way the transaction list did before c18cc6a. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
npm test began with tslint, so a single type error stopped it before any
test ran - and npm run lint started with tslint too, for the same effect.
The pipeline was a chain where each link hid the next.
tslint is now its own command. npm test runs lint, unit and integration;
whoever wants the type check asks for it. CircleCI's lint job already runs
"npm run tslint && npm run lint" explicitly, so types stay checked there.
Removing tslint from the chain let find-unused-loc.js run, which then
failed on 24 loc keys nothing references. They are leftovers of features
this fork does not have - most of the Lightning and LNDHub strings, the
upstream performance test and app intent settings - plus loc.alert.default
and transactions.nameOps_description. Each was checked for dynamic access
before removing it: the only computed loc lookups in the project are
loc.units[...] and loc.addresses[`type_${tab}`], neither of which touches
these. The now-empty "alert" section goes too.
Only loc/en.json is touched, which is the file the script reads; the 50
translation files still carry the keys and can be cleaned separately.
npm test now gets as far as eslint, which reports 165 errors - every one
of them older than this branch (1010 problems at b4a5479 against 964
here).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mechanical only: prettier normalisation (double to single quotes, import spacing, line wrapping), a space after // in comments, nine let bindings that are never reassigned turned into const, and four "cond ? true : false" turned into "!!(cond)", which keeps the boolean semantics exactly. Nothing was written by hand. 964 problems become 179, i.e. 165 errors and 799 warnings become 111 and 68. The rest need a person: 72 unused variables and imports, 13 duplicate object keys, 10 unused styles, 6 react-hooks dependency arrays. Verified unchanged after the rewrite: 218 unit tests over 35 suites, 6 regtest scenarios, and the same 129 type errors as before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nameState.ts carried NAME_EXPIRY_BLOCKS = 36000 and computed "expired" from it. That is a consensus rule, and the chain owns it - a wallet that answers the question itself answers it wrong. Wrong in a measurable way, too: 36,000 is the mainnet value. NameExpirationDepth is overridden to 30 on regtest, so the constant was incorrect on two of the three networks the wallet knows, and the test that covered it only ever confirmed my own arithmetic. Measured on a Doichain Core v31.1.6 regtest node while sorting this out: name_doi "exp229" at height 230 -> expires_in 30 35 blocks later, name_show -> error -4: name expired name_show allowExpired -> expired=true, expires_in=-5 name_doi "exp229" by someone else -> accepted, no name input needed So names do expire, Doichain's own OP_NAME_DOI included - the Namecoin rule applies to it - and the node is what says when. readNameState() now answers only the question it can answer from the index: which operation holds the name, and therefore which outpoint a transfer has to spend. This also removes the "refuse an expired name" guard from the plan for issue 35: taking the outpoint from the chain's answer is enough, and whether that name is still held is for the node to reject if it is not. Doichain#35 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…dress A transfer picked its name input by holder address. A wallet reuses its addresses, so one address holds several names at once - the regtest runs ended up with seven on a single address - and nothing said which of them would be found. The transaction then declared one name while spending the output of another, and Doichain Core rejected it with "tx-namedoi-name-mismatch". The outpoint now comes from the ElectrumX name index: TransactionStatus asks nameShow() which operation holds the name right now, instead of reading a holder address off the transaction on screen - which may be old, the name having moved on since. createTransaction spends exactly that outpoint. Looking for it among our own outputs is also the answer to "is this name ours": a name that moved on, or never was ours, is simply not there, and the error says so rather than leaving it to the node. The value passed on is the one the name carries now, taken from the same answer, rather than the value in the transaction being viewed. The new regtest scenario is the case that used to break: two names on one address, transfer the second, and check the first was not touched. It fails with the old lookup - Core answers tx-namedoi-name-mismatch - and passes with this one. Point 3 of the issue is still open: the transfer form does not show the name's current value, holder or expiry, and does not hide itself when the output on screen no longer holds the name. Doichain#35 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The transfer form decided from the transaction on screen. It appeared for
any name output of a confirmed transaction and hid itself on one condition:
the transaction is outgoing and one of its inputs already carried the same
name. On the transaction that brought a name in that condition never holds,
so the form stayed available long after the name had moved on - offering a
transfer of something this output no longer holds.
The helper behind it was broken anyway:
for (const output of fetchedTx.outputs) {
if (output.scriptPubKey?.nameOp?.name === name) return true;
else return false;
}
Both branches return, so only the first output was ever looked at. eslint
had been reporting it as no-unreachable-loop among the errors nobody could
see until this branch made the linter run.
The screen now asks nameShow() which operation holds the name and renders
that answer: the name's current value and the block it last changed in, the
transfer form only when this very output is the one holding it, where the
name went when it is not, and a plain "could not check" when there is no
answer. The decision itself is describeNameOutput() in utils/nameState.ts,
a pure function with unit tests - the screen only renders what it returns.
Expiry is deliberately not shown. ElectrumX does not report it, only a node
does, and computing it here is what this branch already removed for being
wrong on two networks out of three. "Last changed in block N" is a fact we
have; whether that means expired is the node's to say.
This costs the section a network call: until the answer arrives the form is
replaced by a spinner, and without a connection it does not appear at all.
That is stricter than before and correct - without the chain's answer we do
not know the outpoint to spend, and since the previous commit a transfer
needs exactly that.
Doichain#35
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
getUtxo() asked isNameOutput() for every output it was filtering, and that walked the whole transaction list each time. getTransactions() is not cheap: it concatenates every address's transactions and derives the wallet's addresses to build its owned-address map. Measured on a wallet with 60 unspent outputs, 200 transactions and 40 known addresses: getUtxo(true), no filters 0.0 ms getUtxo(), before 36.6 ms getUtxo(), after 0.8 ms getUtxo() is called whenever a balance, the send screen or coin control is drawn, and the cost grew with outputs times transactions - so this got worse the longer a wallet had been in use. It arrived with the coin selection fix in 259fafb and is mine. The set of name outpoints is now built once per call. Reading a name off a script is also skipped for outputs that cannot carry one: a name script opens with OP_NAME_NEW, OP_NAME_FIRSTUPDATE, OP_NAME_UPDATE or OP_NAME_DOI, so one byte tells plain payments apart without parsing them. isNameOutput() stays for single queries and now answers from the same set. Doichain#37 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Removing 23 dead keys from loc/en.json left them behind in the 37 language files that had been translated - 213 entries in all. Nothing reads them: the localization library takes its key list from en.json, so what is not there is never asked for. They are still worth removing. A translator sees strings the app does not have and spends time on them, and if a key ever comes back under a different meaning the old translation would be picked up silently. Most of them are the Lightning and LNDHub strings, which this fork does not have at all. Removed line by line rather than by re-serialising the JSON, so the diff is 213 deletions and no insertions instead of 37 reformatted files. Every file was parsed again before being written: all 51 are valid JSON, and no dangling comma was left behind by an entry that happened to be last. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
getNameOperations() and nameShow() had never been sent to a server. The regtest setup runs Doichain Core but no ElectrumX, so everything about the name index was assumed - and the outpoint a transfer spends comes from it. Four checks, all structural: every operation the index returns is on the name that was asked for and looks like an operation; the state nameShow() reports is the newest of them; the outpoint it would hand a transfer really carries that name in its transaction, which is what keeps Core from answering tx-namedoi-name-mismatch; and a name nobody registered comes back empty rather than as something. Nothing is pinned to chain data. Half the failures in this suite are balances and heights recorded years ago that fail now only because the chain moved on, and a name's holder, value and height change whenever somebody updates it. Run against big-parrot-60.doi.works while writing this, "doichain" came back as one operation in block 353,030 held by NEYSLcFT6a..., matching what issue 35 recorded. Doichain#35 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
startImport(text, askPassphrase, searchAccounts, offline, onProgress,
onWallet, onPassword) takes four positional arguments before its three
callbacks. Five call sites passed only three, so every callback landed one
place to the left: onProgress was taken for "offline", onWallet for
onProgress, and onPassword was never passed at all.
The import then ran without ever reporting a wallet, and the tests failed
on an empty list - "can import Bech32 Segwit", "can import
Legacy/P2SH/Bech32 from an empty wallet" and "can import watch-only
address". Their data was fine; only the call was wrong.
The suite goes from 8 failures to 6. What is left needs more than a
corrected call:
- "Legacy with uncompressed pubkey" and "minikey" expect a bitcoin WIF
(5KE6..., 5JPy...) and a bitcoin address (1GsJ...) out of a ypub
- "BIP38 encrypted backup" decrypts to a bitcoin WIF (KxqRtpd...)
- "multiple wallets in offline mode" and "watch-only xpub as a zpub"
assert on wallet counts
The first three are upstream test vectors that were never moved to
Doichain, the same thing that was wrong in the segwit and taproot unit
suites.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ElectrumClient asked electrum1-3.bluewallet.io for the balance of two Bitcoin addresses. Those servers answer for Bitcoin and refuse the connection from here, so the test could only ever report ECONNREFUSED. It now connects to the three *.doi.works servers the wallet actually ships with, and checks the shape of what they answer rather than an amount - the balances it compared against were recorded years ago, which is why so much of this suite fails. Currency asserted a rate from Exir, an Iranian exchange that answers 403 to everyone outside Iran. No rate will ever come back, so that assertion now covers the behaviour that does matter when a source dies: the last known rate stays in storage and LAST_UPDATED_ERROR is raised. The remaining sources get one test each instead of sharing one, so a single dead API no longer hides whether the others answer. Refs Doichain#35
The name filter went into AbstractHDElectrumWallet, which left every wallet that does not descend from it - a plain imported WIF, a watch-only address, segwit-bech32, segwit-p2sh - selecting name outputs as ordinary coins again. It is the worse case of the two: the integration suite's own test address, N6dXumt8aJhkAZAnKNzUBNrBJJez2Efu7e, holds seven names and one spendable output, so seven of its eight coins make Doichain Core answer "Non-name transaction has name input" and a payment from it fails more often than not. isNameOutput() and nameOutpoints() move to LegacyWallet, which every wallet type descends from, including the HD ones that had them. No change in behaviour for HD wallets; getUtxo() in LegacyWallet now filters the same way. While here: fetchTransactions() read this._balances_by_external_index[c].u without checking the entry exists, where the line above it guards _txs_by_external_index the same way. fetchWalletTransactions() in blue-app.ts asks for transactions without fetching balances first, so a second fetch threw TypeError on undefined. An address with no balance recorded has nothing unconfirmed to look for, so it now reads as zero. Refs Doichain#37
Eighteen integration tests pinned a balance, a transaction count, a height or a txid as it stood when they were written, and the chain has moved past every one of them: the legacy test address went from 9948820 to 9640839 and from 2 transactions to 8, the watch-only zpub from 6 to 11, a utxo count from 2 to 0. None of those numbers said anything about the wallet - they described the chain - so they are replaced by assertions on what the methods are for: - a balance against the unspent outputs it is made of - a batched query against the same query one address at a time - a transaction's amount recomputed from its own inputs and outputs - an address derived from a zpub, which never changes - the next address to hand out being one the history has not used The legacy suite gains a test for the name filter. That address holds seven names, which is exactly the wallet coin selection breaks on, and the filter had never been exercised outside the HD wallet. Refs Doichain#37
added 20 commits
October 14, 2026 08:56
The About screen displayed img/bluebeast.png, the mascot of the project this is forked from. It now shows img/icon.png, the Doichain mark that is already in the repository with its @2x and @3x variants, which React Native picks by screen density on its own. The size goes to 102x102: the 102x124 it had was the proportion of the mascot, and the Doichain mark is square, so it would have been stretched. bluebeast.png was referenced from this one line and from nowhere else.
The About screen shows the branch from current-branch.json, which was only written by npm's postinstall hook. It therefore named whatever branch was checked out the last time dependencies were installed: it read "upstream-sync_7.1.8" while the work had been on dev for weeks, and nothing short of reinstalling would have corrected it. The file is gitignored, so it is a build artifact that was not being built. A gradle task now runs the same scripts/current-branch.sh that postinstall does, and preBuild depends on it alongside copyFiatUnits, which already copies fiatUnits.json the same way. It declares itself never up to date, because the branch can change with no input to the task changing. The output stream is opened in doFirst rather than at configuration time, so configuring a build that does not run the task cannot truncate the file. Verified by writing "WRONG_BRANCH_MARKER" into the file and running the task, which replaced it with "dev", and by checking that assembleDebug has :app:generateCurrentBranch in its task graph.
125 was refused by Play, which does not accept a version code it has already seen. versionName stays 7.1.9 - the same release, a new upload.
…pp was installed The line under the release number was new Date(firstInstallTime), the time the app was first installed on that device. On the test phone, which has had the wallet since July 2024, it read "17 July 2024" whichever release was running - it described the phone, not the build. A build now stamps itself: scripts/build-time.sh writes build-time.json, run from npm's postinstall and from a gradle task that preBuild depends on, the same way current-branch.json is written. The stamp is UTC so it does not shift with the builder's timezone, and the screen renders it in local time as before. useFirstInstallTime is no longer used here and is gone with it. Verified on the device: the screen now reads "DoiWallet ver 7.1.9 (build 125 master)" over "Wed Oct 14 2026 12:15:49 GMT+0200", which is the 10:15:49Z the task wrote. The build number stays 125 because that is the APK installed; the 126 bump reaches it only on reinstall.
… fork CI cannot install node_modules. Two causes, both older than this branch - the same lines are in the dev branch this is based on. package-lock.json resolved twenty git dependencies as git+ssh://git@github.com, although package.json asks for them over https. npm follows the lockfile, so a runner with no ssh key gets "Permission denied (publickey)" and npm exits 128. Whoever generated the lockfile had git rewriting https to ssh locally. They are now https, which needs no key; all nineteen remaining ones were checked to answer git ls-remote anonymously. rn-qr-generator pointed at github.com/BlueWallet/rn-qr-generator, which no longer exists - GitHub answers 404, over either protocol. It installs here only because node_modules still holds a copy from an older install. It now comes from npm as 1.4.3. That swap is not a guess. The lockfile's integrity for the fork, sha512-+hZFzqkWjxg7Ey9Tw8wjGuyY6I7R8ypQJa23wL0GUyeEBISxGCnz0W/2ULMxvd/w2FethMeTAkXe8+lDy8/vvw==, is byte for byte the integrity npm publishes for 1.4.3: the same tarball. The installed copy was also compared file by file against the npm tarball and every source file matches. Its package.json carries extra react-native and browser fields, which rn-nodeify adds from this project's own postinstall and which it will add again wherever the package comes from.
The iOS workflow cannot get past "Install Dependencies with Bundler": There was an error parsing `Gemfile`: You cannot specify the same gem twice with different version requirements. You specified: fastlane (~> 2.227.0) and fastlane (~> 2.226.0). Bundler cannot continue. Everything after it is skipped, and the cleanup step then reports "bundler: command not found: fastlane", which is the symptom rather than the cause. Both lines are in the dev branch this is based on, so the workflow has not been able to run from it. The 2.226.0 line goes. Gemfile.lock names only fastlane (~> 2.227.0) in its DEPENDENCIES and resolves 2.227.0, so that line was added after the lock was written and never resolved against it; keeping 2.227.0 leaves the lock valid and moves nothing else. Bundler parses the file again now and reports the requirement as ~> 2.227.0.
BuildReleaseApk fails before its first step runs: This request has been automatically failed because it uses a deprecated version of `actions/upload-artifact: v3` The job never starts, so there is nothing in the log but the setup. Both the upload and the matching download move to v4, and actions/cache goes with them - ci.yml is already on cache@v4. The v4 upload is stricter about artifact names, which has to be checked rather than assumed: this workflow uploads one artifact, "signed-apk", once per run, and the browserstack job downloads it by that name. Nothing collides and nothing relied on v3 merging several artifacts into one directory.
fastlane cannot read the file at all: Syntax error in your Fastfile on line 740: unexpected end-of-input, expecting `end' lane :prepare_keystore opens a Dir.chdir block and closes that, but never closes itself, so every lane after it is nested inside it and the file runs out of ends at EOF. Both the iOS and the Android release workflows call lanes that come later, and neither could have seen them even if the file had parsed. Found by diffing against BlueWallet's Fastfile at v7.1.8, which this one comes from and which parses: the only structural difference is this one missing end. The file is broken in the dev branch this is based on too, so it is not a regression from this work. Exactly one end was missing - appending a second one makes ruby -c fail again. ruby -c now answers Syntax OK, and all ten lanes the workflows invoke resolve.
Three navigation defects, all of the same kind: a route that cannot be reached. WalletDetails and WalletTransactions navigate to "ViewEditMultisigCosignersRoot" with a nested screen, and DetailViewStackParamList declares that name, but the navigator registered the bare screen under "ViewEditMultisigCosigners" instead of the stack under the name with Root. The route the callers ask for did not exist, so editing the cosigners of a multisig wallet did nothing at all. navigation/ViewEditMultisigCosignersStack.tsx was there the whole time, holding the screen and its ScanQRCode companion; it is now what the route points at. Its options become NavigationDefaultOptions, because the stack draws its own header and title and the outer screen would have added a second one. gestureEnabled stays off, as before, so a half-finished edit cannot be swiped away. LNDCreateInvoiceRoot and ScanLNDInvoiceRoot were registered with components that no reachable name defines: 32e1293 "removed lnd components" deleted navigation/LNDCreateInvoiceStack.tsx and its pair but left the lazy wrappers and the registrations behind. Rendering either route threw "Element type is invalid". They are gone, so navigating there is now a navigator warning and no screen, which is what an app without Lightning should do. The callers in WalletTransactions, TransactionListItem and the deeplink handler are left alone - they sit on Lightning paths that a fork without Lightning wallets does not reach, and unpicking them is its own piece of work. loc, navigationStyle, CloseButtonPosition, useTheme and the theme it returned served only the options block that went; ReceiveDetailsStackRoot was already dead, as ReceiveDetails is registered in DetailViewScreensStack. tsc drops from 122 errors to 119 and the file is clean to eslint. The deeplink tests still pass, all 22: they assert what the handler returns, not what the navigator renders.
The keys object declared thirteen of its keys twice. JavaScript keeps the last one, so an earlier block of PascalCase values - InsertContact: 'InsertContact' and eleven more - was dead, overridden further down by the snake_case values that the app actually runs on. tsc reported every one of them as TS1117 and eslint as no-dupe-keys. Removing the overridden block changes nothing at runtime, and that is checked rather than argued: evaluating the object before and after gives 51 keys both times, with no key missing, none added and no value changed. Nothing compares against these values as string literals either - every consumer goes through the constant, as CommonToolTipActions.SignPSBT.id. The four literal hits for 'CoinControl' are a navigation route of the same name, unrelated. CopyFromClipboard goes with it. Its text read loc.wallets.list_long_clipboard, a key that c2da51c removed upstream and that no longer exists in en.json, so the menu item would have rendered undefined as its label - but nothing uses the action: the menu in AddressInputScanButton is built from PasteFromClipboard, whose label is there. tsc drops from 119 errors to 105, eslint from 99 to 86, and the file is clean to both.
addMultisigStep2 passes actions={toolTipActions} to the menu on its mnemonics
modal, and toolTipActions is defined nowhere in the file - it appears exactly
once, as that prop. Rendering the modal throws "toolTipActions is not defined",
which is the modal a user opens to type the mnemonics of a cosigner.
Everything else was in place: ToolTipMenu and CommonToolTipActions are
imported, the askPassphrase state is there, and the handler toggles it. Only
the list was missing. ViewEditMultisigCosigners builds the same menu from the
same pieces, and this now matches it.
CommonToolTipActions was among this file's unused imports for exactly that
reason; Switch and scanQrHelper really are unused and go.
cosignPsbt mapped its inputs with a catch that falls through, so an input it
cannot read becomes undefined in the list. The reader below already expects
that - it asks with inputs[cc]?.address - so the return is made explicit
rather than implied, which is what array-callback-return was pointing at.
eslint drops from 86 errors to 81; both files are clean to it.
…eft behind TransactionsNavigationHeader still built a menu of Lightning refill actions - loc.lnd.refill and refill_external - that nothing renders: the menu it does render is the balance show/hide one. It also kept an allowOnchainAddress state whose only writer was verifyIfWalletAllowsOnchainAddress, gutted to an empty function, called by an effect that therefore did nothing. All three go, and useEffect and useState with them. actionKeys and actionIcons stay although only the dead menu read Refill and RefillWithExternalWallet from them: the map is exported and WalletTransactions imports it, comparing against those same two ids in a handler for the menu that no longer exists. That is a loose end of the same removal, but untangling it means touching another screen. TransactionStatus kept a ToolTipMenu commented out since 668ee73, wrapping the very Text that is rendered uncommented right below it - somebody tried putting the name behind a copy menu and backed out. The comment is gone, with the actions array, the onPressMenuItem handler and the ToolTipMenu and Clipboard imports that existed only for it; git holds the attempt. Three styles nothing refers to go too. eslint drops from 81 errors to 71. Both files are clean to it.
handleOnSelfTestPress was wired to nothing, and its navigate('SelfTest') is the
only one in the app - so the self test screen could not be opened at all, by any
route, although it is registered in DetailViewScreensStack and loc carries its
title. The entry is back in About, where upstream has it, with the same flask
and the testID the e2e suite looks for.
That matters more than it sounds: the screen was fixed only yesterday to check
Doichain instead of Bitcoin, and until now there was no way to run it.
Three handlers opening BlueWallet's own twitter, discord and telegram go. They
were wired to nothing either, and a Doichain fork would not point at them.
SendDetails kept two pieces of state nothing reads. utxo is shadowed by utxos,
which comes from the route and really does limit the balance and the coin
selection; the singular one only ever had setUtxo(null) called on it. units was
written in two places and read in none - the unit of a recipient lives on
addresses[i].unit, and upstream has no such state at all.
One thing the removal makes visible rather than causes: the write in the uri
branch was commented "also resetting current unit to BTC", and that reset was
already not happening, because the address built there takes unit: amountUnit.
Left as it is - changing it would change behaviour.
With TextDecoder, bs58check, route.name, beforeRemoveListenerRef and an unused
payjoinUrl in a destructuring, eslint drops from 71 errors to 58 and both files
are clean but for hook dependencies, which are left alone.
Each was checked to be referenced only by its own import line, and the removals move neither the type errors - tsc stays at 104 - nor the tests, which stay at 218 passing. Several are the same Lightning leftovers seen elsewhere: getLNDHub in blue-app, LightningLdkWallet in selectWallet, LightningCustodianWallet in the deeplink test. eslint drops from 58 errors to 38.
…tyles AddressInputScanButton carried an older menu definition - an actions array with its own actionKeys and actionIcons maps, none of them exported and read only by each other. The menu it renders is built further up from CommonToolTipActions. That dead array held the second reference to loc.wallets.list_long_clipboard, a key that no longer exists in en.json. The rest read nothing and were read by nothing: suppressExchangeRateAlert left over from a commented-out alert in currency, a useRoute in create.js, a useSettings destructuring in success.js, a ref in ManageWallets attached to no list, a declaration in the psbt test that is never assigned, and Notifications in create.js, which only a commented-out call kept alive. eslint drops from 38 errors to 24. tsc holds at 103 and the tests at 218.
Types, imports and one handler, each referenced only where it is declared. Several are Lightning leftovers again: navigateToLightningSettings in NetworkSettings, LightningLdkWallet in selectWallet. addExternalTxId in WalletsList is worth a note rather than a quiet deletion: only the import goes. The helper stays exported, and registers a txid with the native transactions monitor - nothing in the app calls it, so external transactions are never handed to that module. Whether they should be is a feature question, not a cleanup one. eslint drops from 24 errors to 14, tsc from 103 to 102, tests hold at 218.
The cosign test computed a pubkey and two derivation paths for bip32Derivation entries that are commented out in both of its psbt.addInput calls. The comment that matters - why a non-segwit input needs the whole previous transaction - stays; the parked block and the values only it read do not. The test still passes. arrDiff in TransactionDetails and the promisifying helper in the deeplink test are called from nowhere. eslint is down to 9 errors from the 102 this started at, tsc to 102 from 122, and the tests hold at 218.
The inline style on the name-op image moves into the stylesheet. react/no-unused-prop-types reads the destructured parameter of a FlatList renderItem as a component prop. It is not one, and item is used on the very next line, so the rule is silenced there with the reason written down. The three calls to helperSwitchAdvancedMode in the e2e spec stay as they are. The helper has never existed in this repository - checked across its history and against BlueWallet v7.1.8 - but both tests that call it are it.skip'd, so nothing runs and nothing breaks. Removing the calls would quietly drop what the tests are for: one of them switches advanced mode on to see a derivation path. They are annotated instead, so whoever un-skips them knows what is missing. eslint is down to 4 errors, all exhaustive-deps.
The component accepted floatingButtonHeight, defaulted it to 0 and then never read it: its padding was insets.bottom alone. WalletsList passes 70, so the bottom of the wallets list sat under the floating button, with the last rows partly hidden and awkward to tap. Both siblings get this right - SafeArea adds (floatingButtonHeight ?? 0) to its bottom inset and SafeAreaScrollView adds it outright, with a comment saying why. This one now does the same. The lint error that led here was "floatingButtonHeight is assigned a value but never used", left over after dropping it from a dependency array where the memo genuinely did not read it. It turned out the memo should have. Two unnecessary dependencies go with it, here and in Add: in both the hook body does not read the value, so removing it only means recomputing less often. eslint is down to 2 errors, both missing-dependency warnings in SendDetails.
Two effects in SendDetails were missing dependencies. navigation is one of them in both, and listing it costs nothing: useExtendedNavigation wraps useNavigation, whose object is stable for the life of the screen, so the effects do not run any more often than before. handlePsbtSign is the other, and it is not that simple. It is a plain async function rather than a useCallback, so it is a new reference on every render. The effect further down both calls it and lists it, which means that effect already runs on every render - and that is why it carries a hand-written guard commented "Verhindert Endlosschleife". Removing it from that array is not an option either: the effect calls it, so dropping the dependency would sign with a stale wallet and stale route params. The fix is to memoise it. That is 95 lines of the signing path, and a dependency array wrong by one entry signs with stale state, so it is left for a change that can be tested on a device. The diagnosis is written at the definition rather than left for the next person to repeat. find-unused-loc then failed on loc.lnd.refill_external, orphaned by this sweep's removal of the dead Lightning refill menu. Gone from all forty locale files, line by line so the other thirty-nine keep their formatting, each parsed again before being written. npm test now passes end to end: lint clean, 218 unit tests, 90 integration tests across twelve suites, no failures. eslint went from 102 errors to none over this sweep and tsc from 122 to 102.
This was referenced Oct 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Sixty-one commits on top of
dev, which this branch still sits directly on top of, so it fast-forwards.The eight issues
#34 | mainnet BIP32 private prefix was
0x0499ade4, Core uses0x0488ade4|60109f97f#35 | a name was looked up by holder address, so a wallet holding two names on one address spent the wrong output |
c1507290beb384233c512ec8f30387aeff8e#36 | a name sent to a P2SH or Taproot address is lost for good |
ec6a7ddb0#37 | payments picked name outputs as ordinary coins, and Core answered "Non-name transaction has name input" |
259fafb359f590610e006a58de4#38 | doichainjs-lib 6.2.0 broke signing a name purchase |
30f4520ad74550b504#39 | every recommended fee was far below what the miner relays at |
16c52dc7c4da655d6f#40 | the wallet never checked which chain a server follows |
705f99bde#41 | block height and time were estimated from Bitcoin's numbers |
672ee413cTwo of these were proved on a Doichain Core v31.1.6 regtest node rather than argued about. #36: Core accepts the transaction and the name ends up at an address nobody controls. #37: a payment that spends a name output is rejected, and the more names a wallet holds the likelier an ordinary payment fails.
A claim I made early on and withdrew: that
_addPsbtInputneeded fixing for segwit name transfers. Regtest disproved it - BIP143 commits to the scriptCode derived from the witness program, not to the full scriptPubKey.Defects found along the way, with no issue of their own
0x80, which is Bitcoin's; Doichain's is 180. The result is a valid Bitcoin WIF that nothing downstream accepts, so the import ended with no wallet, no error and no explanation.8e189571c6f5a106fe7e5615059fe28a3db5Build
c7d050cecraises the target to API level 36, which Google Play now requires. compileSdk moves with it, since targetSdk may not exceed it. AGP 8.7.2 - the version React Native 0.77.2 pins - answers with a warning rather than an error; it is left in place rather than silenced, because the real fix is a React Native upgrade.Not covered by any test: from targetSdk 35 an app can no longer opt out of drawing behind the status and navigation bars, and from 36 the opt-out is ignored outright. The screens need looking at on a device.
Tests
The integration suite went from 59 passing and 24 failing to 90 passing, 0 failing. Unit: 218 passing across 34 suites. Type errors dropped from 128 to 122 with the first forty-five commits, and to 102 by the end.
Most of those 24 failures were not defects. They compared against balances, heights and txids recorded years ago, or asked for Bitcoin's chain data: a BIP47 notification history that exists on Bitcoin and not here, an xpub whose zpub has been used on Bitcoin and not here, Electrum servers that answer for Bitcoin. Those assertions are replaced by ones that do not expire - a balance against the unspent outputs it is made of, a batched query against the same query one address at a time, a transaction's amount recomputed from its own inputs and outputs, an address derived from a zpub.
Where a figure had to be pinned, it was derived independently and cross-checked against the Bitcoin value the test already recorded, by re-encoding the same key with Doichain's version bytes using base58check and secp256k1 alone.
New: a regtest harness running seven name scenarios against a real node, a suite checking the name index against a live ElectrumX server, and a test that fails if a name transfer overpays or underpays.
After the first write-up: sixteen more commits
Build and CI
3bc4d62the dependencies install without an SSH key or a dead host103a45efastlane is declared once in the Gemfile06054a8CI moves offactions/upload-artifact@v3, which GitHub has retired13e45a7theprepare_keystorelane was never closed, so fastlane could not parse the Fastfile at allScreens that could not be reached or threw
6048c20editing the cosigners of a multisig wallet did nothing: callers navigate toViewEditMultisigCosignersRoot, the navigator registered another name. Two Lightning routes pointing at deleted components are gone.fcb7c86the mnemonics modal on the second multisig step threwtoolTipActions is not defined0ff262fthe self test screen could not be opened from anywhere; its entry is back in About031293athe last rows of the wallet list sat under the floating buttonea39eaathirteen tooltip action keys were declared twice; runtime unchanged, checked by evaluating the object before and afterCleanup
6dd2c6e19eac04c32135563754f4368cb010f9f991Lightning leftovers, unused imports, dead locals, styles and helpers5e7d2c8the remaining hook-dependency errors in SendDetails.handlePsbtSignshould be memoised, but that is 95 lines of the signing path, so it is left for a change that can be tested on a device; the diagnosis is written at its definition.eslint went from 102 errors to none, tsc from 122 to 102.
npm testpasses end to end: lint, 218 unit tests, 90 integration tests.CI: why e2e is red
Tests / testpasses.Tests / e2efails in its build step, before any test runs: Metro cannot resolve./LazyLoadViewEditMultisigCosignersStack, whichnavigation/ViewEditMultisigCosignersStack.tsximports and which does not exist in this repository. The stack file was unreachable until6048c20pointed the route at it, so the bundler never looked. The build script has noset -e, so the log ends in an unrelated-lookingdetox.keystore (No such file or directory).The fix is an 11-line lazy wrapper in the style of
LazyLoadWalletXpubStack.tsx. It is not part of this PR and follows in the next update; until it lands, a release bundle ofdevdoes not build.Still open, deliberately
doi-doicoin; the rest fetchbitcoin, so a balance in JPY is wrong by a factor of about 1.9 million. Deferred by the repository owner; the groundwork is done - direct quotes are not a solution, andapi.yadio.io/exrates/USDcovers 60 of the 62 in one request for the DOI/USD × USD→fiat approach thatRateExtractors.BNRalready uses for RON.img/btc-shape.png), which needs artwork rather than code.librealm.so,libimage_processing_util_jni.soandlibcpp.so. The other seventeen are fine.What has not been verified on a device
The fee fix. The transfer that exposed it was made with the old code; the corrected build has not yet sent a name.
Closes #34, closes #35, closes #36, closes #37, closes #38, closes #39, closes #40, closes #41