Repository navigation
Conversation
An Expo React Native app after Safe's mobile companion: open a vault by address, the demo vault, or one the device signer belongs to; Home with balances, mode, pending queue and policy at a glance; Transactions with queue and history and a detail screen that confirms, executes, vetoes or cancels by role; Send with the vault's own risk preview before signing; Security with posture, mode changes, guardians, maximum loss and mode history; Settings with vault label, signer management and a biometrics switch. Deep links open vaults and proposals. Signing uses a key created on the phone or imported, kept in the device keychain and gated by Face ID or Touch ID; every write is simulated against the vault first so refusals read as plain language. The web app's pure modules are shared rather than copied: contract reads move from the React hook into web/src/lib/vaultReads.ts, error texts into lib/errors.ts and posture checks into lib/posture.ts, and the mobile bundler resolves them through an @web alias. Finished proposals keep their historical confirmation counts.
… counts Found by driving the app on an iPhone 17 Pro simulator through idb: proposing a payment, opening it from the queue, confirming, cancelling. - Open the queue navigated to a tab name from the root stack, which React Navigation rejected; it now targets the Main tabs' Transactions screen. - The purpose was hex-encoded before proposing while the contract takes a plain string, so it displayed as hex; it is passed as text now. - Finished proposals keep their historical confirmation counts in the shared read instead of showing zero once live approvals stop being re-read.
…d app icon
Rebuilt every screen on Safe{Mobile}'s skeleton: splash with the logo,
three-page onboarding carousel with dots and one button, Home with the
vault switcher (identicon and approval-threshold badge, bottom sheet),
chain row, big balance, quick actions and Tokens, Policy and Members
tabs, Transactions grouped by date with a plus button, a Confirm
transaction screen with hero amount, To card, advanced details, Risk
review and Confirmations rows and a sticky Sign-with bar, Send with a
big amount field and the vault's Transaction checks card, Security and
Settings as grouped rows, three icon-only bottom tabs.
Skur's own pattern throughout: charcoal ground, #FFD000 as caution tape
and highlighter, Space Grotesk display type, the web logo as splash and
app icon (rendered from the same mark), identicons identical to the
web's. Compound controls carry explicit accessibility labels.
Verified on an iPhone 17 Pro simulator with idb: onboarding, vault
switcher, proposing a payment through the sign bar, confirming it,
expanding confirmations, cancelling it.
…creation, signers, notifications and QR Brings the phone app level with the web interface and with what a Safe mobile companion offers. New screens: Address book (register a recipient, propose a trust change), Members (roles, thresholds, add, change, remove), Policy editor (templates, every control grouped, security-reducing detection, per-asset limits with history), Simulator (the attack scenarios, live policy or any template), Create vault (name, template, members, one signature), Signers (several keys on one phone, which one signs), Updates (what needs your signature, recent activity, notification switch), and a QR scanner used by every address field. Also: local notifications when a proposal newly needs the active signer, recovery on the Security screen, Send accepts a preset asset, a known-recipient picker and QR, Transactions gains the type filter, tapping a token sends that asset, and the vault error screen offers a retry. Deep links now run through a navigation ref that queues links arriving before the navigator mounts, so a notification tapped on a cold start still opens its transaction; opening a different vault returns Home. The wizard's address rows chain focus with the Return key, since three address fields do not fit above the keyboard. The policy field definitions move into web/src/lib/policyFields.ts and are shared by both editors. app.json pins Expo modules to source builds: the precompiled frameworks expect a prebuilt React.framework and crash at launch when it is absent.
…of sprawl Onboarding follows the reference flow: a white ground, the wordmark, one illustration, a large headline, a short subtitle, page dots, and the same dark Get started button on every page. No skip link; the dots carry the progress. The dense screens become short lists. Security is six summary cards (posture, mode, guardians, recovery, maximum loss, history) that open on tap, each header carrying the number you would otherwise scroll for. Policy is the same: templates, one card per control group showing its current values, and history. Home keeps only what a phone home screen needs: the vault, what is pending, the balance, Send and Receive, and the token list. Policy and members moved to Settings, where they already had rows. Deep linking to a tab now pops whatever was pushed on top of it, and only dispatches the pop when something is actually pushed.
…read Three things made the app look like it was reloading constantly. The pull-to-refresh spinner was wired to the query's isFetching, so it appeared on its own every 20 seconds and four more times after every write. It now tracks only a refresh the reader actually started. The vault object was rebuilt on every render and carried a timestamp that changed on every successful read, so every screen re-rendered whether or not anything had changed. It keeps its identity while the data is unchanged. A proposal count change starts a new activity query, and the queue swapped itself for a loader until that returned. The last good queue now stays on screen while the next read is in flight. Measured on the simulator: 70 seconds on the queue with no loader and no row churn, where the spinner previously appeared three times.
…ng it Each page now carries one idea and one crafted illustration built from the app's own surfaces, rather than coloured blobs under three lines of copy. Page one is the vault card as the app draws it, balance and tokens, tilted with its assets orbiting. Page two is the same payment at three sizes with what the vault asks of each: one approval now, two and a day, two and a guardian. Page three is the notification that reaches you over the confirmation it opens, with the policy check answered. Headlines are two lines, subtitles one, and the wordmark, dots and button keep the reference rhythm.
…nstead of paragraphs The onboarding stood on white while the app stood on charcoal, and every screen carried explanatory prose a wallet has no business showing. Both are fixed together. Light and dark are now two palettes behind one set of tokens. Every component and screen reads its colours through useTheme and its styles through useStyles, so a single choice reaches the splash, the onboarding, the tab bar, the sheets and the navigator chrome. Settings gains Light, Dark and System, remembered across launches and following the phone by default. About 3,500 characters of explanation came out of the screens. Transactions, Members, Address book, Policy, Simulator, Updates, Signers, Security and Create vault lose their paragraphs; the hints that remain are a line each. The vault error screen shows one sentence rather than a viem stack trace, and role badges collapse to All roles instead of truncating. Tab deep links no longer warn when no vault is open.
The horizontal pager could come back on whichever page the view last showed. It now scrolls to the start on mount, so a first launch always begins at page one.
…ult screen The demo vault held one token and native KASH, so a portfolio had nothing to show. A parameterised devnet token joins the test USD, and a new demo vault holds sUSD, KASH, sEUR, sBTC and sETH with limits for each. Deployed and funded on the Ark devnet; the deployment record carries the new addresses. Each asset now has a mark rather than a letter in a circle: one colour and glyph per symbol, defined once in web/src/lib/tokens.ts and drawn the same way by the web app and the phone. The screen that opened a vault led with a marketing headline. It is now what a wallet shows: Add a vault, the vaults this signer belongs to, then the ways in as rows, with watching a vault behind a sheet. The Environment section leaves Settings; the network already appears where it matters.
The site described a mobile app nobody could see. It now shows one. A phone mockup component draws the app's own screens inside a device frame: the vault with its five assets, a payment being confirmed, and the queue. They are drawn rather than screenshotted, so they stay sharp at any size, follow the design system and cannot go stale against a screenshot. A Mobile page joins the nav with the three phones, what the app does on a phone, and how its keys work. The landing page gains a strip beside the FAQ that leads to it. Two fixes found while building it: a marketing page opened at whatever scroll position the previous one had, and the phone frames were scaled with a value calc() produced as a length, which scale() rejects, so they rendered full size and overlapped.
The first pass squeezed the app into a small frame: type shrank to eleven pixels, rows crowded, the tab bar was missing and the amounts clipped at the screen edge. The mockups are now authored at a real device size, a 393 by 852 point screen inside a 12 point bezel, and scaled as a unit, so every size is the app's own. They gain what makes a device read as one: the status bar, the Dynamic Island, the home indicator and the bottom tab bar with its pending badge. Content matches the app screen for screen, including the five assets with their marks and the sticky sign bar.
Every 'read the docs' link sent people to a markdown file on GitHub. The site now has its own documentation section. Fourteen pages under four groups: Start (introduction, quick start), Model (roles, risk tiers), Reference (policy, recipients, limits and the circuit breaker, security modes, changing the policy, recovery, the phone app, contracts and addresses) and Assurance (invariants and testing, known limitations). A sticky sidebar, breadcrumb, prev and next paging, and tables built for reading a rule rather than scanning prose. The policy reference and the template table are generated from the same definitions the editor uses, and the address table from the deployment record, so they cannot drift from what the contracts do. The validation rules are the contract's own. Docs, Security model and the in-app Docs entry now open this section. Known limitations are stated on their own page rather than buried in a decisions log.
Redirects the build from EVM to Sui, carrying over the engines a threshold product does not have. Move core (sui/): - types, policy and risk as separate auditable modules - the vault: seven proposal kinds through one lifecycle, requirements pinned at proposal and tighten-only at execution, approvals recounted against the live roster - cumulative velocity per asset, and a loss envelope measured against the balance when the window opened so a drain cannot shrink its denominator - the breaker refuses the payment and latches to Lockdown without reverting, because a latch that reverts is not a latch - a guardian that the contract refuses to give any treasury role - a proposer role for automation: can open a proposal, nothing else - creation as a PTB hot potato, so a half-configured vault is never reachable TypeScript (sdk/): - gRPC and GraphQL only; no JSON-RPC, directly or transitively - BCS layouts mirroring the Move structs, parsed from content rather than json - PTB builders for every action, with the policy built on chain by policy::new - the chain-agnostic engines ported to milliseconds: risk, policy rules, templates, maximum loss, posture and the attack simulator - 39 tests pinning the same vectors as the Move suite Backend (server/): - PostgreSQL projections rebuilt from events, idempotent on ledger position - notification rules addressed by role, so a guardian hears what only a guardian can stop - advisory services that can inform a signer and can never override the vault Docs: the competitive pass on what Sui has today, and the funding narrative.
The interface (app/): - Next.js on @mysten/dapp-kit-react 2.x over gRPC, because dapp-kit 1.x is JSON-RPC only and Sui is decommissioning it - every action is gated on the role the contract requires, so a signer is never offered something the vault would refuse - the payment review is the vault's own preview_transfer, simulated live, so the interface cannot promise what execution would reject - the policy editor shows what a change gives up before it is proposed, and runs the attack scenarios against the edited policy - creating a vault is one transaction: nothing exists until all of it is valid Backend: the notification rules are now a pure module with 14 tests. Who gets interrupted is a correctness question, so it is tested like one. Docs: the security model with the fifteen invariants and the test that proves each, and the migration note — including the one deliberate behavioural change, where the breaker no longer reverts away its own latch.
️✅ There are no secrets present in this pull request anymore.If these secrets were true positive and are still valid, we highly recommend you to revoke them. 🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request. |
…e interface Pins the Sui toolchain version, because the Move compiler and the bundled framework move together and a job that silently upgrades either cannot reproduce a failure. Also applies the migrations against a real Postgres: a migration that does not apply to an empty database is one nobody can deploy.
…assumptions The Bag reads and the string literals now use module-qualified calls instead of method sugar whose availability moves between toolchain versions. Same behaviour, one less thing to be surprised by on a different CLI.
The site was the last surface still pitching the EVM build. It is now part of the Next.js app, in two route groups so the marketing design and the vault interface never have to fight each other's base styles. The design the project already settled on is carried over as-is. The content is rewritten for where this actually sits: the comparison table is against native multisig, smart-account frameworks and offchain policy custody rather than 'a plain multisig', and the claims match what the Move code does — fifteen invariants, no audit, no mainnet. Fourteen documentation pages, including two the EVM build had no need for: automated proposers, and the Move package itself with a reading order for a review. The limitations page says what the design gives up, including the throughput ceiling a shared object implies.
…n SQL BCS is positional and silent: a field at the wrong offset reads the next field's bytes and produces a plausible wrong number rather than an error. These round-trips cannot prove the layout agrees with Move — only a deployment can — but they catch the mistakes that happen while editing the file, and they pin the fixed widths so a changed type shows up as a length failure. The advisory queries were comparing status to a literal 2. They now take the Status constant, and all three were run against a real Postgres to confirm they bind and parse.
…expire The lifecycle had approve, veto and cancel but no way for the signers who were asked to simply say no. Without it a bad proposal sits in the queue until an owner cancels it or its lifetime runs out. Rejecting takes the same role approving would, and it takes as many rejections as the proposal needed approvals — counted against live roles, like approvals. So rejecting is exactly as hard as approving: one signer cannot block the queue, and a proposal the team has turned down is settled the same day. Threaded through the BCS layout, the SDK, the indexer projections, the notification rules (which stay quiet until the rejection actually carries), the queue UI and the docs. Five Move tests, two TypeScript tests, and a migration that widens the vote-kind constraint.
Also floors the rejection threshold at one. A recovery pins zero approvals, because what it needs is guardian confirmations, so comparing rejections to it would have settled on an empty set.
The unit suites cannot prove the BCS layouts match the Move structs, that the PTB builders produce transactions the vault accepts, or that the events decode back into the shapes the indexer projects. A wrong field offset passes every unit test and reads a plausible wrong number off the chain. This creates a vault, funds it, previews a payment to an address the vault has never paid, registers a recipient, opens and approves a payment, proves the activation delay actually refuses an early execution, waits it out, executes, and checks the balance, the recipient's record and every event. It needs a published package, so it runs after publish rather than in CI.
Verified against sui 1.80.1. Six things the compiler and the test run found: - Two borrow conflicts in the setup functions: reading a field of `s.vault` inside the arguments of a call that mutably borrows it. The reads are hoisted. - `Vault` no longer has `store`. It was only there because `VaultSetup` nests it, and the compiler was right to warn: `store` would have made the vault publicly transferable, which it must never be. A struct with no abilities imposes none on its fields, so dropping it just works. - `type_name::get` and `vec_set::size` are deprecated; both renamed. - Two governance paths discarded a return value without saying so. - A test helper froze a clock it was still mutably borrowing. - Every `expected_failure` needed `location = skur::vault`. Without it the attribute expects the abort in the test module, so twenty-one tests failed with the right code and the wrong module. And one wrong expectation of mine: a payment to a recipient still in probation aborts with E_RECIPIENT_IN_PROBATION, not E_TOO_EARLY, because the activation check runs before the timelock check. That ordering is better — it tells a signer what is actually wrong — so the test now asserts it and the security model records it. The interface builds too: 21 static pages. Two fixes there. Turbopack needed the workspace root to follow the SDK's symlink, and the documentation's slug list had to move out of the client module, because generateStaticParams runs on the server and a server component importing a client module gets a proxy rather than the array.
Verified the whole stack against a real chain for the first time: published the package to a throwaway local network and drove it end to end. Every check passes — BCS layouts round-trip, the PTB builders produce transactions the vault accepts, preview_transfer decodes, the chain's tier-to-requirements mapping matches the policy, the activation delay refuses an early execution, probation expires, and every event decodes. It found a bug no unit test would have. describeFailure scanned for the first run of digits in the message, so 'MoveAbort in 1st command, abort code: 119' reported code 1: a signer whose payment was held by a recipient's activation delay was told their policy needed at least one approval. Both patterns are now anchored on text that only ever precedes the real code, and eight tests pin the message shapes a node actually produces rather than the ones I imagined. Also from the live run: - Move.toml was in the pre-2024 shape. published-at and [addresses] are gone, mainnet and testnet are system environments that must not be redeclared, and Move.lock pins the framework revision and is committed. publish.sh was passing --path to a command that takes a positional path, so it would have failed on testnet exactly as it failed locally. - sdk/dist was tracked from before it was ignored. Untracked; CI builds it. - The scripts now import the built artifact rather than the sources, so a packaging mistake fails them instead of hiding behind the loader. scripts/verify-local.sh runs the whole thing: local network, publish, drive.
An incremental build artifact that changes on every run and means nothing to anyone else.
Both consume the SDK's compiled output, which is not committed. A fresh clone therefore failed with vite's 'Failed to resolve entry for package @skur/sdk' — a message that says nothing about the cause. CI happened to work only because the SDK step ran first. Each dependent script now builds the SDK first. Verified by deleting sdk/dist and running the backend tests and the interface build from cold.
GitHub Actions cannot start on this account, so the checks on a pull request say nothing about whether the code is sound. scripts/verify.sh gives the same signal locally, mirroring the workflow job for job: the Move build must be warning-free, then its 75 tests, then the SDK, the backend including migrations against a real Postgres, and the interface. It skips rather than fails the steps whose tooling is absent — a missing Postgres is not a broken migration — and exits non-zero when something actually breaks, which I checked rather than assumed.
…able from its events Running the whole stack for the first time exposed this. The indexer had the vault but not a single member, so GET /members/:address/vaults returned nothing even for the address that created the vault — the interface's 'your vaults' list was empty for every vault that will ever exist. The cause was structural, not a missing query. VaultCreated carries counts, not addresses, and a Move table cannot be iterated, so the founding roster was the one piece of vault state no downstream reader could ever learn. The backend's whole design rests on projections being rebuildable from events alone, and the roster quietly broke that. begin() now emits a MemberChanged per member, exactly as every later change does. Verified by rebuilding the stack: the roster indexes, the vault reports its member count, and 'your vaults' resolves by address. The end-to-end script now asserts that the founding roster appears in the log with the roles it was created with, so it cannot regress. Two smaller things from the same run: - The interface disables Next 16's agentRules, which writes AGENTS.md and CLAUDE.md into the project on startup. This repository does not carry those. - verify-local.sh clears the stale ephemeral publication record before publishing. It refers to a network that was discarded at the end of the previous run, so it is stale by definition — and without clearing it a second run fails with 'your package is already published'.
The plan was to move to Sui, and I had left contracts/, web/ and mobile/ in place with a note explaining why. That was the wrong call. Two implementations of one security model is one more than anyone can keep honest, and keeping both meant every claim on the site, in the docs and in the tests had to be read twice to know which chain it was about. Removed: contracts/ with its Foundry submodules, web/, mobile/, the EVM CI workflow, the ABI export script, and the three EVM documents. The root package.json loses its legacy: scripts and verify.sh loses its --all branch, both of which pointed at directories that no longer exist. Nothing is lost. The build is in this repository's history and on three branches that were never deleted — feat/skur-v1, feat/safe-pattern-ui and feat/mobile-app — and the migration note now says how to read a file out of them or bring the whole thing back. The deployed devnet vault lives on that chain, not here. Verified afterwards: 10 of 10 checks pass, so nothing in the Sui build depended on the EVM one. The engines were ported into sdk/src/engine rather than shared from web/src/lib, which is why.
Removing mobile/ took its .gitignore with it, so the ios/, android/ and .expo artifacts it had been ignoring became visible — and my git add -A committed 114 of them, 1.2MB of generated files, in the commit that was supposed to delete the directory. Untracked, and the root .gitignore now refuses those three paths outright. The directories still exist locally with build output, node_modules and a .env in them; they are yours to delete and nothing in the repository refers to them.
Expo and React Native, reading the chain over gRPC and sharing the vault logic with every other interface through @skur/sdk. The design the project settled on carries over — theme, primitives, identicon — with the chain layer replaced. The signing key lives in the device's secure element, unlocked per transaction rather than per session: a cached signer is a signer that acts without a face. The app is explicit that this is a hot key, appropriate for an approver or an executor whose authority the policy bounds, and not for a guardian — a guardian key on the same phone as an approver key collapses the two control planes the contract works to keep apart. Three things React Native needed, none of them obvious: - Hermes has no CSPRNG behind crypto.getRandomValues, which key generation needs. assertCryptoReady fails at startup rather than mid-signature. - BCS needs text codecs that recent React Native ships, but a silent absence would surface as a corrupted transaction rather than a missing global, so there is a guarded UTF-8 fallback. - The Sui SDK is ESM with an exports map and no CommonJS fallback, and @skur/sdk resolves through a symlink. Metro needs package exports enabled and the workspace in watchFolders. Verified by bundling: 5.8MB of Hermes bytecode for iOS, which is the check that actually matters here — it proves the SDK resolves under Metro, where a typecheck cannot. verify.sh now runs both the typecheck and the bundle.
The app built and installed but threw 'undefined cannot be used as a
constructor' before React mounted, with no stack worth reading because it
happened while the module graph was still loading.
Bisecting the imports narrowed it to @mysten/sui/grpc, and reading the bundle at
the line the stack named found it: the SDK builds an ordinal formatter at module
scope — new Intl.PluralRules('en-US', { type: 'ordinal' }), used to say '1st
command' in an error message. Hermes ships a partial Intl and leaves PluralRules
out, so the constructor is undefined and importing anything from the SDK's
client throws immediately.
Polyfilled for English, installed only when genuinely absent, and
assertCryptoReady now checks it at startup rather than letting it surface as a
module-load failure. Metro also needed mjs in sourceExts, since the SDK's gRPC
entry is built from .mjs chunks.
The client is constructed lazily rather than at import. A top-level new in a
module everything imports means any resolution problem crashes before there is a
component tree to show an error in.
Verified on an iPhone 17 Pro simulator: builds, installs, launches, and renders
the welcome screen. I could not tap through the flow — this environment has no
Simulator GUI and no idb_companion — so the screens beyond the first are
verified by typecheck and bundle only.
I could not drive the app on a device: this machine has no Simulator GUI and no idb_companion. Rather than leave the screens past the first one unverified, they are rendered and pressed here — which is closer to the code than a touch, and repeatable in a way a hand-driven session is not. Twenty-eight tests. The ones that matter cover which action each role is offered, because the contract refuses an action the caller has no role for and an interface that offers it is lying to the person holding the phone. Each case asserts both halves — what appears and what does not. An approver gets Approve and Reject and not Execute; a guardian is never offered Approve on a payment; an agent is offered nothing at all; a disabled Execute does not fire when pressed. Two findings from the app itself: - The Send screen built its refusal message from two text nodes, so the reason a payment was refused read correctly to a person and was invisible to anything searching for it. One string now. - The action buttons had no stable identity, and the Execute button's label changes with its state — 'Not enough approvals yet' until it is ready, which is exactly the state worth testing. They carry testIDs now. And two about the testing library, recorded in the README so they do not cost the time twice: render and fireEvent are both asynchronous in version 14, and an un-awaited fireEvent does not fail its own test — it leaves React's work queue mid-flight and the next test renders an empty tree.
npm run ios failed because the script was called mobile:ios. That is a papercut nobody should have to learn, so ios, android, dev, web, start and indexer are now aliases for the longer names. Also records the Hermes CMake failure in the mobile README. It is worth writing down because the error misleads: it names CMAKE_OSX_ARCHITECTURES, but the cause is a cached internal CMAKE_APPLE_ARCH_SYSROOTS left behind by an earlier configure, which CMake never recomputes. Changing the CMake version does not help — 3.31 and 4.4 fail identically. Deleting the hermes-engine build directories does.
…sh, platform polish Four stubs and gaps that were real bugs: - The switch-vault button was wired to a function that did nothing, and the 'review what is waiting' shortcut set state no navigator read. Both navigate now. - The proposal screen was a component swapped by local state, so Android's hardware back closed the app instead of the proposal and nothing could link to one. The queue and the proposal are a real stack now, which also gives iOS its swipe-back. - An uncaught render error was a white screen. A root error boundary now says what broke, that nothing was signed — signing only happens behind a biometric prompt — and offers a restart that remounts. - The keyboard covered the submit footer on Send and Onboard. The screen primitive avoids it. Offline is a designed state rather than an accident. The query cache persists across restarts with a serializer that survives the bigints vault data is full of — plain JSON.stringify throws on the first balance, so persistence would have failed silently on exactly the data it exists for. A banner states the age of what is on screen; failed reads show a retry instead of a dead notice; reads in flight show skeletons. Push notifications, end to end. The indexer gains an Expo push channel: the message builder is pure and tested, refuses endpoints that are not Expo tokens before Expo has to, and wakes a phone only for alerts. Every rule now carries the deep link to the thing it is about, the app answers skur://vault/… and skur://vault/…/proposal/…, links queue until the navigator and the store are ready — a tap on a notification can cold-start the app — and a tap routes through the same path as a pasted URL. Settings enables it, and states exactly why it cannot when there is no EAS project id, no indexer, or no permission. Platform polish: the approved icon and splash restored from history and wired for both platforms, Android adaptive icon with a monochrome layer, haptics on a transaction settling or failing, screen-capture protection while a private key can be on screen, and the transaction footer shows its steps — unlock, sign, settle — rather than a spinner. 43 tests: the link grammar, the bigint round-trip, the error boundary and its recovery, the progress steps, the offline banner, and the role gating that was already there. Both platforms bundle: 5.98MB iOS, 5.97MB Android.
…sign, onboarding persistence Everything here was found by driving the app on an Android emulator against a live localnet, not by tests. Each fix carries the test that would have caught it. Hermes polyfills (ran fine in Jest/Node, died on device): - structuredClone: the Sui SDK clones commands while building every transaction, so tx.build() threw 'undefined is not a function' after the user had already passed the biometric prompt. - AbortSignal.timeout / .any / .abort and AbortSignal.prototype.throwIfAborted: waitForTransaction uses all of them, so a payment executed on chain and then the app crashed while waiting to report it. - Promise.withResolvers: used by the gRPC transport. Keystore redesign — the address now lives in its own non-authenticated entry: - On Android, any read of an authenticated keystore entry raises the system biometric prompt no matter what the read passes, so signerAddress() cost a fingerprint at every launch and decoded the secret key just to show something public. - unlockSigner no longer pre-authenticates with LocalAuthentication before the gated read prompts again: one fingerprint per action instead of two, and the one that remains is the one that actually releases the key. Onboarding never persisted: - Onboard awaited refreshSigner() right after creating the key, which swapped the screen out before its Continue button — the only caller of finishOnboarding — could be pressed. Every cold start began at the welcome tour. The confirmation screen now stays until Continue, and a device that already holds a key skips the tour regardless. Stored state is validated on restore: - The previous build wrote an EVM address under the same storage key; restoring it pointed every screen at a vault that cannot exist on Sui. The open-vault id must now be a normalised Sui object id (0x + 64 hex) on load and in the manual-entry form. - Only settled queries are persisted: a query dehydrated while pending is rehydrated as a promise nothing resolves, and react-query rejects it over the UI on restore.
…dark Every screen from the design reference, rebuilt on the real chain instead of sample data, in Skur's own palette (ink ground, #ffd000 accent) with a dark-mode switch that flips the whole app and a Light/Dark/System control that can hand the choice back to the OS. Onboarding (1–7), one flow with back navigation: - Launch, feature tour (3 pages, pager dots), create-or-join fork - Create Account: name + email, local-only, with the honest line in place of a password — the signing key in the secure element is the credential - Create Organisation: name, local description, the network pinned by the build - Add Members: roster with role chips mapped to contract capabilities, guardian warning - Connect Wallet: device key first (create/import, one biometric), the six external wallets listed and marked 'Soon' rather than pretending; creating an organisation builds the vault on chain and opens it — thresholds clamped to the roster, because a 2-of-1 vault is a locked box and the contract rightly refuses one Main app on five tabs (8–21): Home dashboard (treasury value, pending/member stats, recent activity — and no fiat figure the app cannot know), Treasury overview (portfolio bar, send/request, per-asset bars, the enforcement card), Transactions with All/Sent/ Received, New Transaction with a send/request toggle, proposal details with a Details card, live on-chain simulation sheet, review-before-signing checks and an approval-flow timeline, Team with governance-routed invitations, Roles & Permissions, Spending Policies with an owner-gated editor that proposes rather than decrees, Security Center with a controls meter over the existing posture/brake/max-loss, Activity Logs derived from what actually happened, Settings with profile and appearance, Help & Support. Root gate fixed: creating the key mid-flow no longer unmounts onboarding one step before it creates the organisation — the flow owns the screen until it says it is done. 59 tests (14 new), typecheck clean, both platforms bundle; the create-org path verified live on the Android emulator: key → faucet → vault on chain → funded dashboard.
…e mobile screens Brand: - The actual mark - two interlocked strokes, #FFD000 - extracted from the brand file with clean alpha and applied everywhere a logo appears: mobile app icon, Android adaptive + monochrome/notification icons, splash (yellow on black, as the brand presents itself), the in-app Logo component, and the web: both shells nav marks, public/mark.png, and Next icon.png/apple-icon.png so the favicon is the mark too. Design fidelity, from a second pass over the reference screens: - Sparse screens no longer hug the status bar: Screen gained a centered mode, and the launch and welcome screens sit in the middle of the viewport like a designed thing. - Treasury signature donut is real now (react-native-svg): segments by share, the total in the hole, legend beneath - no fiat figures invented. - Send / Request is the reference segmented pair of buttons, not an underline tab. - Spending Policies: each band is its own card with an icon chip, as drawn. - Security Center: shield hero up top, the checklist, and a posture meter card at the bottom in place of a made-up score. - Settings rows carry their leading icons; Create Organisation description is a proper textarea. Native rebuilds for the new assets and the svg module on both platforms; verified live on the Android emulator (fresh onboarding to a funded dashboard) and on the iOS simulator (same dashboard against the same chain). 59 tests, typecheck, both bundles, verify.sh 11/11.
- QR scanner behind the icon in the recipient field (expo-camera): accepts only a Sui address, says so when a QR carries anything else. - Create Account gains the or-divider and Continue with Wallet: skip the profile and go straight to the signing key, then pick a vault. - Connect Wallet carries the Need help? link. - Transaction rows name the recipient the vault knows (To: Ops payout), falling back to the short address. - Approvals show the approvers as identicons under the count, as the reference draws them. 59 tests, typecheck clean.
- PhoneMockup: the redesigned mobile app drawn in CSS — dashboard variant with the brand donut, treasury tiles and the five tabs; security variant with the shield, the posture meter and the guardian brake. In the hero it leans on the desktop mock; on the guardians card it stands alone, because the phone is the guardian story. - The desktop miniature sidebar carries the real mark instead of the letter S. Typecheck clean, 23 static pages build.
The two design languages were fighting after all: Next bundles both route groups'
stylesheets, so marketing's generic selectors (.shell { display: flex }, .pill, .btn)
leaked into /app and shattered its entry page - tabs wrapping vertically, the no-package
notice breaking one word per line, the hero laid sideways. Every marketing rule is now
scoped under the .land root the marketing shell renders (theme attributes stay on <html>,
so [data-theme] guards wrap .land rather than replacing it), and the dapp's own sheet is
the only one that reaches /app.
Also: the risk-engine diagram carries the mark instead of a letter S; the hero becomes a
proper triptych (phone left, desktop centre, risk ticker right - nothing overlaps); the
phone's tab bar gets five equal cells so no label clips.
Typecheck clean; 23 pages build.
A dark panel inside a light section inherited the page's near-black text, so its heading and checklist vanished into the ground. The panel now declares its own foreground, like every other dark panel on the page already did.
… and an address-vs-vault hint Three things conspired to turn a wallet click into a blank page: - The indexer API sent no CORS headers, so every browser fetch for 'your vaults' died in preflight and the UI fell back to 'the indexer is not reachable' — which is what pushed people to paste an id into Open-by-id in the first place. The API now allows the browser origin (everything it serves is public chain state). - A vault id names the shared vault object, not a wallet address, but the two look alike. Pasting an account address led to a vault page that threw on a missing object with no guard, and the whole route unmounted to white. The read error now says exactly that. - Both dapp routes are wrapped in an error boundary, so any render crash lands as a readable panel instead of a blank page. Typecheck clean, 23 pages build.
The not-configured dead end becomes a working door. When no package exists on the active network, both dapp pages now offer to publish it with the connected wallet: the compiled modules (sui move build --dump-bytecode-as-base64, served beside the app, gitignored) ride an ordinary transaction, the sender pays the storage, and the UpgradeCap lands in the publisher's wallet — whoever publishes holds the upgrade rights, explicitly. No CLI, no faucet access on the serving machine. Typecheck clean, 23 pages build.
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.
Skur on Sui — the whole build
44 commits that turn the repo from an EVM prototype into a working Sui product: a Move
authorization core, a TypeScript SDK, an indexer/notification backend, a web console, and
native iOS + Android apps — one brand, one security model, verified end to end on live
chains.
The core (
sui/)Four Move modules (~2,400 lines), 75 tests named after the attacks they defend against:
outflow and the vault's mode set the tier; the tier sets approvals, guardian sign-off and
the wait. Execution re-derives everything against live state and applies the stricter of
pinned vs live.
a loss-envelope circuit breaker that latches the vault into Lockdown.
roles; policy firewall where tightening is instant and loosening waits, vetoable;
an agent role that can propose and nothing else.
The surfaces
sdk/): gRPC + GraphQL only (no JSON-RPC anywhere), PTB builders, eventdecoding, and chain-agnostic engines (risk, templates, max-loss, posture) shared by every
client. 56 tests.
server/): PostgreSQL indexer + read API (CORS-enabled) + notification ruleswith Expo push delivery. 22 tests.
app/): marketing site + dapp console on@mysten/dapp-kit-react2. Onesubmission path for every transaction — the page builds bytes, the wallet only signs,
the page executes. Publishing the package itself is a button: any funded wallet can put
Skur on a network and keep the UpgradeCap.
mobile/): the full 21-screen product on iOS and Android — onboarding thatcreates an organisation on-chain from the phone, device key in the secure element behind
one biometric per action, treasury donut, QR recipient scanning, push → deep link into
the exact proposal, light/dark, offline cache with stated age. 60 tests, and the complete
payment lifecycle (propose → approve → execute) proven on-device against a live chain.
Verification
scripts/verify.shruns the 11 gates CI runs: Move build+tests, SDK, server (+migrationsagainst a throwaway Postgres), web typecheck, mobile typecheck+tests+both bundles. Green at
every commit boundary above; the flows were additionally driven by hand on the Android
emulator and iOS simulator, which is where the Hermes polyfills, the keystore redesign, the
locale amount-parsing fix and the data-first error states came from.
Explicitly not claimed
No independent audit, no mainnet deployment. The README, the apps and the site all say so.