Commit Graph
100 Commits
Author SHA1 Message Date
SatoshiQaziMuhammed 46d7e66f99 Fix open Dependabot and code scanning alerts (#21)
* build(rust): bump vulnerable crates in native binding lockfiles

- rand 0.8.5 -> 0.8.6 (GHSA-cq8v-f236-94qc) in hydra-dx-math and sr25519-bizinikiwi
- keccak 0.1.5 -> 0.1.6 (GHSA-3288-p39f-rqpv) in hydra-dx-math and sr25519-bizinikiwi
- bytes -> 1.11.1 (GHSA-434x-w66g-qw3r) in hydra-dx-math and metadata_shortener
- time 0.3.41 -> 0.3.47 (GHSA-r6v5-fh4h-64xc) in hydra-dx-math
- tracing-subscriber 0.3.19 -> 0.3.20 (GHSA-xwfj-jgwm-7wp5) in hydra-dx-math

Lockfile-only updates via cargo update --precise; Cargo.toml requirements
are unchanged. time 0.3.47 pulls serde 1.0.229/serde_core and requires
rustc 1.88, which the stable toolchain used by CI satisfies.

* ci: restrict default GITHUB_TOKEN permissions to contents: read

None of these workflows write through GITHUB_TOKEN: artifact upload,
Firebase, S3, Play Store, gh-pages and the Appium dispatch all use their
own credentials. Reusable workflows they call inherit the same cap.

* fix(dapp): send only fixed, JSON-quoted error messages to dApp pages

The polkadot.js responder forwarded Throwable.message of any rejection
error to the page and both responders interpolated the message into
JavaScript without escaping. Map polkadot.js rejections to a fixed public
message (unknown errors become "Request failed"), give MetamaskError an
explicit publicMessage, and quote both with JSONObject.quote so a message
can no longer break out of the string literal.

* ci: stop installing the removed legacy SDK 'tools' package

android-actions/setup-android installs 'tools platform-tools' by default.
sdkmanager now fails with "Failed to find package 'tools'", which aborts
dependency installation for every build and lint job. Only platform-tools
is needed; nothing in the workflows uses the legacy tools directory.
2026-10-03 00:39:49 +04:00
SatoshiQaziMuhammed d8b31739e7 ci: fail the build when a 64-bit native library is not 16 KB aligned (#20)
* ci: fail the build when a 64-bit native library is not 16 KB aligned

Play rejects updates whose 64-bit native libraries are not aligned for 16 KB memory
pages, but nothing in the build says so. A misaligned .so surfaces only as a policy
issue in Play Console, days after a release has gone out — which is how one reached
production here unnoticed.

The check reads what the build actually produced rather than trusting the dependency
list, because a single transitive dependency shipping an unaligned library is enough
to fail the whole bundle. It walks every .aab and .apk under app/build/outputs, pulls
each arm64-v8a and x86_64 .so, and compares its largest LOAD segment alignment against
16384. 32-bit ABIs are skipped: those devices use 4 KB pages.

Verified against the v1.2.0 release bundle before it was cleaned up: 14 libraries
scanned, and it caught libpl_droidsonroids_gif.so at 0x1000 — the one library Play
flagged. A check that cannot fail on known-bad input is not a check.

* test: make the tamper-detection test actually tamper

checkTamperedDataFails wrote to encoded[lastIndex - 1] but computed the new value from
encoded.last(). Whenever those two bytes already differed by exactly 0x01 the write was
a no-op, the ciphertext stayed valid, decryption succeeded, and the expected
AEADBadTagException never arrived — so the test failed for reasons unrelated to what it
checks. One run in 256.

Flip a bit in the byte being written instead, which changes the ciphertext every time.
2026-08-03 07:25:46 -07:00
SatoshiQaziMuhammed 39dd67ba94 fix: drop the unused GIF library that blocks 16 KB page size support (#19)
Play rejects updates whose native libraries are not aligned for 16 KB memory pages,
enforced since Nov 1 2025. Measuring the released bundle showed exactly one library
short, and only on one ABI:

  arm64-v8a   7/7 aligned at 0x4000
  x86_64      6/7 aligned; libpl_droidsonroids_gif.so at 0x1000

That library comes from pl.droidsonroids.gif:android-gif-drawable 1.2.19, inherited
from upstream and never used here: no GifDrawable, GifImageView, GifTextView or
pl.droidsonroids reference exists in any Kotlin, Java or XML file in the project.

Removing it beats upgrading to 1.2.32 — an unused dependency should not be carried at
any version, and this also drops roughly 175 KB of dead native code across four ABIs.

Everything else in the bundle was already aligned, and jniLibs.useLegacyPackaging is
already false, so nothing more is needed for the requirement.
2026-08-03 06:52:49 -07:00
SatoshiQaziMuhammed 27aff873a6 feat: tell users a new version exists, and let them rate the wallet from inside it (#17)
Two gaps, both invisible until you look for them.

There was no update mechanism at all. Someone on an old build stayed on it until they
happened to open the Play Store on their own. For a wallet that is worse than an
inconvenience: today's multisig signing fix would have reached nobody who was not
already looking.

InAppUpdates asks Play on every foreground. A user a few days behind gets the flexible
flow — the download runs in the background and the wallet stays usable, because
interrupting someone mid-transfer to force an update is its own kind of harm. Past
fourteen days of staleness, or on a release marked priority 4+ in Play Console, it
switches to immediate. onResume finishes an interrupted immediate update and installs a
flexible one that completed while the app was backgrounded; without that the first
leaves a user stuck and the second never installs.

Ratings had the same shape of gap: people who would happily rate the wallet never do,
because nothing ever asks. Play's in-app card asks without sending them to the store.

Play answers neither "has this user rated" nor "what did they choose" — by design. It
also throttles to a handful of showings a year and silently drops the rest. So the gates
in AppReviewTracker are not there to avoid nagging, which Play already handles; they
exist to spend those few real chances well: three successful operations, three days
since first use, ninety since the last ask, and not within two days of an error.

Recording sits in RealExtrinsicService, the single point every on-chain action passes
through, so transfers and staking are covered without a hook per screen. The tracker
swallows everything it touches — a rating counter must never be able to fail a transfer.

Both features no-op outside a Play install, so neither can be verified from a Firebase
build; that needs an internal testing track.

Version 1.2.0.
2026-08-03 04:52:07 -07:00
SatoshiQaziMuhammed 56c37615a0 fix(multisig): approve operations on Pezkuwi chains again by dropping the custom era extension (#16)
* diag: probe both era encodings when building the multisig weight-estimation extrinsic

Approving a multisig operation on Pezkuwi Asset Hub fails with "Failed to encode
extension CheckMortality". The on-device stack puts it in wrapInFakeExtrinsic,
reached only from estimateCallWeight — asMulti needs a max_weight, so a throwaway
signed extrinsic is built to measure the inner call. Plain transfers never reach
that path, which is why sending HEZ works while approving does not.

The same failure on Polkadot Asset Hub was fixed by gating the era encoding on
chain identity, and Pezkuwi Asset Hub satisfies that gate: its genesis hash matches
the constant, so PezkuwiCheckImmortal should already be in use. Reading the code
cannot say which side actually fails.

So build the extrinsic both ways and report each outcome with its full cause chain.
One reproduction then names the encoding the chain accepts, and the permanent fix
follows from that rather than from another guess.

Diagnostic only — the probe and the DIAG constant come out with the real fix.

* style: drop the blank line ktlint flags before the closing brace

* fix(multisig): approve operations on Pezkuwi chains again by dropping the custom era extension

Approving a multisig operation on Pezkuwi Asset Hub failed with "Failed to encode
extension CheckMortality", so no signatory could approve from the wallet — the
bridge treasury included. Initiating an operation worked, which made the break look
narrower than it was.

Only multisig reaches the failing code. asMulti needs a max_weight, so
estimateCallWeight builds a throwaway signed extrinsic to measure the inner call.
Plain transfers never build one, which is why sending HEZ kept working.

PezkuwiCheckImmortal was the cause, not the cure. It passes a raw
DictEnum.Entry("Immortal", null) as the era, and the SDK rejects it:

  EncodeDecodeException: Entry(name=Immortal, value=null) (Entry)
    is not a valid instance of Era (EraType)

The premise it was written on — that Pezkuwi's pezsp_runtime Era breaks the standard
codec — does not hold. Measured on device against Pezkuwi Asset Hub, the standard
CheckMortality(Era.Immortal) encodes without complaint, and ExtrinsicBuilderFactory
has been signing every ordinary transfer through it with a mortal era all along. The
era type resolves from metadata by index, so the renamed module path never mattered.

So the chain gate goes and both custom extensions go with it: PezkuwiCheckImmortal
had one caller, PezkuwiCheckMortality had none. One path for every chain.

Verified on device (Pezkuwi Asset Hub, versionCode 340): a build that tried both
encodings logged the custom one failing and the standard one succeeding, then
carried a real 3-of-5 approval through to MultisigExecuted with the inner call Ok
and 10 HEZ leaving the multisig.
2026-08-03 04:30:27 -07:00
Satoshi Qazi Muhammed 8c4550780e fix(push): retry FCM token requests instead of failing on the first timeout
Enabling notifications started failing with

  java.util.concurrent.ExecutionException: java.io.IOException: SERVICE_NOT_AVAILABLE

SERVICE_NOT_AVAILABLE is Firebase saying its endpoint is momentarily
unreachable. It is transient, and Firebase's own guidance is to retry with
backoff. requestToken() made a single attempt with no catch, so a few seconds of
that ended the flow and put a raw stack trace in front of the user.

Nothing about the app or its Firebase project changed — the project is healthy
and the API key still authenticates. The app simply had no tolerance for a
service that is documented as intermittently unavailable, so it worked until the
first time Google was slow to answer.

Now four attempts with 1s/2s/4s backoff.

The wrapping matters: the IOException arrives inside an ExecutionException, so
catching IOException directly would never have matched and the retry would have
been dead code. The check walks the cause chain instead. Failures that are not
network-related — a wrong google-services.json, a missing Play Services install —
are rethrown on the first attempt, since retrying those only delays the same
error.
2026-07-31 20:22:17 -07:00
Satoshi Qazi Muhammed df745e9147 fix: point CHAINS_URL/EVM_ASSETS_URL/etc back at wallet-utils master
wallet-utils master now has the coordinated Bitcoin/Tron/Solana chain
config content (its own compat-revert was reverted back and merged with
pending/post-fix-release now that this app version can parse it) - drop
the temporary pending/post-fix-release override used during development.
2026-07-18 23:51:38 -07:00
Satoshi Qazi Muhammed d999f48891 fix: gate PezkuwiCheckImmortal on chain identity, not extension presence
Confirmed via on-device logcat trace: approving a multisig operation on
Polkadot Asset Hub failed with "Failed to encode extension CheckMortality"
because isPezkuwi checked for the "AuthorizeCall" signed extension, which
Polkadot Asset Hub also declares - so PezkuwiCheckImmortal's raw DictEnum
value was used there too, and it isn't a valid instance of Polkadot's own
Era type codec. Switched the gate to the existing chain.isPezkuwiChain
(genesis-hash based) check already used for the bizinikiwi signing context
split. Also removes the temporary diagnostic logging added while tracing
this bug and the earlier deep-link routing fix.
2026-07-18 19:04:12 -07:00
Satoshi Qazi Muhammed e0feb9581b feat: route USDT buy/sell to Bridge flow when no provider is available
USDT has no buy/sell provider on either bridge leg (Pezkuwi wUSDT / Polkadot
USDT); Bridge is the actual way to acquire it there, so route straight to it
instead of a dead-end "not supported" message.
2026-07-18 17:55:55 -07:00
Satoshi Qazi Muhammed 3f6e6441c6 debug: trace isPezkuwi detection and CheckMortality encoding failure
Real on-device failure while approving a multisig operation on Polkadot Asset
Hub: 'Failed to encode extension CheckMortality'. isPezkuwi is detected via
signedExtensions.any { it.id == "AuthorizeCall" } - confirmed via direct
metadata inspection that AuthorizeCall exists on BOTH Polkadot Asset Hub and
Pezkuwi Asset Hub (both on Metadata V16), so this heuristic can't actually
distinguish the two chains. Need to confirm what isPezkuwi evaluates to at
the moment of the real failure, and exactly where the throw happens
(setTransactionExtension vs buildExtrinsic), before proposing a fix.
2026-07-18 17:23:36 -07:00
Satoshi Qazi Muhammed b700637d3d debug: trace RootViewModel.handleDeepLink top-level entry; add PEZ Airdrop label + trust-score gate to Mining Simulation
Deep link debug: DeepLinkModule wiring at app/root/di/deeplink/DeepLinksModule.kt
confirmed MultisigDeepLinks IS included in the assembled handler list (ruling that
out), yet the earlier MultisigOperationDetailsDeepLinkHandler-level Log.e calls never
fired at all - not even the very first one in matches(). Logging one level up, at
the true entry point, to see the raw Uri/scheme/authority/path as parsed and the
final Result, before guessing further.

Mining Simulation: adds a 'PEZ Airdrop' label above the square (user request), and
refuses to activate + shows an explicit error if trust score is 0, instead of
silently starting a permanently-zero-rate session.
2026-07-18 15:58:41 -07:00
Satoshi Qazi Muhammed fe8f907c12 debug: add temporary Log.e tracing to multisig deep link handler
The manifest fix (84bbff6) confirmed the intent-filter now resolves and the
app receives the intent (RootActivity gets focus), but the details screen
never opens and nothing crashes or logs anything - suspecting the deep link
coroutine may be permanently suspended on automaticInteractionGate.awaitInteractionAllowed()
(the app's PIN/background-recheck gate), which an ADB-driven test may never
satisfy. Tracing each step to confirm before concluding. To be removed once
the real cause is confirmed.
2026-07-18 14:46:25 -07:00
Satoshi Qazi Muhammed 84bbff68ba fix: use literal scheme/host in the /open/... deep link intent-filter
android:scheme="@string/deep_linking_scheme"/android:host="@string/deep_linking_host"
compiled fine but never resolved at runtime - confirmed by decoding the actual built
APK's binary manifest, which showed the raw unresolved resource ids
(@7F13034E/@7F13034A) instead of literal values, unlike every sibling intent-filter
in this same manifest (buy-success, request, wc, https), which all use literal
strings and work correctly. PackageManager can't match a <data> scheme/host that
never resolves, so this intent-filter - covering every /open/... deep link,
including the just-added first-signer multisig operation case (PR #14) - has never
been reachable on any real device since it was written, regardless of the
correctness of the Kotlin handling code behind it. Confirmed by attempting the
actual deep link on-device (ADB VIEW intent, unresolved) before finding this.
2026-07-18 12:56:24 -07:00
Satoshi Qazi Muhammed f83c3549b0 fix: point Mining Simulation info button at the correct Telegram invite link 2026-07-18 10:29:00 -07:00
Satoshi Qazi Muhammed d4da42c07d feat: add Mining Simulation card to fill empty space in Pezkuwi Trust Score card
Cosmetic, client-side-only preview of a trust-score-weighted PEZ emission
share, meant to incentivize referral usage (higher trust score -> visibly
faster mining). Not tied to any real distribution mechanism or backend.

- MiningSimulationFormula: pure calc. Rate per minute = (92.5M PEZ / 43200
  min era) * (trustScore / 500,000 reference total) - trust score is a
  direct multiplier so zero trust score always yields zero rate. Diamonds
  only accrue while a 24h session is active.
- MiningSimulationRepository: local-only persistence via the existing
  Preferences wrapper (same pattern as BannerVisibilityRepository), keyed
  per accountId. Tapping the square freezes the live total as the new base
  and starts a fresh 24h session - never resets progress on tap. A full
  43200-minute era rolling over resets the total to 0 (that era's pool is
  considered distributed) independently of session taps.
- UI: square button in the dashboard card's previously-empty right column,
  red when inactive (needs a tap), gold (#FDB813, matching the existing
  Trust Score color) while actively mining. Small Telegram icon opens
  https://t.me/DKSKurdistanBot via the existing Browserable mixin.
2026-07-18 10:00:24 -07:00
Satoshi Qazi Muhammed 40f8db48bc fix: resolve asset by on-chain Statemine id, not local Chain.Asset.id
Root cause found via device-side Log.e tracing (now removed): the on-chain
query, off-chain call-data fetch, and call decode all worked correctly - a
real pending approval was found and its call decoded - but chain.assetsById
is keyed by this app's own local Chain.Asset.id, which is NOT the same as
the raw pallet_assets id these constants hold (e.g. USDT is local id 1 but
on-chain id 1984 on Polkadot Asset Hub - they only happened to match by
coincidence on Pezkuwi Asset Hub, masking the bug there). Every result was
silently dropped at this single lookup. Fixed by reverse-resolving through
findAssetByStatemineAssetId instead of indexing assetsById directly.
2026-07-17 20:30:53 -07:00
Satoshi Qazi Muhammed 63dd3e2d03 debug: add temporary Log.e tracing to Pending Signatures pipeline
Nothing renders on-device despite the multisigsApiUrl fix and a confirmed
real pending approval on-chain, with zero exceptions surfacing anywhere -
every runCatching in this path swallows silently with no logging. Adding
Log.e at each step (account resolution, signatory gate, on-chain
keys/entries, off-chain fetch, call decode, transfer parse) to find where
it actually drops the result. To be removed once the real cause is found.
2026-07-17 19:30:24 -07:00
Satoshi Qazi Muhammed e716e6e277 Fix Pending Signatures: query the bridge's own hardcoded multisig, not the generic multisig-account feature
The previous commit reused MultisigPendingOperationsService, but that
service only syncs accounts formally registered as a MultisigMetaAccount
in this wallet - it silently returns an empty NoOpSyncer for every other
account type. The bridge's 5 signatories (Serok etc.) use their own
regular wallets to approve calls against the bridge's separate,
hardcoded multisig (BridgeMultisigConstants) - they never "are" that
multisig account, so the feature was structurally guaranteed to show
nothing, not just empty by coincidence.

Fix queries the bridge's own multisig accounts directly, mirroring the
exact pattern this screen's existing renewal-signing code already uses
(BridgeMultisigRuntimeApi's runtime-metadata accessors, a local mirror
of feature-account-impl's off-chain call-data indexer client - same
module-boundary reasoning already documented in that file) rather than
the generic, differently-scoped service:

- BridgeMultisigInteractor.getPendingApprovals(): enumerates
  Multisig.Multisigs entries for both bridge multisig accounts via
  QueryableStorageEntry2.keys()/entries() (a general runtime-storage
  capability, not multisig-feature-specific), fetches real call content
  from the same off-chain indexer the generic feature uses (also
  general-purpose, keyed by address+hashes - never required the account
  to be a MultisigMetaAccount either), and filters to calls this
  signatory hasn't approved yet.
- BridgeMultisigInteractor.submitApproval(): reuses
  composeBridgeMultisigAsMulti generalized to an arbitrary call+
  timepoint (previously only used for the renewal call) - refuses to
  proceed if the call content is unknown, never blind-signs.
- BridgeViewModel formats the real parsed transfer (amount+destination)
  via AssetSourceRegistry.tryParseTransfer, already used elsewhere for
  the exact same "decode a transfer call" job - no fabricated preview
  data.

Reverts the feature-multisig:operations cross-feature dependency and
FeatureApi wiring added in the previous commit - no longer needed since
everything now lives in this screen's own existing self-contained
domain layer, matching how the rest of BridgeMultisigInteractor already
works (and already explains, in its own doc comments, why a synthetic
MultisigMetaAccount was deliberately avoided for exactly this reason).
2026-07-17 16:42:40 -07:00
Satoshi Qazi Muhammed 6f14c8bf05 Move Pending Signatures from the wallet home screen to the Bridge screen
Wrong screen in the previous commit - moved it to sit right below the
existing per-leg renewal sign buttons on the Bridge input screen, which
is where it was actually asked for: a signatory renewing their own
wUSDT/Polkadot USDT allowance there should also see (and be able to
jump to) other pending multisig operations - e.g. someone else's large
bridge swap - waiting on their signature, without navigating elsewhere.

BalanceListFragment/ViewModel/Module are reverted to their prior state.
The reusable pieces (PendingSignatureModel, item_pending_signature_row
layout) are kept and repurposed for the Bridge screen; the RecyclerView
SingleItemAdapter/card layout built for the home screen are removed
since Bridge is a plain (non-RecyclerView) fragment - rows are inflated
directly into a LinearLayout container instead, same underlying data
and MultisigCallFormatter reuse as before.

The feature-assets -> feature-multisig:operations dependency and the
FeatureApi wiring (AssetsFeatureComponent/Dependencies/Holder) from the
previous commit stay as-is - Bridge is the same Gradle module, so no
change needed there.
2026-07-17 14:43:18 -07:00
Satoshi Qazi Muhammed 707d1baba4 Add a Pending Signatures card to the wallet home screen
Surfaces multisig operations awaiting the current signatory's own
approval directly on the main balance screen, instead of requiring
navigation to the separate pending-operations list. Each row shows the
formatted amount, destination, chain, and approval progress, with a
red Sign button that opens the existing operation-details/approve flow
(fee estimation, signatory balance check, and validation are all
already handled there - not reimplemented here).

Reuses the already-tested MultisigCallFormatter from
feature-multisig:operations (added as a new feature-assets dependency,
exposed via the standard FeatureApi/FeatureContainer pattern - see
AssetsFeatureHolder/AssetsFeatureComponent/AssetsFeatureDependencies)
rather than re-deriving call-preview formatting independently. The
card follows the same SingleItemAdapter + ConcatAdapter pattern as the
existing Pezkuwi Dashboard card on the same screen.

Once a pending operation is actually signed, it naturally drops off
this list on the next sync (no separate client-side "signed" state is
tracked in parallel with the real on-chain approval status).

String only added to the base (English) strings.xml for now - not yet
translated into the other ~14 locales this app otherwise maintains.
2026-07-17 13:03:12 -07:00
Satoshi Qazi Muhammed e1a010b779 Fix: keep net.i2p.crypto.eddsa classes from being stripped by R8
Signer.kt/SignatureVerifier.kt in substrate-sdk-android look up the
"EdDSA" JCA provider by name at runtime (Signature.getInstance(...,
"EdDSA")), backed by net.i2p.crypto:eddsa's EdDSASecurityProvider. With
no keep rule, R8 stripped/renamed its classes in the minified release
build, so the provider's own registered service implementation could no
longer be found - "class configured for Signature (provider: EdDSA)
cannot be found", reproduced on-device sending SOL (every Solana
transaction signs with Ed25519, so this path is always hit).
2026-07-17 10:40:01 -07:00
Satoshi Qazi Muhammed b09ba4c303 Remove dead, non-functional BizinikiwSr25519Signer stub
Never referenced anywhere (self-declaration only, confirmed via grep),
sitting since the initial commit. Its RistrettoPoint.multiply()/add() are
both no-ops returning `this` unchanged, BASE is all-zero bytes, and
keccakF1600() never writes output back into the Strobe state - none of
this could ever produce a valid signature. The real, live bizinikiwi
signing path is the native JNI binding in bindings/sr25519-bizinikiwi/
(PezkuwiKeyPairSigner -> BizinikiwSr25519), unaffected by this removal.
Leaving broken placeholder crypto sitting in a wallet's signer package
is its own audit red flag even when provably unused.
2026-07-17 09:07:35 -07:00
Satoshi Qazi Muhammed 16427654ae Remove CHANGELOG_PEZKUWI.md dev-notes file, not app source/APK-related
Was a context-recovery scratch doc from the initial rebrand port (Feb 2026):
self-contradicted (top says debug cleanup done, bottom checklist still
unchecked), and its only non-obvious content (SDK/runtime type-lookup
gotchas) is already explained in the actual source docstrings
(PezkuwiCheckMortality.kt, PezkuwiAddressConstructor.kt) or preserved
in memory for future reference.
2026-07-17 08:13:51 -07:00
Satoshi Qazi Muhammed fc6bfa2867 Revert "Gitignore scratch/planning-note naming patterns"
This reverts commit 335859c9a0.
2026-07-17 07:39:12 -07:00
Satoshi Qazi Muhammed 335859c9a0 Gitignore scratch/planning-note naming patterns
Prevents a repeat of docs/PACKAGE_STRUCTURE_REBRAND.md (a never-executed
planning draft that sat in the repo for months) - working notes belong
in a scratchpad, not the tracked tree. Real docs (BRAND.md, README.md,
docs/*) are unaffected.
2026-07-17 07:38:07 -07:00
Satoshi Qazi Muhammed 43af39bfe7 Remove stale, never-executed package-rebrand planning draft; gitignore this pattern
docs/PACKAGE_STRUCTURE_REBRAND.md was a 2026-01-23 planning note (status:
BEKLEMEDE, never actioned, references a contributor's personal machine
path) that had been sitting in the repo as dead weight since. Real,
maintained docs (BRAND.md, README.md, docs/*) are unaffected.
2026-07-17 07:37:38 -07:00
Satoshi Qazi Muhammed 163e68dcd3 Correct the previous crash fix: revert schema, add a real tolerant reader instead
The previous commit's SafeOptional/custom() field wrapper was wrong and
broke real tests (RealLocalAccountsCloudBackupFacadeTest, Solana/Tron
backfill migration tests - Mockito WantedButNotInvoked, IllegalArgumentException).

Root cause of that failure: EncodableStruct.get() (in the external
substrate-sdk-android library) only returns null for an unset field
when field.dataType is specifically an instance of the library's own
`optional<*>` class - not any DataType that merely behaves like one.
optional<T> is not open, so no wrapper class can satisfy that check;
swapping in a custom DataType just moves the same crash from read-time
to access-time.

Correct fix: leave the MetaAccountSecrets schema exactly as it always
was (plain `.optional()`, real library instances, so get() keeps
working), and instead add readMetaAccountSecrets() - a resilient
top-level reader that tries the normal Schema.read() first and, only
if that throws, falls back to reading each field by hand in schema
order, stopping at the first one whose bytes don't exist and leaving
every field after it genuinely unset. Because those fields were never
set, EncodableStruct.get()'s existing (correct) optional-field handling
returns null for them exactly as it already does for any account that
was never migrated - no library behavior is being routed around,
just extended to tolerate a shorter/older stored blob.
2026-07-17 06:08:17 -07:00
Satoshi Qazi Muhammed 9a11dc2c3d Fix crash reading MetaAccountSecrets for accounts predating Tron/Bitcoin/Solana
Found on a real device: opening the backup/export screen for an
existing wallet crashed with
java.lang.IndexOutOfBoundsException: Cannot read 412 of 412
  at .../scale/dataType/optional.read
  at .../scale/Schema.read (x3)
  at SecretStoreV2$getMetaAccountSecrets$2.invokeSuspend

Root cause: MetaAccountSecrets is a positional (SCALE) binary schema.
optional<T>.read() unconditionally reads a presence-flag byte with no
EOF handling - reading a blob that predates a trailing field (written
before that field existed) throws instead of yielding null. This is
also why Solana's own address was never getting backfilled: the
migration's own read of the account's stored secrets was throwing on
every attempted run, silently caught by its own per-account try/catch.

Confirmed via substrate-sdk-android's actual source (Dsl.kt/Delegate.kt)
that Tron/Bitcoin's identical schema-growth pattern carried the exact
same latent risk - it likely only avoided crashing so far by chance
byte-alignment in previously-stored blobs, not because the format
tolerates it.

Fix: read Tron/Bitcoin/Solana's keypair/derivationPath fields (every
field added after the original Substrate+Ethereum schema) through a
SafeOptional DataType that catches a failed read and returns null,
via the library's own custom() field-declaration escape hatch. Write
behavior is byte-for-byte unchanged - only reading a shorter/older
blob now correctly yields null for fields it doesn't have, instead of
crashing every caller of getMetaAccountSecrets (balance sync, signing,
backup/export, this Solana backfill migration itself).
2026-07-17 02:19:10 -07:00
Satoshi Qazi Muhammed db9962a00e Add SOL to default token order, right after TRX
Mirrors how TRX/BTC were inserted into this list when they shipped
(351114b, 55d405c) - SOL was missed when Solana support was added.
2026-07-17 02:16:31 -07:00
Satoshi Qazi Muhammed 71e4c8dc7a Fix CI: sender/recipient hex fixtures in Solana transaction tests were 1 hex digit short
Both SolanaTransactionTest and RealSolanaTransactionServiceTest carried
a transcription error from the earlier solders cross-validation: the
sender/recipient pubkey hex strings (and one of expectedMessageHex's
concatenated chunks) were each missing their trailing hex digit,
producing odd-length hex that BouncyCastle's decoder throws on
(StringIndexOutOfBoundsException) - all 5 SolanaTransactionTest cases
failed at field-init time as a result. Re-derived every fixture
programmatically from the same solders-verified reference message
(rather than hand-splitting the hex string again) and confirmed the
full round trip (build -> compare) matches byte-for-byte.
2026-07-16 20:54:39 -07:00
Satoshi Qazi Muhammed 086812345e Fix CI: missing Solana address imports in ChainExt.kt, unused import in RealSolanaTransactionService.kt
ChainExt.kt called toSolanaAddress/solanaAddressToAccountId/
emptySolanaAccountId/isValidSolanaAddress without importing them - an
earlier-session oversight that only surfaced once CI actually compiled
the runtime module (no local Kotlin compiler available to catch this
sooner). ktlint separately flagged an unused AccountId import.
2026-07-16 20:00:36 -07:00
Satoshi Qazi Muhammed ecdb5ee44c Add native Solana balance and transfer support
- SolanaTransaction: hand-rolled compact-u16/legacy-Message/signed-tx
  wire-format primitives, cross-validated byte-for-byte against the
  solders Python library's own serialization of an identical System
  Program transfer message.
- SolanaApi: JSON-RPC client (getBalance/getLatestBlockhash/
  getFeeForMessage/sendTransaction) over a single POST endpoint, unlike
  Tron/Bitcoin's path-per-resource REST style.
- SolanaNativeAssetBalance: getBalance polling, same design as Bitcoin's
  mempool.space polling (no push/subscription mechanism available).
- RealSolanaTransactionService: builds a single-instruction transfer
  message, signs it raw via the existing Ed25519 signer dispatch
  (skipMessageHashing - Solana signs the message directly, no external
  hashing), broadcasts via sendTransaction. Fee is Solana's actual
  getFeeForMessage answer, not a client-side estimate.
- TransactionExecution.Solana(hash) variant; wired into AssetsModule via
  a new SolanaAssetsModule, following the Bitcoin/Tron precedent exactly.
2026-07-16 18:49:29 -07:00
Satoshi Qazi Muhammed d35687f2ce Add native Solana support: address derivation, accounts, signing, backfill migration, CloudBackup wiring
Same read/send-capable chain-family pattern as Tron/Bitcoin, but for
Ed25519/SLIP-0010 instead of secp256k1/BIP32 (Solana's account id IS its
raw public key, so no separate address-hash derivation step is needed).

- SLIP-0010 Ed25519 keypair derivation (Bip32Ed25519KeypairFactory),
  cross-validated against an independent slip10 reference implementation
  and pinned to a known-good test vector.
- Chain.isSolanaBased / Chain.Asset.Type.SolanaNative wired through every
  chain-mapper, account-model, and signer dispatch site (mirrors the
  existing Tron/Bitcoin wiring exactly).
- SolanaAddressBackfillMigration derives Solana keys for wallets created
  before this support existed, same self-healing no-stuck-flag design as
  the Tron/Bitcoin backfills - also fixed a latent bug in those two
  migrations where MetaAccountSecrets/MetaAccountLocal.addXAccount()
  dropped sibling chain-family fields on a re-run.
- Room migration 75->76 adds isSolanaBased/solanaPublicKey/solanaAddress
  columns.
- CloudBackup's already-reserved solanaAddress/SolanaSecrets fields are
  now actually read/written by RealLocalAccountsCloudBackupFacade; also
  fixed isCompletelyEmpty() to check solana, so a Solana-only wallet
  doesn't get silently dropped from backup.
2026-07-16 18:49:09 -07:00
Satoshi Qazi Muhammed 6fd7a56382 Bridge: add retry on origin failure, dedupe fee constant
Retry: OriginFailed only offered "Done" (start over from the input
screen) - the origin transfer never left the wallet in that state, so
retrying is just re-running submit() from scratch, same as Swap's own
retryClicked(). The execution screen's action button now shows "Try
again"/retries when OriginFailed, "Done" otherwise - derived from the
same state observer already driving the alert banner, not a second
LiveData to keep in sync.

FEE_PERCENT was duplicated as separate 0.001 literals in BridgeViewModel
and BridgeExecutionViewModel. Moved to BridgeMultisigConstants (already
the shared home for this feature's cross-cutting constants) so there's
one number to change if the server-side fee_basis_points ever does.

Deliberately NOT touching the deeper architecture items (LiveData vs
StateFlow, extracting submit logic to a domain interactor) - Bridge's
sealed BridgeExecutionState already gives it the single-source-of-truth
property that motivated Swap's StateFlow design, and its submit/wait
logic isn't shared with any other screen, so there's no duplication or
desync bug those changes would actually fix here. This scope is staying
USDT.p<->USDT only going forward (other bridge/swap pairs route through
the separate pezsnowbridge DApp) - the real trigger for that refactor
(a second bridge pair reusing this logic) will not happen in this
screen, so it's not deferred tech debt, it's a closed decision.
2026-07-16 15:57:12 -07:00
Satoshi Qazi Muhammed ec0ec9153f Bridge: lock down execution navigation, predict manual review upfront
Execution screen safety fix: hardware back and the toolbar home button
could navigate away mid-submission (the transfer's own ViewModel scope
gets cancelled on leaving, same class of risk as leaving Swap's execution
screen mid-flight, which that screen has always prevented). Now matches
SwapExecutionFragment exactly - back suppressed, home button hidden, the
only way out is the Done button once the operation has actually resolved.

Bridge previously only found out "this needs manual 3-of-5 review" after
watching the destination balance for up to 90s and giving up - honest, but
slow to tell the user something the input screen's own on-chain checks
already knew before submission. The consent gate checked the automation
key's remaining on-chain allowance but not usdt-bridge's separate hard
per-tx cap (max_single_tx, 50,000 USDT) - a large single swap could read
as "fast" against a freshly-renewed 200,000 allowance while the backend
would still force it to manual review. Added that check
(BridgeMultisigConstants.MAX_SINGLE_TX), forwarded the resulting
prediction through BridgeExecutionPayload.expectManualReview, and added a
new AwaitingManualReview state so the execution screen shows the same
clear message ("needs approval from 3 of 5 signatories, ~2 hours,
t.me/pezkuwidestek") immediately instead of after a false wait -
regardless of whether manual review is needed because the amount itself
is large or because other users' swaps have temporarily used up the
automation key's approval.
2026-07-16 10:23:01 -07:00
Satoshi Qazi Muhammed d0db0a54f7 Update launcher icon to the latest goat badge design
Source: pwap-web/shared/images/pezWallet - white_background_last.png -
a refined round-badge design (wallet flap detail added). Same crop+resize
process as the previous icon passes, matched to the exact scale/position
already in use so only the artwork changes.
2026-07-15 22:19:02 -07:00
Satoshi Qazi Muhammed 272921dcdf Match the execution screen's visual polish to the app's own Swap flow
The structural separation (dedicated screen, no amount input) was already
in place, but the screen itself was much sparser than Swap's own execution
UI - just a label and a timer, no "Do not close the app!" warning and no
summary of what's actually being bridged. Added both, reusing components
already proven on Bridge's own input screen (BridgeAssetInputView in
non-editable mode, same as the main screen's fromCard/toCard) rather than
reaching into feature-swap-impl's Swap-specific components.

Also added the signatories' contact channel (t.me/pezkuwidestek) to the
existing "still processing" message, and made explicit that funds are safe
and on-chain even when auto-pay doesn't complete within the wait window -
this is the message shown when a real transfer lands in manual review, not
just the pre-submit consent gate.
2026-07-15 21:47:32 -07:00
Satoshi Qazi Muhammed 3c06ae00e0 Add pre-submit consent gate for amounts exceeding auto-pay approval
Symmetric across both bridge directions, checked in priority order before
any funds move:
1. Real reserve exceeded (withdrawal only) - the multisig doesn't hold
   enough real USDT on Polkadot Asset Hub. No signature can fix this, so
   this stays a hard block (existing bridge_wusdt_to_usdt_blocked message),
   never offered as "proceed anyway".
2. Automation-key approval exceeded (either direction) - funds exist, the
   automation key just isn't currently approved to auto-pay that much.
   Resolvable by 3-of-5 signing, so this is now an explicit opt-in: a
   warning banner names the ~2h typical review window and the signatories'
   contact channel (t.me/pezkuwidestek), gated behind a WarningCheckBox the
   user must tick before Swap re-enables. Previously there was no signal at
   all here - a large amount would just silently queue for manual review
   after debiting the user, with no way to know that going in.

Both getWusdtRemainingAllowance/getPolkadotUsdtRemainingAllowance query the
real on-chain Assets.Approvals amount per leg - not a guess. Confirmed
on-chain (2026-07-16) the Polkadot leg has never had an approval granted at
all, so every wUSDT->USDT withdrawal currently hits the consent gate
regardless of amount, symmetric with what happens once a signatory grants
one (mirrors the existing wUSDT-side renewal flow exactly, including the
same 40,000/200,000 20%-threshold pattern) - added a second sign button so
a signatory can actually grant it from this screen.

swapClicked()'s confirmation gate now also enforces the consent requirement
server-side (well, client-domain-side) rather than only via the button's
enabled look, consistent with how it already re-checks balance/reserve.
2026-07-15 19:46:24 -07:00
Satoshi Qazi Muhammed cf8127c8f8 Fix missing imports (launch, getAssetIconOrFallback) from the Bridge split
Dropped while rewriting BridgeViewModel/adding BridgeExecutionViewModel -
first real CI run caught it.
2026-07-15 16:31:44 -07:00
Satoshi Qazi Muhammed 52e5645409 Split Bridge into input and dedicated execution screens, matching Swap
The single-screen design (amount input + live balance validation + submit
+ destination-wait, all in one ViewModel) was structurally fragile: the
live origin-balance observer and the amount-vs-balance validation ran in
the same reactive scope as execution, so a real successful transfer could
flash a false "insufficient balance" error the instant the balance dropped
post-submission. Patching that race (clearing the field, then an isExecuting
flag) kept fixing symptoms without addressing why the class of bug existed:
the app's own Swap flow avoids it entirely by keeping amount entry and
execution as separate screens/ViewModels, so validation code for the input
screen simply doesn't exist on the execution screen.

BridgeExecutionViewModel/Fragment now own the actual submit + destination-
wait, reached by navigating with a BridgeExecutionPayload once
BridgeViewModel.swapClicked() re-validates amount against the latest known
balance/reserve - a real confirmation gate, not just a UI button-disabled
look the domain layer never itself checked. A single BridgeExecutionState
sealed class (SubmittingOrigin/OriginFailed/WaitingForDestination/
DestinationConfirmed/DestinationPendingReview) replaces what used to be six
independently-updated LiveData flags that had to be kept in sync by hand.

Also fixed two real bugs found via live device testing with real funds:
- BridgeMultisigConstants.POLKADOT_USDT_ASSET_ID was 1 (the wallet's own
  chains.json ordinal for USDT on Polkadot Asset Hub) instead of 1984 (the
  real on-chain Assets pallet id, confirmed directly against the chain).
  getPolkadotUsdtReserve() queried asset 1, which the multisig has never
  held anything at, so the wUSDT->USDT reserve check always reported 0
  USDT available regardless of the multisig's real (and growing) holdings.
- The deposit-wait label's text was static XML, never updated on the
  success path - a genuinely completed bridge (destination balance
  confirmed, checkmark shown) still read "Waiting for confirmation..."
  forever, indistinguishable from actually being stuck. The new execution
  screen's label is driven by BridgeExecutionState instead.
2026-07-15 16:12:03 -07:00
Satoshi Qazi Muhammed cb8f0d8aae Replace TODO("Not yet implemented") with an explicit UnsupportedOperationException
subscribeAccountBalanceUpdatePoint() on Bitcoin/Tron-native/TRC20/EVM-native
balance sources is only ever called from RealCrossChainTransactor's XCM
arrival detection, which is Substrate-only - none of these chain families
can be an XCM cross-chain destination, so the method is intentionally never
reachable. A bare TODO() reads as forgotten/unfinished work; swapping it for
the same UnsupportedOperationException pattern UnsupportedAssetBalance
already uses elsewhere in this codebase makes the same never-called
guarantee explicit and documented instead of implicit.
2026-07-15 12:51:58 -07:00
Satoshi Qazi Muhammed 14c8b3c27c Change launcher icon badge background from black to white
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.
2026-07-15 12:08:04 -07:00
Satoshi Qazi Muhammed 6e07348695 Bridge: clear entered amount after a successful origin-chain submit
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.
2026-07-15 09:33:53 -07:00
Satoshi Qazi Muhammed 974296704f Fix missing viewModelScope import in BridgeViewModel
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.
2026-07-15 08:12:22 -07:00
Satoshi Qazi Muhammed 437dfff813 Bridge: submit deposit via real submit-and-await execution, not generic Send
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.
2026-07-15 07:59:35 -07:00
Satoshi Qazi Muhammed b51e2ecf58 Add real deposit-wait progress to Bridge screen, reusing the swap timer UX
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.
2026-07-15 06:10:41 -07:00
Satoshi Qazi Muhammed a5454b38f0 Fix icon: shrink+center the goat artwork instead of edge-to-edge fill
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.
2026-07-15 05:14:22 -07:00
Satoshi Qazi Muhammed f655ef5a33 Fix ktlint: blank line before commented declaration in BridgeMultisigConstants 2026-07-14 22:20:33 -07:00
Satoshi Qazi Muhammed c60967e7af Fix ktlint: blank line before commented declaration in BridgeViewModel 2026-07-14 21:50:18 -07:00
Satoshi Qazi Muhammed 6c70c91730 Fix Bridge swap destination + add confirmed Kurdish translation
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.
2026-07-14 21:22:04 -07:00
Satoshi Qazi Muhammed ae2754d7a3 Update launcher icon to the cleaner goat-head logo (v2)
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.
2026-07-14 21:21:44 -07:00
Satoshi Qazi Muhammed a8dc9add24 Replace dead legacy-bot status check with a real per-amount reserve check
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.
2026-07-14 18:57:59 -07:00
Satoshi Qazi Muhammed 6d27949cbf Replace launcher icon with the new goat-head PezkuwiWallet brand logo
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.
2026-07-14 18:44:28 -07:00
Satoshi Qazi Muhammed 09b1b37b49 Sync bridge renewal threshold/topup with server-side 40K/200K config
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.
2026-07-14 16:50:35 -07:00
Satoshi Qazi Muhammed 7a2d330a0a Retire DOT-HEZ bridge from the Bridge screen
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.
2026-07-14 14:41:56 -07:00
Satoshi Qazi Muhammed 2ac8005c10 Skip (not fail) BalancesIntegrationTest when a chain's RPC is unreachable
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.
2026-07-14 09:52:45 -07:00
Satoshi Qazi Muhammed 4b454cc884 Add missing query() to QueryableStorageEntry3
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.
2026-07-14 07:59:24 -07:00
Satoshi Qazi Muhammed 40c867b22c ci: pass TRONGRID_API_KEY through to the ktlint job too
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.
2026-07-14 07:43:41 -07:00
Satoshi Qazi Muhammed be2e1088e5 Merge branch 'feature/btc-native-send' into fix/multisig-first-signer-deep-link
# Conflicts:
#	feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgeFragment.kt
#	feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgeViewModel.kt
#	feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/di/BridgeModule.kt
2026-07-14 07:36:14 -07:00
Satoshi Qazi Muhammed 3def96792a Fix Bridge multisig compile errors surfaced by CI
- 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.
2026-07-14 07:29:35 -07:00
Satoshi Qazi Muhammed 4941a95ee5 feat(dashboard): collapsible Pezkuwi card, minimal by default
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.
2026-07-14 07:27:01 -07:00
Satoshi Qazi Muhammed cfdeff8ab7 Add signatory-only sign button to Bridge screen
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.
2026-07-14 06:59:18 -07:00
Satoshi Qazi Muhammed ee63f15dd9 Fix nullable AccountModel handling in onDepositorClicked
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.
2026-07-13 23:14:16 -07:00
Satoshi Qazi Muhammed 68a5b3ed9e Support first-signer case for /open/multisigOperation deep link
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.
2026-07-13 23:04:02 -07:00
Satoshi Qazi Muhammed 7401783436 feat: block Bridge swap when send amount exceeds available balance
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.
2026-07-13 12:10:16 -07:00
Satoshi Qazi Muhammed e0cd1f5def fix: Bridge cards use AssetIconProvider and displayNameWithAssetStandard
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.
2026-07-13 11:43:03 -07:00
Satoshi Qazi Muhammed 3b5e15457d fix: remove unused Chain import flagged by ktlint
The type is only used via inference (chainRegistry.getChain() return),
never referenced explicitly.
2026-07-13 09:33:29 -07:00
Satoshi Qazi Muhammed 398f2fc996 Redesign Bridge screen UI to match Swap's modern card layout
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.
2026-07-13 09:28:30 -07:00
Satoshi Qazi Muhammed 4d1fb0256f fix: crash when generating an icon for a P2SH/P2PKH recipient
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.
2026-07-13 06:52:34 -07:00
Satoshi Qazi Muhammed 9629e25e69 fix: reuse TronAddress.kt's existing Base58Check instead of duplicating it
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.
2026-07-13 06:00:31 -07:00
Satoshi Qazi Muhammed d4faa64c19 feat: support sending BTC to P2SH/P2PKH destination addresses
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.
2026-07-13 05:54:37 -07:00
Satoshi Qazi Muhammed ff19c7c4ae debug: temporarily log raw QR content and decode failures
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.
2026-07-13 04:49:02 -07:00
Satoshi Qazi Muhammed ccf56c0599 fix: recognize BIP21 bitcoin: URIs when scanning a QR code
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.
2026-07-12 20:36:20 -07:00
Satoshi Qazi Muhammed 671534de57 fix: add missing isBitcoinBased to Chain(...) in RealTronTransactionServiceTest
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.
2026-07-12 19:22:33 -07:00
Satoshi Qazi Muhammed dc244fb0bb fix: remove duplicate SignerProvider binding from WalletFeatureDependencies
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.
2026-07-12 19:12:03 -07:00
Satoshi Qazi Muhammed 60493f9aeb merge: reconcile with feature/trc20-tron-send into one combined 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.
2026-07-12 19:02:08 -07:00
Satoshi Qazi Muhammed a8ba753bde chore: temporarily point CHAINS_URL at pending/post-fix-release for device testing
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.
2026-07-12 17:18:33 -07:00
Satoshi Qazi Muhammed 1eaff6d0a7 fix: expose SignerProvider through WalletFeatureDependencies
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.
2026-07-12 16:01:48 -07:00
Satoshi Qazi Muhammed 6239526d43 feat: Bitcoin transaction construction, signing, and sending
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.
2026-07-12 15:51:37 -07:00
Satoshi Qazi Muhammed 1830024ed0 fix: add BitcoinNative branch to exhaustive Chain.Asset.Type whens
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.
2026-07-12 15:07:30 -07:00
Satoshi Qazi Muhammed af0bfa1560 fix: thread bitcoinAddress/bitcoinPublicKey through PolkadotVaultMetaAccount
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.
2026-07-12 15:00:49 -07:00
Satoshi Qazi Muhammed 69b311cb47 feat: Bitcoin API client, balance reading, and node health checks
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.
2026-07-12 14:59:05 -07:00
Satoshi Qazi Muhammed db1476f119 fix: add missing Bitcoin helper imports in ChainExt.kt
CI caught 5 unresolved references (toBitcoinAddress, bitcoinAddressToAccountId,
emptyBitcoinAccountId, bitcoinPublicKeyToAccountId, isValidBitcoinAddress) -
the functions existed in common/utils/BitcoinAddress.kt but were never
imported here.
2026-07-12 14:53:17 -07:00
Satoshi Qazi Muhammed 36b5a17644 feat: Bitcoin key derivation, secrets storage, and chain-family plumbing
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.
2026-07-12 14:47:07 -07:00
Satoshi Qazi Muhammed 2a0ccffbe4 fix: correct transcription error in BitcoinTransactionTest's BIP143 fixture
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.
2026-07-12 13:45:55 -07:00
Satoshi Qazi Muhammed b32f3e8d50 feat: Bitcoin native SegWit protocol primitives (Bech32, DER, BIP143, raw tx)
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.
2026-07-12 13:27:38 -07:00
SatoshiQaziMuhammed 438a4510c8 ci: pass TRONGRID_API_KEY through to the Gradle build (#13)
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.
2026-07-12 05:21:18 -07:00
Satoshi Qazi Muhammed 32002db5da feat: send TronGrid API key on every request to raise the anonymous rate limit
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).
2026-07-12 05:08:22 -07:00
Satoshi Qazi Muhammed 04c0c41a2d fix: transparently retry TronGrid calls on HTTP 429 instead of surfacing it to the user
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.
2026-07-12 04:54:58 -07:00
Satoshi Qazi Muhammed bc9087136c fix: Tron transfers signed with the wrong key (or crashed) due to missing multi-chain-encryption case
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.
2026-07-11 19:20:02 -07:00
Satoshi Qazi Muhammed e1d199931c fix: native TRX fee estimation crashes when amount is not yet entered
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.
2026-07-11 18:08:33 -07:00
Satoshi Qazi Muhammed 0eab8a8ea2 fix: TRC-20 balance always read 0 for never-activated holders
Trc20AssetBalance/RealTronGridApi.fetchTrc20Balance() read through
TronGrid's /v1/accounts/{address} REST endpoint - but a TRC-20 balance
lives entirely in the token contract's own storage, not the holder's
Account object. An address that has only ever received TRC-20 tokens
(never native TRX, never otherwise "activated" on-chain) has no Account
object at all, so /v1/accounts silently returns `data: []` for it
regardless of its real token balance, and the old code treated that the
same as a genuinely empty/zero account.

Found via a live test: a real wallet received 5 USDT-TRC20, the exchange
confirmed the transfer complete on-chain, but the app kept showing 0.
Independently verified live: /v1/accounts returned empty for the address,
while a direct balanceOf(address) contract call (triggerconstantcontract)
correctly returned 5000000.

Rewrote fetchTrc20Balance() to read via balanceOf(address) instead -
reuses the existing triggerConstantContract() call already used for TRC-20
transfer fee estimation, plus a new encodeBalanceOfParameters() ABI helper
alongside the existing transfer() one. Added a regression test pinned to
this exact real address/balance so this can't silently regress again.
2026-07-11 15:21:24 -07:00
Satoshi Qazi Muhammed cf02896a58 fix: apply the chain/asset-standard label to the main balance list too
The Send/Receive/etc. network picker (NetworkFlowViewModel) already showed
"Ethereum (ERC-20)"/"Tron (TRC-20)" for a multi-chain token's per-chain rows,
but the main Assets dashboard's own expandable per-token breakdown (tap a
token like USDT to see every chain it exists on) is a completely separate
code path (TokenAssetMappers/TokenAssetViewHolder) that still showed a bare
chain name - found via a real device screenshot of that specific screen.

Moved the shared display-name-with-label logic to a public extension
(Chain.displayNameWithAssetStandard(), runtime/ext/ChainExt.kt) so both
screens build the exact same string instead of duplicating (and now
diverging) the same logic twice.
2026-07-11 14:07:59 -07:00
Satoshi Qazi Muhammed ff7624c1ac temp: point CHAINS_URL at pending/post-fix-release, not master
wallet-util's master/main got reset to the last content the still-live
Play Store release can parse (see wallet-util repo history around
2026-07-11 - an unrelated production incident, not a regression on this
branch). master no longer serves Tron config/icons, so this branch's test
builds would silently regress to "no Tron" - not because of anything wrong
here, but because the shared config source moved out from under it.

pending/post-fix-release preserves exactly what master had before the
reset. MUST be reverted to "master" before this branch is merged - this
override should never ship.
2026-07-11 09:09:17 -07:00
Satoshi Qazi Muhammed 07c9848118 fix: isolate per-chain/per-asset failures in ChainSyncService.syncUp()
A single malformed or not-yet-understood remote chain/asset entry used to
abort the whole sync via a plain .map{} - chainDao.applyDiff() never even
gets called, so a brand new install (empty local DB) ends up with zero
cached chains forever, i.e. a completely empty tokens list, until the
remote data or the app's parsing code changes. This is exactly what
happened in production: master's config received a batch of upstream
changes the still-live app version couldn't parse, and every fresh install
got stuck with a blank list while existing installs (which already had a
populated local DB from a prior successful sync) were unaffected.

Mirrors the same mapListNotNull + runCatching pattern already used on the
read side (ChainRegistry.currentChains) - one bad chain, or one bad asset
within an otherwise-fine chain, is now logged and skipped instead of taking
the rest of the sync down with it.
2026-07-11 08:55:56 -07:00
Satoshi Qazi Muhammed 141d2b1f42 fix: Networks screen never showed live health/feedback for Tron's toggle
nodesHealthState() only ever looked at wssNodes(), so an HTTPS-only chain
(Tron - no wss endpoint at all) always got an empty node list here: the
enable/disable switch itself worked correctly (it reads/writes
chain.isEnabled directly, unrelated to this list), but with nothing ever
rendering underneath it, the screen looked frozen/unresponsive - this is
almost certainly why a single real toggle needed several taps to land
correctly, since there was no visible confirmation whichever tap actually
took effect.

Falls back to httpNodes() when a chain has no wss nodes, and adds a real
TronNodeHealthStateTester (GET /wallet/getchainparameters, needs no address)
instead of naively reusing EthereumNodeHealthStateTester's eth_getBalance
call, which TronGrid doesn't speak and would have always reported the node
as down regardless of its actual health.
2026-07-11 06:50:27 -07:00
Satoshi Qazi Muhammed e055e1a84c feat: order and label per-chain token breakdown by ecosystem + standard
The per-chain list shown when picking an action (Send/Receive/Swap/Buy/Sell/
Gift) for a token that exists on multiple chains (e.g. USDT) now orders
Ethereum and Tron right after the existing Pezkuwi/Polkadot/Kusama priority
chains instead of falling into the alphabetical "everything else" bucket,
and appends a token-standard label - "(PEZ-20)"/"(ERC-20)"/"(TRC-20)" - to
disambiguate which issuance this is, since a bare chain name alone doesn't
convey that.

The label is intentionally chain-specific, not derived from Chain.Asset.Type,
since every Statemine-type chain (Polkadot AH, Kusama AH, Pezkuwi AH) shares
the same asset type but only Pezkuwi AH's issuance needs the PEZ-20 label.
2026-07-11 06:44:35 -07:00
Satoshi Qazi Muhammed e1679367d7 fix: make Tron address backfill self-healing instead of one-shot
TronAddressBackfillMigration was gated behind a global SharedPreferences
flag ("has this migration ever run on this install") rather than checking
per-account whether a Tron keypair is actually missing. Once that flag was
set - even by a run that found nothing to fix, or partially failed - the
migration never ran again on that install, so an account that lost its Tron
keypair some other way (the cloud-backup schema gap fixed in c78b325) could
never be repaired without a fresh reinstall.

backfillIfNeeded() already does a cheap, correct per-account check (skips
instantly if the account already has a keypair, isn't SECRETS-type, has no
entropy, etc.), so there's no need for an outer one-shot gate at all - just
run it unconditionally on every app start.
2026-07-11 05:29:11 -07:00
Satoshi Qazi Muhammed c78b325e6d fix: cloud backup schema dropped Tron address/keypair on every round trip
WalletPublicInfo/WalletPrivateInfo had no tron field at all, so any wallet
created via the (default) cloud-backup wallet creation flow, or restored
from a cloud backup, silently lost its Tron address and keypair even though
AccountSecretsFactory/SecretsMetaAccountLocalFactory derived them correctly
moments earlier - this is why TRX/USDT-TRC20 never appeared for any wallet,
new or old, confirmed via a device-side diagnostic dump of the actual DB row
(tronAddress=NULL) after a fresh wallet creation.

Also reserves (but does not wire up) solana/bitcoin fields in the same
schema, since native derivation for those doesn't exist yet - adding them
now means that work won't need another silent-drop-prone pass through this
same schema later.

Adds a regression test exercising the exact apply-diff path that lost the
data, plus tron support in the shared cloud-backup test builder DSL.
2026-07-11 04:09:34 -07:00
Satoshi Qazi Muhammed c713ebf6b2 debug: dump all meta accounts' tronAddress/tronPublicKey presence at startup
hasAccountIn=false was observed live for the currently active account despite
every derivation/mapping code path (creation, backfill, DB->domain mapping)
tracing correctly - need to see the actual DB state per account to know
whether this is a write-time or read-time gap.
2026-07-11 02:10:00 -07:00