main
40
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
caff8a6be6
|
feat: remove a wallet that holds nothing
ci / gate (push) Canceled after 6m53s
An open wallet can be removed from the accounts page (#86): its file is deleted, it leaves the session, and its account numbers and names leave the settings. Whether it holds nothing is decided in Rust, never by the webview, and a refusal says why: - every account must hold zero, free plus reserved, on every mainnet chain, read live; a chain that cannot be read refuses rather than assumes; - no reversible transfer may be pending to or from any of its accounts; - a phrase wallet's wormhole address must hold no unspent leaf, with the zk-tree scan current so none is unseen; - the wallet whose key controls the intents account is refused while that account holds any token; if it holds none, the record goes with the wallet and the next creation makes a new account. The control asks once, inline, and names what happens; no browser dialog. Keystore::remove reads the file first, so a symlink or a stranger's file is refused before anything is deleted; Session::forget drops the one wallet and keeps the password. Checked in a dev run with three wallets: Remove on a raw-key wallet holding nothing asked, deleted its file, took it out of the sidebar and cleaned its settings, with the other two left open. Closes #86 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUx7SPonBnB3ofU4mH41WT |
||
|
|
20f171be96
|
fix(ui): say what the Swap screen is, now that QTC is on NEAR Intents
ci / gate (push) Successful in 12m27s
The Settings toggle, the Swap screen and the setting's doc all said no post-quantum asset is on NEAR Intents yet. QTC has been since October (#71), and the wallet's own intents account swaps it from the accounts page. The Swap screen is the other route: a 1Click quote paid to a one-time deposit address, for assets this wallet does not hold (#7), and QTC is not among them until a solver quotes it. All three now say so. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUx7SPonBnB3ofU4mH41WT |
||
|
|
102f659900
|
feat(ui): a dashboard of the whole wallet, first after unlock
ci / gate (push) Canceled after 3m6s
The default route is now a dashboard (#85): one figure for everything the wallet holds, priced in USD at 1Click's index prices, and a bar splitting it by chain. The accounts page moves to /accounts. Rust reads and prices; the webview draws: - Each mainnet chain's accounts (the balances read as the accounts page does) and its shielded balance (the wormhole cache's unspent leaves, no scan), and the intents account's tokens, one view call. - Prices come from GET /v0/tokens, cached a minute. A price arrives as a JSON number, is rendered to 8 places and parsed as a decimal; every product is integer arithmetic on base units, brought to 8 decimals first so a 24-decimal token cannot overflow u128. Three tests pin it, including a million wNEAR. - What cannot be valued is named, not guessed; a chain not reached yet is started, marked, and the UI asks again every five seconds. The form follows the house data-viz method: a hero figure (48px, the body sans, proportional figures), and part-to-whole as a single bar with a 2px surface gap between segments rather than a ring, which hides close values and has no room for labels. Chains keep a fixed colour slot for good (Quantus bronze, NEAR Intents blue, three more validated for later); the legend table beneath carries every value and holding, so colour never carries a fact alone. Both palettes passed the validator against the wallet's own surfaces; the light mode's contrast warning on two slots is what the table answers. Checked in a dev run: unlock lands on the dashboard, the index priced QTC at $140.23, both chains read live, and an empty wallet drew the recessed track. A bar with segments wants a wallet that holds something, which the operator's will show. Closes #85 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUx7SPonBnB3ofU4mH41WT |
||
|
|
700850c75e
|
fix(ui): the lock screen says nothing about what it guards
ci / gate (push) Successful in 12m37s
It announced how many wallets the password would open. Before the password is given, the screen shows a password field and nothing else: not a count, not names. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUx7SPonBnB3ofU4mH41WT |
||
|
|
8cfd88b39b
|
chore(release): 0.4.1
One password unlocks every wallet (#84). 0.4.0 shipped from
|
||
|
|
4ea85465c6
|
feat: one password unlocks every wallet
ci / gate (push) Successful in 14m23s
Unlocking each wallet with its own password was a convoluted UX, and the wallets people already use open everything with one. This drops per-wallet passwords entirely (#84). The files stay in quantus-cli's format, so there is no vault: the password is simply the one every wallet file is encrypted with. - unlock(password) opens every wallet in the keystore. A file it does not open (one the CLI wrote under a password of its own) is named in the outcome's `unopened`; a password that opens nothing is wrong. - The session holds the password while open and wipes it on lock, so creating, restoring or importing a wallet asks only for a name and writes the file under the same password. On first run, with no wallet yet, the flow sets it. With wallets on disk and the session locked, a new wallet is refused, so a second password cannot come into being. - wallet_rekey re-encrypts one file from its own password to the wallet's, once; the accounts page offers it for every unopened file. Importing a CLI file re-encrypts it the same way at import. - password_change re-encrypts every file: all decrypted before any is written, every new file written before any replaces its old one, so a wrong password or a full disk changes nothing. - lock takes no wallet. The per-wallet lock buttons, the open-another page and the wallet picker on the lock screen are gone; the sidebar offers "add a wallet", and Settings a password change. Checked in a dev run from an empty data directory: the first import set the password, the second inherited it, a wrong password was refused, a CLI file dropped beside them was reported and re-keyed, and a password change moved all three; the lock screen has one password field and no picker. quantus-cli exported a re-keyed file's phrase with the new password and refused the old, so the CLI keeps opening these files. Closes #84 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUx7SPonBnB3ofU4mH41WT |
||
|
|
038f09774f
|
chore(release): 0.4.0
The NEAR Intents account (#71): one per installation, created by the sponsor blackbeard-observer.near and controlled by one wallet's ML-DSA-65 key derived from its phrase (NEP-649). The wallet still holds no classical key. From the Accounts page: QTC in over the bridge from any account, swaps inside Intents on a Ref pool or 1Click with the sponsor's 30 bps fee, cash-out to USDC/USDT/ETH/BTC on Ethereum, Base, Arbitrum, Solana, Bitcoin or NEAR, and QTC back to Quantus. The sponsor relays every NEAR leg and pays its gas (#73, #74, #75, #76, #77, #81, #82). Also: the version in the sidebar links to its release (#70); the wormhole scan is one paced pass for every open wallet, under the node's rate limit; QTC maps to nep141:qtc.omft.near in the asset registry. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUx7SPonBnB3ofU4mH41WT |
||
|
|
4f3b446cc3
|
feat(near): cash out from the intents account to an address on another chain
ci / gate (push) Successful in 13m0s
The intents panel can now pay a token it holds out through 1Click (#77): USDC on Ethereum, Base, Arbitrum, Solana or NEAR; USDT or ETH on Ethereum; BTC. - cashout.toml ships the destinations with their 1Click asset ids, checked against GET /v0/tokens today, and the address form each takes. - check_address refuses a mistyped payout address before anything is priced or signed, because nothing refunds a payout to the wrong address: - EVM: 0x and 40 hex digits, with the EIP-55 checksum enforced when the address is mixed case; - Bitcoin: bech32 or base58; - Solana: base58 of 32 bytes; - NEAR: an account id. - near_cashout takes a firm 1Click quote with recipientType DESTINATION_CHAIN, the fee as appFees, and refunds to the account. It checks the echoed recipient, its type, the refund, the assets and the amount against what was asked, funds it with one mt_transfer signed by the owning wallet's key, and has the sponsor relay it. - The 1Click request builder now serves swaps and cash-outs: None pays the account itself, Some(address) pays out on the destination chain. Checked live from the dev account: - 10 wNEAR quoted at 46.38 USDC on Ethereum, Ethereum's withdrawal included, with a floor of 45.91; - the same EIP-55 example address with its last character's case flipped was refused on its checksum. Refs #77 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUx7SPonBnB3ofU4mH41WT |
||
|
|
814355c9f3
|
feat(near): swaps inside the intents account, on a Ref pool or 1Click
ci / gate (push) Canceled after 5m23s
The intents panel can now swap any token the account holds into QTC, wNEAR or USDC (#76). Rust prices it both ways and takes the larger output. - Pool: best_pool lists Ref's DCL pools for the pair and skips any that are not running or whitelist other accounts (they refuse with E407). It quotes each on chain. - pool_swap_calls is one delegate action: first the fee's mt_transfer, ceil(amount × bps) in the input token to the sponsor's recipient; then the ft_withdraw to the pool, with Ref's Swap message, a floor (default 1% slippage), and the account as client_echo. - The fee is computed exactly as the sponsor's rule (observer#51) checks it, so the relay accepts it. - 1Click: a quote inside Intents (recipient and refund are the account) carrying the fee as appFees. When it wins, a firm quote is taken, its echoed recipient, refund, assets and amount checked against what was asked, and it is funded by one relayed mt_transfer. - near_swap prices again at confirm time, checks the balance and the sponsor's terms, signs with the owning wallet's key, and relays it. Measured while building this: without a partner key, 1Click adds its own 20 bps and passes on half of an app fee (30 asked, 15 recorded). The Distribution Channel key is what makes the 1Click leg pay the full fee. Checked live with quotes from the dev account: - 1 QTC → 24.75 wNEAR on the QTC/wNEAR pool, with a 0.003 QTC fee; no 1Click quote; - 10 wNEAR → 46.83 USDC on a pool, against 46.78 on 1Click; - 10 wNEAR → 0.393 QTC. Executing one needs a funded account, which the release test (#78's route on a real wallet) will do. Refs #76, #82 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUx7SPonBnB3ofU4mH41WT |
||
|
|
4b407de277
|
feat(near): one intents account per installation, whichever wallet's QTC moves
ci / gate (push) Canceled after 25s
The operator's call: a person has no use for several intents accounts,
so the wallet makes one and routes every swap through it. The sponsor
cannot tell one installation from another and does not need to; the
wallet simply never offers a second.
- Settings record intents_account: the owning wallet, the account id and
its public key, none of it secret. settings_set keeps it Rust's to
change, as it does the account numbers.
- near_intents replaces near_account:
- recorded: it is read, with its owning wallet named and marked open
or locked. Balances read either way; signing waits for the owner.
- not recorded: an open phrase wallet's existing sponsored account
with that wallet's key is adopted and recorded. That covers a
restore, or a second machine.
- otherwise the first phrase wallet's account is described, with the
other phrase wallets offered as owner, and creation if the
sponsor's terms are acceptable.
- near_create_account refuses when an account is recorded or any open
wallet's account already exists.
- Deposits take QTC from any account; withdrawals pay any open wallet's
account. Both use the recorded account, and a withdrawal is signed by
the owner's key.
- The Accounts page shows one NEAR Intents panel, not one per wallet.
Checked in a dev run: with nothing recorded, the dev-phrase wallet's
existing bffbod6n6ey7jdk4j.blackbeard-observer.near was found, recorded
in settings.json and shown as the one active account, with nothing
created.
Refs #71, #74, #81
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUx7SPonBnB3ofU4mH41WT
|
||
|
|
52ac684661
|
feat(near): QTC into the intents account and back out to Quantus
ci / gate (push) Failing after 1m35s
The intents panel now moves QTC across the PoA bridge (#75). In: the bridge's permanent Quantus deposit address for the account, checked as a Quantus address before it is shown. "Send QTC here" opens Send with it filled in (Send takes ?to= now), so a deposit is an ordinary transfer reviewed and signed like any other. Out: a withdrawal to one of the wallet's own Quantus accounts. It is the ft_withdraw #72 proved, qtc.omft.near to itself with the WITHDRAW_TO: memo: - qtc_withdraw_call in wallet-core builds it and refuses anything under the bridge's 0.01 QTC minimum. - The command checks the Intents balance and the sponsor's terms, signs a delegate action with the phrase's NEAR key (nonce one past the key's, valid for about 120 blocks), and hands it to the sponsor, which relays it and pays the gas. - The bridge keeps 0.002 QTC, and the panel says so. NearRpc::final_block now returns the height with the hash, for the delegate's expiry. QtcBridge is the new port and PoaBridge its adapter. Checked in a dev run with the dev-phrase account: - the deposit address appeared (qzkgR6o1…); - Send opened with it filled in; - a 0.05 QTC withdrawal was refused because the account holds no QTC. A live test (ignored) confirms the bridge gives #72's account the address it used on mainnet. A full round trip with real QTC is still to do before #75 closes. Refs #75 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUx7SPonBnB3ofU4mH41WT |
||
|
|
384c79446b
|
feat(near): the sponsor creates the intents account from the wallet
ci / gate (push) Failing after 1m27s
The Accounts page now offers "Create account" when the wallet's NEAR Intents account does not exist yet. The sponsor (#81) creates it as b<…>.blackbeard-observer.near with the wallet's ML-DSA-65 key, and pays its NEAR fees from then on, so the panel no longer shows gas. - NearSponsor is the port. HttpSponsor reads the terms, asks for an account and relays a signed delegate, keeping the sponsor's own reason when it refuses. - The wallet acts only on terms it accepts: the sponsor is running, it names the account the wallet expects, and its fee is at most MAX_FEE_BPS (50). Otherwise the panel says why and offers nothing. - The account the sponsor reports must be the one the wallet derived from its key, or the wallet refuses to use it. - The sponsor answers once its transaction is in a final block, but the account is made by a receipt a block or two later. The command waits for it to be readable before reading the summary back. Verified in a dev run with the dev-phrase fixture: Create account made bffbod6n6ey7jdk4j.blackbeard-observer.near (0.0025 NEAR deposited, 182 bytes, the sponsor logged "created"). It read back as active once refreshed, which is the race the wait now covers. The live tests (ignored by default) read the terms and send a malformed key, which the sponsor refuses without creating anything. Closes #74 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUx7SPonBnB3ofU4mH41WT |
||
|
|
f4cc7a1605
|
feat(near): the intents account on the Accounts page
ci / gate (push) Failing after 1m25s
Every open wallet with a phrase now shows its NEAR Intents account. Its NEP-649 key is derived from the seed on each look and dropped; only the public side leaves Rust. The account's name is a function of that key, b<base32 of the handle's first 10 bytes>.blackbeard-observer.near, so the sponsor (#81) creates it there, a restored phrase finds it again with nothing stored, and the leading b sorts it onto shard 8, which our nodes serve. The panel says whether the account exists and whether this wallet's key is on it, and shows its NEAR for gas and every token it holds inside Intents (one mt_batch_balance_of over the registry's Intents ids). The logic is wallet_core::near::account_summary, written against the NearRpc port and tested with a fake. The command is a shim, and the app now holds the shard-routed client from #73 and near.toml, which a file in the data directory can replace. Corrected on the way: the asset registry said no post-quantum asset is on Intents. QTC has been, as nep141:qtc.omft.near, since early October. Quantus's native QTC now maps to it, the token-list fixture gains 1Click's QTC entry, and the "unmapped asset" tests use Planck's token instead. The readme no longer says the swap module waits for Quantus's MPC fork. Verified in a dev run with the dev-phrase fixture: the panel showed bffbod6n6ey7jdk4j.blackbeard-observer.near as not created yet. Refs #74 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUx7SPonBnB3ofU4mH41WT |
||
|
|
b059a8d5d0
|
feat(ui): the running version in the sidebar, linked to its release
ci / gate (push) Successful in 13m48s
The sidebar now reads "wallet v0.3.0" under the mark, and the version opens its release tag on git.lair.cafe in the system browser. The number is the binary's own, from app_info, so a packaged build always names the tag it was cut from. The opener's allow-list gains this repository's URLs and nothing else on that host. On a dev run with crystal_alice unlocked: the tagline rendered with the version in mono, not uppercased; opening the tag URL resolved, and git.lair.cafe/other/repo was refused by the opener. Closes #70 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUx7SPonBnB3ofU4mH41WT |
||
|
|
bfb016d76f
|
chore(release): 0.3.0
Restoring a phrase finds the accounts it already uses (#69), and any open wallet can be rescanned from the accounts page. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW |
||
|
|
fb42138651
|
feat: find the accounts a restored phrase already uses
ci / gate (push) Has been cancelled
Restoring a phrase now scans for the account numbers it already uses, instead of opening account 1 and leaving the rest to be guessed at. Each number is derived under both schemes, the chain is asked in one request which of those addresses exist (a System::Account entry is there for an account that has ever been funded), and the scan carries on while a batch held any, stopping after a gap of 20 empty numbers, as the mobile wallet's import does. Numbers found are opened and remembered like a hand-added one, so they come back on the next unlock. The same scan is a "Find accounts" button on every open wallet with a phrase, and it says what it found. A wallet with no phrase, or a chain that is not connected, offers no scan; a failure during restore is swallowed, since the wallet is open either way and the button retries. On the dev node: 7 DEV sent from the CLI to account 3 of the dev phrase, then that phrase restored here. Without pressing anything the wallet opened accounts 1, 2 and 3 under both schemes, account 3 showing the 7 DEV, after looking at 40 numbers; settings recorded 1 and 2. On a freshly created phrase, Find accounts looked at 20 numbers, added nothing, and said so. Closes #69 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW |
||
|
|
70dc6ea650
|
chore(release): 0.2.1
Nullifiers never leave the client (#68): the wormhole spent check reads padded, shuffled buckets of UsedNullifiers instead of looking each nullifier up by key. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW |
||
|
|
91ec5fbadb
|
chore(release): 0.2.0
ci / gate (push) Failing after 40s
Both ML-DSA accounts from every phrase with no scheme choice (#65), several wallets open at once (#66), and accounts added to an existing wallet (#67). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW |
||
|
|
42d226cf9e
|
feat: add accounts to an existing wallet
ci / gate (push) Waiting to run
"Add account" on a wallet that carries a phrase derives the next account number under both schemes, numbered as Quantus's clients number them (m/44'/189189'/<account>'/0'/<scheme>', the mobile SDK's pathForIndex), opens them at once, and remembers the number in settings under the wallet's name; the next unlock re-derives them from the phrase. The keystore file stays quantus-cli's single keypair, so nothing about the added accounts is written there, and nothing secret is written anywhere. Accounts show "Account N" wherever they are listed or picked unless the person named them. A single-key wallet offers no Add account and the command refuses it by name. settings_set keeps the stored account numbers, so a save from the webview's cached settings cannot erase one added since. Pinned against quantus-cli 2.2.2 imports of the dev phrase at --derivation-path: account 1 is qzm41eg2… (ML-DSA-65) and qzn2nAa4… (ML-DSA-87), account 2 is qzohziq6… (ML-DSA-65). Driven on the dev node: restore, Add account showed those two addresses as Account 2; after a restart and unlock they were still there; 4 DEV from crystal_alice to Account 2 and 1 DEV back from it, both submitted. Closes #67 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW |
||
|
|
cb0fe536cd
|
feat: both ML-DSA accounts from every phrase, and several wallets open at once
ci / gate (push) Successful in 12m43s
No scheme choice anywhere. Create and restore no longer ask for ML-DSA-65 or ML-DSA-87, and the commands behind them take no scheme. A wallet that carries a phrase opens with an account under each scheme at its conventional path, ML-DSA-65 first and ML-DSA-87 labelled legacy, plus the file's own keypair when it sits at another path, de-duplicated by account id. A raw key such as crystal_alice keeps its one account. The default follows Quantus's own clients: quantus-cli's --scheme default, the mobile SDK's DilithiumSchemeExtension.current and the extension's DEFAULT_TYPE are all ML-DSA-65, and the extension already shows both accounts with no key type to choose. The keystore file stays quantus-cli's and records the ML-DSA-65 keypair; the rest is derived on every open. Any number of wallets open at once. The session holds a list of open wallets, each with its accounts and seed; unlocking adds one, locking takes a wallet name or locks all, signing finds the account in whichever wallet holds it, and one idle clock locks them all. Session status lists the wallets and a flat account list where every account names its wallet. The sidebar lists open wallets with a lock each, an "open another" page and "lock all"; accounts groups rows by wallet; send, receive and history pick from every account of every open wallet, grouped by wallet and labelled by scheme; the wormhole panel is per wallet. Driven on the dev node: crystal_alice open, the public dev phrase restored beside it with no scheme field, and its two accounts appeared, qzq29m9… (ML-DSA-65) and qzjrYTUnn… (ML-DSA-87, legacy), the addresses quantus-cli lists for that phrase. 5 DEV from crystal_alice to the legacy account, 2 DEV from it to its ML-DSA-65 sibling, both in blocks. Locking devphrase left crystal_alice signing while a transfer from devphrase's account was refused; lock all returned to the lock screen. Unit tests cover both-schemes derivation for every CLI fixture, side-by-side wallets, per-wallet lock and the shared idle rule; the secret-leak test passes with both signers held. Closes #65 Closes #66 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW |
||
|
|
9fd50f9904
|
feat(ramp): the FiatRamp port with a stub provider, opened in the system browser
ci / gate (push) Successful in 10m55s
wallet_data::ramp::StubRamp builds a session URL on our own host from the request (direction, asset, destination, currency) and nothing else; it is what proves the plumbing before a provider is signed (#44). The ramp_session command decodes the address, builds the request, refuses a URL that is not https, and hands the URL back; the receive screen's Buy and Sell buttons pass it to the opener, whose capability allows blackbeard.observer and the explorer and nothing else. The wallet's webview never navigates: driven live, the client returned https://blackbeard.observer/ramp?direction=buy&asset=dev%2Fnative&…, Buy handed it to the opener without error, and the webview stayed on #/receive. doc/reproducibility.md records what the build pins and what a second build of the same tag is expected to differ in; the release workflow exports SOURCE_DATE_EPOCH from the tagged commit. The diffoscope comparison waits on the first tag. Closes #43 Refs #51 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW |
||
|
|
c59ba49e90
|
feat(swap): orders as a state machine persisted in SQLite, and the preview screen
ci / gate (push) Waiting to run
wallet_core::orders is the pure part: Quoted → AwaitingDeposit → Processing → Success | Refunded | Failed, with Expired when a quote's deadline passes before a deposit is seen; it never moves backwards, a quote past its deadline expires without a network call, and the poll delay doubles from ten seconds to five minutes (five seconds while processing). Orders persist through an OrderStore port on the same SQLite file; on start the app follows every order still open, and the follower asks the provider with backoff and jitter, persists each move and emits orders:updated. Commands: swap_assets (what 1Click lists and the registry maps), swap_preview (a dry quote), swap_order_create (a wet quote persisted with its deposit address and deadline) and orders_list. The swap screen sits behind a preview setting, says plainly that every pair today is paid from an external wallet, quotes, shows the deposit address with a QR, memo and deadline, and lists orders with their states. Driven live on the dev network: BTC on bitcoin to USDC on near, 0.001 BTC quoted at 75.568313 USDC, at least 74.812629, about 805 s. Reserving a deposit address failed with 1Click's "Failed to get quote": a wet quote needs the partner credential the relay will hold (#40), which is the operator's registration. Until then the screen previews and the poller has nothing to follow; the state machine and the store are covered by unit tests, including the mid-order restart the store makes possible. Refs #39 Refs #41 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW |
||
|
|
14d1a39519
|
feat: the updater, offered never applied; and a test that greps for secrets
ci / gate (push) Successful in 11m44s
tauri-plugin-updater with a minisign public key in tauri.conf.json (the private key and its password are repository secrets, generated for this and never in the repo), the manifest endpoint on the same host as the dnf repo, and updater:default plus process:allow-restart granted with their reasons. app_info now says how the binary was installed; the rpm and deb opt out because dnf and apt own their updates, the AppImage, NSIS and dmg opt in. The shell shows an offer three seconds after settling, with the two versions, and applies it only on "Update and restart". The release workflow signs the bundles (createUpdaterArtifacts), writes latest.json for linux-x86_64 pointing at the release asset with its signature, and the publish job rsyncs it to rpm.lair.cafe last, so a manifest never names an asset that is not yet released. Windows and macOS entries come with #34; the run-N-then-N+1 check waits on two published releases. The secret handling audit is a test, secrets_do_not_leak.rs: create a wallet from a known phrase, open it, unlock, sign, derive the wormhole keys, use both caches, lock, all under trace logging into a sink; then assert the log, every file in the data directory and the Debug output of every secret-holding type contain none of the words, the seed, the secret key or the wormhole secret, as hex, as raw bytes or as the decimal list a derived Debug prints. OpenedWallet and Session gain redacting Debug impls so the test can log them. The sample of the secret key starts inside K: an ML-DSA secret key opens with rho, which is also the public key's first 32 bytes, and the first draft of this test flagged the public key for it. Closes #50 Refs #35 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW |
||
|
|
3db8b05032
|
feat: the wormhole address behind mining rewards, and its shielded balance
ci / gate (push) Waiting to run
keys/wormhole.rs derives the wormhole keys from the seed at the wormhole coin type (m/44'/189189189'/i'/0'/0'), pinned to what quantus-node's `key quantus --scheme wormhole` prints for the public dev phrase at index 0 and 1, and derives a miner's address from its published inner hash (bob's, as the observer shows it). The nullifier that spends a leaf is mirrored on the Poseidon core alone, pinned to four vectors produced once with the circuit crate, so the wallet does not carry the prover to know what it has spent. substrate/wormhole.rs reads ZkTree::LeafCount, scans ZkTree::Leaves in pages of 500 locally computed keys for leaves paying an address, and checks Wormhole::UsedNullifiers. Nothing on chain indexes leaves by recipient, so the scan runs once from zero and then only over what was appended; the SQLite cache keeps the leaves and the frontier under a ShieldedCache port. The wormhole_summary command derives the keys from the session's seed for the call, scans, marks spent leaves, and answers with the address, the inner hash and the shielded and received totals; the accounts page shows it with copy and rescan. A raw-seed wallet with no seed is told so rather than shown a fake address. Verified on mainnet through the public endpoint: a full scan of 107,127 leaves in 16 s found 31 leaves paying bob's wormhole address for 9.34 QTC, the observer's 30-reward attribution of 9.33 plus the one since; the dev phrase restored in the app shows qzq32vN2ZtX2T9YrvCh69CMvbC2nWmEq4tCjRWKg4CyuxeShg and inner hash 0x5a5891e7…, the node's own numbers, with the tree scanned to its end. doc/threat-model.md names the assets, the boundaries and the file behind each mitigation; its review by someone else is what closes #48. Closes #45 Refs #48 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW |
||
|
|
34738a8e32
|
feat(ui): the same accounts on every other network, side by side
ci / gate (push) Waiting to run
An "Other networks" table under the accounts: every profile the settings allow other than the selected one, each connected on demand and following heads like the main table, with the account's spendable and total balance there and the chain's state. One seed gives the same address on every Quantus-family chain (they share coin type 189189), and the table says so rather than pretending they are different accounts. Run on the dev network with testnets shown: Quantus mainnet through wss://quantus.blackbeard.observer at block 54032 and Planck at 1111122 listed beside the dev chain, both at zero for crystal_alice; a Polkadot address in the send form refused as "a Polkadot address, not a Local dev node address". Closes #37 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW |
||
|
|
41c3ba7073
|
feat: history from blackbeard.observer, cached in SQLite, on its own screen
ci / gate (push) Successful in 11m41s
History comes from the observer's account activity route, chosen over a relay because the observer already indexes every chain with a profile and serves per-account activity keyset-paginated (recorded on #23). A HistorySource port in wallet-core with an observer adapter in wallet-data reads it; the rows a person looks for are typed (transfers in and out, wormhole exits, reversible schedule, cancel and execute, and calls the account signed) and the rest dropped, while the cursor always advances over the raw rows. The wormhole pallet mirrors every native transfer as NativeTransferred, so only rows with the wormhole's own account as a party count as wormhole movements; the mirrors would otherwise list each send twice. A saved response pins the parser. A HistoryCache port with a rusqlite adapter keeps what the indexer said under <data>/history.sqlite, keyed by chain, account and position, with a per-account record of when the indexer last answered and how far it had read. The history_page command stores as it fetches and, when the indexer cannot be reached, serves the cache with from_cache set and the reason attached. Profiles carry the indexer URL (none for the dev node, and the screen says so). The history screen pages with "Older", refreshes, shows an offline chip with the cache's age, and opens the account on the public explorer through tauri-plugin-opener, granted open-url for those two hosts only, with the reason in the capability file. Verified on mainnet: two pages of 45 and 46 rows for an active account through the client, the cache file written; with the indexer overridden to a dead address, the same call served 50 cached rows marked offline with the error and the last fetch time, and the screen showed "offline: cached 1 min ago". An unknown account reads as "Nothing yet". The observer's ignored live test reads two pages. Closes #23 Closes #30 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW |
||
|
|
dc3a87c0e6
|
feat(ui): pending reversible transfers with a measured countdown and a cancel
ci / gate (push) Waiting to run
The accounts page lists PendingTransfersBySender for every account of the open wallet, refreshed every five seconds, with when each executes: at a block, counted down at the block interval the chain manager now measures over its last 32 best heads (exposed as ChainStatus.block_interval_ms, with the profile's nominal time as the fallback and labelled as such), or at a timestamp, counted down by the clock. Cancel prepares ReversibleTransfers.cancel and goes through the same review pane as a send, which moves out of the send route into ReviewAndSign. The call decoder now renders a 32-byte argument by its metadata type: an AccountId32 as SS58, anything else as hex. Before, a cancel's tx_id was dressed up as an address. Unit tests cover both the interval and the rendering; the remaining Option<u64> fields are typed `number | null`. On the dev node: a 2 DEV transfer with a 60-block window listed at block 314 with "49 blocks, about 52 s" against a measured 1.1 s interval; a 3 DEV transfer with a 1000-block window cancelled through review, the tx_id shown as hex, and the balance back up by 3 less the fee. Block 314 arrived 14 s after the estimate: the dev chain's interval swung between 0.6 and 1.7 s per ten blocks over that minute. See the issue for what that means for the acceptance criterion. Closes #29 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW |
||
|
|
52774f1d88
|
feat(ui): send: a form, a review of what Rust decoded, then sign and watch
ci / gate (push) Successful in 9m49s
The form collects text. Review shows the call decoded from the bytes that will be signed, from PreparedTransferInfo: pallet and call, each argument as Rust rendered it, the fee, the mortality and the nonce. Submit hands back only the prepared id, so nothing the form holds can reach the signature; Back discards the prepared transaction on the Rust side. A cancellation window is offered as presets or a custom block count, described in the chain's nominal block time, and hidden for a high-security account whose delay is fixed. After submit the tx:status events drive a stage list until finalized, failed or dropped. Event subscriptions now go through one helper that copes with `listen` resolving after the effect is torn down, which under StrictMode left a second listener alive and showed every stage twice. Optional u64 fields are typed `number | null` rather than `number`. On the dev node, from crystal_alice: 3 DEV reviewed as Balances.transfer_keep_alive with the recipient and "3 DEV", signed and in a block; 2 DEV with a 200-block window reviewed as ReversibleTransfers.schedule_transfer_with_delay, in a block, and then listed by reversible_pending with cancellable_by_me; a prefix-42 address refused at prepare with the cross-chain message; 5,000,000 DEV signed and reported Failed: Arithmetic(Underflow). Closes #28 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW |
||
|
|
e4cde9a3db
|
feat(ui): receive: address, QR, checkphrase and copy; accounts named in place
ci / gate (push) Waiting to run
The receive screen shows one account's address as text, as a QR (drawn in the webview by qrcode-generator, since an address is public), and as the five-word checkphrase Rust derives, with a copy button through tauri-plugin-clipboard-manager granted only write-text, with its reason. Accounts can be named in place; the labels live in settings under account_names, keyed by address, and show on the receive screen too. Checked against the dev node: the checkphrase for crystal_alice reads Toast-Terrific-Kitten-Warrior-Churn, the same as `quantus wallet list`; a `quantus send` of 7 DEV from crystal_bob raised the accounts row from 99,957.786510548 to 99,964.786510548 within the next head; Copy flips to Copied; a rename is written to settings.json and survives a lock. Closes #27 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW |
||
|
|
7101ead194
|
feat(ui): onboarding: create a phrase, restore one, or open a quantus-cli file
ci / gate (push) Waiting to run
Three routes under /onboarding, reachable from the lock screen and from the shell, since a second wallet is the same flow as the first. Create picks a scheme (ML-DSA-65 by default), shows the 24 words once, and stands the backup challenge between seeing them and having a wallet; a failed challenge drops the phrase on the Rust side and starts over. Restore takes a phrase and a scheme, with a note that the same phrase gives a different account under each. Import opens a native file chooser through tauri-plugin-dialog, the first plugin, granted only dialog:allow-open with its reason in the capability file; the path goes to Rust, which reads the file. Every path ends unlocked on the accounts page. Driven through the WebKit inspector against the dev node: create wrote onboard_test.json and unlocked; restoring the same phrase under a new name produced the same address; importing a fixture file with the right password opened it and with a wrong one mapped to wrong_password. The chooser itself is a Wayland window nothing here can type into, so the permission is verified by the open promise staying pending rather than rejecting; CLAUDE.md and the inspector script now say so. Closes #26 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW |
||
|
|
fbb7c71db9 |
feat(ui): the app shell: lock screen, sidebar, network switcher, status bar, settings
ci / gate (push) Successful in 9m14s
The frame every screen sits in. A lock screen fronts everything while the session is locked: pick a wallet file, type a password, and the password goes to Rust and nowhere else. Unlocked, a sidebar carries the sections, the network switcher and a lock button with the minutes until the idle rule fires; a status bar shows the chain in use, its endpoint, best and finalized heights and spec version, and says "connecting…" or the last error otherwise. Testnets are labelled in the switcher, the status bar and above their content, as on the observer: a balance on a testnet is not money and the chrome says so before the number does. Settings persist as JSON in the app-data directory, written atomically: network, idle lock, whether testnets are offered. Changing the network re-keys every query; changing the idle lock takes effect on the next check. The theme (system, dark, light) is a local preference applied before first paint from the entry point, because the CSP allows no inline script. Design tokens are the observer's, lifted whole so the wallet and the scoreboard read as one family. The accounts page shows spendable and total per account with the block the numbers were read at and how far that is from final, refreshed by Rust on every head; send, receive and history are placeholders naming their issues. Wide integers now cross the IPC typed as `number`: they are JSON numbers on the wire, which `bigint` cannot be serialised to, and none of them (block heights, seconds, nonces) approaches 2^53. Amounts still travel as strings. Closes #25 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW |
||
|
|
95bcfe9ca4 |
feat(substrate): transfers prepared for review, priced, signed by the session, submitted and watched
ci / gate (push) Waiting to run
`SubstrateAdapter` implements the chain-adapter port for Quantus. `prepare` builds the call from a chain-neutral intent, decodes it back from its own bytes through the runtime metadata into a summary a person can check (a 32-byte account renders as the chain's SS58 address, an amount in the token's units), prices it by assembling a full extrinsic with a zeroed signature of the right size and asking `TransactionPaymentApi_query_info`, and returns the signer payload for the session to sign. `submit` refuses if the runtime spec moved or the payload no longer matches, assembles with the session's signature, and streams statuses; a dispatch error is reported at inclusion, named `Pallet::Variant` where the chain names it, so nobody waits for finality to learn a transfer failed. Reversible transfers: `reversible_for` routes through `ReversibleTransfers::schedule_transfer_with_delay`; a high-security account is routed through the pallet's default-delay call whether it asked or not, since the chain's extension refuses a plain transfer from one. `pending_reversible` lists what is waiting with the execution block from the scheduler's lookup, whose task name is the tx id itself, and `CancelReversible` cancels. Three things the dev node taught, each now written into the code: - Mortality must be anchored at the *best* block. Finality on this proof-of-work chain trails the tip by a hundred blocks, and an era born at the finalized head names a birth block whose hash the runtime resolves differently: "Transaction has a bad signature", for both schemes, while immortal transactions from the same keys passed. Probed all four scheme-by-era combinations to isolate it. - Everything the UI shows is read at the best block, with the block number and the finalized height alongside so confirmations can be shown. subxt's `at_latest()` is the finalized block; the pending list read there showed the state of a hundred blocks ago while the chain's events showed every schedule landing. - subxt's runtime-update subscription died on this node with "header not found" after a reorg and took the connection with it. It is advisory now: on error the runtime version is polled every ten heads and a change forces a reconnect, which refetches metadata. Live, against `quantus-node --dev` with the funded dev accounts: a 3 DEV transfer from crystal_alice to crystal_bob is included and moves bob's balance; a reversible transfer with a 200-block window is scheduled, shows as pending with its execution block, and is cancelled; an overspend fails as `Token(FundsUnavailable)`. Fee for a plain transfer: 0.010157927 DEV. In the app: `transfer_prepare` (amount parsed from its decimal form in Rust), `transfer_submit` (signs through the session, emits `tx:status`), `transfer_discard`, `reversible_pending`, `reversible_cancel_prepare`. Closes #22 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW |
||
|
|
26559ee148 |
feat(substrate): balances, nonce and high-security status, refreshed on every finalized head
ci / gate (push) Successful in 6m43s
`account_info` reads `System::Account` and `ReversibleTransfers::HighSecurityAccounts` at the latest finalized block through subxt's dynamic storage API, so a number the UI shows cannot be reorganised away and no generated code has to track the runtime. An account the chain has never seen is zero and nonce zero, not an error; the live test reads the dev phrase's account on Planck and gets exactly that. The next nonce comes from `system_accountNextIndex`, which counts the pool. Spendable is free less frozen; total is free plus reserved. A high-security account carries its guardian and its delay, in blocks or milliseconds as the pallet stores it, so the send path (#22) can route it through the reversible-transfers pallet instead of a plain transfer the extension would reject. The connection manager now publishes finalized heads on a watch channel. `balances_watch` spawns one task per chain that re-reads every open account on each head and emits `balances:updated`; `balances` reads once. Both refuse with `Locked` when no wallet is open. Closes #21 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW |
||
|
|
78c06120f6 |
feat(substrate): a supervised connection per chain, failing over and following upgrades
ci / gate (push) Waiting to run
`ChainManager::start(profile)` spawns a supervisor that tries the profile's endpoints in order, refuses a node whose genesis is not the profile's (by name, so a typo in an override file cannot quietly sign for the wrong chain), records the runtime version at connect, follows finalized heads, applies subxt's runtime updates as the chain upgrades, and on any failure rotates to the next endpoint after an exponential backoff with full jitter, capped at a minute. A head that fails to arrive for two minutes counts as a failure. `status()` reports the endpoint in use, spec and transaction version, finalized height, head age, the last error and the failure count. Backoff is a pure type with the schedule pinned in tests. The live test gives Planck's profile a dead first endpoint and the real second one: it connects to the second at spec 148 with zero failures, and mainnet's profile pointed at Planck's node is refused with "is not quantus". Supervisors start on demand from `chain_connect`, never at boot, because a profile whose endpoint does not exist yet (#53) would otherwise retry forever for nobody. `chain_status` reads without starting anything. Not yet exercised: a stream dropping mid-session and a runtime upgrade arriving while connected. Both go through the same loop and will be driven on the dev node in #24. Closes #20 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW |
||
|
|
f0b3c26714 |
feat(profiles): chains as data, shipped in the binary and overridable by file
ci / gate (push) Successful in 6m18s
`wallet-core/src/profiles.toml` carries what runtime metadata does not: genesis hash, SS58 prefix, symbol and decimals, coin type, the endpoint list, block time, and the rule for when the FIPS 204 signing context applies. Three profiles ship: Quantus mainnet at spec 152 with QTC, Planck with PLK (in the wallet only to prove the multi-chain UX), and a local dev node with an empty genesis hash that means "do not check". Endpoints are the names decided on #20 and #53, one per chain; until those exist an override file points a dev run at the public nodes. The override file is the same shape with every field optional: a known id has fields replaced, an unknown id must be complete and is appended. A mainnet endpoint must be wss, a genesis hash must be 66 characters or empty, a context at most 255 bytes; anything else is refused with the profile named. Adding a fourth chain is a TOML entry and no Rust, which the test does. `signing_context_for(profile, spec)` and `genesis_matches(profile, hash)` are the two pure functions the adapter (#19, #20) will call at connect. The app loads `<app data>/profiles.toml` or WALLET_PROFILES and exposes `chains_list`. Closes #18 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW |
||
|
|
18caa2b81c |
feat(session): password in, signatures out, locked again after idling
ci / gate (push) Waiting to run
`wallet_core::session::Session` is the only place decrypted key material lives between commands: one wallet open at a time, its signers and, when the file carried a phrase, its seed. Every signature goes through `Session::sign`, which applies the idle rule before it looks at the key, so a stale session refuses rather than signing one last time. Time is passed in, so the rule is tested at 59, 118 and 119 seconds without waiting. The seed is now a heap allocation behind a wiping box. Moving an inline 64-byte array through `Some(...)` and a function argument can leave copies in dead stack frames that no destructor visits; moving a box moves a pointer. A Linux test unlocks with a runtime-built seed and a signer whose secret is a second runtime-built pattern, locks, then reads every writable anonymous mapping in /proc/self/mem and asserts neither pattern is anywhere in the process. It passes in debug and release. The comparison copies are XOR-masked so the test cannot find itself. In the app: `unlock` opens a file, turns its phrase into a seed and drops the phrase; `lock` and `session_status` do what they say; a ticker thread applies the idle rule every five seconds and emits `session:locked` so the lock screen appears without a click. Ten minutes until settings exist. Closes #17 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW |
||
|
|
de55d1dfb9 |
feat(mnemonic): generate, validate, verify the backup, and the checkphrase
ci / gate (push) Has been cancelled
A new wallet's phrase is 24 words from OS entropy through the hdwallet crate, which wipes both the entropy and the phrase's own working buffers. Validation runs the same pinned bip39 the hdwallet crate stretches with, so a phrase that validates here derives there; it normalises case and whitespace and rejects the wrong count, an unknown word, and a bad checksum with an error a person can act on. The checkphrase is Quantus's human-readable address checksum through qp-human-checkphrase, with the 2048-word list vendored because the crate loads it from the working directory at runtime, which a desktop app cannot rely on. The value for the dev phrase's ML-DSA-65 account is pinned to what `quantus wallet view` prints. Creation is three commands so the webview sees a phrase exactly once: begin generates and holds it and returns the words with a four-word backup challenge; confirm passes the challenge or refuses, then derives at the scheme's conventional path and writes the file; cancel forgets. No command returns a phrase that already exists. Import from a phrase and from a CLI file, and listing, are the other commands. App state is a mutex around the pending creation; the keystore directory is the platform app-data directory unless WALLET_KEYSTORE_DIR says otherwise. Closes #16 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW |
||
|
|
5776cfd218 |
feat(keystore): read and write quantus-cli wallet files, byte for byte
ci / gate (push) Successful in 4m56s
`wallet_data::keystore` is the CLI's format: AES-256-GCM over the JSON of the wallet data, keyed by Argon2id under the frozen profile (m=19456, t=2, p=1) and a 16-byte salt, with the PHC string carrying the profile and salt and never the digest. A file whose PHC string does embed a digest is refused, as the CLI refuses it. On open the address is re-derived from the decrypted key and the file refused if it differs, so a file that lies about its address cannot be opened, let alone signed with. Legacy files with no `scheme` are ML-DSA-87; files with no `wallet_type` are hot. The directory rules are the CLI's too: a wallet name is a plain filename, writes go through an unpredictable temporary file and a no-clobber rename at mode 0600, and a symlink where a wallet should be is refused. Proof in both directions. Eight real files open here: six written by quantus-cli 2.2.2 for the dev phrase, one per scheme and path, and the two fixtures the extension fork tests against. A file written here lists in `quantus wallet list` with the expected address (ignored test, run with QUANTUS_CLI set). A test greps the written file for phrase words and secret key bytes and finds neither. `Keystore` is a port in wallet-core: list, open, create, import a file, export a file. Opening yields a `Signer` and, when the file carries one, the phrase in a wiping holder; neither leaves Rust. Closes #15 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW |
||
|
|
d951ce07e7 |
feat(ui): a typed command client, and one file allowed to touch the IPC
ci / gate (push) Waiting to run
Commands now return `Result<T, WalletError>`; Tauri rejects the promise with the serialised error, and `ui/src/api/wallet.ts` is the one place that turns it back into a thrown `WalletError` with the same `kind` the Rust enum has. Anything that is not a `WalletError` is a bug on one side of the boundary and is rethrown untouched so it is loud. Screens never call `invoke` themselves. React Query holds what Rust knows, with retries off (a wallet must not silently retry a command that failed) and refetch-on-focus off. CI counts the files importing Tauri's IPC module and fails unless the count is exactly one. The first command is `app_info`, so the window shows a version that came across the boundary as a typed struct rather than a string. Closes #13 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW |
||
|
|
257b021066 |
feat: scaffold the workspace, the tauri shell and the webview
ci / gate (push) Successful in 4m24s
The house shape (architecture/generic.md §1): wallet-entities for the vocabulary and the IPC wire types, wallet-core for the four ports that are the architecture (KeyScheme, ChainAdapter, SwapProvider, FiatRamp), wallet-data for the adapters that will implement them, and wallet-app as a Tauri 2 shell whose commands are one-line shims. The webview is Vite + React + TypeScript and reaches Rust only through ui/src/api/wallet.ts, typed by what ts-rs exports from the entities crate on every `cargo test`. Amounts are integer base units plus decimals, serialised as strings, because the webview has no integer wide enough and a wallet that rounds is a wallet that lies. The webview never receives a secret; readme.md and CLAUDE.md say why and what will bite. Two things the scaffold learned: Tauri's `version` field takes a JSON file or a literal, never Cargo.toml, so the release workflow stamps it; and Prettier must skip the generated types, which carry ts-rs's own style. Placeholder icon; the mark comes later. CI targets the rust-gtk3 runner. Refs #1 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW |