mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-17 15:31:38 +02:00
Optional REGISTRATION_REQUIRE_PAYMENT gate: a new wallet must settle an invoice on an external, non-custodial payment processor before it can register. Pull model — the grant reads the processor's authenticated API as the source of truth, so there is no inbound webhook to secure and a settled invoice cannot be forged over HTTP. Returning wallets and self-hosting bypass it, exactly like the invite and PoW gates. - infra/payment: a pluggable PaymentProvider seam (neutral Invoice / InvoiceStatus / InvoiceRequest) with a BTCPay-compatible first adapter that also serves this project's xmrcheckout/wowcheckout apps + the BTCPay Monero plugin, so "operator picks any coin" costs one adapter. A future ETH/native adapter (BTCPay serves neither) implements the same trait. Only `Settled` grants (unknown status fails safe); the API key rides in a redacting Secret and hostile responses are size-capped. - payment_invoices: invoice<->pubkey_hash binding + atomic single-use consume (UPDATE ... WHERE consumed_at IS NULL AND pubkey_hash=$ RETURNING), so one invoice grants at most one registration and never a different identity. - POST /auth/payment-invoice mints + binds; enforce_payment (bind -> processor `Settled` -> atomic consume) runs beside enforce_invite; payment_invoice_id on the register request. - Fail-closed config: https-in-prod, positive-decimal price, >= 1 confirmation (bars a 0-conf settled-then-double-spent free registration), bounded expiry. /capabilities advertises payment_required + price; OpenAPI + .env.example. Tests: adapter status/speedPolicy mapping + config validation (unit), and L1 binding + atomic single-use (concurrent-consume-one-wins, mis-bind rejected). Reviewed adversarially across payment-integrity, leak/oracle, and config/DoS lenses; the confirmed findings (0-conf, expiry bound, Settled-means-paid-in-full adapter contract, rotation-branch bypass guard) are addressed here.
31 lines
2.1 KiB
SQL
31 lines
2.1 KiB
SQL
-- Pay-to-register: a per-registration payment invoice created via an external,
|
|
-- non-custodial payment processor (the BTCPay-compatible adapter covers this
|
|
-- project's own xmrcheckout/wowcheckout apps, BTCPay Server, and its Monero
|
|
-- plugin). One composable registration gate alongside PoW and invite codes.
|
|
--
|
|
-- The backend is a THIN client: it creates the invoice on the operator's
|
|
-- processor, binds it here to the registrant's `pubkey_hash`, and — in the pull
|
|
-- model — grants the registration only after reading `Settled` from the
|
|
-- processor's authenticated API and ATOMICALLY consuming this row (single-use).
|
|
--
|
|
-- No funds and no view/spend keys ever touch this table: only the processor's
|
|
-- opaque invoice id, the identity the invoice is bound to, and the price we
|
|
-- asked for. The payer pays the OPERATOR's own wallet directly (view-only
|
|
-- detection on the processor side), so this stays non-custodial like every other
|
|
-- chain path. Self-hosting bypasses the gate entirely (leave the gate off).
|
|
--
|
|
-- `invoice_id` is the processor's id, NOT attacker-chosen; the row is the
|
|
-- binding authority (the processor-side metadata bind is only defense-in-depth).
|
|
CREATE TABLE payment_invoices (
|
|
invoice_id TEXT PRIMARY KEY, -- processor's opaque invoice id
|
|
pubkey_hash TEXT NOT NULL, -- registrant identity bound to it (hex sha256 of the BTC pubkey)
|
|
provider TEXT NOT NULL, -- adapter kind that minted it (e.g. 'btcpay')
|
|
amount TEXT NOT NULL, -- operator-configured price, as a decimal string (no float math)
|
|
currency TEXT NOT NULL, -- price currency ('XMR', or a fiat code the processor converts)
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
consumed_at TIMESTAMPTZ -- NULL = unspent; set once, atomically, when it grants a registration
|
|
);
|
|
|
|
-- Binding lookup on completion: a registrant's still-unspent invoices.
|
|
CREATE INDEX idx_payment_invoices_unspent ON payment_invoices (pubkey_hash) WHERE consumed_at IS NULL;
|