fix(dot init): stop double-deriving the product account; key username on wallet root#188
Merged
Merged
Conversation
… on wallet root
Three coupled bugs in `dot init`'s identity block.
1. Double-derivation. `IdentityLines` called `productAccountDisplay`
on `loggedInAddress`, which was already the product account (the
output of `createPlaygroundSessionSigner.publicKey`). The helper
then ran `deriveProductAccountPublicKey` again on the SS58-decoded
pubkey, producing a doubly-derived ghost (`5DHk4grp...`). The H160
we printed didn't match the playground-app's `derivedH160` for
the same account.
Fix: a `SessionAddresses = { rootAddress, productAddress,
productH160 }` triple is computed once in `auth.ts` via the new
`derivePlaygroundProductPublicKey` helper in `sessionSigner.ts`,
then threaded through `ConnectResult`, `LoginStatus.success`, and
`SessionHandle`. The signer and the display now share one
derivation function, so a future change to product-account params
can't desync the two.
Verified end-to-end against the playground-app's published log
for the test mnemonic `train snow there sponsor artwork zebra
gossip depth narrow blame change private`:
address = 5GGpUaN7XNaUp3nEVDPBSR4SQLxFxQsiPHbFwf69Apr3HgDZ
derivedH160 = 0x47f68a0851a663dfacb4610d673ec708f05576b0
These now match what `dot init` prints. A frozen-vector
regression test in `src/utils/auth.test.ts::deriveSessionAddresses`
locks the contract.
2. Username key. `lookupUsername` was being called with the product
account; usernames live at `Resources.Consumers[<rootAccountId>]`
on the People parachain. Storage is written by mobile's
`Resources.register_person` keyed on the SSO `rootAccountId`, not
any product derivation. Polkadot-desktop's `useSessionIdentity`
reads the same root key. `IdentityLines` now calls
`lookupUsername(addresses.rootAddress)`.
3. "logged in" label. The row labelled "logged in" used to show the
product account, which a user reasonably expects to be their
wallet identity. It now shows `addresses.rootAddress` (the
SSO-handshake `rootUserAccountId`). The product account is on its
own row underneath with the full SS58 + H160.
This still won't match what the mobile app shows as "Wallet
account address" on its debug screen (hard `//wallet` junction
the host can't reproduce from a public key) — but it does match
what the SSO handshake actually transmits and is the key the
username lookup is keyed on.
CLAUDE.md gets a new "Accounts: root, product, and what the mobile
app shows" section under "Non-obvious invariants" so the next
maintainer doesn't have to re-derive this from host-papp's codec.
Mechanically:
- `src/utils/sessionSigner.ts`: export
`derivePlaygroundProductPublicKey(rootAccountId, ref)`. Single
source of truth for product-account math; called by the signer
constructor and by `auth.ts::deriveSessionAddresses`.
- `src/utils/auth.ts`: new `SessionAddresses` type and
`deriveSessionAddresses(session)` helper. `ConnectResult.existing`,
`LoginStatus.success`, and `SessionHandle` carry the bundle.
`SessionHandle.address` is kept as a back-compat alias for
`addresses.productAddress` because `signer.ts::resolveSigner`
spreads the handle into `ResolvedSigner` and downstream deploy /
registry code reads `.address` for the signing key.
- `src/commands/init/IdentityLines.tsx`: take `SessionAddresses`,
render three rows, look up username against the root.
- `src/commands/init/InitScreen.tsx`: track `addresses` instead of
`loggedInAddress`. The "logged in" row is now rendered inside
`IdentityLines` only.
- `src/commands/init/QrLogin.tsx`: pass the full `SessionAddresses`
through `onDone`; suppress the duplicate "logged in" row on
success.
- `src/commands/init/index.ts`: rename `existingAddress` to
`existingAddresses`.
- `src/commands/init/identityLine.ts` + `identityLine.test.ts`:
deleted. The helpers encoded the exact wrong shape and have no
remaining callers.
- `src/utils/auth.test.ts`: three regression tests for
`deriveSessionAddresses` — frozen vector match, product != root,
H160-tracks-SS58.
Contributor
E2E Test Pass · ✅ PASSTag:
Sentry traces: view spans for this run |
4 tasks
shawntabrizi
pushed a commit
to shawntabrizi/playground-cli
that referenced
this pull request
May 27, 2026
The mobile app (Polkadot Android NIGHTLY) submits the RFC-0010 `Resources::claim_long_term_storage` extrinsic on the People parachain and then waits 30 s for the authorization to propagate to Bulletin Chain. That wait is .logFailure()-wrapped — failures are swallowed and the slot key is returned regardless. The CLI then polls Bulletin via `checkAuthorization` from @parity/product-sdk-bulletin for 75 s; if propagation lags, the poll throws "Mobile returned Bulletin allowance key X, but it is not authorized on Bulletin yet" and `dot init` aborts before even running funding/mapping. Verified end-to-end against the polkadot-app-android-v2 source (`RealTransactionStorageSlotAllocator.kt:53-114`, AWAIT_BULLETIN_TIMEOUT = 30s, `.logFailure(...)` at the `awaitAllocationVisibleOnBulletIn` call site) and RFC-0010's "Bulletin submission — implicit allocation" flow. The host has no on-chain step to add here — RFC-0010 puts all the chain-side work on the mobile. The CLI's only correct move is to be resilient to the propagation lag. Four fixes in this PR, plus a React warning from paritytech#188: 1. `BULLETIN_AUTHORIZATION_URL` + `bulletinAuthorizationHelp(slot)` in `src/config.ts` and `src/utils/allowances/bulletin.ts`. Both `waitForBulletinSlotAuthorization`'s timeout error and the cached-key-not-authorized error now print the slot SS58 + the manual-faucet URL (`https://paritytech.github.io/polkadot-bulletin-chain/authorizations`). Pattern lifted from PR paritytech#186. 2. `requestAndStoreBulletinAllowanceSigner` now persists the slot key + marker BEFORE the propagation wait. A 75 s timeout no longer throws away a perfectly valid key the mobile derived from `mnemonic + //allowance//bulletin//<productId>` — the key will still be valid the moment the chain catches up. 3. `AccountSetup` makes a Bulletin propagation timeout a SOFT failure rather than aborting `dot init`. The row shows the help string + slot SS58; funding + Revive auto-map mapping still run because they depend on the product account on Asset Hub, not on Bulletin authorization. The final `setup complete some account setup steps failed` warning surfaces correctly. 4. `storeSlotAccountKeysFromOutcomes` is now a single read-modify-write so two slot keys returned in one `requestResourceAllocation` call (e.g. `BulletInAllowance` + `StatementStoreAllowance`) can't race-clobber each other. The previous `Promise.all(...storeSlotAccountKey)` had each call load the JSON, mutate one resource, save the JSON; interleaved writes would drop the second sibling's key. Also fixes a React "Cannot update a component while rendering a different component" warning introduced in paritytech#188 (the account- derivation PR). `QrLogin` was calling `onDone(...)` from inside a `setStatus(updater)` callback — which runs during React's render phase, so the parent's `setAddresses` / `setAuthResolved` triggered mid-render of QrLogin. Now snapshots the resolved `SessionAddresses` in a `useRef` inside the `onStatus` callback and calls `onDone` after the promise resolves, outside any updater. Why this slipped past CI: `vitest.config.ts` only picks up `*.test.ts`, not `*.tsx`, so React-strict-mode warnings never fire in the test suite. Tests: 538 pass + 1 skipped (was 536). Two new regression tests: the help helper formats SS58 + URL correctly, and `storeSlotAccountKeysFromOutcomes` preserves sibling keys.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to #185. Three coupled bugs in
dot init's identity block, surfaced when comparing the CLI's output to the playground-app's[playground.dot] selected accountlog and to the mobile app's developer-tools debug screen.1. Stop double-deriving the product account
IdentityLinescalledproductAccountDisplay(loggedInAddress).loggedInAddresswas already the product account (the output ofcreatePlaygroundSessionSigner.publicKey). The helper ss58-decoded it and randeriveProductAccountPublicKeya second time, producing a doubly-derived ghost address whose H160 didn't match what the playground-app uses.Fix: a
SessionAddresses = { rootAddress, productAddress, productH160 }triple is computed once inauth.ts::deriveSessionAddressesvia a new shared helperderivePlaygroundProductPublicKeyinsessionSigner.ts. Both the signer constructor and the display now go through the same function, so they can't drift.Verified end-to-end against the playground-app's log for the test mnemonic
train snow there sponsor artwork zebra gossip depth narrow blame change private:address5GGpUaN7XNaUp3nEVDPBSR4SQLxFxQsiPHbFwf69Apr3HgDZderivedH1600x47f68a0851a663dfacb4610d673ec708f05576b0Frozen-vector regression test in
src/utils/auth.test.ts::deriveSessionAddresseslocks the contract — three tests: known-vector match,product != root, and H160 moves in lock-step with the product SS58 when the root changes.2. Username lookup keyed on the wallet root
lookupUsernamewas being called with the product account. Usernames live atResources.Consumers[<rootAccountId>]on the People parachain — mobile'sResources.register_personwrites them keyed on the SSOrootAccountId. Polkadot-desktop'suseSessionIdentity(session)reads the same root key.IdentityLinesnow callslookupUsername(addresses.rootAddress).3. "logged in" label
The row used to show the product account, which the user reasonably expects to be their wallet identity. It now shows
addresses.rootAddress(the SSO-handshakerootUserAccountId). The product account is on its own row underneath with the full SS58 + H160.This still won't match what the mobile app shows as "Wallet account address" on its debug screen (hard
//walletjunction; the host can't reach it from a public key alone) — but it does match what the SSO handshake actually transmits and is the storage key the username lookup uses.What the user sees now
Documentation
CLAUDE.md gets a new "Accounts: root, product, and what the mobile app shows" section under "Non-obvious invariants" so the next maintainer can re-verify the host-vs-mobile derivation map without re-reading host-papp's handshake codec.
Mechanical changes
src/utils/sessionSigner.ts: exportderivePlaygroundProductPublicKey(rootAccountId, ref)— single source of truth for product-account math.src/utils/auth.ts: newSessionAddressestype +deriveSessionAddresses(session)helper.ConnectResult.existing,LoginStatus.success, andSessionHandlecarry the bundle.SessionHandle.addresskept as a back-compat alias.src/commands/init/IdentityLines.tsx: takeSessionAddresses, render three rows, look up username against root.src/commands/init/InitScreen.tsx: trackaddressesinstead ofloggedInAddress. The "logged in" row now lives insideIdentityLinesonly.src/commands/init/QrLogin.tsx: pass fullSessionAddressesthroughonDone; suppress the duplicate "logged in" row on success.src/commands/init/index.ts: renameexistingAddress→existingAddresses.src/commands/init/identityLine.ts+identityLine.test.ts: deleted. The helpers encoded the exact wrong shape and have no remaining callers.Test plan
pnpm format:check(145 files clean)pnpm lint:license(172/172 files)pnpm test— 536 passed, 1 skipped (added 3 new regression tests in thederiveSessionAddressesblock)dot initpost-merge: confirm three rows render with full root + product SS58 + H160; confirmusernamelookup completes against the root (returns(no username set on chain)for accounts that haven't claimed one, otherwise the actual username)