Skip to main content
← Back to Blog

Smirk Wallet v0.3.0: Nostr identity, three slate transports, and a backend you can run

v0.3.0 makes Smirk federated end to end: a NIP-07 provider with tiered signing, Grin slates that travel over Nostr instead of a shared server, and smirk-backend-core released so you can host the whole thing yourself.

Smirk Wallet v0.3.0

v0.3.0 is out. It is the release that removes the last places Smirk assumed our infrastructure: the backend host is configurable, Grin is fully non-custodial, and the server that used to be ours is now a public repo you can run yourself.

The wallet ships as a Chrome MV3 and Firefox extension, plus a Tauri 2.x desktop shell for Windows, macOS and Linux built from the same code path. Capacitor mobile is deferred to v0.4.

Federation

Nothing in the wallet points at api.smirk.cash any more. homeDomain is per-wallet configuration, and the backend it names is one instance of smirk-backend-core, which is now public under MIT.

Wallet AWallet Bbackend-corehomeDomain: a.examplebackend-corehomeDomain: b.exampleElectrum,LWS, grin-lwsElectrum,LWS, grin-lwsNostr relays: identity, DMs, slates.Wallet to wallet, across backends.

The two backends share nothing. A wallet on a.example can pay a wallet on b.example because identity and message transport ride Nostr relays, not a shared database.

Nostr identity, and why signing is tiered

The wallet exposes a NIP-07 window.nostr provider, so Nostr-native sites request a public key and get events signed without a second extension. A multi-identity vault keeps separate keypairs apart, with an in-wallet switcher.

The part worth explaining is what a connected site may sign without asking again. A Nostr signature is a bearer credential, so that is a security decision rather than a convenience setting. Kinds are sorted into three tiers, and the wallet handler is the enforcement point: it computes sessionCovered=false for anything not session-grantable and refuses to persist a money-tier kind into a session at all, so the rule holds even if the approval UI is compromised.

signEvent()from a sitekind tierclassifiermoney: 17, 27235, 30402, 22242always prompt, never session-grantablesession-grantable: 1, 7, 1059time-boxed grant, no re-prompt in windowdefault: everything elseprompt per event, unknown kinds land here

Kind 27235 is the instructive one. NIP-98 HTTP auth reads like plumbing, and it was session-grantable in an early draft. But the backend mints a full session from a NIP-98 event via POST /auth/nostr, so a grant meant for social posting would have let a site collect an auth token for the user's own backend. It is money-tier now, with an origin guard on the event's u tag.

Three transports for a Grin slate

Grin is interactive: a payment needs slatepack legs to travel between sender and receiver. A slate is one payload, and how it gets there is a separate choice with different trust properties.

Nostr gift-wrap (NIP-59 kind 1059)federated default, crosses backends, relays learn neither sender, amount, nor slatenpub / NIP-05Manual clipboarduniversal fallback, no network at all, age-encrypted to the recipient addressgrin1… addressBackend relaysame-instance shortcut only, both parties on one backend, never a requirementbackend user_id

What you type in the send box picks the transport. An npub or NIP-05 handle routes over Nostr. A bare grin1… address falls back to clipboard, unless a same-instance lookup can bridge that address to its owner's npub, in which case it is upgraded to gift-wrap. The backend relay is a convenience for two users on one host and is never on the critical path, because there is no cross-backend relay federation and there deliberately isn't going to be.

The backend

Rust, Axum, PostgreSQL. It gives a wallet chain reads, fee estimation and broadcast, Nostr identity, a price feed, a Grin slatepack mailbox and the public social-tips subsystem, without ever holding a spend key or a seed.

It stores public addresses, public keys, and for Monero and Wownero the incoming view keys. View credentials are forwarded per request to whatever scans with them and are not persisted. Signing and broadcast happen in the wallet.

| Chain | Source | |---|---| | Bitcoin, Litecoin | Electrum / Fulcrum | | Monero, Wownero | light-wallet-server, stateless view-key forwarding | | Grin | grin-lws, or grin-wallet view-only plus a node |

Chains are independently feature-flagged. One that is not configured reports enabled: false from /capabilities instead of failing when something calls it, so a Bitcoin-only deployment is a supported outcome rather than a half-broken one.

The HTTP contract is generated from the handlers into openapi.json, which is also what the wallet's client is generated from, with a CI gate that fails on drift.

Grin became fully non-custodial in this release. The backend no longer holds Grin balances, outputs or transaction records; those endpoints and the server-side wallets table are gone. Grin outputs use the standard deterministic rewind-nonce scheme, so a wallet restored from a seed phrase rediscovers its outputs from the chain the way Bitcoin and Monero restores already did. Outputs from earlier builds stay spendable.

Two supporting pieces are public for the same reason: grin-lws, a standalone Grin light wallet server, and a small non-custodial BTC/LTC payment processor exposing a BTCPay-style invoice surface where funds go straight to operator-controlled addresses.

Notes

v0.3.0 is feature-complete and has been through a security review, but the schema and API are young and may still move. If you self-host, run your own chain backends where you can and read the code first.

Also in this release: taproot (bc1p…, ltc1p…) recipients, cross-chain sends blocked before they can destroy funds, encrypted public tip keys at rest, a fee you sign that matches the fee you were shown, and connected sites made visible and revocable in Settings.