Skip to main content
← Back to Blog

Smirk Wallet v0.3.0: Nostr identity, federated backends, and Grin slate transports

v0.3.0 makes Smirk federated end to end: a configurable backend host, a NIP-07 provider with tiered signing, Grin slates over NIP-59 gift-wraps, and smirk-backend-core released under MIT.

Smirk Wallet v0.3.0

v0.3.0 removes the remaining assumptions about our infrastructure. The backend host is per-wallet configuration, Grin is fully non-custodial, and the server is a public repo.

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

No component points at api.smirk.cash. homeDomain is per-wallet configuration naming an instance of smirk-backend-core, released 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 no state. A wallet on a.example pays a wallet on b.example because identity and message transport ride Nostr relays rather than a shared database.

Backend origins: onion and LAN

A backend URL must be https, with three exemptions where requiring TLS would make a legitimate instance unreachable:

OriginAccepted over plain httpReason
.onion (16 or 56 char)yesOnion services carry their own transport authentication and encryption, and no CA issues certificates for them
Private ranges, .localyesA self-hoster running a box on their own LAN
localhostyesLocal development

Everything else is rejected. The onion pattern matches the label immediately before .onion, so abcdefghijklmnop.onion.example.com is a normal public host and is refused.

The backend's admin plane binds to loopback by default. Exposing it requires an SSH tunnel or an onion: TOR_ADMIN_ONION=<hash>.onion adds that one host to the admin Host allowlist, which otherwise accepts only loopback names. A request arriving with any other Host gets a 403, which is the DNS-rebinding guard. A non-loopback bind additionally requires ADMIN_ALLOW_PUBLIC_BIND=true.

There is no bundled Tor daemon and no I2P support. The wallet accepts onion origins; routing to them is the operator's Tor instance.

Nostr identity and tiered signing

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

A Nostr signature is a bearer credential, so which events a connected origin may sign without a fresh prompt is a security decision. Kinds are classified into three tiers.

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

The wallet handler is the enforcement point. It computes sessionCovered=false for any kind that is not session-grantable and refuses to persist a money-tier kind into a session, so the classification holds independently of the approval UI.

Kind 27235 is NIP-98 HTTP auth. The backend mints a full session from a NIP-98 event via POST /auth/nostr, so it is money-tier, with an origin guard on the event's u tag.

Grin slate transports

Grin is interactive: a payment requires slatepack legs to travel between sender and receiver. The slate is one payload; the channel is a separate choice.

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

The address in the send box selects the transport. An npub or NIP-05 handle routes over Nostr. A bare grin1… address defaults to clipboard, and is upgraded to gift-wrap when a same-instance lookup resolves that address to its owner's npub. The backend relay serves two users on one host; there is no cross-backend relay federation.

Counterparty keys are pinned on first use, so a later key change for the same NIP-05 handle is surfaced rather than trusted.

smirk-backend-core

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

Stored: public addresses, public keys, and for Monero and Wownero the incoming view keys. View credentials are forwarded per request to the chain backends that scan with them and are not persisted. Signing and broadcast happen in the wallet.

ChainSource
Bitcoin, LitecoinElectrum / Fulcrum
Monero, Wownerolight-wallet-server, stateless view-key forwarding
Gringrin-lws, or grin-wallet view-only plus a node

Chains are independently feature-flagged. An unconfigured chain reports enabled: false from /capabilities rather than failing at call time, so a Bitcoin-only deployment is a supported configuration.

The HTTP contract is generated from the handlers into openapi.json, which is also the source for the wallet's generated client. A CI gate fails the build on drift.

Grin is 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 removed. Grin outputs use the standard deterministic rewind-nonce scheme, so a wallet restored from a seed phrase rediscovers its outputs from the chain, matching how Bitcoin and Monero already restored. Outputs created by earlier builds remain spendable.

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

Also in this release

Taproot (bc1p…, ltc1p…) recipients in Send and Swap. Cross-chain sends blocked, where a Monero address could previously be used as a Wownero destination. Public tip keys encrypted at rest. A custom fee rate below the relay minimum now fails rather than being silently raised at broadcast. Connected sites visible and revocable in Settings.

v0.3.0 is feature-complete and security-reviewed. The schema and API may still change; run your own chain backends where possible.