Skip to content

Name operations, fees, chain identity: issues #34-#41 and what turned up with them - #42

Merged
davidreband merged 61 commits into
Doichain:devfrom
davidreband:dev
Oct 15, 2026
Merged

davidreband merged 61 commits into
Doichain:devfrom
davidreband:dev

Conversation

@davidreband

@davidreband davidreband commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

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 uses 0x0488ade4 | 60109f97f
#35 | a name was looked up by holder address, so a wallet holding two names on one address spent the wrong output | c1507290b eb384233c 512ec8f30 387aeff8e
#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" | 259fafb35 9f590610e 006a58de4
#38 | doichainjs-lib 6.2.0 broke signing a name purchase | 30f4520ad 74550b504
#39 | every recommended fee was far below what the miner relays at | 16c52dc7c 4da655d6f
#40 | the wallet never checked which chain a server follows | 705f99bde
#41 | block height and time were estimated from Bitcoin's numbers | 672ee413c

Two 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 _addPsbtInput needed 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

  • Importing a minikey or a BIP38 backup did nothing at all. Both wrote the recovered key as a WIF with the hardcoded version byte 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. 8e189571c
  • A name transfer paid 1,000,000 satoshi too much, every time. Measured on a real transfer: a fee of 1,018,055 where about 25,000 was due. The name output must stay out of coin selection, so its input is added afterwards - and its value was then assigned to no output and went to the miner. The app displayed the fee it had computed, not the one the transaction pays. 6f5a106fe
  • The transfer form was offered on names the wallet cannot sign for. The chain check asked only whether an output still holds its name, which is true of the very transaction a user opens to look at a name they have sent away. 7e5615059
  • The self test screen always reported a failure. It stopped at its first check, which compared a Bitcoin WIF against a Bitcoin address; everything after that line never ran. Four of its checks only execute on a device and all four were broken there. fe28a3db5

Build

c7d050cec raises 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

  • 3bc4d62 the dependencies install without an SSH key or a dead host
  • 103a45e fastlane is declared once in the Gemfile
  • 06054a8 CI moves off actions/upload-artifact@v3, which GitHub has retired
  • 13e45a7 the prepare_keystore lane was never closed, so fastlane could not parse the Fastfile at all

Screens that could not be reached or threw

  • 6048c20 editing the cosigners of a multisig wallet did nothing: callers navigate to ViewEditMultisigCosignersRoot, the navigator registered another name. Two Lightning routes pointing at deleted components are gone.
  • fcb7c86 the mnemonics modal on the second multisig step threw toolTipActions is not defined
  • 0ff262f the self test screen could not be opened from anywhere; its entry is back in About
  • 031293a the last rows of the wallet list sat under the floating button
  • ea39eaa thirteen tooltip action keys were declared twice; runtime unchanged, checked by evaluating the object before and after

Cleanup

  • 6dd2c6e 19eac04 c321355 63754f4 368cb01 0f9f991 Lightning leftovers, unused imports, dead locals, styles and helpers
  • 5e7d2c8 the remaining hook-dependency errors in SendDetails. handlePsbtSign should 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 test passes end to end: lint, 218 unit tests, 90 integration tests.

CI: why e2e is red

Tests / test passes. Tests / e2e fails in its build step, before any test runs: Metro cannot resolve ./LazyLoadViewEditMultisigCosignersStack, which navigation/ViewEditMultisigCosignersStack.tsx imports and which does not exist in this repository. The stack file was unreachable until 6048c20 pointed the route at it, so the bundler never looked. The build script has no set -e, so the log ends in an unrelated-looking detox.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 of dev does not build.

Still open, deliberately

  • 59 of the 62 fiat currencies show the price of bitcoin, not of Doichain. Only USD, EUR and GBP read doi-doicoin; the rest fetch bitcoin, 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, and api.yadio.io/exrates/USD covers 60 of the 62 in one request for the DOI/USD × USD→fiat approach that RateExtractors.BNR already uses for RON.
  • The bitcoin watermark on the wallet cards (img/btc-shape.png), which needs artwork rather than code.
  • Three native libraries are built for 4 KB pages, so Play reports no 16 KB support: librealm.so, libimage_processing_util_jni.so and libcpp.so. The other seventeen are fine.
  • R8 is off, so nothing is shrunk, optimised or obfuscated.

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

David Reband and others added 30 commits October 9, 2026 11:13
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
David Reband 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant