Skip to content

mobile: merge main, recovery gap limit, and each family's key ladder (#505) - #794

Merged
Ryun1 merged 823 commits into
feature/mobile-walletfrom
mobile/merge-main-505
Oct 28, 2026
Merged

Ryun1 merged 823 commits into
feature/mobile-walletfrom
mobile/merge-main-505

Conversation

@Ryun1

@Ryun1 Ryun1 commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Refs #450, #505

Brings feature/mobile-wallet up to main and ports the two recovery changes that broke the mobile core in the process. Three commits, best reviewed one at a time.

Commits

  1. 7203f48e Merge main into feature/mobile-wallet (812 commits since 22 Sep). The copy review this branch had merged directly landed on main squashed (Make every word count across the wallet (rebased #466, with review fixes) #475, Restore the copy review's final commits that #475 left out #489), so conflicts take main's side and changes outside conflicts are kept. Mobile-specific resolutions: encrypted-store re-exports KDF_ITERATIONS from store/envelope-shape (value and legacy counts unchanged), the multisig limits and recovery-row helpers stay exported for the generators, and the generators follow main's API moves. Derivation, signature and vault-envelope vectors regenerate unchanged.
  2. 4f0e71b9 Recovery gap limit and strkd's reserved index. The core walks recoveryScanTarget (20 past the last active index, capped at maxScannedIndexes) instead of a fixed 0-19 window, and refuses RESERVED_AGENT_ACCOUNT_INDEX (Reserve account index 0x41474E54 for strkd agent accounts #336) for manual search, restore, creation and the next free index.
  3. ecc8a8c3 Each family on its own key ladder (Seed import finds zero Ready (Argent X) software accounts — wrong master seed, wrong axis #505). ⚠️ Changes key derivation and the vault format; please review closely.
    • Every key goes through kms::private_key with a Ladder: Account (m/44'/9004'/i'/0/0), Address (Braavos, m/44'/9004'/0'/0/i), ArgentX (Ready: the ETH key at m/44'/60'/0'/0/0 seeds m/44'/9004'/0'/0/i; refused under a BIP-39 passphrase).
    • Vault v4 → v5: each record stores its ladder. Migration labels every existing record Account, which is how it was derived, so no record changes address, including any Braavos record an earlier scan found off index 0. Keychain/Keystore entries, the DEK and the password slot are unchanged; the v1→v5 golden differs only in its payload.
    • The scan uses recoveryScanShapes; candidates carry their ladder into restore (iOS and Android drivers updated); vendor coordinates are never offered for creation; nextAccountIndex counts only this wallet's tree.
    • The parity fixture gains 18 Ready vectors from the extension's deriveAccount; all 46 derivation vectors match.

Follow-up commits

Parity catch-ups with main: relay URLs in canonical form (stored ones read back rather than failing closed), the common-password meter (word list generated from the extension), bridged .e tickers with their spoof check (#645), "Portfolio price · … · 24h" and per-row "24h price" labels, per-address price freshness (#357) with unbacked prices dropped and no fiat off mainnet (Sherlock 185), the activity hash read from the row's expansion, the Argent refusal wording, and the nostr-relay.test.ts split.

Known failures (accepted for now)

core has 6, each a deliberate follow-up:

  • history_parity (4): main's ActivityEntry gained direction, tokenAddress, feePaid, origin and seenOnChain. direction matters: mobile still reads in/out from summary words, so a token named RECEIVED or SWAP can flip the arrow and fiat sign (Sherlock 196). Next up.
  • privacy_parity (1): the extension now accepts a discovery reply over the 64-token cap; mobile still refuses it whole (fails closed). Needs a read of the extension's handling before loosening an untrusted-response bound.
  • discovery_rows_are_worded_like_the_extension: main added Braavos deployment and signing advisories; mobile's Braavos stays view-only, so it keeps its own wording rather than promise a deploy it refuses.

UI follow-up: the extension shows an activity row's hash in its expansion; mobile still shows it in the summary.

Not verified

  • iOS has not been built: the Swift edits (RecoveryScanDriver.swift, AccountsBridgeTests.swift) are uncompiled; this PR's ios job is the first build.
  • Nothing ran against a live chain or a real Ready/Braavos phrase.

Test plan

  • cargo test --locked --no-fail-fast in mobile/core: 6 fail, all listed above; cargo clippy --all-targets -D warnings clean
  • Android: :app:assembleDebug, :app:compileDebugAndroidTestKotlin; :app:testDebugUnitTest 51/52 (above); mobile/scripts/lint.sh kotlin clean
  • bun run check:types clean; mobile:assets and mobile:parity regenerate with no further diff
  • iOS build and BrambleTests (CI)
  • Import a real Ready phrase and a real Braavos phrase on device

Drafted with Claude; reviewed by a human before posting.

derek-flossman and others added 30 commits October 13, 2026 17:18
…-2026-10-13

Batch 6: v0.10.6 — privacy SDK rc.8, dataset tooling, terminal fixes, workflow clean-up
)

The curated mainnet registry lists bridged USDC as `USDC.e`, which
isTokenSymbol refused. Since #618 a token scan drops that row instead of
failing, but bridged USDC itself was never suggested. The owner chose
option (b): widen the shared grammar rather than rename the ticker.

isTokenSymbol now admits a 2..12 uppercase-alphanumeric ticker with at
most one lowercase `.e` suffix; every other rule is unchanged (no `.E`,
no second suffix, no other dotted form). The previous commit makes the
spoof check read `X.e` as `X`.

wallet.addToken and the watch-asset builder uppercased chain metadata
before validating it, which would turn `ETH.e` into a refused `ETH.E`.
Both now go through one shared normalizeTokenSymbol in core/json, which
keeps the suffix's lowercase `e`, so a dapp-watched `ETH.e` is stored and
hidden as a spoof of ETH rather than slipping past the check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous commit refused a never-used Braavos Ledger account with the
generic "invalid ... request" errors, and the onboarding picker printed
whatever error the background returned without translating it. Owner
rule: a raw developer-style error must never reach the reader.

- The background now returns two distinct stable diagnostics:
  "braavos ledger account not found" (read as neither deployed nor
  funded) and "braavos ledger account status unavailable" (a chain read
  failed without proving activity). A network with no curated tokens has
  no balance to read, so an undeployed address there is "not found".
  A missing or failing derivation refuses as "wallet unavailable".
- friendly-error-copy maps them to the approved "No Braavos account found
  for this key." and the existing "Couldn't reach the network, try
  again". That sentence was two literals in token-ops; it is now
  NETWORK_RETRY_MESSAGE in core/network-error, used by token-ops and the
  copy table, so the wording has one source.
- The picker's add/create catch now passes the error through
  friendlyError, as status lines do. Capitalised sentences such as
  "Account already exists" still show as written; an unmapped lowercase
  diagnostic now shows the generic fallback instead of itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…647)

Since #594 a revoke over a tracked grant marks the record `revoking`,
and only a zero reading settles it. A non-zero reading cannot tell "not
mined yet" from "reverted", so after a rejected or reverted revoke the
row kept saying the revoke was on its way indefinitely.

The record now keeps the canonical hash of the transaction that carried
the revoke (`revokeHash`, present only beside `revoking`).
`recordApproveAllowances` requires the submitting transaction's hash, so
every submitting path passes it; a newer revoke replaces an older one's
hash and a new grant replaces both. The allowance listing reads that
hash's status from the wallet's own activity reconciliation, which keeps
checking hashes after the panel closes: a rejected or reverted revoke
clears the mark and leaves the grant listed and revokable; an accepted
one drops the record, unless the same round read a non-zero amount, in
which case the record is left for a later zero reading. The listing
shows the settled records. The hash never reaches a display view.

The allowance log is an unversioned plain store whose policy is to
discard invalid rows, so the field is optional rather than migrated: a
revoke recorded before it keeps loading and settling on readings
alone, and a hash that is not one canonical non-zero felt, or that has
no `revoking` mark beside it, drops that record on read. The settle and
revoke-marking rules move to allowance-revoke.ts, and the record types
to allowance-record.ts (re-exported from the store), to keep the store
under the file-size budget without an import cycle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The retention caps evicted the smallest raw amounts first, but base
units are not comparable across tokens: 1000 USDC (10^9 base units)
ranked below half an ETH (5 * 10^17). Eviction now ranks by risk in
three tiers: untracked tokens go first, then tracked tokens by their
decimal-normalised amount (compared exactly by cross-multiplying the
powers of ten), and unlimited grants last, whatever their token. Both
the per-scope and the total cap use the same ranking.

The log reads the tracked tokens on every write through an injected
`trackedTokens`. The background passes `trustedTokens`, the rule the
pre-sign verifier already applies, so a likely-spam or dapp-named token
cannot lend itself few decimals to outrank real grants. Without the
list, or when reading it fails, every record ranks as untracked, which
is the previous raw order with unlimited grants last, and the write
still goes through.

The unlimited cut-off moves to allowance-retention.ts, which now reads
it, and stays re-exported from allowance-discovery. `allowanceTarget`
moves to allowance-revoke.ts (re-exported from the store) to keep the
store under the file-size budget; the background store construction
moves to portfolio/allowance-log.ts for the same reason in index.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With RPC now the only balance source (#650), readBalance's fall-through
to 0n meant an RPC timeout rendered a funded account as empty, and
because it never threw, withRpcFallback never failed over to the next
endpoint either.

A token whose read fails on both entrypoint spellings now throws. A
transport error fails over to the next endpoint, and a refresh that
still cannot read every token rejects, so useAsyncResource keeps the last
good balances and the portfolio shows its existing error banner. Only a
successful contract response establishes zero. The rejection carries the
resource layer's existing generic "Request failed" text rather than the
raw RPC detail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eview)

An accepted revoke outcome deleted the record whenever this round had no
non-zero reading. A failed allowance read is no reading at all, so a
pair read once as non-zero (re-granted after the revoke) and then not
read was deleted, hiding a live grant; an accepted relative decrease
does not establish zero either.

An accepted revoke now never deletes a record by itself; only a zero
chain reading does, through the existing path. Accepted with a non-zero
reading clears `revoking` and `revokeHash` and keeps the grant, so the
display takes the chain's figure. Accepted with no reading keeps the
pending mark until a reading settles it. A failed outcome is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eview)

memoryAllowanceStore's `record` and `applyDelta` read `entries` before
awaiting the token ranking, so two concurrent writes each applied to the
same snapshot and the later one overwrote the earlier. Each write now
awaits the ranking first, then reads and replaces `entries` with no
await in between.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The #646 handoff re-check read referenceUsdPrice, which goes through the
AVNU feed's shared 55-second cache, so the check right before the wallet
saw the calls could reuse the very price the quote was checked against.

freshReferenceUsdPrice reads the same feed past the cache (cacheMs 0) and
resolves null when it cannot, so the re-check refuses rather than falling
back to the cached price. Both Multiply handoffs (LendingTicket and
LendingManageModal) use it; quote time keeps the cached reader. A test
gives getJson a real TTL cache, moves the price between quote and
handoff, and pins that only the handoff reader sees the new price and that
a failed fresh read is null, not the cached value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The anchor's two refusals shared their messages with unrelated quote
failures ("Ekubo Multiply route amount mismatch" also covers malformed
totals, "Could not quote Vesu (un)wind route" also covers a quoter
outage), so a caller could not tell "price too far" from "no price" and
map each to reviewed copy.

They now throw VesuMultiplyAnchorError with a stable `code`:
price_premium_exceeded or price_unavailable (also for unusable decimals).
The messages are unchanged pending copy approval. A test pins each code and
that a quoter outage is not reported as an anchor refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rst (#647 review)

Clearing the pending mark on "accepted + non-zero reading" left the
recorded pre-revoke figure in place. The listing read allowances before
revoke outcomes, so the reconciler could flip the revoke to accepted in
between, and a lagging or failed-over node can answer with a pre-revoke
figure anyway. With the mark cleared, later zero readings no longer
acted and the display took the larger recorded amount, so a revoked
grant showed as live for good.

Revoke outcomes are now read before the allowance readings. On an
accepted revoke with a non-zero reading the record takes that reading
as its exact amount (any lower bound resolved), drops the spent hash and
keeps the pending mark: a genuine re-grant is listed at the chain's
figure, and a stale reading is followed by a zero reading that drops
the record. A non-zero reading is adopted only if it passes the store's
amount rule.

Note on #647's wording: the issue asked for a schema version bump. The
allowance and activity logs are unversioned plain stores that discard
invalid rows, not the vault, so this branch adds optional fields with
strict validators instead. Rolling the branch back would drop stored
rows that carry the new fields (`tokenAddress`, `revokeHash`) on read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… review)

The outcome lookup took any activity row with the revoke's hash on the
network, last one winning, including chain-discovered rows and rows in
another scope. Only the row this wallet recorded for the transaction it
submitted (`origin: "wallet"`) in the revoking record's own scope is now
an outcome, and conflicting reports for one hash resolve to `failed`,
which can only clear a mark, over `accepted`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r passed (#646)

The Multiply call builders took any parsed Ekubo quote, and the parser is
exported, so a future caller could have built calls whose swap cap the
anchor never checked. The anchor now records each quote it passes, and both
builders refuse any other, including a copy spread into the anchored shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tences (#646)

The price anchor's two refusals reused the quote path's developer text
("Ekubo Multiply route amount mismatch", "Could not quote Vesu wind route").
They now carry the copy Derek approved on 2026-10-13, and both display points
show it: the lending screens at quote time and the wallet request dialog at
handoff, where it is not mistaken for a wallet decline. New tests also pin
that a refusal while quoting builds no calls, never reaches requestTransaction,
and that the next attempt quotes again rather than reusing the refused quote.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two contracts that share a ticker (native USDC and bridged USDC.e both
report "USDC" in older metadata) showed as two identical "USDC" options.
Where labels collide, each address-keyed option now names its token with the
UI's short address, "USDC · 0x053c…68a8". Unique labels and older
symbol-keyed rows read as before, and the order and keys are unchanged. Copy
approved by Derek, 2026-10-13.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The panel's translation showed any unmapped capitalised message as written,
so background diagnostics such as "Invalid Ledger account index", "Krusty KMS
returned invalid class hash JSON" and "Unlock session changed during vault
creation" reached readers verbatim.

friendly-error-internal draws the line explicitly:
- FACT_KEPT: diagnostics reviewed as security or cryptographic facts (keys,
  signatures, hashes, commitments, chain ids, pinned classes). Never collapsed.
- INTERNAL_DIAGNOSTICS: plumbing that mentions a fact word (a KMS parse
  failure is not a failed check). Always collapsed.
- INTERNAL_FAMILIES: the prefixes our plumbing uses, for unreviewed strings.
  Collapsed only when FACT_TERMS finds no fact, so an unreviewed failed check
  is shown as written rather than hidden.

friendlyError takes an optional context sentence that replaces the generic
fallback, so a screen can say which action failed. Ledger rejection codes now
read as the device catalog's own sentence instead of "Something went wrong",
and a lowercase diagnostic naming a failed check keeps its words.

No new sentences: the reachable, actionable strings map to sentences the
wallet already uses (import phrase/key, unrecognised release, unsafe swap
route), now single-sourced in friendly-error-shared. The revoke refusal that
names an unconfirmed token is shown as written until reviewed copy replaces it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y blockers (#652)

Toasts translated their raw diagnostic, but several status lines, the Ledger
account picker's HD path, the transfer review line and the privacy "service"
blocker printed error.message as written, including lowercase diagnostics.

- errorLine now passes the caller's (translated) fallback as context, so a
  plumbing failure names the action that failed.
- Status lines in onboarding, recovery, key import, the token catalog, the
  multisig contact form, deploy quotes, the transfer review and private
  send/disclosure use errorLine instead of the raw message.
- The Ledger picker translates both add paths with "Couldn't add that
  account" as context; its unreviewed "Could not add the account." fallback is
  gone.
- A privacy "service" blocker wraps whatever the lane threw, so its message
  is translated (network failures keep their existing sentence).
- A determinate transfer failure says "not sent" followed by a translated
  reason, never the "may not have gone through" sentence of its fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tests/conformance/internal-error-copy reads every message the panel's sources
throw (new …Error("…"), RefusedBeforeWrite, and the guard prefixes passed
through helpers) and runs it through the real classifier. Each must be
collapsed (plumbing never comes out verbatim), a fact (never the generic
fallback; reviewed facts come out as written), or listed as a reviewed reader
sentence, guarded fact, or pending-copy string. A new thrown message fails
until someone decides which.

The issue's named strings are pinned as collapsed; the Ledger picker tests pin
both add paths: vault and KMS plumbing reads "Couldn't add that account", a
bad device key keeps its fact.

error-copy-coverage scanned replies only up to 120 characters, which is how
the 170-character revoke refusal escaped it; the limit is now 400, and the one
long curated swap sentence it then found is recorded as reviewed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… facts (#652)

The lending, swap, privacy and NFT screens and the wallet connect error
printed cause.message, so diagnostics such as "Could not quote Vesu wind
route" or "Invalid AVNU build response" reached the reader.

userErrorMessage (protocols/support/user-error.ts) now decides: reviewed
copy first (empty table, so a future sentence is a one-line change);
transport errors collapse; a message naming a security or cryptographic
fact (mismatch, substituted route, proof, hash, unregistered contract, a
failed verification, an unanchored Multiply route) is always kept as
written; the terminal's own plumbing families collapse to the screen's
existing fallback sentence; anything else, including errors raised by
the connected wallet or a Ledger behind it, passes through unchanged.

No new user-facing sentences: collapsed messages reuse each screen's
existing fallback. A conformance test scans every thrown literal in
apps/terminal/src and fails on plumbing that would show, a fact that
would be hidden, or a new message nobody classified.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cline (#652)

walletRequestError read any message containing reject/refus/cancel/denied
as the reader declining in the wallet. A WalletError is the terminal's
own (a handoff re-check, a price-anchor refusal) and cannot come from the
wallet, so it now skips the decline match and goes through
userErrorMessage, which keeps facts and hides plumbing. Wallet errors keep
the existing decline mapping.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Data banners read "Couldn't load X: <error>", and an HttpError message
carries the request URL and a response body prefix (ResponseTooLargeError
the URL and byte limit). The shared toMessage helpers now go through
userErrorMessage, so transport detail and plumbing collapse to the
existing "Request failed" / "Extended request failed" text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… plumbing a decline (#652)

"AVNU returned no route for this pair" and "Ekubo returned no Multiply
liquidity" tell the reader to change the pair or size, so they are no longer
collapsed (only "returned invalid …" parse failures are). The swap execute
step collapsed plumbing to "Wallet rejected the swap.", which claims a decline
that did not happen; it now uses the terminal's existing neutral sentence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rce (#69)

Order placement on the Perps screen is off everywhere (Decision A). The
banner and the ticket each carried their own copy of the reason, and both
said placement "isn't available in your region", which reads as a regional
lock rather than a feature that is off for everyone.

Move the decision and its reason into perps-placement.ts, record why
Extended differs from the on-chain protocols, and render the banner and the
ticket from that one constant, so correcting the wording is a one-line
change. The wording itself is unchanged here; the replacement sentence
needs copy review.

Tests: the banner, ticket note and CTA title all show the recorded reason;
driving every ticket control sends only body-less GETs and no socket
frames; and a source guard fails if any terminal source gains an Extended
account-API path, an API-key header or an order signer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mergeActivity promoted a pending wallet row to accepted when the event scan
found a Transfer carrying its hash, on the claim that "a reverted transaction
emits none". That is wrong: a reverted Starknet transaction still pays its fee,
and the fee payment emits a Transfer from the account that the scan cannot
tell apart from a value transfer. A reverted send could read as accepted until
the status re-poll caught up.

Excluding the fee leg instead would need the block's sequencer address and the
receipt's actual_fee, which the scan does not have, and would still be a
heuristic. The status re-poll already reads the receipt's execution status
every minute for an hour, then keeps observing to L1 finality; an observation
that never resolves is retired with its "could not confirm" notice, which is
truthful. So no discovered row settles a local row's status any more, from
either lane, and the lane parameter that existed only for promotion is removed
from the merge. The lane stays on discovery results as provenance.

Tests: a reverted transaction whose only event is its fee Transfer (decoded
from a real-shaped event) stays pending/preconfirmed; a value Transfer does not
settle either; the background refresh leaves the row pending for both lanes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
)

AVNU fell back to 10 bps and Ekubo to 50 bps when a caller passed no
tolerance, each from its own constant marked "confirm", and the swap ticket
started from a third, inline 50. 10 bps is tight enough that ordinary
movement between quote and execution often reverts an AVNU swap.

Single-source the default in protocols/support/slippage.ts (50 bps, the
recorded owner choice) and read it from both venues and the ticket's
initial setting. The ticket already shows the tolerance as "Max slippage"
in the quote details and in the wallet review details, so no copy changes.
The private-swap limit (shared/privacy.ts) is a separate fixed cap that
the server also enforces, and is left alone.

Tests: an AVNU quote with no tolerance asks the build API for 0.5%
(fails on the old 10 bps default), and an Ekubo build with no tolerance
floors clear_minimum at 50 bps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1% key (#153)

The Ekubo builder carried VERIFY markers on the sqrt-ratio bounds and the
fee-tier table. Both are now checked against Ekubo's own source and
mainnet state, with the source cited at each constant.

- Sqrt-ratio bounds: match constants::MIN_SQRT_RATIO / MAX_SQRT_RATIO in
  EkuboProtocol/starknet-contracts src/math/ticks.cairo, unchanged from
  v1.0.0 to v5.1.0 (66dc73ab). Core's swap accepts a limit within both
  bounds inclusive, so the clamp never makes a swap revert. No value change.
- Fee tiers: 0.01%, 0.05% and 0.30% key initialised ETH/USDC, STRK/USDC
  and STRK/ETH pools on mainnet Core (get_pool_price at block 16329354),
  as Ekubo's pair-pools API also lists. The 1.00% tier did not: it used
  the double-precision BigInt(0.01 * 2 ** 128), while the real 1% pools
  were created with the exact floor(2^128 / 100). Ekubo quotes therefore
  never considered a 1% pool. Use the on-chain value.

Tests pin all four (label, fee, tick spacing) triples and the exact 1%
fee (both fail on the old table). check:api gains a live check that every
tier keys an initialised ETH/USDC pool on mainnet Core.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
strkd derives agent accounts, which AI agents may drive under a permission
grant, from the same phrase on hardened account index 0x41474E54 ("AGNT"), and
keeps them there so a mainstream wallet never surfaces them. Bramble allowed a
manual index up to 0x7FFFFFFF, so a user could land on that branch.

RESERVED_AGENT_ACCOUNT_INDEX sits beside RECOVERY_SCAN_LIMITS and the shared
isRecoveryAccountIndex refuses it. That one validator already guards the manual
discovery index (first-run and settings), restore and import selections, and
the create/preview accountIndex, so every place a user can choose an index
refuses it; the panel's manual search refuses it before any scan with the
existing "Enter a valid non-negative account index" toast. The sequential
recovery walk is capped at maxScannedIndexes (200), pinned by a test, and also
skips the index explicitly in case either bound moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
derek-flossman and others added 21 commits October 17, 2026 16:12
… revoke, dapp invoke and declare (#719)

An undeployed account deploys itself the first time it does anything on chain.
#716 recorded that deployment in Activity for token sends only; every other
action that can trigger it recorded just its own row, so the deployment and
the fee it paid were missing from Activity and CSV exports.

Each of those handlers now records the accepted deployment the same way the
transfer handler does: from deploymentTransactionHash on success, and from the
failure (failedSubmissionDeployment) when the action after it is refused or
loses its reply, each inside afterBroadcast so the reply is never changed. A
lost reply stays status unknown. The declaration path tags its own
post-deployment failures through the new markDeploymentFailure and returns
its deployment hash. The dapp-invoke failure helpers move into
provider-invoke-failure.ts to keep provider-ops.ts under the line cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex review of #722 noted that a declaration whose reply was lost (the
deployment before it accepted or not) answered the site with a plain error, so
the site could not tell an uncertain submission from a refusal and might
re-declare. It now returns the typed status-unknown reply, as the dapp invoke
path does; a plain refusal still reads as failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…vity

activity: record the automatic account deployment for NFT send, swap, revoke, dapp invoke and declare (#719)
send: Max on STRK leaves room for the network fee (#720)
…clear

recovery: add Clear, confirm a valid phrase, and fill empty account groups (#718)
Add a [0.10.7] section covering every change merged into release/v0.10.7,
folding in the two lines that were under [Unreleased]. The release workflow
stamps the date on promotion and uses this section as the GitHub Release body.

Move v0.10.3 and v0.10.4 into CHANGELOG_ARCHIVE_0.9.md so CHANGELOG.md stays
under the 500-line file standard.

Refs #691

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs(changelog): curate the v0.10.7 release notes
Brings the mobile branch up to main (812 commits since 22 Sep). The copy
review this branch had merged directly landed on main squashed (#475, #489),
so conflicting hunks take main's side; changes outside conflicts are kept.

Mobile-specific resolutions:
- vault: re-export KDF_ITERATIONS from encrypted-store, which main split
  into store/envelope-shape; the value and accepted legacy counts are unchanged
- multisig limits, recovery-row helpers and toast timings stay exported for
  the mobile generators, over main's logic
- generators follow main's API moves (passphrase-strength, privacy failure
  state, allowance listing, account networkId, deploy failure scraper, the
  onboarding account row's elevation selector)

Regenerated mobile assets and parity fixtures. Derivation, signature and
vault-envelope vectors are unchanged. Known follow-ups, failing until mobile
catches up with main's behaviour: the recovery scan cap (maxScannedIndexes),
the dictionary-aware passphrase meter, stricter relay URLs, the Argent
refusal wording, portfolio/activity parity, and nostr-relay.test.ts over the
500-line limit.
…kd's index

main replaced the fixed 0-19 recovery window with a gap limit (b5c5398):
the scan reaches defaultCount past the highest index with on-chain evidence,
capped at maxScannedIndexes. The core refused the new catalog field and kept
the fixed window, so an account at index 21 behind one at 18 was missed.

scan_target mirrors recoveryScanTarget and bounds each batch, completion and
the progress line. The catalog now carries RESERVED_AGENT_ACCOUNT_INDEX (#336)
and one is_usable_account_index refuses it for manual search, restore
selections, record creation and the next free index, as isRecoveryAccountIndex
does in the extension.
…es (#505)

Mobile derived every account at m/44'/9004'/i'/0/0. Ready (Argent X) roots its
stark tree in the Ethereum key at m/44'/60'/0'/0/0, and Braavos steps the
address field (m/44'/9004'/0'/0/i), so a real Ready phrase found no accounts
and a Braavos phrase only its first. The extension fixed this in d9edd3a.

Every key now goes through kms::private_key with a Ladder: Account (this
wallet's), Address (Braavos) or ArgentX (Ready, via Krusty's
derive_argent_legacy_private_key, never under a BIP-39 passphrase). Each
vault record stores its ladder; signing, the STRK20 viewing key, network
projection and re-derivation on open all use it.

Vault 4 -> 5 labels every existing record Account, which is how each was
derived, so no record changes address. That includes a Braavos record an
earlier scan found off index 0 on this wallet's ladder: it stays where it is
rather than moving to Braavos's ladder, where that index is another account.
The Keychain and Keystore entries, the DEK and the password slot are
unchanged; the v1->v5 golden differs only in its payload.

The scan walks recoveryScanShapes: OZ and Argent 0.4.0 on this wallet's
ladder, every Argent release on Ready's, every Braavos class on Braavos's.
Selections carry the candidate's ladder (default: the family's vendor
ladder), selection keys include the derivation kind, a vendor coordinate is
never offered for creation, and nextAccountIndex counts only this wallet's
tree. The parity fixture gains 18 Ready vectors from the extension's own
deriveAccount; all 46 derivation vectors match.

Swift's KeyPath is a stdlib type, so the enum is Ladder. The selection field
is last so Kotlin's positional constructors keep their meaning.
The merge took nostr-relay.test.ts to 509 lines, over the 500-line budget
that bun run check enforces. The Sherlock 129 paging block moves to its own
file and both share the scripted relay sockets from tests/support.
main dropped the class label from the extension's unsupported-class refusal;
the generated copy already followed, so only the two app tests still expected
"(argent 0.3.1)".
main's isNostrRelayUrl accepts a relay only as its WHATWG serialization
prints it, so WSS://relay.example is now refused. Mobile accepted any case
in the scheme, which made it looser than the extension. It now refuses
every spelling the parser would rewrite: an upper-case scheme or host, the
default or a zero-padded port, a dot segment, a query without a path, a
quote in the query, and an IP literal not in its plain form. Each refused
case was checked against the extension's own validator.

An earlier build may have stored such a spelling, and the coordination file
fails closed on an invalid relay, which would stop every multisig's sync.
Reading the file now brings stored URLs to the current rule, as the
extension's normalizeStoredWebSocketServiceUrl does: case and the default
port are fixed, a custom relay with no current spelling falls back to the
reviewed defaults, and such a checkpoint is dropped so the account resyncs.
main's setup meter reads a common password in disguise, a repeated chunk or
a keyboard run as weak (guessable.ts, Sherlock 152), so "Password1" and
"p@ssw0rd2024" no longer read fair or strong. The mobile meter still
credited them as random.

The word list is generated from the extension's own COMMON_WORDS into
mobile/core/generated/common-passwords.txt by mobile:assets, so the
mobile-generated drift test fails if the two lists part. is_guessable ports
the check: the repeat test over UTF-16 units like the regex, each edge and
leet reading, and both readings of "1". The parity vectors gain ten
passphrases that exercise every branch; all 25 match the extension.
#645)

main admits one lowercase .e suffix in the shared symbol grammar, so the
curated USDC.e is accepted, and normalises chain metadata so ETH.e is not
refused as ETH.E (a4ef27f). Its spoof check compares each ticker in its
full and base fold, so ETH.e still reads as a spoof of ETH, and the two
curated USDC contracts no longer flag each other (95f7af5).

Mobile refused USDC.e and kept the one-fold check, which would have let the
widened grammar through without its guard. Both halves are ported, and the
generated spam vectors gain the #645 cases, checked against the extension's
own tokenSpamReasons.
main's hero reads "Portfolio price · <percent>% · 24h" and each token row
tags its percentage " · 24h price" (cb37c70), so neither reads as a change
in holdings. The badge scraper also stopped at the new nested span and
recorded nothing; it now reads the badge's whole text.
… off mainnet

Three price-cache fixes on main, ported together:

- f5fddf7 (mc-wallet #357): freshness was one cache-wide time, so any
  token's refresh made a months-old price read as current. Each address now
  carries its own refresh time; a price is fresh only when every wanted
  address answered within the TTL, and asOf is the oldest contributing
  capture. A cache written before this has no per-address times and reads
  as not fresh.
- b78b050: an address the feed answers without a usable price ({} or one
  currency) loses its cached price and change, so a refresh never vouches
  for a price the feed no longer stands behind.
- 663438a (Sherlock 185): off mainnet there is no fiat by design. The
  cache holds only mainnet prices and ETH/STRK share addresses across
  networks, so Sepolia was showing mainnet USD values. Prices, changes and
  charts are now empty there.

The replayed getPrices timelines gain a "dropped" case for b78b050; every
timeline matches the extension's own createPriceService.
…the hint

The copy review moved a row's transaction hash and its copy control out of
the summary into the expansion's "Transaction" fact (93826d2); the row
scraper still read the summary, so it recorded none. It now reads the hash
row on its own and the counterparty's copy control from the remaining facts.

The pre-confirmed hint follows the extension's shorter wording.
@Ryun1
Ryun1 marked this pull request as ready for review October 28, 2026 12:13
@cursor

cursor Bot commented Oct 28, 2026

Copy link
Copy Markdown

This PR is too large for Bugbot to review. It changes 48,338 lines and 3,802,456 characters. Split the change into smaller pull requests to get a review.

@Ryun1
Ryun1 merged commit 5db8237 into feature/mobile-wallet Oct 28, 2026
5 of 7 checks passed
@Ryun1
Ryun1 deleted the mobile/merge-main-505 branch October 28, 2026 12:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants