Repository navigation
mobile: merge main, recovery gap limit, and each family's key ladder (#505) - #794
Merged
Merged
Conversation
…-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>
… 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
Release v0.10.7
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
marked this pull request as ready for review
October 28, 2026 12:13
|
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
requested review from
l-henri,
robertkodra and
teddyjfpender
as code owners
October 28, 2026 12:13
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.
Refs #450, #505
Brings
feature/mobile-walletup tomainand ports the two recovery changes that broke the mobile core in the process. Three commits, best reviewed one at a time.Commits
7203f48eMerge main into feature/mobile-wallet (812 commits since 22 Sep). The copy review this branch had merged directly landed onmainsquashed (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 takemain's side and changes outside conflicts are kept. Mobile-specific resolutions:encrypted-storere-exportsKDF_ITERATIONSfromstore/envelope-shape(value and legacy counts unchanged), the multisig limits and recovery-row helpers stay exported for the generators, and the generators followmain's API moves. Derivation, signature and vault-envelope vectors regenerate unchanged.4f0e71b9Recovery gap limit and strkd's reserved index. The core walksrecoveryScanTarget(20 past the last active index, capped atmaxScannedIndexes) instead of a fixed 0-19 window, and refusesRESERVED_AGENT_ACCOUNT_INDEX(Reserve account index 0x41474E54 for strkd agent accounts #336) for manual search, restore, creation and the next free index.ecc8a8c3Each family on its own key ladder (Seed import finds zero Ready (Argent X) software accounts — wrong master seed, wrong axis #505).kms::private_keywith aLadder:Account(m/44'/9004'/i'/0/0),Address(Braavos,m/44'/9004'/0'/0/i),ArgentX(Ready: the ETH key atm/44'/60'/0'/0/0seedsm/44'/9004'/0'/0/i; refused under a BIP-39 passphrase).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.recoveryScanShapes; candidates carry their ladder into restore (iOS and Android drivers updated); vendor coordinates are never offered for creation;nextAccountIndexcounts only this wallet's tree.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.etickers 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 thenostr-relay.test.tssplit.Known failures (accepted for now)
corehas 6, each a deliberate follow-up:history_parity(4):main's ActivityEntry gaineddirection,tokenAddress,feePaid,originandseenOnChain.directionmatters: 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:mainadded 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
RecoveryScanDriver.swift,AccountsBridgeTests.swift) are uncompiled; this PR'siosjob is the first build.Test plan
cargo test --locked --no-fail-fastinmobile/core: 6 fail, all listed above;cargo clippy --all-targets -D warningsclean:app:assembleDebug,:app:compileDebugAndroidTestKotlin;:app:testDebugUnitTest51/52 (above);mobile/scripts/lint.sh kotlincleanbun run check:typesclean;mobile:assetsandmobile:parityregenerate with no further diffBrambleTests(CI)