Commit Graph

33 Commits

Author SHA1 Message Date
2ro ce72e09ea2 README: init-once seed handling, file-based secret delivery as the recommended path 2026-07-04 22:48:02 -04:00
2ro 3da0e15e74 Merge security-mnemonic: init-once wallet open + Secret zeroize-on-drop 2026-07-04 22:47:03 -04:00
2ro fd763a086b pay.html: bump invoice slatepack textarea to rows=7 so ENDSLATEPACK is not clipped 2026-07-04 22:42:30 -04:00
2ro febb01dd79 Remove BADGE-INTEGRATION.md internal handoff note 2026-07-04 22:42:30 -04:00
2ro 3cb18614d2 Merge badge-black: black GoblinPay badge for light checkout surfaces 2026-07-04 22:41:41 -04:00
2ro e82593e7ee gp-core: zeroize Secret buffers on drop
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.
2026-07-04 22:36:01 -04:00
2ro ce1a59f553 gp-wallet: init-once wallet open (mnemonic creates, password opens)
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.
2026-07-04 22:35:54 -04:00
2ro 9f3380ca90 grin1 rail Phase 1 complete: in-process arti onion service (GRIM tor.rs port)
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"]}}
2026-07-04 22:31:05 -04:00
2ro 19c5f1023d brand: black GoblinPay badge (Apple Pay style) for light checkout surfaces
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.
2026-07-04 22:27:23 -04:00
2ro 34a44043f2 grin1 rail: operator opt-in, packaged default OFF (owner ruling)
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).
2026-07-04 22:27:10 -04:00
2ro f6fed1f3c3 README: 10-confirmation standard, confirmed webhook, branding, live relay pool
- 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.
2026-07-04 22:20:05 -04:00
2ro 975d1bd6ce grin1 rail: cancel stored context on invoice expiry (Phase 2)
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.
2026-07-04 21:59:32 -04:00
2ro 8f9bad98b0 gp-server: Grin Foreign API v2 on loopback for the onion service (Phase 1)
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).
2026-07-04 21:55:32 -04:00
2ro b6d0fe4706 gp-server: issue invoice slate on create + two-rail pay page (Phase 2/3/4)
- 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).
2026-07-04 21:49:16 -04:00
2ro a6d1a601b9 gp-core: grin1 rail invoice model + render gate + config flag (Phase 2/3)
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).
2026-07-04 21:49:06 -04:00
2ro 2b791a18a6 gp-wallet: native invoice-flow primitives (issue / finalize+post / cancel)
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.
2026-07-04 21:39:48 -04:00
2ro 580c313e6d confirm: configurable confirmation depth as a first-class invoice state
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.
2026-07-04 21:29:07 -04:00
2ro becfa14aec qr: plain QR by default (center logo now opt-in)
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.
2026-07-04 21:04:21 -04:00
2ro c5f56306c5 brand: self-contained Goblin mark in QR center + GoblinPay wordmark on connector page
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.
2026-07-04 20:48:47 -04:00
2ro 33c5ee568f chore: retire the dormant Nym client (Nym → Tor migration cleanup)
ci / fmt / clippy / test (push) Has been cancelled
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.
2026-07-04 06:31:18 -04:00
2ro 89eb4901bd fix(gp-server): silence clippy field_reassign_with_default in checkout tests
ci / fmt / clippy / test (push) Has been cancelled
2026-07-04 06:15:17 -04:00
2ro e9fc2f3d94 chore: gitignore local .claude/ workdir (not project config) 2026-07-04 06:15:15 -04:00
2ro 3b823b4750 docs(internal): Tor migration plan (GP_NYM already off; cleanup scope)
ci / fmt / clippy / test (push) Has been cancelled
2026-07-04 03:40:09 -04:00
2ro 3d36117d7b docs: bundled relay, GP_NYM production posture, connectors + deploy in README
ci / fmt / clippy / test (push) Has been cancelled
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.
2026-07-03 03:22:53 -04:00
2ro 3fdf4a230c M11: reproducible deploy pipeline
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.
2026-07-03 03:22:43 -04:00
2ro bba1dd5cba M9: Medusa v2 payment provider connector
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.
2026-07-03 03:22:36 -04:00
2ro c32ddfa9ff M8: bundled relay — RelayMode::Bundled runs a co-located nostr-rs-relay
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.
2026-07-03 03:22:29 -04:00
2ro b8434fdd36 config: GP_CHECKOUT_METHODS to pick which checkout methods show
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.
2026-07-02 19:29:43 -04:00
2ro 94d0c0edba checkout: first-class grin1 / Slatepack payment method
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.
2026-07-02 19:22:35 -04:00
2ro 3a80f7d505 checkout: show the GoblinPay mark on the QR and the pay page
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.
2026-07-02 17:00:36 -04:00
2ro c362a9af21 checkout: encode amount (and memo) in the invoice QR pay-URI
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.
2026-07-02 15:26:14 -04:00
2ro 3f5b1fe49b docs: drop internal milestone references from the README
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.
2026-07-02 13:20:48 -04:00
2ro bd67bfc92e GoblinPay: receive-only Grin payment server
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.
2026-07-02 04:29:54 -04:00