From 7cfd81962e96b8d6a489f1c2aa85ebd116c01b25 Mon Sep 17 00:00:00 2001 From: 2ro <17595647+2ro@users.noreply.github.com> Date: Sun, 5 Jul 2026 01:02:46 -0400 Subject: [PATCH] docs: setup wizard as the recommended path, quick starts, API integrator guide - README: a Setup (recommended path) section leading with `gp-server setup`; the env-var table reframed as the advanced/manual path. Deploy section notes the plugin zip script. - docs/woocommerce-quickstart.md, docs/medusa-quickstart.md: four-step operator quick starts (install, run setup, paste the three values, test a payment). Medusa notes its webhook route differs from the WooCommerce default. - docs/api-integration.md: the direct-integration path (the way magick.market integrates) - create-invoice, GET /invoice/{id}, bearer auth, and the payment.confirmed webhook payload + HMAC signature + retry semantics, with the base-units vs display-string amount distinction called out honestly. --- README.md | 71 ++++++++++++- docs/api-integration.md | 186 +++++++++++++++++++++++++++++++++ docs/medusa-quickstart.md | 84 +++++++++++++++ docs/woocommerce-quickstart.md | 72 +++++++++++++ 4 files changed, 409 insertions(+), 4 deletions(-) create mode 100644 docs/api-integration.md create mode 100644 docs/medusa-quickstart.md create mode 100644 docs/woocommerce-quickstart.md diff --git a/README.md b/README.md index b78fb9b..50a69fe 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,51 @@ mandatory baseline. Supporting directories: `migrations/` (raw sqlx SQL), `templates/` (Askama, zero JS), `static/` (one hand-written CSS file, no build step). +## Setup (recommended path) + +The fastest way to stand up a till is the built-in wizard. Install the binary +and unit (`deploy/install.sh` does this and then offers to run the wizard for +you), then: + +``` +sudo gp-server setup +``` + +It asks five questions, each with a default: + +1. the public URL customers reach this till at, +2. your shop's website URL (used to build the webhook URL), +3. the Grin seed: press Enter to generate a fresh till seed (shown once, write + it down) or paste an existing 24 words, +4. the currencies your shop prices in (default `usd`), +5. an advanced yes/no for the grin1/Tor rail (default no). + +Everything else it does for you: + +- generates the wallet password, the API token, the admin token, and the + webhook secret (you never invent or type a secret); +- creates the encrypted wallet on the spot from the seed, so the seed is + consumed once and never lives in the service environment afterwards (it + exists only encrypted at rest and in your written backup); +- probes a curated list of healthy mainnet Grin nodes and picks the first that + answers, falling back automatically; +- defaults the relays to an external vetted pool (the wallet's proven relays); +- writes `/etc/goblinpay.env` (mode 0640, holds the config plus the bearer + tokens) and `/etc/goblinpay/secrets/wallet_password` (mode 0400), exactly + where the shipped `gp-server.service` looks (`EnvironmentFile` + + `LoadCredential`); +- prints the webhook URL and the three values to paste into WooCommerce + (GoblinPay URL, API Token, Webhook Secret) plus the private admin token. + +Re-running is safe: the wizard refuses to overwrite an existing wallet or config +unless you pass `--reconfigure` (which keeps the existing seed and password and +only rewrites the config/tokens). Flags: `--reconfigure`, `--prefix DIR` (write +under a prefix instead of `/`), `--node URL` (skip the node probe), `--batch` +(read scripted answers from a non-terminal stdin). + +The env-var reference below is the advanced path for operators who want to +configure GoblinPay by hand; the wizard hides all of it. + ## Configuration Everything is environment variables, defaults are safe for local use. @@ -301,15 +346,33 @@ Both the WooCommerce and Medusa connectors act on the `payment.confirmed` webhook idempotently: they complete the order if it is not already complete, and otherwise just note the confirmation. +Quick starts and the integrator guide live in `docs/`: + +- [`docs/woocommerce-quickstart.md`](docs/woocommerce-quickstart.md) - install + the plugin, run `gp-server setup`, paste three values, test a payment. +- [`docs/medusa-quickstart.md`](docs/medusa-quickstart.md) - the same for a + Medusa v2 store (note: set `GP_WEBHOOK_URL` to the Medusa route after setup). +- [`docs/api-integration.md`](docs/api-integration.md) - integrate directly + (the way magick.market does): your service calls create-invoice, the customer + pays the till wallet-to-till over the encrypted Nostr rail (no coins pass + through the API or your service), and you grant on confirmed status. Covers + `POST /invoice`, `GET /invoice/{id}`, bearer auth, and the `payment.confirmed` + webhook payload + retry semantics. + Refunds are unsupported/manual everywhere (GoblinPay is receive-only). ## Deploy `deploy/` holds a reproducible deployment: a hardened systemd unit -(`gp-server.service`) with `deploy/install.sh` for bare metal, and a -`docker-compose.yml` that brings up the server, the bundled relay, and an -auto-HTTPS Caddy proxy. CI (`.github` / `.gitea` workflows) runs fmt, clippy, -and tests. See `deploy/` for details. +(`gp-server.service`) with `deploy/install.sh` for bare metal (which ends by +offering to run `gp-server setup`), and a `docker-compose.yml` that brings up +the server, the bundled relay, and an auto-HTTPS Caddy proxy. CI (`.github` / +`.gitea` workflows) runs fmt, clippy, and tests. See `deploy/` for details. + +`deploy/package-woocommerce.sh` builds `goblinpay-woocommerce.zip` (a single +top-level `goblinpay-woocommerce/` folder) for a WooCommerce release, so the +shop owner's step is Upload Plugin -> Activate -> paste the three values the +wizard printed. ## Credits diff --git a/docs/api-integration.md b/docs/api-integration.md new file mode 100644 index 0000000..b066ef2 --- /dev/null +++ b/docs/api-integration.md @@ -0,0 +1,186 @@ +# Integrating with the GoblinPay API + +This is the direct-integration path: your service creates an invoice, the +customer pays GoblinPay **directly, wallet-to-till**, and you grant value once +the payment reaches confirmed status. It is the same path magick.market uses. + +The important property: **no coins ever pass through the API or through your +service.** Your backend only ever calls create-invoice and reads status. The +actual payment is a private, encrypted Grin transfer that travels from the +customer's Goblin wallet to the till over the Nostr gift-wrap rail (or, for +non-Goblin wallets, over the optional grin1/Tor rail). Your server is never in +the money path; it is only in the *authorization* path. + +``` +your service ──POST /invoice──▶ GoblinPay ──returns nprofile + pay_url──▶ you show the customer +customer's wallet ═══ encrypted Grin payment ═══▶ GoblinPay till (never touches you) +GoblinPay ──payment.confirmed webhook──▶ your service (or you poll GET /invoice/{id}) +your service grants the goods on "confirmed" +``` + +## Authentication + +Every API call carries a bearer token: + +``` +Authorization: Bearer +``` + +`GP_API_TOKEN` is generated for you by `gp-server setup` (shape `gp_live_…`). +With no token configured the write API is closed (returns `503`), never open. +A missing or wrong token returns `401`. + +## Create an invoice + +``` +POST /invoice +Authorization: Bearer +Content-Type: application/json +``` + +Request body (provide **either** `amount_grin` **or** `amount_fiat` + +`currency`): + +| Field | Type | Notes | +|---|---|---| +| `amount_grin` | integer | Exact amount in **base units (nanogrin)**. 1 GRIN = 1_000_000_000 nanogrin. | +| `amount_fiat` | string | Decimal fiat amount (e.g. `"12.50"`). Priced to Grin at create time via the rate oracle. | +| `currency` | string | ISO code for `amount_fiat` (must be in `GP_RATE_CURRENCIES`). | +| `order_ref` | string | Your order id. Used as the memo/subject match key and echoed back. Optional but recommended. | +| `memo` | string | Human note shown on the checkout page. Optional. | +| `match_mode` | string | Per-invoice override: `memo`, `derived`, or `amount`. Optional; defaults to the server's `GP_MATCH_MODE` (`derived` is recommended). | + +Example: + +```bash +curl -sS https://pay.myshop.com/invoice \ + -H "Authorization: Bearer $GP_API_TOKEN" \ + -H 'Content-Type: application/json' \ + -d '{"amount_grin": 2000000000, "order_ref": "order-1042", "memo": "Order #1042"}' +``` + +Response (`200`): + +```json +{ + "invoice_id": "…", + "token": "…", + "pay_url": "https://pay.myshop.com/pay/…", + "recipient_pubkey": "…", + "npub": "npub1…", + "nprofile": "nprofile1…", + "qr_svg": "", + "amount": "2 GRIN", + "status": "open", + "confirmations": 0, + "confirmations_required": 10, + "order_ref": "order-1042", + "memo": "Order #1042" +} +``` + +To collect payment you have two choices, both in the response: + +- **Redirect / link** the customer to `pay_url` (the hosted checkout page, + which renders the QR, live status, and the manual-paste fallback), or +- **Render your own QR** from `nprofile` (a Goblin wallet scans `nostr:`) + or embed the ready-made `qr_svg`. + +### About the `amount` fields (read this) + +Be careful, because base units and display strings both appear: + +- In the **request**, `amount_grin` is **base units (nanogrin)** - an integer. +- In the **response**, `amount` is a **human display string** (e.g. `"2 GRIN"`, + or `"12.50 usd (~150 GRIN)"` for a fiat invoice). It is for display only; do + not parse it for accounting. +- In the **webhook** (below), `payment.amount` is **base units (nanogrin)** as + an integer, and `payment.amount_grin` is the human decimal string. + +When you reconcile, trust the base-unit integers (`amount_grin` you sent and +`payment.amount` in the webhook), not the display string. + +## Read invoice status + +``` +GET /invoice/{invoice_id} +Authorization: Bearer +``` + +Returns the same JSON shape as create, with the current `status`, +`confirmations`, and `confirmations_required`. Status advances: + +``` +open ──▶ paid ──▶ confirmed +``` + +- `open`: created, not yet paid. +- `paid`: the payment was received and matched to this invoice (in the mempool + / low confirmations). +- `confirmed`: the paying kernel reached `confirmations_required` + (`GP_CONFIRMATIONS`, default 10). **Grant the goods on `confirmed`.** + +Polling `GET /invoice/{id}` server-to-server is a complete integration on its +own if you would rather not run a webhook endpoint. + +## The `payment.confirmed` webhook + +If you set `GP_WEBHOOK_URL` (and `GP_WEBHOOK_SECRET`, which the wizard +generates), GoblinPay POSTs a signed JSON event to your endpoint on each +payment event so you do not have to poll. + +Body: + +```json +{ + "event_id": "5f3c…", + "event_type": "payment.confirmed", + "payment": { + "slate_id": "…", + "amount": 2000000000, + "amount_grin": "2", + "status": "confirmed", + "confirmations": 10 + }, + "invoice_id": "…", + "order_ref": "order-1042" +} +``` + +- `event_type` is `payment.received` (first seen) or `payment.confirmed` + (reached `GP_CONFIRMATIONS`). **Grant on `payment.confirmed`.** +- `payment.amount` is **base units (nanogrin)**; `payment.amount_grin` is the + human decimal string. `payment.confirmations` is present only on + `payment.confirmed`. +- `invoice_id` / `order_ref` tie the event back to your order (`invoice_id` + may be null for an unmatched payment). + +### Verifying the signature + +Every delivery carries two headers: + +``` +X-GoblinPay-Signature: sha256= +X-GoblinPay-Delivery: +``` + +`` is `HMAC-SHA256(GP_WEBHOOK_SECRET, raw_request_body_bytes)`. Recompute +it over the **raw bytes** you received (do not re-serialize the JSON) and +compare in constant time. Reject on mismatch. + +### Retry and idempotency semantics (as implemented) + +- **At-least-once.** Deliveries are persisted; a mid-retry crash resumes. +- **Ack with 2xx.** Any `2xx` marks the delivery done. Any other status, or a + transport error, reschedules it. +- **Backoff.** `min(BASE * 2^(attempts-1), 3600s)`, doubling per failed attempt + up to a 1-hour cap. +- **Give up after 12 attempts** (`MAX_ATTEMPTS`). +- **Deduplicate on `event_id`** (also the `X-GoblinPay-Delivery` header). A + retried delivery repeats the same `event_id`, so make your handler + idempotent: if you have already granted this order, just return `2xx`. + +## Refunds + +GoblinPay is receive-only: there is no refund API. Refunds are handled +out-of-band by sending Grin back from your wallet. diff --git a/docs/medusa-quickstart.md b/docs/medusa-quickstart.md new file mode 100644 index 0000000..c6270a7 --- /dev/null +++ b/docs/medusa-quickstart.md @@ -0,0 +1,84 @@ +# Medusa quick start + +Take Grin payments in a Medusa v2 store. You run the GoblinPay till on your own +server and add the GoblinPay payment provider to your Medusa app. + +## 1. Install GoblinPay and run the wizard + +On your server: + +```bash +sudo ./deploy/install.sh # installs, then offers the wizard +# or, if already installed: +sudo gp-server setup +``` + +The wizard makes your till wallet, generates every secret, picks a healthy Grin +node, and writes the config. Note the three values it prints at the end: the +**till URL**, the **API Token** (`gp_live_…`), and the **Webhook Secret** +(`whsec_…`). + +> **One Medusa-specific step.** The wizard fills in a *WooCommerce* webhook URL +> by default. Medusa uses a different route, so after setup, edit +> `/etc/goblinpay.env` and change `GP_WEBHOOK_URL` to your Medusa route (see +> step 3), then `sudo systemctl restart gp-server`. Everything else the wizard +> generated (tokens, secret, wallet) is reused as-is. + +Start the till: + +```bash +sudo systemctl start gp-server +``` + +## 2. Add the provider to your Medusa app + +Copy the `connectors/medusa` directory into your app (e.g. +`src/modules/goblinpay`), or install it as `medusa-payment-goblinpay`. Register +it in `medusa-config.ts` under the payment module's `providers` with +`id: "goblinpay"`, and set its options from the environment: + +```ts +options: { + baseUrl: process.env.GOBLINPAY_URL, // your till URL + apiToken: process.env.GOBLINPAY_API_TOKEN, // the gp_live_… value + webhookSecret: process.env.GOBLINPAY_WEBHOOK_SECRET, // the whsec_… value + matchMode: "derived", +} +``` + +In your Medusa `.env`: + +``` +GOBLINPAY_URL=https://pay.myshop.com +GOBLINPAY_API_TOKEN= +GOBLINPAY_WEBHOOK_SECRET= +``` + +Enable the `goblinpay` provider in the region(s) that should offer Grin +(Medusa admin → Settings → Regions → Payment Providers). + +## 3. Point the till's webhook at Medusa + +The Medusa payment webhook route id is `_`, both +`goblinpay`, so set on the GoblinPay server (`/etc/goblinpay.env`): + +``` +GP_WEBHOOK_URL=https://YOUR-MEDUSA-HOST/hooks/payment/goblinpay_goblinpay +``` + +Then `sudo systemctl restart gp-server`. The `GP_API_TOKEN` and +`GP_WEBHOOK_SECRET` the wizard generated must equal the `apiToken` and +`webhookSecret` you set in Medusa (they already do if you copied them across). + +## 4. Test a payment + +Place a test order and choose Grin (GoblinPay). The storefront shows the +GoblinPay QR or redirects to the `/pay/` page. Pay from your +[Goblin wallet](https://goblin.st); the order's payment flips to captured once +GoblinPay delivers the webhook. If a delivery is ever missed, the provider falls +back to polling `GET {baseUrl}/invoice/{invoice_id}`. + +## Notes + +- **Refunds** are manual (receive-only): send Grin back from your wallet. +- For the full provider reference see `connectors/medusa/INSTALL.md`. diff --git a/docs/woocommerce-quickstart.md b/docs/woocommerce-quickstart.md new file mode 100644 index 0000000..6b3fb58 --- /dev/null +++ b/docs/woocommerce-quickstart.md @@ -0,0 +1,72 @@ +# WooCommerce quick start + +Take Grin payments on your WooCommerce store in four steps. You will run the +GoblinPay till on your own server, then paste three values into WooCommerce. + +## 1. Install GoblinPay and run the wizard + +On your server (a small Linux box is plenty): + +```bash +# build + install the binary and the service unit, then it offers the wizard +sudo ./deploy/install.sh +# or, if GoblinPay is already installed: +sudo gp-server setup +``` + +The wizard asks a few questions (all with defaults) and does the rest: it makes +your till wallet, generates every secret, picks a healthy Grin node, and writes +the config. Two answers matter for WooCommerce: + +- **Your till URL** - either a subdomain like `https://pay.myshop.com`, or a + path on your existing shop domain like `https://myshop.com/pay` if you would + rather not add a DNS record (put a reverse proxy in front either way; the + wizard prints the exact snippet). +- **Your shop URL** - `https://myshop.com`. The wizard turns this into your + webhook URL for you. + +When it finishes it prints three values and a webhook URL. Keep that screen. +Then start the till: + +```bash +sudo systemctl start gp-server +``` + +## 2. Install the plugin + +Download `goblinpay-woocommerce.zip` (a GoblinPay release artifact; you can also +build it yourself with `deploy/package-woocommerce.sh`). In WordPress: + +**Plugins → Add New → Upload Plugin →** choose the zip **→ Install → Activate.** + +## 3. Paste the three values + +**WooCommerce → Settings → Payments → GoblinPay (Grin) → Manage:** + +| Field | Paste | +|---|---| +| GoblinPay URL | your till URL (e.g. `https://pay.myshop.com`) | +| API Token | the `gp_live_…` value from the wizard | +| Webhook Secret | the `whsec_…` value from the wizard | +| Matching mode | Per-invoice identity (recommended) | + +Tick **Enable Grin payments via GoblinPay** and **Save changes**. The wizard +already pointed the till's webhook at your shop, so nothing else to wire. + +## 4. Test a payment + +Place a test order and choose the Grin method at checkout. You are shown a QR +(or redirected to the hosted checkout). Pay it from your +[Goblin wallet](https://goblin.st): scan, approve, done. The order moves to +**processing** once the payment confirms on chain (default 10 confirmations). + +Watch **WooCommerce → Status → Logs** (source `goblinpay`) if you enabled debug +logging, or the till's own admin dashboard, to follow the payment. + +## Notes + +- **Refunds** are manual: GoblinPay is receive-only, so refund a customer by + sending Grin back from your wallet. +- **Run the till hot but light:** it has its own seed, so keep only a small + working balance on it and sweep to your main wallet regularly. +- For the full plugin reference see `connectors/woocommerce/INSTALL.md`.