docs: setup wizard as the recommended path, quick starts, API integrator guide
ci / fmt / clippy / test (push) Has been cancelled
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
Reference in New Issue
Block a user