mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-18 06:41:27 +02:00
A browser-based roguelike synchronized with crypto block timing. A run is a race against the next block: reach the exit before the block lands, or the house wins. The same codebase runs the solo dungeon, the social Tavern, and operator-selected PvP and race rulesets, with separate free and paid leaderboards. Self-hostable under the MIT License. See README.md to run it, docs/ARCHITECTURE.md for the design, and docs/DEPLOY.md to operate an instance.
172 lines
10 KiB
Markdown
172 lines
10 KiB
Markdown
# Payments
|
|
|
|
The game does not implement per-chain wallet or daemon code inline. It talks to pluggable **payment
|
|
providers** behind one contract, so an operator routes each chain to whichever backend they run.
|
|
|
|
## Provider contract
|
|
|
|
`src/payments/providers/paymentProvider.js` defines the interface. All amounts are atomic units for
|
|
the chain (BigInt or an integer-valued string), never decimal floats.
|
|
|
|
```
|
|
createInvoice({chain, amountAtomic, description, userId, orderId})
|
|
-> {invoiceId, address, uri, amountAtomic, expiresAt, raw}
|
|
getInvoiceStatus(ref)
|
|
-> {status: 'pending'|'processing'|'paid'|'expired'|'invalid', complete, paidAtomic, confirmations, raw}
|
|
startWatch(ref, onUpdate, intervalMs) / stopWatch(ref)
|
|
sendPayout({chain, address, amountAtomic}) -> {txids, raw} (optional)
|
|
validateAddress(chain, address) -> {valid, reason} (optional)
|
|
supportsChain(chain) -> boolean
|
|
```
|
|
|
|
`PaymentProviderRegistry` maps `chain -> providerId` and is operator-configurable. `getProvider(chain)`
|
|
returns the routed provider; if that provider does not support the chain, the registry falls back to
|
|
any registered provider that does, and finally to the default provider.
|
|
|
|
## Providers
|
|
|
|
| Provider | id | Serves | Implementation |
|
|
|----------|-----|--------|----------------|
|
|
| BTCPay Greenfield | `btcpay` | BTC, LTC by default | `src/payments/providers/btcpayProvider.js`, a BTCPay Server client |
|
|
| xmrcheckout | `xmrcheckout` | XMR | Same class against a checkout app that exposes Greenfield-compatible routes |
|
|
| wowcheckout | `wowcheckout` | WOW | Same class, matched pair to xmrcheckout |
|
|
| Native Monero/Wownero | `native-monero` | XMR, WOW | `src/payments/providers/nativeMoneroProvider.js`, wraps `walletRPCService` |
|
|
|
|
BTCPay, xmrcheckout, and wowcheckout all speak the same Greenfield contract, so one `BTCPayProvider`
|
|
class with a constructor-configurable `id` serves all three; they differ only in base URL, store id,
|
|
API key, and chain set. Auth on all three is `Authorization: token <apiKey>`. Invoices are priced in
|
|
the chain's own crypto by passing the chain ticker as the Greenfield `currency`, and the receiving
|
|
address is read from the invoice's payment-methods endpoint.
|
|
|
|
The native provider keys invoices by their Monero/Wownero subaddress and returns the wallet's
|
|
`addressIndex`, which is what lets monitoring be restored after a restart.
|
|
|
|
## Bootstrap and routing
|
|
|
|
`buildProviderRegistry({env, walletService})` in `src/payments/providers/index.js` is the single
|
|
wiring seam:
|
|
|
|
- The native provider is registered whenever a `walletService` is supplied, and claims XMR and WOW.
|
|
- Each Greenfield gateway activates from its own environment trio:
|
|
`BTCPAY_URL` / `BTCPAY_STORE_ID` / `BTCPAY_API_KEY` (chain list from `BTCPAY_CHAINS`, default
|
|
`BTC,LTC`), `XMRCHECKOUT_URL` / `_STORE_ID` / `_API_KEY` (XMR), and
|
|
`WOWCHECKOUT_URL` / `_STORE_ID` / `_API_KEY` (WOW).
|
|
- A dedicated checkout gateway wins over native for its own chain.
|
|
- `PAYMENT_ROUTING` overrides every mapping. It accepts JSON (`{"BTC":"btcpay","XMR":"native-monero"}`)
|
|
or a compact `CHAIN:provider,CHAIN:provider` string; unparseable values are ignored.
|
|
- The default provider, used for chains with no explicit route, is the native provider when present,
|
|
otherwise the first registered gateway.
|
|
|
|
With no gateway environment set, only the native provider is registered and every chain routes to it,
|
|
which is the single-chain wallet-RPC flow. `GameModeManager` builds this registry in its constructor
|
|
unless one is injected.
|
|
|
|
The general environment template is `src/.env.example`. The gateway variables above are read straight
|
|
from the process environment; add them to the deploy host's environment when enabling a gateway.
|
|
|
|
## Invoice lifecycle
|
|
|
|
**Creation.** `gameModeManager.createPaymentRequest` resolves the provider for `CRYPTO_TYPE` and calls
|
|
`createInvoice`. Any stale `pending` payments for the user are expired first, so a user holds at most
|
|
one active invoice. The row is inserted into `payments` with `provider_id`, `provider_invoice_id`,
|
|
`address_index`, the expected atomic amount, product grants, and the fairness commitment fields.
|
|
Invoices expire at the provider-supplied `expiresAt`, or 30 minutes from creation when the provider
|
|
does not supply one.
|
|
|
|
**Monitoring.** `network/paymentHandlers._monitorAddress` records a socket-to-payment mapping, arms a
|
|
30 minute expiry timer, and calls `provider.startWatch(watchRef, onStatus, 2000)`. The watch reference
|
|
is the subaddress for the native provider and the invoice id for a gateway. `onStatus` receives a raw
|
|
wallet-style status (`in_mempool`, `confirmed`, `complete`, `amount`, `required`, `confirmations`,
|
|
`receipts`): the native provider passes the wallet's own status through untouched, and
|
|
`BTCPayProvider.getWalletStatus` maps a Greenfield invoice into the same shape (`Settled` and
|
|
`Complete` become confirmed and complete, `Processing` becomes in-mempool, paid and required amounts
|
|
come from the payment-methods response converted through `ChainProfile` decimals). Both feed the one
|
|
handler, `_handlePaymentStatus`.
|
|
|
|
**Confirmation.** A mempool sighting queues a single-game entry or announces a pending purchase; a
|
|
confirmed status applies the entitlement. Underpayment emits `payment_underpaid` and leaves the
|
|
invoice open so the balance can be topped up to the same address. Confirmation is idempotent: an
|
|
in-memory set is only a fast path, and the database status transition under `FOR UPDATE` is the source
|
|
of truth, so a restart cannot double-apply. When paid fairness binding is required, an entry whose
|
|
invoice carries no bound fairness proof is refused at both the queue and confirm steps and reported as
|
|
`payment_review_required` rather than started.
|
|
|
|
**Receipt evidence.** Every confirmation must be backed by durable receipts in `payment_receipts`
|
|
(migration `037_payment_receipt_evidence.sql`). Native receipts are `chain_output` rows identified by
|
|
`txHash:outputId` and carrying the invoice's `address_index`; gateway receipts are `provider_invoice`
|
|
rows identified by the invoice id. A unique index on `(provider_id, evidence_id)` makes each piece of
|
|
evidence consumable exactly once, and a database trigger rejects any transition into `confirmed` whose
|
|
confirmed receipts do not cover `expected_amount`. This is why one transaction with several outputs,
|
|
or one invoice topped up by several transactions, both settle correctly.
|
|
|
|
**Late payments.** `src/services/latePaymentReconciler.js` scans expired invoices, re-reads status
|
|
through the owning provider (`getWalletStatus` for gateways, wallet RPC for native), and records
|
|
durable evidence in `payment_late_reviews` for manual adjudication. A late receipt never grants an
|
|
entitlement on its own. See [FINANCIAL_RECONCILIATION.md](FINANCIAL_RECONCILIATION.md).
|
|
|
|
## Payouts
|
|
|
|
Payouts run through the native wallet service, not through the routed provider: the checkout gateways
|
|
are receive-only. `gameModeManager` sends winnings via `walletService.processPayout` for single
|
|
payouts and `processBatchPayout` for batches, both of which use `transfer_split` and return
|
|
`tx_hash_list`. `src/payments/payoutRetryService.js` retries failures. `NativeMoneroProvider`
|
|
implements `sendPayout` so the contract is complete, and it delegates to the same batch call.
|
|
|
|
Payout address validation prefers `walletService.validateAddress`, which is network-aware
|
|
(mainnet/stagenet/testnet), and falls back to the provider's `validateAddress` only when the wallet
|
|
service exposes none.
|
|
|
|
Payout behaviour is gated by `PAYOUTS_ENABLED` as a master switch with subordinate per-mode flags
|
|
(`DIRECT_PAYOUTS_ENABLED`, `CREDITS_PAYOUTS_ENABLED`, `MATCH_PAYOUTS_ENABLED`). See
|
|
[PRODUCTION_DISCLOSURES.md](PRODUCTION_DISCLOSURES.md).
|
|
|
|
## Money and chain parameters
|
|
|
|
All amounts are atomic integers. Arithmetic goes through `src/money/atomic.js` (`toBig`, `toSafe`,
|
|
`sum`, `add`, `mulByDecimal`, `format`), and decimals come from `src/chain/chainProfile.js`
|
|
(`decimalsFor`, `atomicDivisor`). No floating-point money anywhere.
|
|
|
|
| Chain | Decimals | Mean block time | Family | URI scheme |
|
|
|-------|----------|-----------------|--------|------------|
|
|
| WOW | 11 | 300000 ms | monero | `wownero` |
|
|
| XMR | 12 | 120000 ms | monero | `monero` |
|
|
| BTC | 8 | 600000 ms | utxo | `bitcoin` |
|
|
| LTC | 8 | 150000 ms | utxo | `litecoin` |
|
|
| GRIN | 9 | 60000 ms | mimblewimble | `grin` |
|
|
|
|
An unknown or unset chain resolves to an XMR-shaped default (12 decimals, 120000 ms, monero family)
|
|
so a mis-set `CRYPTO_TYPE` degrades instead of crashing. `meanBlockTimeMsFor` is what block-time-aware
|
|
difficulty scales against; per-chain dungeon sizing lives in `NETWORK_TUNING` in
|
|
`src/game/difficultyConfig.js`.
|
|
|
|
Payment QR codes are generated by `src/payments/qrService.js` as `monero:` or `wownero:` URIs with
|
|
`tx_amount` and a truncated `tx_description`. It returns `null` when the optional `qrcode` package is
|
|
absent, and the address and amount are always shown as text as well.
|
|
|
|
## Enabling a gateway
|
|
|
|
1. Mint a store API key in the gateway's own admin interface.
|
|
2. Set that gateway's `*_URL`, `*_STORE_ID`, and `*_API_KEY` on the deploy host, using an address the
|
|
host can actually reach. Verify reachability from the host itself; an overlay-network address may
|
|
not route from the service account.
|
|
3. Restart the service. Invoices for that chain are created and confirmed through the gateway with no
|
|
code change.
|
|
|
|
## File map
|
|
|
|
```
|
|
src/payments/providers/paymentProvider.js contract + registry
|
|
src/payments/providers/btcpayProvider.js Greenfield client (btcpay / xmrcheckout / wowcheckout)
|
|
src/payments/providers/nativeMoneroProvider.js wraps walletRPCService
|
|
src/payments/providers/index.js buildProviderRegistry (env -> registry)
|
|
src/payments/walletRPCService.js Monero/Wownero wallet RPC, payouts
|
|
src/payments/payoutRetryService.js payout retry worker
|
|
src/payments/qrService.js payment-URI QR codes
|
|
src/chain/chainProfile.js decimals, block times, families, URI schemes
|
|
src/money/atomic.js atomic-integer money arithmetic
|
|
src/game/gameModeManager.js invoice creation, confirmation, receipts, payouts
|
|
src/network/paymentHandlers.js monitoring and status handling
|
|
src/services/latePaymentReconciler.js post-expiry evidence and review records
|
|
src/migrations/037_payment_receipt_evidence.sql receipts, late reviews, confirmation-evidence trigger
|
|
```
|