* ci: run the gate before the deploy, not after it
master is the live branch — every app fetches its config from
raw.githubusercontent.com/.../master/... — and the flow was arranged so that content
reached it first and was checked afterwards.
auto-pr.yml fired on a push to *master* and opened a master → main PR. Code Quality ran
on that PR, i.e. on the way into the mirror, long after the config was already being
served. A check that reports "the live config is broken" is not a gate.
The same inversion made master unwritable by the front door: five required status
checks, and no workflow triggering on a PR into master, so the only way to change the
live branch was to bypass its own protection. That is not a hypothetical — it was
bypassed twice on 2026-08-10, and the second time was to undo the first.
Now: work lands on main, Code Quality decides, and promote-to-live moves master to the
exact commit that passed. It refuses anything that is not an ancestor of main, and uses
--force-with-lease so a master that moved underneath it is a failure rather than a
silent overwrite. main already ran Code Quality on push, so no trigger change is needed.
Also: each daily sync opened a PR and nothing closed the previous one. Thirty-one
branches spanning 2026-02-10 to 2026-08-08 were cleared by hand on 2026-08-11, every one
superseded by the next day's run. A sync is a snapshot of upstream, so an older open
sync PR is never the right thing to merge — it is noise that hides whether anything is
genuinely waiting. The sync now closes what it supersedes.
Branch protection still needs moving in the same direction and cannot be done from a
commit: main requires no PR, no approvals and no checks, while master carries the five
checks that can never run there. The settings change is proposed separately.
* ci: require an admin's approval before anything reaches the live branch
main is where work lands and where Code Quality decides; promote-to-live then moves
master to whatever passed, and every app reads master directly. An approval on main is
therefore the last human judgement before a change is served to wallets in the field —
and until now main required no review at all.
One approval, from either admin. Requiring two would mean two of two, which stalls
whenever one of them is the author.
* fix(pezkuwi): finish the Asset Hub cleanup and guard it against coming back
Three things left half-done, and a check so they stay done.
## Assets that were never created on chain
chains.json and v22/android/chains_minimal.json still declared Pezkuwi Asset Hub
assetIds 1001, 1002 and 1003 as DOT, ETH and BTC. Queried against
wss://asset-hub-rpc.pezkuwichain.io on 2026-08-11, assets.asset() returns None for all
three; only 1 (PEZ) and 1000 (wUSDT) are Live. They were added from a template — the
icons pointed at Nova's own repo — and shown to users, where any transfer would fail.
This is the second removal. The first, on branch fix/remove-nonexistent-asset-hub-assets
(2026-07-09), cleaned 66 files across v10-v22 but never touched these two, and was never
opened as a PR. A third branch, feature/asset-level-balance-test-fixture, went the other
way and added all three to a regression fixture. Both branches sat for a month.
## Assets served from a working branch
252 references under chains/ fetched icons from pending/post-fix-release. Every file
they named is byte-identical on master, main and that branch, so the dependency bought
nothing — and would have broken silently the day the branch was tidied away, which
nearly happened during this cleanup. Repointed to master, the branch the apps read.
## A fixture nothing keeps in step
sync_from_nova.py publishes chains, xcm, icons and config from the overlay, but never
tests/. So tests/pezkuwi_assets_for_testBalance.json and its overlay source are kept
aligned by hand. They agree today; nothing would have said so if they stopped.
Rather than invent a publish step whose conventions I would be guessing at, the drift
is now asserted.
## The check
scripts/check_pezkuwi_integrity.py, wired into the Code Quality job. Four assertions,
one per regression above, each carrying why it exists. Two of the four have already
recurred once, and the Nova sync will keep proposing the first one back — the current
sync branch (f6c3ebb9) reverts the isSufficient declaration, which is why the second
assertion exists.
Mutation-tested: restoring a ghost asset, removing isSufficient, repointing one icon at
a working branch, and nudging the fixture each fail it; reverting each passes.
* fix(overlay): declare sufficiency at the source, not in the generated output
The isSufficient declaration was added to chains/ — which sync_from_nova.py regenerates
from nova-base plus pezkuwi-overlay on every run. So the fix had a shelf life of exactly
one sync, and the sync branch already waiting (sync/nova-base-f6c3ebb9, 2026-08-08)
reverts it: its output carries {"assetId": "1000"} with no sufficiency, because it was
generated from an overlay that does not declare any.
Editing generated files is how this repo keeps losing the same change. The phantom
Asset Hub assets were removed twice and came back twice for the same reason.
Verified rather than assumed: added the field to
pezkuwi-overlay/chains/pezkuwi-chains.json, ran scripts/sync_from_nova.py, and all three
generated files — chains.json, v22/android/chains.json, v22/android/chains_minimal.json
— came out carrying isSufficient: true. git reported no change to any of them, meaning
the sync now produces exactly what the manual edit produced, so the two agree instead of
fighting.
The phantom assets do not return either: the overlay lists only HEZ, PEZ and USDT, so
regeneration drops 1001/1002/1003 by construction rather than by anybody remembering.
* fix(pezkuwi-asset-hub): declare PEZ and USDT as sufficient assets
The wallet refused USDT.p transfers to any account holding no HEZ:
Your transfer will fail since the destination account does not have
enough HEZ to accept other token transfers
It is a false positive, and the chain disagrees with this config.
DeadRecipientValidation skips the whole check when the destination asset is
self-sufficient:
skipIf = { assetSourceRegistry.isAssetSelfSufficient(destinationChainAsset) }
and StatemineAssetBalance reads that flag from HERE, not from the chain:
chainAsset.requireStatemine().isSufficient → runCatching { … }.getOrDefault(false)
`isSufficient` was absent from typeExtras in every version of this config, so it
resolved to false, the validation ran, and every transfer to a fresh account was
warned against.
Measured on chain (asset-hub-rpc.pezkuwichain.io):
asset 1 PEZ sufficient=true minBalance=1 18 accounts
asset 1000 wUSDT sufficient=true minBalance=10000 21 accounts
Both are sufficient, so neither needs the recipient to hold native balance.
Proof it works in practice rather than only in theory: account
5E2yqgUVVNjU… holds 1750.00 USDT with free=0, providers=0, sufficients=1 and
nonce=0. Zero native balance, no provider reference, and it has never signed a
transaction — so it was brought into existence by the incoming USDT transfer
itself, which is exactly what the wallet was saying could not happen.
Applied to v22 (the version the app fetches, runtime/build.gradle CHAINS_URL) and
to the top-level chains.json.
Noted, not changed here: this config also declares assets 1001/1002/1003 on
Pezkuwi Asset Hub as DOT, ETH and BTC. None of the three exists on chain —
assets.asset() returns None for all of them. They are shown to users and any
transfer would fail.
* Revert "fix(pezkuwi-asset-hub): declare PEZ and USDT as sufficient assets"
This reverts commit af1a8a5468.
* fix(pezkuwi-asset-hub): declare PEZ and USDT as sufficient, matching Polkadot AH
Pezkuwi Asset Hub's two statemine assets carried only `assetId` in typeExtras. The
equivalent entries on Polkadot Asset Hub — USDT (1984) and USDC (1337) — carry
`isSufficient: true`. Same kind of asset, same file, two different declarations.
The omission is not inert. `LocalToDomainChainMapper` reads
typeExtras["isSufficient"] as Boolean? ?: STATEMINE_IS_SUFFICIENT_DEFAULT // false
so an absent field resolves to false, `StatemineAssetBalance.isSelfSufficient` returns
false, `DeadRecipientValidation`'s skipIf does not fire, and the validation runs — at
`validOrError`, which is DefaultFailureLevel.ERROR, so the transfer is blocked rather
than warned about.
The chain says otherwise. Measured on asset-hub-rpc.pezkuwichain.io, 2026-08-10:
asset 1 PEZ sufficient=true minBalance=1 18 accounts
asset 1000 wUSDT sufficient=true minBalance=10000 21 accounts
Neither needs the recipient to hold native HEZ, and one account demonstrates it:
5E2yqgUVVNjU… holds 1750.00 USDT with free=0, providers=0, sufficients=1 and nonce=0 —
no native balance, no provider reference, never signed a transaction. It exists because
the incoming USDT transfer created it, which is exactly what the wallet was reporting
could not happen.
43 of the 67 statemine assets in this config declare the field. The 24 that omit it are
genuinely non-sufficient on chain — the Polkadot AH meme tokens. Pezkuwi Asset Hub was
the only case where the declaration disagreed with the chain.
Applied to v22 (the version runtime/build.gradle CHAINS_URL fetches) and the top-level
chains.json, in the same shape as the Polkadot entries.
The "Your Voice Matters" card used pezkuwiwallet://pezkuwi/open/governance.
That path prefix-matches the referendum deep link handler (/open/gov), which
requires an id; without one it reports "Referendum not found". Worse, a
matched-but-failed deep link is persisted and never cleared, so it replays on
every app launch.
Replace it with a builders/developer call-to-action (image: Gre Miraza scene)
that links to an external Telegram invite. External https links bypass the
in-app deep link engine entirely, so this card cannot get stuck.
prod + dev kept in sync; en/ku/tr localised.
Assets banner set (prod + dev kept in sync):
- Remove pezkuwi-newroz-001 (expired) and pezkuwi-explorer-001
- Add pezkuwi-book-001: "Cloud Nation" book promo (image = English cover),
action null ("on Amazon" in copy)
- Add pezkuwi-hiring-001: Pezkuwi Digital States hiring 10,000 paid roles,
action -> https://welati.pex.network
- Welcome card: "Kurdistan" -> "Stateless Nations" (en/ku/tr)
- Community card action -> new Telegram invite (t.me/+DUWJ8wtt5qI4Njgy)
Localisation covers all app-supported languages of the 6-language standard
(en/ku/tr); fa/ar/kmr are not app locales so no dead files were added.
New foreground images (250x180, transparent, text-free) added under
resources/images/. Backgrounds reused from freed slots.
TRX.svg used a full-bleed r=24 circle instead of the r=18 padded frame
every other token icon uses, so it looked oversized/edge-to-edge in the
token list while others sit inside a consistent inset disc.
An earlier fix patched only the merged root icon; the pezkuwi-overlay
source stayed r=24, and sync_icons() copies the overlay over root
unconditionally, so a later pipeline run silently reverted it. Fix both
the overlay source (source of truth) and the merged output here.
Same silent-drop class as 6b82731: main got the sharpened icons (fd9a9e4)
after master's compat revert had reverted them, and pending/post-fix-release
never touched them again - so the merge kept master's stale copies. Taken
verbatim from main.
The hardcoded-secret scan flags any 64-hex 0x value in JSON as a possible
private key. The restored balance-test fixtures store public 32-byte
account IDs under "account" fields - public data by definition, tripping
the scan as a false positive. Scope the exclusion to the exact field name
rather than loosening the pattern itself.
master's earlier compat-revert (e533923) deleted these paths; since pending/
post-fix-release never touched them again afterward, git's merge saw
'deleted on one side, untouched on the other' and silently kept them deleted
instead of conflicting. Restored verbatim from pending/post-fix-release:
test fixtures (tests/, pezkuwi-overlay/tests/), the pezkuwi-overlay TRX icon
copy, TAOapp dapp icon, and 7 preConfigured chain detail JSON files.
subquery-multisigs-prod.novasama-tech.org and subquery-proxy-prod.novasama-tech.org
never resolve in DNS - verified via getent/curl. Real upstream (novasamatech/nova-utils
global/config.json) uses subquery-accounts-prod.novasama-tech.org for both. Removed the
Pezkuwi overlay override entirely (it was never meant to diverge from Nova's own
multisig/proxy indexer) and corrected the merged output the Android app actually fetches.
Confirmed impact: the new Bridge Pending Signatures feature (feature-assets
BridgeMultisigInteractor.getPendingApprovals) calls this URL to decode arbitrary
pending multisig call content and silently returns nothing on failure (by design,
to avoid blind-signing) - this was the reason it never showed anything, verified by
submitting a real as_multi call on Polkadot Asset Hub and confirming the on-chain
approval existed but the app rendered nothing until this fix.
Both hardcoded a guessed subdomain (subquery-multisigs-prod, subquery-proxy-prod)
that never resolves in DNS. Real Nova indexer is subquery-accounts-prod - dropping
the override lets the merge pass through nova-base's own correct value instead.
Popular is a plain ordered array (app renders it verbatim, no sorting) -
reordered/added these 3 pezkuwi-owned dapps to the front in both the
release (dapps_full.json) and debug (dapps_dev.json) variants, keeping
the rest of the existing list in its prior relative order after them.
The solid #66F9A1 background was never Solana's actual brand color -
the real mark (and our own chain-icon gradient) uses a purple-to-teal
diagonal gradient (#9945FF -> #14F195), confirmed against Solana's own
official horizontal color logo. Same circle geometry/logomark as the
previous fix, only the fill changed from solid to gradient.
SOL.svg was a lone full-bleed 32x32 canvas with r=16 (the circle exactly
filled the icon bounds), while every other token icon (BTC, DOT, KSM,
HEZ, PEZ, LINK, TAO) uses a 48x48 canvas with an r=18 background circle,
leaving a margin so the app's dark background forms a consistent ring
around each token in the asset list. Reflowed the same logomark path
(unchanged) into that convention via a scale/translate group, matching
BTC.svg's own transform pattern.
The earlier PR (#55) only updated the pezkuwi-overlay source file, not
the generated chains/v*/[android/]chains*.json outputs the app actually
fetches at runtime (BuildConfig.CHAINS_URL points at
chains/v22/android/chains.json on this exact branch) - this is why a
real-device install still showed no Solana network/token. Mirrors
Bitcoin's own commit (78b0af9), which updated the overlay AND all 63
generated files in one change. Inserted right after the Bitcoin entry
in every v2-v22 chains.json/chains_dev.json/android/chains.json, using
ensure_ascii=False to avoid re-escaping unrelated non-ASCII chain names
already present in these files (caught and reverted a first attempt
that had done this by accident). Also copies the Solana icons into
pezkuwi-overlay/icons/ to match Bitcoin's dual-location icon pattern.
New pezkuwi-overlay chain entry mirroring the Tron/Bitcoin native-coin
config shape: solana:mainnet chainId, public mainnet-beta RPC endpoint,
Solana Explorer links, and a native SOL asset (9 decimals). New brand
icons (gradient chain icon + colored token icon) added alongside.
The previous commit added OnFinality's public-ws endpoint as a
failover, based on search results that turned out to be stale.
Direct WebSocket handshake test against
wss://aleph-zero.api.onfinality.io/public-ws returns HTTP 200 with an
"API Deprecated" HTML body, not a real upgrade - OnFinality has retired
free public access for this specific chain (their other chains'
public-ws endpoints, e.g. Altair/Bittensor, still work fine, so this
is Aleph-Zero-specific, not a platform-wide change). Keeping a
non-functional node in the config is worse than no fix, since it
masks the real failure mode. See conversation/memory for the fuller
investigation (dRPC's public endpoint completes a WS handshake but
rejects standard RPC methods; ws.azero.dev itself is a real, current,
likely-transient outage - the Foundation's main site is healthy).
Aleph Zero was configured with a single node (wss://ws.azero.dev,
the Foundation's own endpoint) and no nodeSelectionStrategy, so any
outage there fails the chain outright with no fallback - confirmed
live via direct curl (502 Bad Gateway from the endpoint's own nginx,
backend unreachable), which is what was timing out
BalancesIntegrationTest.testBalancesLoading/testFeeLoading[Aleph Zero]
in wallet-android CI. Aleph Zero itself is healthy and active (live
Foundation site/explorer, active aleph-node development, OnFinality/
Dwellir both offer public RPC for it) - this was a config gap, not a
dead chain. Added OnFinality's public endpoint as a second node with
roundRobin selection, matching the pattern already used for our own
Pezkuwi chain's 2-node setup.
Master never got the Bitcoin icon files (they landed only on
pending/post-fix-release, same branch CHAINS_URL is currently testing
against) - Tron's icon happened to already exist on master from an
earlier, unrelated commit, so it worked by coincidence while Bitcoin's
404'd. Confirmed live: master/icons/tokens/colored/BTC.svg -> 404,
pending/post-fix-release/... -> 200.
Same caveat as the CHAINS_URL override itself: revert to master once
this lands there in the coordinated release.
Adds a Bitcoin entry (assetId 0, type "bitcoinNative", precision 8) mirroring
Tron's chain-family shape: "bitcoinBased" + "noSubstrateRuntime" options, a
mempool.space REST API URL as the "node" (same role TronGrid's URL plays for
Tron), and mempool.space itself as the block explorer.
Adds gradient chain icon + colored token icon (orange #F7931A, built from
the existing white Bitcoin glyph already vendored from Nova, matching this
repo's established icon-construction pattern for every other coin).
Lands on pending/post-fix-release, not master - per the existing plan to
ship Tron and Bitcoin support together in one coordinated release once
wallet-android's corresponding send features are done.
icons/chains/gradient/Tron.svg is what NetworkFlowRvItem actually renders
per-row (chain.icon, not the token icon) - missed this one when restoring
the token-level TRX.svg earlier. Also restoring the pezkuwi-overlay copy
for consistency. Both are static assets, unrelated to the chains.json
parsing incompatibility that forced master back to 7a087cf.
Static asset, unrelated to the chains.json schema/parsing incompatibility
that forced master back to 7a087cf - the live app's chains.json doesn't
reference this file right now (no Tron chain config), so it's inert for
production, but pending/post-fix-release's config (and the eventual
coordinated release) already points at this exact path.
These two icon files are static assets unrelated to the chains.json
schema/parsing incompatibility that forced master/main back to
7a087cf - restoring just them doesn't reintroduce any of that risk.
Their URLs in chains.json are hardcoded to fetch from "main" regardless
of which branch/version served the chain list itself, so live users
(and this app's icon rendering, unrelated to the chain-sync issue) were
seeing the old blurry versions again once main got reverted.
The live Play Store release (wallet-android 85bde7e, published 2026-06-15)
was built against whatever was on this branch's HEAD at the time - which,
since master had received no commits between 2026-03-02 and this week, was
commit 7a087cf. This week's changes (nova-base sync, Tron config/icons,
balance test fixtures, etc.) are real and wanted, but they've made master
incompatible with that still-live app version, and live users installing
fresh right now get a completely empty tokens list because of it (the app's
chain sync silently fails whole-hog on any single malformed/incompatible
chain entry, leaving new installs with zero locally-cached chains).
This makes master's served content match 7a087cf exactly, stopping the
bleeding for current live users without needing an emergency app release.
None of this week's work is lost - it's all preserved on
pending/post-fix-release and will come back once wallet-android's Tron
send feature is complete and both repos can ship together in one
coordinated release.
auto-merge.yml only listened for the "Code Quality" workflow_run event.
Whenever "Security" (CodeQL etc., usually the slower of the two) finished
after Code Quality, the merge attempt fired too early, failed against the
still-pending required check, and nothing ever re-triggered it - the PR sat
open until someone noticed and merged it by hand. This is the actual cause
behind the recurring "master->main sync PR stuck" issue, not a one-off fluke.
Now listens for both workflows, and verifies every required check is
actually green (via `gh pr checks --required`) before merging rather than
trusting that the one workflow which fired us means everything is done -
whichever of the two finishes last will now successfully trigger the merge.
TRX.svg used a full-bleed r=24 circle (no margin) instead of the r=18
padded-circle frame every other token icon uses, making it visually
inconsistent in the token list. Scaled its icon path down to fit the
standard frame instead of redrawing it.
Also brings HEZ/PEZ's sharper re-render (already pushed to main in 962daf7)
onto master, since master and main currently serve different assets to the
app (main/master divergence is a known, separately-tracked issue) and this
keeps both consistent for now.
Both icons embedded a tiny (36-42px) raster PNG inside the badge - visibly
blurry at normal display sizes. Re-rendered from the original high-resolution
source artwork instead, and switched the background circle from r=24
(edge-to-edge, no margin) to r=18, matching the padded-circle framing every
other token icon (DOT, KSM, LINK, etc.) already uses.
sync-nova-base.yml checked out and PR'd against whatever branch GitHub
considers the repo default - which is 'main', a read-only mirror kept in
sync FROM master (auto-pr.yml/auto-merge.yml only flow master -> main,
never the reverse). master is what the live app actually fetches configs
from (see wallet-android's CHAINS_URL etc. all pointing at .../master/...).
Net effect: every one of the 29 accumulated 'Sync from Nova Base' PRs
(#1 through #36, 2026-02-09 through 2026-07-09) landed on a branch nothing
downstream ever reads, so 5 months of upstream Nova chain/RPC/asset
maintenance never reached production regardless of whether those PRs got
merged. Explicit ref/base: master makes future runs target the branch that
actually matters; the 29 stale main-targeted PRs are being closed as
superseded now that #36's content has been applied to master directly (see
next commit).
The FullSyncPaymentUpdater ordering-bug fix isn't Pezkuwi-specific - it
affects any chain's assets on a busy shared subscription connection.
'USDT on Polkadot Asset Hub not showing' was one of the 3 original
symptoms reported at the start of this investigation, alongside Tron
being disabled and Pezkuwi's own tokens missing. Add it to the fixture
so the fix's coverage is actually verified end to end, not assumed.
These 3 assetIds (1001/1002/1003) were added in 173f08a with icons
pointing at novasamatech/nova-utils (Nova's own repo, not Pezkuwi's)
and generic names/priceIds - clearly copied from a template rather than
verified against Pezkuwi's actual chain state.
Verified live via @pezkuwi/api against wss://asset-hub-rpc.pezkuwichain.io:
api.query.assets.asset(1) -> Live, real supply (PEZ, correct)
api.query.assets.asset(1000) -> Live, real supply (USDT, correct)
api.query.assets.asset(1001) -> None (DOT, does not exist)
api.query.assets.asset(1002) -> None (ETH, does not exist)
api.query.assets.asset(1003) -> None (BTC, does not exist)
This is what caused wallet-android's StatemineAssetBalance to silently
never complete a sync for these 3 assets - it was fetching details for
assets that were never created in pallet-assets, not a wallet bug. Also
removes them from the new pezkuwi_assets_for_testBalance.json fixture
so the test doesn't assert on assets that were never real.