The Secret wrapper (mnemonic, wallet password, API/admin/webhook tokens, nsec)
now zeroizes its backing String on drop. Defense in depth: the value no longer
lingers in freed heap after the config or a transient clone is dropped. The
seed is also handed to grin-wallet as a ZeroingString on the create path, so
both the gp-core copy and the wallet-stack copy are wiped.
zeroize 1.x is already in the dependency tree via the grin/nostr stacks.
The mnemonic is now consumed only on first-run wallet creation. Once the
encrypted seed file exists, boot needs only GP_WALLET_PASSWORD; GP_MNEMONIC
becomes optional. When still supplied against an existing wallet it is used
solely as a non-destructive cross-check that it matches the seed at rest
(never re-derives or re-creates). gp-server warns the operator to remove
GP_MNEMONIC from the environment once the wallet is initialized.
open_at now takes Option<&str> for the mnemonic; first-run with no mnemonic
fails fast with a clear message. Adds tests for reopen-without-mnemonic and
first-run-without-mnemonic; updates the round-trip test callers.
gp-server/src/tor.rs — the GRIM onion-service pattern on the same arti 0.43
stack (grim/src/tor/tor.rs: start_service ~:507 -> spawn/run, run_service_proxy
~:766 -> the hsrproxy setup, add_service_key ~:819 -> add_service_key):
- The till wallet's index-0 slatepack address key IS the onion identity
(HsIdKeypair via the standard SHA-512 seed expansion), so grin1 address ==
onion address. gp-wallet exposes slatepack_secret_seed() +
slatepack_address_pubkey(); unit tests prove seed -> address-pubkey and
onion(seed) == onion(grin1-decoded pubkey) equivalence both ways.
- One stable service, launched at boot when GP_GRIN1_RAIL=on and a wallet is
loaded; proxies onion:80 -> 127.0.0.1:GP_GRIN1_FOREIGN_PORT (the Foreign API
v2). Arti state/cache/keystore under <GP_DATA_DIR>/tor/, 0700 (fs-mistrust).
Startup logs the onion identity before bootstrap and again once running.
- Scope cuts vs GRIM, deliberate: no bridges/pt-client (server), no restart
supervisor (systemd owns the process lifecycle).
tests/tor_roundtrip.rs (#[ignore], network): REAL Tor round trip — real foreign
handler on loopback, onion service with a throwaway key, SEPARATE arti client
dials the .onion over the live Tor network and POSTs check_version. VERIFIED
PASSING locally in 44.5s: reply
{"id":1,"jsonrpc":"2.0","result":{"foreign_api_version":2,"supported_slate_versions":["V4"]}}
Add the light-surface counterpart to the white wordmark: a flat black
rounded-rect badge with the goblin mark (reused wallet head geometry) next
to the "Pay" wordmark. Does the job the black Apple Pay badge does at
checkout: a compact, instantly recognisable payment-method mark.
- static/goblinpay-badge-black.svg: self-contained inline SVG, no external
font (mark reuses goblin-mark-white geometry; "Pay" uses the same system
font stack as the existing wordmark).
- gp-server: serve it at /static/goblinpay-badge-black.svg.
- WooCommerce: wire the badge into the checkout payment-method row for both
the classic gateway (get_icon override) and the Blocks checkout (data-URI
icon in the method label).
- BADGE-INTEGRATION.md: note for applying the badge to the hosted pay page
after the grin1 rail work merges (pay.html left untouched to avoid conflict).
The existing white wordmark stays for dark surfaces; this only adds the
black-badge variant.
GP_GRIN1_RAIL now defaults OFF. Unset/off, GoblinPay behaves like the pre-rail
server: no arti/Tor code path runs, no loopback Foreign API listener, no
invoice slates issued, and the pay page shows ONLY 'Pay with Goblin' (no
switcher, no grin1 UI, footer unchanged) — even for an invoice armed while the
rail was on. Enabled, the two-rail switcher renders with the Goblin tab
default-selected; the switcher chrome appears only when BOTH rails are
available (a single rail renders its panel directly).
- config: default flip + docs; env example + README gain the opt-in block.
- checkout: build_info gates all grin1 UI on the flag; tests for both states
(off => no rail-tab/rail-radio/panel-grin/grin1 strings; on => switcher with
rail-goblin checked).
- sqlx 0.8 -> 0.9 (workspace): its libsqlite3-sys range (<0.38) unifies with
arti's rusqlite, resolving the one-links-per-native-lib conflict that
otherwise forbids sqlx-sqlite + in-process arti in one binary. Feature map:
runtime-tokio + sqlite (bundled, as before); dynamic COLUMNS queries wrapped
in sqlx::AssertSqlSafe (compile-time composed strings).
- Invoice lifecycle open -> paid -> confirmed; GP_CONFIRMATIONS (default
10); status APIs expose confirmations + confirmations_required;
checkout shows Confirming n of 10; payment.confirmed webhook and
idempotent connector handling documented.
- Branding: wordmark header on pay pages and the WooCommerce panel;
plain QR by default with GP_QR_LOGO opt-in.
- GP_RELAYS default is the live wallet pool (relay.floonet.dev,
relay.0xchat.com, offchain.pub).
- Slatepack checkout described as the address-less manual exchange;
unshipped rails stay undocumented.
A late payer I2 must not settle an expired invoice. Add a periodic sweep that,
for each open grin1 invoice past its expiry, cancel_tx's its stored wallet
context (finalize then fails with no context) and only then marks it expired.
Cancel-first ordering means a node hiccup leaves the invoice open and retries
next pass rather than expiring it un-cancelled.
- gp-core: due_grin1_slates (select open+due grin1, no status flip) + mark_expired.
- gp-server: foreign::spawn_expiry_cancel sweep (60s), launched with the rail.
Serve the grin1 rail's receiving surface as JSON-RPC 2.0 at POST /v2/foreign on
127.0.0.1:<GP_GRIN1_FOREIGN_PORT>, wire-compatible with stock grin-wallet:
- gp-wallet: receive_slate / finalize_slate operate on VersionedSlate objects
(not armor), returning the reply slate in the sender's version plus the ledger
bundle; check_version re-exported. finalize uses foreign::finalize_tx (posts
the tx). VersionedSlate/Slate/VersionInfo re-exported.
- gp-server foreign.rs: check_version / receive_tx / finalize_tx dispatch.
receive_tx records + AMOUNT-matches (plain-send mapping); finalize_tx settles
the issued invoice by slate id (flip paid, record payment at the INVOICE's
amount since an I2 zeroes it), confirmation poll carries it to confirmed.
Heavy wallet calls run off the workers; settlement is idempotent.
- main.rs: launch the loopback foreign server when the rail is armed + a wallet
is loaded.
The arti onion transport (onion:80 -> loopback) is provisioned at deploy time
per the grim tor.rs port; the till's index-0 slatepack key is the onion identity
(grin1 address == onion address). Tested: settlement DB path (paid flip, amount
source, idempotency, unknown-slate no-op) + check_version/dispatch envelopes.
Full slate round trip is a deploy-time gate (needs a live node + Tor).
- invoices.rs: arm_grin1_rail issues a native Grin invoice slate for every
exact-amount invoice at create time (off the async workers; best-effort) and
stores its armored I1 + slate id.
- checkout.rs: build_info surfaces the invoice slatepack (primary Grin method)
and gates the plain-send address panel behind the render gate.
- pay.html: reworked into a CSS-only two-rail switcher — 'Pay with Goblin'
(nprofile) and 'Pay with any Grin wallet' (invoice slatepack primary,
plain-send address only when the gate allows). Zero-JS meta-refresh + wordmark
header + dark theme preserved; plain QRs; no em dashes.
- style.css: rail switcher styles (hidden radios + :checked sibling combinator).
Migration 0009 adds invoice.rail/slate_id/slatepack. New invoice functions:
- attach_grin1: arm an invoice on the grin1 rail with the issued I1 slate id
and armored slatepack.
- get_by_slate_id: look up the invoice a returning finalize settles.
- plain_send_allowed: the Phase-3 render gate (no jitter) — only the earliest
open grin1 invoice for a given exact amount may show the plain-send address
panel; newer amount collisions hide it. Ordered by rowid (true insertion
order), amountless/off-rail invoices never show plain-send.
Config: GP_GRIN1_RAIL (default on) + GP_GRIN1_FOREIGN_PORT (default 3416).
Add the receiver-initiated invoice-flow half of a standard Grin interactive
transaction on the upstream grin-wallet stack:
- issue_invoice(amount): owner::issue_invoice_tx -> stored context keyed by
slate id, returns the armored I1 slatepack (plain armor) + slate id.
- finalize_invoice_slatepack(i2): loads context by slate id, owner::finalize_tx,
then post_tx over DIRECT HTTP; returns slate id + kernel excess for the
ledger. Non-I2 / unknown-context slates error cleanly (no double-post).
- cancel(slate_id): owner::cancel_tx on expiry so a late I2 cannot settle.
Kernel-excess read factored into a shared helper (reused by receive_slatepack).
uuid 0.8 pinned to grin-wallet's line for slate-id parsing. Offline error-path
unit tests added; full cross-wallet round trip is a deploy-time gate.
House standard: a payment is final only after GP_CONFIRMATIONS (default 10)
on-chain confirmations. Add a terminal `confirmed` invoice state after `paid`
and drive it from the existing kernel-confirmation poll.
- config: GP_CONFIRMATIONS (default 10, must be >= 1).
- invoice: additive `confirmed` status (open -> paid -> confirmed); `paid`
stays a real, backward-compatible state. mark_confirmed() + a confirmations
reader off the paying payment.
- poll: track live confirmation depth per payment; advance the paying invoice
paid -> confirmed at the threshold and enqueue a payment.confirmed webhook
(once, on the transition). Self-terminating per payment.
- APIs: GET /invoice/{id}, GET /pay/{token}/status and GET /payment/{id} now
expose status + confirmations + confirmations_required.
- checkout: received -> confirming (n of N) -> confirmed, zero-JS meta-refresh
kept live through the confirming state.
- webhook: payment.confirmed carries confirmed_height + confirmations (the woo
and medusa connectors already consume this event).
- migration 0008: payment.confirmations, invoice.confirmed_at.
- tests: threshold transition, config default 10, payload shape, API wiring.
Owner call: no logo inside payment QR codes. GP_QR_LOGO unset now maps to
Logo::None; the inlined Goblin mark stays available as GP_QR_LOGO=builtin and
custom image URLs still work. ECC stays at level H (the pre-existing level).
Wordmark header and connector page refresh are unchanged.
The QR center logo was an <image href=/static/...> that 404s when the qr_svg
is embedded cross-origin (WooCommerce, Medusa), so it was disabled in prod.
Inline the Goblin mark as vector shapes instead: a Logo enum (None/Builtin/Href)
where Builtin draws the mark directly, keeping the QR self-contained on any
origin at ECC level H. GP_QR_LOGO still accepts a custom image URL or off.
WooCommerce order-received panel: add the GoblinPay wordmark header (Apple Pay
style) and refresh the dated card to the brand dark theme; allow g/circle in the
QR SVG sanitiser so the inlined mark survives wp_kses.
Production has run GP_NYM=off for over a day: GoblinPay already reaches its
relays over clearnet, which is the end state the ecosystem's Nym → Tor move
aims for. This removes the now-dead ported Nym client and its plumbing. Pure
subtraction — the code path that remains is exactly the one already running
live, so there is no behavior change beyond "GoblinPay still boots and still
moves money." Per TOR-MIGRATION-PLAN.md.
Removed:
- crates/gp-nostr/src/nym/ (mod, transport, nymproc, dns — 599 lines).
- smolmix + hickory-proto (and the nym-only rand) deps from gp-nostr, and
their transitive tree from Cargo.lock.
- GP_NYM config plumbing (gp-core config: field, parse, default, summary),
the opts.nym service branch, the main.rs warm-up wiring, and the admin
dashboard Nym row.
- The nym leg of the CI sibling-checkout gate (.github + .gitea), the
`COPY nym` / sibling mentions in Dockerfile/compose/install.sh, and the
GP_NYM copy in README, .env.example, and the three connector docs.
GoblinPay does not gain a Tor transport of its own: it is receive-only
infrastructure, and the sender privacy that matters rides the paying
customer's own Goblin Wallet. Content encryption (NIP-44 in a NIP-59
gift-wrap) and the slatepack (grin1) path are untouched. The optional
onion-dialing and relay-list trim the plan flags are left for the owner.
Document bundled mode and GP_BUNDLED_RELAY_URL, state GP_NYM=off as a supported server-side-clearnet posture (the payer's wallet still provides privacy) rather than debugging-only, and add Connectors (WooCommerce/Medusa/REST) and Deploy sections. The README ends with the AI pair-programming credit line.
Multi-stage non-root Dockerfile (builds -p gp-server against the nip44/nym siblings; excludes the goblin-tree dev crate), a full docker-compose (server + bundled nostr-rs-relay + auto-HTTPS Caddy), a hardened systemd unit (DynamicUser, ProtectSystem=strict, NoNewPrivileges, seed via LoadCredential), an install.sh bare-metal bootstrap, .env.example, and an fmt+clippy+test CI workflow for Gitea and GitHub.
A minimal receive-only GoblinPay payment provider for Medusa v2 under connectors/medusa (service + module registration + types, README + INSTALL), modeled on connectors/woocommerce and the medusa-plugin-btcpay reference: create-invoice on initiate, an HMAC-verified webhook flips the payment to captured, status polling as the webhook-miss fallback. Refunds throw (receive-only, manual). Also refresh the store.rs docstring now that WooCommerce and Medusa have shipped.
Make bundled mode actually self-contained: resolve() now leads the relay set with GP_BUNDLED_RELAY_URL (default ws://127.0.0.1:7777), which the checkout nprofile advertises, so a merchant needs no third-party relay. External mode uses only GP_RELAYS. Ship the relay as a vendored, unmodified nostr-rs-relay config (deploy/relay/nostr-rs-relay.toml); the compose service arrives with the deploy pipeline. Fix the stale "bundled is a later milestone" comment and reconcile the GP_NYM=off wording to a supported server-side-clearnet posture.
Operators can now choose which payment methods the hosted /pay page
offers: GP_CHECKOUT_METHODS=nostr, =slatepack, or nostr,slatepack
(unset = both, current behavior). Gates the two checkout sections
independently; slatepack still also requires a loaded wallet. The Nostr
ingest service (GP_INGEST) and the /invoice JSON API are unchanged.
The hosted /pay page now shows the wallet's grin1 Slatepack address (with
a QR and the exact amount) as a payment method alongside the Goblin/Nostr
option. A payer sends the amount from any Grin wallet via the Slatepack or
file method, pastes the S1 into the existing paste box, receives an S2,
and finalizes to complete the payment. Reuses the existing offline
receive_tx flow bound to the invoice token; the Nostr gift-wrap path, the
invoice matcher, and the proof/confirm logic are unchanged. No Tor
listener. The grin1 address is the wallet's stable index-0 address.
The hosted checkout page gains the GoblinPay wordmark header, and the QR
center logo defaults to a new high-contrast GoblinPay mark (dark P on
brand gold) instead of the generic goblin mark. Still overridable via
GP_QR_LOGO (url/path/off); the legacy /static/goblin-mark.svg route is
kept for operators who pinned it.
The checkout QR now carries the payable amount so a scanning Goblin
wallet auto-fills it:
nostr:<nprofile>?amount=<decimal GRIN>&memo=<percent-encoded>
The amount param is added only for an exact-amount invoice; open-amount
invoices stay a bare nostr:<nprofile>. The memo is percent-encoded. The
URI never includes the invoice token or any key - only the already-public
recipient nprofile, relay hints, amount, and the memo shown on the page.
The human-readable nprofile/npub page strings are unchanged.
Strip the internal milestone scaffolding (Status: milestone N, the (M4)/(M5)/
(M5b)/(M6) feature tags, and the milestone-11 roadmap line) from the public
README. The feature docs, config, API, and webhook contract are unchanged --
only the internal planning labels are removed.
A self-hostable Grin payment server for shops, creators, and sites: show a
code, Grin lands in your wallet, with a verifiable Grin payment proof on
receive. Workspace crates (gp-core / gp-nostr / gp-server / gp-wallet /
gp-goblin-sender), a WooCommerce connector, a hosted /pay/<token> checkout,
and NIP-44 v3 gift-wrapped payment DMs carried over the Nym mixnet. All
secrets are read from the environment; none are committed.