Repository navigation
Bramble mobile: iOS and Android wallets on a Rust krusty-kms core - #450
Draft
teddyjfpender wants to merge 170 commits into
Draft
teddyjfpender wants to merge 170 commits into
teddyjfpender wants to merge 170 commits into
Conversation
Bramble registered with autoSetup off, so activation published the viewing-key commitment but never opened the account's channel to itself. The pinned SDK adds that channel implicitly on the first batch that needs it and warns (USER_LINKAGE) when a single batch opens more than one channel; the wallet refuses on any warning. So an account whose first shield named another wallet had to open two channels at once and always failed, before proving, with the bare "strk20 privacy leak". Confirmed on mainnet on 2026-09-22 against the gateway logs: registration at 09:10 UTC, then two shield attempts that ran channel discovery and never reached the prover. Turn autoSetup on for the registration builder so the self channel opens alongside the viewing-key commitment, say so in the review copy, the dapp approval summary, and the docs, and pin the option with a test. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The multicall, invoke, Ledger, recovery, and NFT checks now fail when the behavior they describe is missing. A stuck onboarding view transition no longer covers the page, which was timing those checks out in headless Chromium. Fixes #410 Co-authored-by: Cursor <cursoragent@cursor.com>
UI Lab typechecks this module with the DOM timer, which returns a number, while the annotated handle was Node's Timeout. Use the same setTimeout the annotation names. Co-authored-by: Cursor <cursoragent@cursor.com>
A session of UI work from user feedback. It shares one thread: the wallet asked people to leave a screen, or to read a control that did not look like one. **One token dropdown, opened in place.** Choosing a token left the screen twice over — Send pushed a whole stage, Swap opened a modal sheet — and the two lists drifted. Both now open the same dropdown under their trigger: tickers in labelled groups, what the account holds first, then the rest of AVNU's verified catalog, with a search on the catalog group and a "Manage tokens" row for a token it does not carry. Choosing an untracked entry still goes through the wallet's add-token path, which reads the token's facts from the CONTRACT, so the catalog stays a directory and never a source of truth. Send's asset stage and Swap's sheet, chips and 59 lines of sheet styling are gone. **The AVNU list is reachable from the tokens screen** as its own screen, where each entry offers Add, Hide or Show against the wallet's own registry. **Send's recipient screen** carries the step rail (Recipient, Amount, Confirm), stacks the address book above My accounts instead of tabbing between them, and offers "Save address" for an address that is neither yours nor saved — which hands it to the address book, prefilled. **The public/private control is a switch**, not a word that renamed itself to the state it was already in, which is what made it read as static text. The switch is a shared ui-kit control, so the address book's new "Pro" toggle is the same component. **The address book asks for two things**: a label and an address, with everything else behind that Pro toggle. It also canonicalises the address before saving — the background takes lowercase hex only, so an address pasted as 0x04F2… was refused as a malformed request even though Send had just accepted that exact text. Also: the three-dots menu drops "Wallet settings", which the dock already carries, and the empty address book no longer states that it is empty. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The UI Lab checks failed on four stories after the token-picker rework: - the production-view coverage contract had no scenario for the new token-catalog view; - the address book story asserted the wallet card and key fields as visible, but they now wait behind the form's Pro switch, and the lab rendered the form without its binding, so the switch did nothing; - the swap scenario still picked ETH through the old picker's data-swap-pick-address attribute, so the pick silently stopped and the fixed fixture quote rendered with its labels swapped, which the frozen review story caught as "19,260 ETH"; - the swap keyboard contract looked for the old modal sheet and typed Escape at the host document rather than into the frame. Add the token-catalog checkpoint and story, bind the production address book form in its checkpoint and make the story open Pro first, pick ETH through the dropdown option, and rewrite the keyboard contract for the dropdown: focus enters the panel on open, Escape closes it and returns focus to its trigger. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Four review findings on the token-picker rework: - A token picked from the dropdown wrote the row's public figures into the amount card without re-measuring the source bucket, because the bucket binding only followed clicks on the hidden fact rows. Follow dropdown options too, so a private send keeps its private balance after the asset changes. - The AVNU catalog kept the first screen's paint and applied an in-flight result regardless of network, so a shell refresh painted a detached element and a network switch could pin another network's list. Paint the live screen, drop a result from a superseded request or the network the user left, and clear the loading flag on a network change. - While the list loaded, an empty catalog read as a search miss. An empty list is loading or failed, and the status line says which. - Every dropdown binding added an outside-click listener to the document and never removed it; Swap rebinds on every paint. Keep one listener per panel id per document, replace it on rebind, and let a listener whose panel left the document remove itself. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A shell refresh or a network change while AVNU's list was in flight painted the first panel, or pinned the previous network's tokens, because the fetch never dropped its loading flag or its captured root. Co-authored-by: Adrien Lacombe <adrienlacombe@users.noreply.github.com>
Catalog search compared the ticker to itself and left hits in list order, so a token name or a pasted contract reported nothing and an exact symbol did not lead its prefixes. Co-authored-by: Adrien Lacombe <adrienlacombe@users.noreply.github.com>
The Send address was cleared on the first bind, so the next shell paint rebuilt an empty form and dropped the address and any label already typed. Co-authored-by: Adrien Lacombe <adrienlacombe@users.noreply.github.com>
A ticker letter sits inside nearly every contract, so substring matching on the address kept the whole catalog visible. A pasted contract still matches with different padding, and a 0x prefix still narrows to that start. Co-authored-by: Adrien Lacombe <adrienlacombe@users.noreply.github.com>
Receiving needs one thing: the address, as a QR and as text. The screen asked a user to read past a payload switch, a card explainer, two copy buttons and a wallet-identity disclosure to find it, and the copy control sat apart from the address it copied. The address IS the copy control now: one wide row carrying the exact canonical string, with a clipboard mark on the right saying so. The QR sits above it and the network below, because sending on the wrong network loses funds. Everything else — the Address / contact-card switch, the card explainer, Copy wallet card, and the signer and coordination keys — moves behind a Pro switch that starts off, the same control the address book uses. The fields stay in the DOM, so the card's derivation and its failure path are unchanged. A multisig receive screen has no card or keys, so it has no switch and keeps its hint. Receive takes its own stylesheet with the rules it already owned: the send layer was at the line cap and could not hold the new ones. Refs #403. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…istive tech Two review findings on the Receive rework, plus the lab story it broke: - Closing Pro hid the payload switch but left the QR on whatever payload was last chosen, so a user who picked the contact card and then turned Pro off saw a card QR under "Your Starknet address" with no control to change it, and other wallets cannot scan that payload. Closing Pro now puts the address back on the QR. - The address lived only inside the copy button, whose aria-label was the copy prompt, so assistive tech heard "Copy address" and never the address. The address element is now the button's description. - The receive story asserted the old identity disclosure; it now opens Pro and asserts the switch, the keys, and the card behind it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
"Add a new account" listed "Restore a Ready Ledger account" beside the routes that create one. Bringing over an account from Ready is importing something the user already owns, which is exactly what the Import screen is for — it sat one level too high, and made the create list read as five unrelated options. It moves in beside the recovery phrase and the private key, renamed to "Ready Ledger account" now that its screen already says these are existing accounts. The plain Ledger route stays on the chooser above, because it also creates accounts on Bramble's own derivation path. Routing is unchanged: both screens bind the same tile handler, so the tile reaches the same connect gesture from its new home. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A first send from an undeployed account is two transactions, and the review quoted them as two lines: "Estimated transaction fee" and "Estimated account deployment fee". The first read as the cost of sending, so the figure a user acted on was the smaller half of what left their account. The review now leads with the total and names the deployment share under it. The background already computed that total for the affordability check; it is what the user pays, so it is what the review shows. The account being created is still stated — hiding it would trade one wrong impression for another. A Ledger signs both transactions, so the review says the device will ask twice rather than leaving the second prompt as a surprise after the confirm press. That cannot change without a relayer, and Ready does not use one either. Checked on Mainnet against a fresh Ready account: DEPLOY_ACCOUNT and INVOKE, both sent by the account, 0.028335 + 0.031929 STRK, same block. Ready's advantage was never a bundle — it was one confirmation and one figure. Refs #381. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Home flagged an account that is not on chain yet with a danger-coloured chip and an alert icon, as though something had gone wrong. Nothing has: the account works, it receives funds, and its first send creates it — which the review now quotes as one fee. The chip keeps the shortcut for a user who would rather deploy first, in the neutral voice, and its tooltip says what actually happens instead of implying a blocked account. Refs #381. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The file is at the 500-line cap on its own, and gains lines from both this branch and #415; the close cases are a coherent unit and move whole. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Home flagged an account that is not on chain yet with a chip reading "Deploy account", or "Fund with STRK" when it could not pay. Neither is a thing the user has to do any more: the first send creates the account and the review quotes both fees as one figure, so the chip announced a chore that no longer exists. It is gone from Home. Deploying first is still a choice worth keeping, so it moves to Settings → Security & permissions, under "This account", with a line saying what happens if you do nothing. The dedicated deployment flow behind it is unchanged. The Shield tile keeps its own "Fund with STRK" state: shielding really does need STRK, whatever the account's deployment state. Refs #381. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rity hub Two review findings on the one-fee change: - The NFT review built the same DTO as the ERC-20 review and fed the same fee renderer, but never set `signsTwice`, so a Ledger first-send of an NFT hid that the device prompts twice. Set it from the same rule, type the field on the shared review response, and cover an undeployed Ledger account in the NFT message tests. - The Deploy account row moved to Security & permissions, but nothing in the wallet linked to that screen, so removing the Home chip left the deploy-first journey unreachable. The Settings hub now links to Security & permissions. The review response types move to runtime-review-types.ts to keep the runtime types file under the 500-line cap. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
sumFeeEstimates fails closed on unlike units. The review was still quoting the transaction fee as the estimated total and calling the deployment fee a share of it. Co-authored-by: Adrien Lacombe <adrienlacombe@users.noreply.github.com>
The failure copy was on screen, but the reviewed key stayed set because neither estimate was flagged failed. Retry then submitted the send, and Confirm stayed enabled. That review is now treated as blocked, same as a missing component, on Send and on NFT transfer. Co-authored-by: Adrien Lacombe <adrienlacombe@users.noreply.github.com>
The drawer listed every account in one flat list with a per-row folder picker, so the folder an account was in was something you opened a panel to discover rather than something you could see. Accounts are now listed under the folder they belong to, and moved by dragging a row onto another folder. Two folders are structural and exist in every wallet: "My accounts", where an account lives until it is filed somewhere else, and "Archived accounts", which starts empty, always sorts last, and starts collapsed. An account in no folder is shown under the home folder, so a new account appears there without anything being written; filing it elsewhere is what writes. Neither can be deleted — only renamed and re-iconed, on the manage view where the rest of folder management already lives. Both default folders are seeded on the write path as well as the read path. The pure setters no-op on a group id they cannot find, so without that a wallet which had never stored a group would accept a drop onto "My accounts" and silently do nothing. Seeding the written list too also keeps a newly created folder stored in display order rather than after the archive. "Add folder" sits under the last folder and creates one in place, so the whole flow stays in the drawer. Drag and drop is the only way to move an account; moving one by keyboard is the gap left on this screen, noted in the binding module. The filter chips, the per-row quick picker and the assign panels they opened are gone, along with the transient state that survived repaints for them — TransientGroupState is now just the manage view's expanded group. The group writes moved out of preferences.ts, which the split put back under the file-size cap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The drawer always listed My accounts and Archived accounts, but the manage screen read stored groups raw, so a wallet that had never saved a folder could not rename them. Creating a folder also opened the archive, because the client treated the last stored group as the one just created after the write path started sorting the archive last. Default folders are seeded for that screen the same way as the drawer, they have no delete control, and the created folder is the last group that is not a default. Co-authored-by: Adrien Lacombe <adrienlacombe@users.noreply.github.com>
A balance repaint emptied the inline folder name and pulled focus back onto that field. The sheet-dismiss gesture also captured pointer moves that start on an account row, so a filing drag closed the drawer. Co-authored-by: Adrien Lacombe <adrienlacombe@users.noreply.github.com>
A drop into a collapsed folder hid the account that just moved. The manage view also counted only stored ids, so an account the drawer shows under My accounts looked absent, and clearing that row did not move it. Co-authored-by: Adrien Lacombe <adrienlacombe@users.noreply.github.com>
The file-standards gate scans every module outside the vault boundary for secret-material words, and "seed" is one of them; the doc comment on displayedAccountGroup used it in the ordinary sense of installing default groups. Say that instead so the gate reads the model as what it is: non-secret account grouping. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
dragleave on the folder section also fires when the pointer enters a heading or row inside it, so the highlight flashed off across every child. Co-authored-by: Adrien Lacombe <adrienlacombe@users.noreply.github.com>
A deleted folder's id stayed in the collapsed set, so a later folder that reused the same slug opened already closed. Co-authored-by: Adrien Lacombe <adrienlacombe@users.noreply.github.com>
Send listed every account of the user's own in one flat block, so an account filed into a folder was found in a different place depending on which screen you were looking at. The recipient list now groups accounts with the same split the drawer uses: My accounts, the user's folders, then the archive, with shared accounts after them. The archive renders minimized, like the drawer. A search reaches into it anyway: an address the user is looking for has to be findable wherever it is filed, so the stage would otherwise answer "No matching recipient" for an account the wallet holds. Clearing the search closes it again. A folder with nothing to show drops out while searching rather than leaving a heading over empty space. Folder state here is DOM-local. The recipient stage is toggled rather than re-rendered, so a collapse survives without threading state through the entrypoint the way the drawer has to. Also lower-cases avnu in every string the wallet shows: the swap header and route row, the token catalog screen, the "Pick tokens from the avnu list" entry, the swap build refusals, and the activity summary a swap records. The summary is read by the activity formatter, whose pair regex keys off "swap ... for ..." rather than the provider name, and no dedup guard matches it — only the STRK20 registration summary is matched that way. Rows already stored keep their original casing. Identifiers such as AVNU_BASE_URLS are unchanged; this is display copy. The recipient stage moved to its own module to stay under the file cap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A collapsed archive with no accounts still showed "No accounts in this folder." The folder mark stays chevronRight and rotates while the folder is open, so a toggle cannot leave the glyph pointing the way it did at render. Co-authored-by: Adrien Lacombe <adrienlacombe@users.noreply.github.com>
First journey of the wording review. The 49 user-facing strings in the settings journey were reviewed string by string against the new docs/COPY_STYLE.md; 19 changed and the rest were confirmed as they are. The feature was called "folder" in the account drawer and "group" in settings — the same thing under two names on adjacent screens. The UI now says folder throughout. The code keeps `group` (AccountGroups, DEFAULT_GROUP_IDS, the stored preferences.groups key) because renaming a persisted key needs a vault migration, which this is not; the doc comments that quote a UI label were updated so they stay accurate. "Tap to show" was a bug, not a register: this is a desktop extension driven by a click, and the reveal result line read "Revealed. Tap to show, then write it down offline." — announcing the secret was revealed and then telling you to reveal it. The long disclosure paragraphs were tightened without dropping disclosure. Each was rewritten from its list of facts and checked back against it, so the explorer, telemetry, clipboard, Ledger-recovery and backup-restore copy still say everything they said before in fewer words. "Helps us understand feature usage" was the only first-person plural in the extension and is now "Helps Bramble". docs/COPY_STYLE.md records the rules, most of which were already being followed, plus the terminology table and the two categories excluded from wording changes: activity summaries, which dedup guards match on, and the approval-review sentences, which are what a user reads before signing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The extension formats dates through Intl, and ICU builds disagree on one en-GB month: Bun on the Linux CI runner writes "21 Sept", Bun on macOS "21 Sep". The same code therefore produced a different fixture on each, and the drift test failed on CI against a fixture made on a Mac. The generator now pins the core's spelling, so the fixture depends only on the code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
outcomeReceiptFor treated any answer from the chain as success. A
ConfirmationOutcome is `succeeded` or `failed`, and rejected and reverted
both arrive as `failed` with a message, so a deployment or NFT transfer
the chain refused still turned its receipt into "Account deployed" or
"Transfer complete" over a hash that did not land.
The receipt now has Swap's three endings. Succeeded: the confirmed copy.
Failed: "Deployment failed" / "Transfer failed" over the chain's reason,
with the failure mark and tone. Out of patience: "submitted" and "Not
confirmed yet", as before. The receipt opens before the chain answers, so
it ships both marks and the paint shows one; the failure tone keeps the
single shrinkable column so the hash still ellipsizes.
As in Send and Swap, the toast is now the chain's answer, in its tone.
The handoff toast ("Deployment submitted", "Transfer submitted") is gone:
it said what the receipt heading says at the same moment.
The rejected message read "No transfer was completed", which is false
under "Deployment failed". It is shared by all four journeys, so it now
says what is true of each: "Nothing changed."
The NFT send suite went over the size limit with the receipt tests, so
its gallery-binding tests move, unchanged, to their own file, and the
send stub now returns a full-length hash.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The wasm-bindgen `--target web` init only returns early once an instance has finished loading. Two calls that overlap both instantiate the module, and the one that finishes last replaces the instance under every object the other made. Their getters and FinalizationRegistry frees then run against the wrong heap; the two instances hand out the same addresses, so a late free releases a live object and corrupts the allocator. That is the CI flake in the mobile parity suites: mobile-privacy-wire.ts starts an init when it is imported, the first parity test starts another, and on a slow runner the two overlap. A stackless "Out of bounds memory access" from a finalizer then lands in an unrelated test, and every later Krusty call traps in malloc. It hit 3 of 5 runs of 1af4c79; delaying the first instantiation by 50 ms reproduces the same six failures locally. createKrustyKms now shares one init promise per init function, so every KMS built from a module, or from a copy of its exports, uses one instance. A failed init fails every KMS built from it. The extension builds one KMS, so its behaviour is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
chore/copy-review changed the rejected outcome every journey shares from "No transfer was completed." to "Nothing changed.", which is true under a failed deployment as well as a failed send. The merge was clean in git, but the transaction fixture and the Swift and Kotlin copy tables it renders still held the old wording, so the mobile-generated drift gate failed. Regenerated with `bun run mobile:parity`; the transactions doc follows. Mobile's deploy flow already reports a rejected or reverted deployment as a failure rather than a deployed receipt, so no behavior changes here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
20 tasks
This was referenced Oct 1, 2026
This was referenced Oct 5, 2026
This was referenced Oct 12, 2026
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.
3 of 5 tasks
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.
mobile: merge main, recovery gap limit, and each family's key ladder (#505)
This branch has not been deployed
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
Refs #453
Bramble for iOS and Android. The native apps sit on one Rust core,
bramble-core(mobile/). It is built on the publishedkrusty-kmscrates, pinned=0.11.0: the same release the extension loads as WASM. The core owns key custody, the vault, and all Starknet protocol logic. It is sans-IO: Swift and Kotlin render UI and move bytes, and never hold a private key.mobile/SCOPE.mdcovers the architecture, security posture, phases, and known limits. Each workstream has a design doc undermobile/docs/.What's in it
bun run mobile:assets): tokens, derived colours, elevation, metrics, 59 icons, wordmark, token logos, networks. A drift test lives inbun run check. Dark by default, flat fields.FLAG_SECURE, no-backup vault.Deferred, and why
Documented decisions
Test plan
bun run check: types, lint, standards, pins, and all tests, including the mobile drift and parity conformance tests.cd mobile/core && cargo fmt --check && cargo clippy --all-targets --locked && cargo test --lockedTODO
bun run mobile:lintpasses: SwiftLint--strict, ktlint and detekt report zero findings, there are no suppressions, and the pinned tools are checksum-verified.7df68dda(90 unit tests with 2 opt-in live tests skipped, and 25 UI tests, 0 failures). Stale tests are updated to the merged app, and portfolio balance reads in UI tests now go to the offline fixture node instead of live Sepolia../gradlew :app:assembleDebug), and 40 JVM tests plus 36 instrumented tests pass on an API 35 emulator. The opt-in live test is skipped.pr-guardspasses. In.github/workflows/mobile.yml,core,lintandandroidwere green on the previous run. Theiosjob's only failures were two SwiftLint analyzer findings, which7df68ddafixes; the local analyzer re-run was cut short, so the CI run on7df68ddais the confirmation.argent-signer-arrayauthorization. They are proven bit-for-bit against the extension's own signing path: 7 scenarios and 9 refusals.OnboardingFlowTest.importMainnetThenLockUnlockwas flaky once, most likely a slow mainnet response.tx-observation.ts.backupPendingflag. After unlock, the app resumes at Save phrase and then Confirm backup.To try it:
mobile/scripts/build-core-ios.sh debug && cd mobile/ios && xcodegen generate && open Bramble.xcodeproj(seemobile/README.md).🤖 Generated with Claude Code