docs: setup wizard as the recommended path, quick starts, API integrator guide
ci / fmt / clippy / test (push) Has been cancelled

- 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.
This commit is contained in:
2ro
2026-07-05 01:02:46 -04:00
parent fba3e6ff03
commit 7cfd81962e
4 changed files with 409 additions and 4 deletions
+67 -4
View File
@@ -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
+186
View File
@@ -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>
```
`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 <GP_API_TOKEN>
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": "<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:<nprofile>`)
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 <GP_API_TOKEN>
```
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=<hex>
X-GoblinPay-Delivery: <event_id>
```
`<hex>` 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.
+84
View File
@@ -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=<the gp_live_… value from the wizard>
GOBLINPAY_WEBHOOK_SECRET=<the whsec_… value from the wizard>
```
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 `<provider id>_<identifier>`, 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/<token>` 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`.
+72
View File
@@ -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`.