Source: pwap-web/shared/images/pezWallet - white_background.png, same
goat artwork as the current icon with the badge recolored. Cropped to
its badge bounds and resized to match the exact scale/position the
black version already used (from the shrink+center fix), so only the
color changes, not the composition. Regenerated every foreground/
legacy/round slot across all densities and both build flavors, plus
the separate solid-fill adaptive background layer so it matches the
new badge color instead of showing dark corners through the mask.
The origin transfer already happened once submitBridgeTransfer's dispatch
result comes back success, so the amount input still holding the just-spent
value made the live balance-vs-amount check flag a false "insufficient
balance" error right next to the real success/waiting state - reading as if
the transfer failed or was retried, when it had already gone through once.
Clearing the field via the existing fillAmountEvent mechanism removes the
stale comparison instead of adding new state.
androidx.lifecycle.viewModelScope is a KTX extension property - it must be
imported in every file that uses it directly, not just inherited via
BaseViewModel (which imports it for its own use only). CI caught this on
the previous commit's real-submission wiring.
swapClicked() used to hand off to the generic Send flow's confirm screen,
which only fire-and-forget submits and pops back immediately regardless of
real dispatch outcome - a real user hit exactly that: tapped confirm, the
screen closed like it succeeded, funds never arrived. Now the bridge builds
its own WeightedAssetTransfer and calls
SendUseCase.performOnChainTransferAndAwaitExecution directly, mirroring the
same real submit-and-await pattern the app's own Swap execution screen
already uses, so Success/Error reflect what actually happened on-chain.
Tapping Swap on the Bridge screen previously gave zero feedback afterward -
the same countdown/success UX already used for swap execution didn't exist
here, so a user had no way to tell whether anything was happening. Moved
ExecutionTimerView (+ its layout) from feature-swap-impl to common, since it
was already fully self-contained (no swap-specific coupling, all its
resources already lived in common) - both features now share the one
component instead of duplicating it.
Wired into BridgeViewModel: after a swap is submitted, actively observe the
destination balance (walletInteractor.assetFlow) for a real increase within
a bounded window, showing the countdown while watching and Success only on
a confirmed balance delta - never a cosmetic timer that "completes"
regardless of whether funds arrived. If the window elapses without
confirmation, it says so plainly (may need manual 3-of-5 review) rather than
showing a false success or a false error.
The horns were touching the canvas corners - looked cramped and got cropped
tightly by circular/squircle adaptive-icon masks on real devices. Now
scaled to 62% of the canvas and centered on transparent padding, matching
how other apps' icons actually look on a real home screen.
BRIDGE_ADDRESS_GENERIC was still the retired legacy single-key bot's
address (5C5CW7xD...), not the real 3-of-5 multisig custody
(BridgeMultisigConstants.MULTISIG_ADDRESS). Confirmed live: a real user
swap sent there landed at the dead address and was never detected by
usdt-bridge's listener (which only watches the multisig account). Funds
were recovered separately using the still-intact legacy seed; this fixes
the actual destination so future swaps land where the listener watches.
Also added the user-confirmed Kurmancî translation for
bridge_wusdt_to_usdt_blocked (values-ku), replacing the now-stale text left
untouched in the previous commit pending native review.
Source updated in pwap-web/shared/images/pezWallet.png - simpler design,
no border/text baked in this time. Regenerated the same way as the first
icon pass: padded to square, resized per density into every existing slot.
BridgeViewModel's wUSDT->USDT gate called http://217.77.6.126:3030/status -
the legacy single-key bridge bot's endpoint. That service was stopped this
same session (see res/validators-tiki.md), so the check always threw,
always caught, and always reported "inactive" - the red warning reflected a
dead dependency, not real reserve state.
Replaced with a direct on-chain read of the multisig's actual USDT balance
on Polkadot Asset Hub (BridgeMultisigInteractor.getPolkadotUsdtReserve, via
a new Assets.Account storage query mirroring the existing Approvals one).
The gate is now per-request: a specific withdrawal is blocked only if it
exceeds the real reserve, not on a global on/off flag requiring full 1:1
parity between total circulating supply and total reserve - matching how
the Rust listener's own reserve-shortfall check already works. Re-checked
on every amount change, not just once per screen load.
bridge_wusdt_to_usdt_blocked now takes the available reserve amount as a
parameter; updated the English and Turkish strings. Left values-ku
untouched deliberately - not fabricating Kurdish translation text without
native review.
Source: pwap-web/shared/images/pezWallet.png. Padded to square (source is
979x1053, corners already fully transparent so the padding is seamless),
then resized per density into every existing icon slot - legacy ic_launcher/
ic_launcher_round and the adaptive foreground layer all use the same full
graphic, matching how the previous icon was already structured; the solid
dark adaptive background layer is untouched since it's still a compatible
fill color for the new logo.
Server-side (pezbridge_bot_config.json, usdt-bridge's bridge_config.json)
was already bumped to a 40,000/200,000 wUSDT renewal threshold/topup this
session, but this app's own BridgeMultisigConstants still had the old
3,000/10,000 values - it construct/checks the exact same on-chain call, so
the mismatch wasn't just a stale display, it meant a signer's app would
consider a 20,000-remaining allowance "fine" while PezbridgeBot was already
paging everyone to renew it.
The DOT<->HEZ bridge backend (the legacy single-key bot) is being
permanently decommissioned - it had zero multisig protection and no
liquidity gate at all on that leg (unconditionally "active" regardless
of pool state, the single least-safe path in the whole bridge). Once
USDT bridging is solid, HEZ is reachable indirectly (bridge USDT, swap
for HEZ within Pezkuwi's own ecosystem) without a dedicated native
bridge that would need its own from-scratch security work (native
currency has no Assets.approve_transfer-style delegation to reuse).
- BridgePair/BridgeDirection collapsed to USDT-only (USDT_TO_WUSDT/
WUSDT_TO_USDT) - kept the pair/pairOptions/picker structure itself
since it costs nothing and is the natural seam for a future pair,
rather than hardcoding a single value inline.
- Removed the now-dead DOT/HEZ exchange-rate fetching (CoinGecko
polling, fallback rate) and liquidity-gate logic entirely, not just
the unreachable enum branches.
- Removed the separate "Bridge DOT ↔ HEZ" entry from the asset
selector bottom sheet (BuySellSelectorMixin) - both it and "Bridge
USDT" opened the exact same screen with no preset, so keeping a
DOT-HEZ-labeled entry pointing at a USDT-only screen would have been
actively misleading, not just redundant.
- bridge_title was the generic toolbar/picker-sheet title but hardcoded
"DOT ↔ HEZ Bridge" regardless of pair - fixed to "USDT Bridge".
- Removed now-unreferenced strings (wallet_asset_bridge,
bridge_pair_dot_hez, bridge_rate_format(_reverse),
bridge_hez_to_dot_note/warning/blocked) from the base strings.xml.
Confirmed via research: feature-bridge-api/feature-bridge-impl modules
exist but are unrelated orphaned scaffolding, not depended on by :app,
zero code overlap with this screen - correctly out of scope here.
Aleph Zero's only configured public node (wss://ws.azero.dev) is
currently down (confirmed via direct WebSocket handshake - real,
external, isolated outage on their side, chain itself is healthy and
active). No free/working alternative public RPC was found to add as
failover. Rather than hard-fail CI on an external dependency neither
our code nor config controls, convert connectivity-class exceptions
(TimeoutCancellationException, directly or wrapped) into a JUnit
assumption-skip - this mirrors how production balance-sync code
already isolates per-chain RPC failures instead of treating them as
hard errors. A real bug (bad decoding, wrong assertion, etc.) still
fails the test exactly as before.
QueryableStorageEntry2 has always had a single-key-tuple query(k1, k2)
alongside entries()/keys(), but QueryableStorageEntry3 only ever grew
entries()/keys() - nothing in the codebase needed a direct 3-key point
query until BridgeMultisigRuntimeApi's Assets.Approvals lookup. Added
query(k1, k2, k3), mirroring QueryableStorageEntry2's implementation
exactly.
PR #13 fixed this for android_build.yml's Gradle invocation, but
code-quality.yml's ktlint job runs its own separate ./gradlew ktlint
without the same env passthrough - runtime/build.gradle reads this
secret unconditionally during project configuration, so ktlint failed
outright as soon as Tron support (which needs it) was merged into
this branch.
- module(Modules.X) needs substrate_sdk_android's module() extension
imported explicitly, not just the Module type.
- accountIdIn is a MetaAccount member, not a top-level import.
- submitExtrinsicAndAwaitExecution(...).requireOk() resolves to the
Result<T>-preserving overload, so wrapping it in runCatching produced
Result<Result<T>>; unwrap with getOrThrow() first so the T-returning
overload applies.
Card now opens in a slim single-line pill showing only Trust Score.
Tapping it expands to the full card (citizen status, world Kurdish
count, referral actions); a chevron in the expanded header collapses
it back. Expand state persists across scroll/recycling within the
session but resets to collapsed on a fresh app launch.
Any of the USDT bridge multisig's 5 known signatories now sees a
status button on the Bridge screen: green when the automation key's
spending allowance is healthy, red with a Sign action once it drops
below threshold. Signing submits the renewal's as_multi approval
directly from the wallet, mirroring the same on-chain action already
available via pwap-web and pezbridge-sign.pex.mom - a third
independent channel for the same operation, none of which is a
single point of failure.
CI caught this: depositorAccountModel's inner type became AccountModel?
when the depositor field went nullable, but onDepositorClicked still
called it.address() directly instead of null-checking first.
The pending-operation model assumed a call already exists on-chain
(Multisig.Multisigs entry with a Timepoint) before it could be
reviewed or signed. A signatory who is the first to see a brand-new
call - reachable via the deep link before anyone has submitted
anything - hit "operation not found" and the screen refused to open,
even though the deep link already carried the call's data in its
callData param (previously decoded only to format Executed/Rejected
dialog text, then discarded for the Active case that needed it).
- PendingMultisigOperation.timePoint/depositor are now nullable to
represent "not yet submitted"; composeMultisigAsMulti's
maybeTimePoint parameter already accepted null (used for ordinary
first-time multisig-origin submissions via MultisigSigner), so the
pallet-call layer needed no changes.
- New PendingMultisigOperation.notYetSubmitted(...) factory and
MultisigOperationDetailsInteractor.buildNotYetSubmittedOperation()
build a synthetic operation from the deep link's callData, hash
verified against the URL's callHash rather than trusted blindly.
- OperationIsStillPendingValidation now skips its on-chain
"still pending" check for a not-yet-submitted operation, since it
would otherwise always fail and block the very first submission.
- MultisigOperationPayload carries the callData through so both the
details screen and the full-details screen (independently reachable
via "Call Details", since that button is available whenever call
data is known) can construct the synthetic operation.
The Bridge screen let a user type any amount and enabled the Swap
button regardless of what they actually held, unlike the Swap screen
which shows a live "Max: X" and disables on overflow. The confirm-step
ValidationSystem would still catch it before signing, but only after
routing through Send - no immediate feedback on the Bridge screen
itself.
Wires WalletInteractor.assetFlow(chainId, assetId) to the currently
selected origin side (re-subscribed on every direction/pair change),
tracking Asset.transferable - the same field the real transfer
validation checks. Reuses the existing MaxAmountView/MaxAvailableView
widget the Swap screen already uses for the "Max: X" display, and adds
a matching error-bordered state to BridgeAssetInputView. The
"Insufficient balance" string (bridge_insufficient_balance) was
already translated into every locale but never wired to any code.
The redesigned Bridge cards resolved asset icons via the chain-icon
helper (asset.icon.asIconOrFallback()), which treats the value as a
ready-to-load URL. Chain.Asset.icon is frequently a bare filename
resolved through AssetIconProvider's base-URL/color-mode logic, so
any asset relying on that path rendered a blank icon (Polkadot Asset
Hub's USDT, and the DOT/HEZ utility assets on the affected side).
Switched to assetIconProvider.getAssetIconOrFallback(asset), the same
path every other asset row in the app uses.
Also switched the card subtitle from chain.name to
chain.displayNameWithAssetStandard(), so Pezkuwi Asset Hub shows its
PEZ-20 token-standard label, matching Send/Receive/the balance list.
Replaces the segmented pair/direction buttons and plain-text amount
sections with token-icon cards (BridgeAssetInputView), a one-tap flip
button, and a bottom-sheet pair picker (BridgePairListBottomSheet).
BridgeViewModel gains only additive presentation data (fromCard/toCard/
pairOptions, derived from the existing pair/direction state and the
same chainId/assetId mapping swapClicked() already used) - all bridge
business logic (rate, fee, minimum, liquidity warnings, submission)
is unchanged.
Confirmed live via logcat: tapping "send" to a P2SH address force-closed
the app - FATAL EXCEPTION: IllegalArgumentException: Bech32 string is
mixed case, thrown from Chain.accountIdOf() -> bitcoinAddressToAccountId()
(native-SegWit-only) via the Confirm Send screen's address icon generator,
a completely separate call path from the transaction-building fix already
shipped. Chain.accountIdOf() now falls back to decodeBitcoinDestination()
for P2SH/P2PKH, safe here because this generic accountId is only ever used
for opaque purposes (identicons, presence checks) - the real send path
already bypasses it entirely and builds the scriptPubKey straight from the
typed BitcoinDestination.
CI caught a redeclaration - Base58Check is already a chain-agnostic object
in this same package (TronAddress.kt), used for Tron's own Base58Check
addresses. Bitcoin's legacy addresses are the same shape (1 version byte +
20-byte hash), just a different version byte - reuse it directly.
Confirmed live via logcat: the real "QR can't be decoded" error was
neither a QR format issue (the earlier BIP21 fix, while correct, wasn't
the cause here) nor a scan failure - the raw QR content was a bare P2SH
address (3...), which this wallet's native-SegWit-only address validation
rejected outright. A real exchange (OKX) withdrawal address turned out to
be P2SH-only with no way to request native SegWit instead.
Adds Base58Check decoding and BitcoinDestinationAddress.kt (P2WPKH/P2SH/
P2PKH, each producing the correct scriptPubKey shape - these differ from
each other, not just cosmetically). This wallet's OWN address/derivation
stays native-SegWit-only and untouched (bitcoinAddressToAccountId is
unaffected) - only the SEND path now recognizes wider destination types.
BitcoinTransactionService/RealBitcoinTransactionService now take the raw
recipient address string instead of a pre-resolved AccountId - converting
to AccountId this early would have discarded which script shape the
destination actually needs, silently building a P2WPKH output for a P2SH
recipient (a real fund-misdirection risk, not just a validation gap).
Test vectors: Satoshi's genesis P2PKH address, a well-known P2SH vanity
address, and the real OKX withdrawal address from this session's live QR
scan, all cross-verified against an independent Python implementation
before writing the Kotlin equivalent.
Reverts the temporary QrDiag logging added to find this - no longer needed.
Neither the raw scanned QR string nor the InvalidFormatException from a
failed decode was ever logged - the failure path only shows a generic
toast, making it impossible to diagnose from logcat alone. Temporary,
will revert once the actual failure is identified.
Confirmed live: sending BTC back out via QR-scanning an external wallet's/
exchange's receive address failed with a generic "QR can't be decoded"
error. Root cause: the QR decoder only ever recognized two shapes -
substrate:<address>:<pubkey> triples and bare address strings - neither of
which matches the bitcoin:<address>?amount=... BIP21 URI most wallets and
exchanges generate for a BTC address. Adds BitcoinUriQrFormat, tried
alongside the existing formats, that strips the scheme and query string
before validating the address the normal way.
Test predates the isBitcoinBased field (added on the Bitcoin branch after
Tron's send branch already had this test file) - CI caught the missing
constructor argument.
The merge silently combined two independent additions of the same
dependency (my fun signerProvider() and Tron's val signerProvider) since
they landed on non-conflicting lines - Dagger correctly caught this as
[Dagger/DuplicateBindings]. Keeping Tron's val form, since it was already
CI-verified on that branch.
Bitcoin was branched off main independently instead of continuing on top
of the Tron send branch, which caused real divergence: both branches touch
the same shared signing/DI code (multiChainEncryptionFor, getMetaAccountKeypair,
NodeHealthStateTesterFactory, AssetsModule/TypeBasedAssetSourceRegistry,
CloudBackup schema, Fee model), and both need to ship together in one
coordinated release once wallet-util's master is unblocked. Merging now
reconciles that before it gets any harder, and gives one branch/versionCode
history for device testing instead of two that silently diverge.
Conflict resolutions of note:
- SecretStoreV2/SecretsSigner: getKeypair and multiChainEncryptionFor now
recognize Tron AND Bitcoin accounts (isTronBased/isBitcoinBased both
threaded through, checked before the Ethereum fallback).
- AccountDataSourceImpl: both address-backfill migrations now run
sequentially in one coroutine (Tron's fix for a race against the legacy
migration applies equally to Bitcoin's backfill).
- CloudBackup: kept Tron's forward-looking Solana schema reservation
alongside Tron's and Bitcoin's now-real fields.
- ChainRegistryModule/NodeHealthStateTesterFactory: adopted Tron's
self-contained short-lived OkHttpClient (simpler than threading the
shared app-wide client through RuntimeDependencies, which is reverted).
- RealSecretsMetaAccount: extended Tron's chain-account MultiChainEncryption
fix to also cover Bitcoin, for the same underlying reason.
- ChainExt.isValidAddress: Tron's branch independently closed the
pre-existing Tron validation gap; kept alongside Bitcoin's own check.
wallet-util's master lacks the Tron/Bitcoin chain config (reset to the last
Play-Store-compatible state, see wallet-util history around 2026-07-11) -
this mirrors the exact override feature/trc20-tron-send used, so a real
device build can see the Bitcoin chain entry. MUST be reverted to master
before this branch merges.
CI caught [Dagger/MissingBinding] for SignerProvider in
WalletFeatureComponent - AccountFeatureApi already exposes it, but
WalletFeatureDependencies (feature-wallet-impl's own dependency interface)
never re-declared it, so it wasn't reachable by BitcoinAssetsModule's
provideBitcoinTransactionService.
RealBitcoinTransactionService builds and signs native SegWit transactions
client-side (mempool.space only exposes UTXOs/fee-rate/broadcast, not
construction, unlike TronGrid): greedy largest-first UTXO selection over
confirmed UTXOs, inputs*68 + outputs*31 + 11 vsize heuristic for fees
(matching pezkuwi-exchange's proven approach), RBF-signaled inputs, and a
546-sat dust threshold that folds sub-dust change into the fee rather than
creating an uneconomical output.
Each input gets its own BIP143 sighash, signed via the existing raw-hash
signing primitive (NovaSigner.signRaw with skipMessageHashing) already used
for Ethereum/Tron, then DER-encoded with low-S normalization - no new
signing call path was added, only the DER encoding step.
Also fixes multiChainEncryptionFor/getMetaAccountKeypair to recognize
Bitcoin accounts: previously neither function had any Bitcoin (or Tron)
branch, so signing would have silently used the wrong keypair. Threads a
bitcoin flag through mapMetaAccountSecretsToKeypair/DerivationPath and
AccountSecrets.keypair(chain)/derivationPath(chain) the same way ethereum
already is.
CI caught onChainAssetId's when as non-exhaustive; a codebase-wide sweep
for every other exhaustive when on Chain.Asset.Type found one more
(existentialDepositError in Common.kt) that would have failed the same way.
CI caught "no parameter with name 'bitcoinAddress'" in AccountMappers.kt -
every other MetaAccount subclass got these constructor params threaded
through in the Phase 2 batch, but PolkadotVaultMetaAccount only got the
isBitcoinBased guard in supportsAddingChainAccount(), not the params.
Adds a mempool.space-style REST client (RealBitcoinApi, with the same
retry-on-429 pattern as TronGridApi), a polling AssetBalance implementation,
and BitcoinNodeHealthStateTester so the Networks screen can show live
health for Bitcoin nodes (mirrors TronNodeHealthStateTester's rationale:
mempool.space speaks plain REST, not JSON-RPC, so the existing Ethereum/
Substrate testers can't be reused).
Wires Chain.Asset.Type.BitcoinNative through the asset-type mappers and
TypeBasedAssetSourceRegistry, and BitcoinAssetsModule into AssetsModule -
transfers/history stay on the Unsupported stubs until send support lands.
CI caught 5 unresolved references (toBitcoinAddress, bitcoinAddressToAccountId,
emptyBitcoinAccountId, bitcoinPublicKeyToAccountId, isValidBitcoinAddress) -
the functions existed in common/utils/BitcoinAddress.kt but were never
imported here.
Derives a BIP84 (native SegWit) keypair alongside the existing substrate/
ethereum/tron ones, stores it in MetaAccountSecrets/CloudBackup, persists
bitcoinPublicKey/bitcoinAddress on meta_accounts (DB migration 74->75), and
threads isBitcoinBased through Chain/ChainLocal/mappers/ChainExt/ChainRegistry
the same way isTronBased was wired. Adds BitcoinAddressBackfillMigration,
mirroring TronAddressBackfillMigration, so existing wallets get a Bitcoin
address without re-import.
Input 0's sequence was set to 0xeeffffffL, but BIP143's doc shows "eeffffff"
as the on-wire (little-endian) bytes, not the integer value - the correct
value whose LE encoding is eeffffff is 0xffffffee, not 0xeeffffff. Caught by
CI: 83/84 common module tests passed, only bip143Sighash failed. Isolated by
building a standalone Kotlin+JDK repro outside Gradle/Android (no JDK available
in this environment) - every other intermediate value (hashPrevouts, outpoint,
scriptCode, hashOutputs) matched BIP143 exactly; only hashSequence was wrong,
narrowing it to this one transcription error rather than the implementation.
Phase 1 of native BTC send/receive support (native SegWit/P2WPKH only, bc1q...
addresses - Taproot and legacy/P2SH-SegWit explicitly out of scope). Hand-rolled
since no Bitcoin library exists on this project's classpath (bitcoinj/PSBT/Base58/
Bech32) - same rationale as this session's Tron work needing its own Base58Check.
- Bech32/Bech32m codec (BIP173/BIP350) + segwit address encode/decode
- DER ECDSA signature encoding with BIP62 low-S normalization
- HASH160/P2WPKH scriptPubKey construction and address<->accountId conversion
- BIP143 sighash computation and raw segwit transaction serialization
Every primitive is verified byte-for-byte against official BIP173/BIP350/BIP143
test vectors (fetched directly from github.com/bitcoin/bips at implementation
time) - see each *Test.kt's doc comment for exact vector provenance. No JDK is
available in this environment to run these locally; also cross-verified via an
independent Python transliteration of each function before committing.
Isolated from the Tron feature branch so it can land on main independently -
android_build.yml is a reusable workflow pinned to @main by every caller, so a
secret it doesn't declare/pass through never reaches the build regardless of
which branch's source gets checked out.
Complements the retryOn429 fix - having a key means requests are far less likely to hit
the rate limit at all, rather than just retrying transparently after they do. Wired the
same way as INFURA_API_KEY/DWELLIR_API_KEY: buildConfigField read from local.properties or
CI secret, added TRONGRID_API_KEY to android_build.yml's secret passthrough (every caller
already uses secrets: inherit, so no per-workflow changes needed beyond this).
TronGrid's public (no API key) endpoint rate-limits aggressively under normal, human-paced
usage - confirmed live during send-flow testing, where every Confirm tap (each re-running
fee estimation + broadcast) started hitting bare 429s after only a few attempts, with no
way through except retrying by hand until one happened to land outside the rate-limit window.
Added retryOn429 (exponential backoff, same shape as the existing test-only helper in
TronBalancesIntegrationTest) at the RealTronGridApi level so every call - fee estimation,
broadcast, balance reads - retries transparently. Broadcast is safe to retry on 429
specifically since it means TronGrid rejected the request before processing it, not that
the transaction may already be in flight.
SecretsSigner.multiChainEncryptionFor() only recognized substrate accounts, standard
Ethereum accounts, and explicit per-chain override accounts. A Tron account's accountId
matches none of those (same secp256k1 scheme as Ethereum, but a different SLIP-44 derivation
path/keypair), so it fell through to null and crashed on the `!!` - found live testing the
send flow end-to-end, reproduced on every Confirm tap.
Fixing just the null case would have signed Tron transfers with the Ethereum keypair
instead of the Tron one (getMetaAccountKeypair only distinguished ethereum/substrate),
producing an invalid signature. Threaded a proper isTronBased flag through
down to mapMetaAccountSecretsToKeypair so Tron gets its own MetaAccountSecrets.TronKeypair.
TronGrid's createtransaction rejects amount=0 with a ContractValidateException.
estimateNativeFee called it unguarded (unlike the TRC-20 path, which already
wraps its dry run in runCatching), so the send screen's reactive fee loader
surfaced this as a generic "Network not responding" error whenever it ran
before the user typed an amount - found live testing the send flow end-to-end.