jwinterm 7cec74be6c publish-readiness sprint pass 1 — reliability + security + metadata
Six fixes from the 2026-06-15 deep review batch (workflow result at
2026-06-15-deep-review-raw.json). Targets the highest-impact + smallest-
scoped items; bigger restructure (B3 persistent retry queue, full B4
transactional idempotency, P1 IPaymentMethodHandler integration) is a
separate effort.

[B5] GrinRPCClient HttpClient timeout (15s).
  Default HttpClient.Timeout is 100s. A stuck grin-wallet blocked the
  30s payment monitor tick for up to 100s, queueing subsequent
  async-void timer ticks under load. Now bounded to 15s — generous
  for healthy responses (most < 200ms), short enough that the next
  monitor tick gets a fair turn.

[B4 partial] Status endpoint emits the settlement webhook on the
  Broadcast→Confirmed transition.
  This is the direct root cause of the three stuck staging orders the
  user reported 2026-06-15. The customer-side /status poll runs every
  5s; the background monitor every 30s. Both end up at the same
  confirmation branch, but only the monitor's branch was dispatching
  InvoicePaymentSettled. Whichever caller won the race to flip the DB
  to Confirmed silently locked out the OTHER caller (the monitor's
  loop guard `if (invoice.Status != Broadcast) continue;` makes
  Confirmed rows unreachable). Now: the Status endpoint also
  dispatches the webhook when IT wins the transition. Note: still
  non-idempotent — full atomic guard (SettlementWebhookSent column +
  transactional lock + UpdateInvoiceStatusIfBroadcast returning bool)
  is queued as the next pass (B4 complete).

[B5 follow-up] Broadcast-webhook dispatch is now awaited in
  GrinCheckoutController.SubmitSlatepack (line ~282).
  Pre-2026-06-15 the call was fire-and-forget; under any contention
  the controller returned the customer's redirect before the POST
  left the box, dropping the InvoiceProcessing event entirely. Now
  the request waits for the dispatch to either succeed or fail
  (bounded by the new RpcTimeout = 15s, so customer wait is
  bounded).

[B2] Open-redirect guard on stored RedirectUrl.
  GrinCheckoutController.Checkout previously did `Redirect(invoice
  .RedirectUrl)` verbatim. Attacker who can call CreateInvoice with
  a hostile URL (B1, separate) — or who later mutates the row via
  DB access — could 302 customers to a phishing page under this
  trusted BTCPay hostname. New IsSafeRedirect helper requires:
  absolute https://, well-formed URI, non-loopback host, no
  embedded user:pass@. Per-store allowlist is TODO P9.

[P3] Plugin dependency condition >=1.12.0 → >=2.3.9.
  Matches README + SETUP.md + the actual BTCPay submodule we
  compile against. The prior condition predated the .NET 10
  cutover and would let the plugin silently install on hosts we've
  never validated against.

[P4] csproj metadata for the auto-generated plugin manifest.
  Added <Authors>, <Company>, <Copyright>, <PackageProjectUrl>,
  <RepositoryUrl>, <RepositoryType>. <Product> + <Description> +
  <Version> were already correct. BTCPayServer.PluginPacker
  derives the manifest JSON from the loaded plugin's properties,
  which derive from these csproj fields via
  BaseBTCPayServerPlugin's reflection getters. With these set, the
  plugin-store listing carries proper attribution + provenance
  links.

Build verified: dotnet build → 0 errors, 9 warnings (all pre-
existing — package version mismatches in BTCPay's transitive deps +
the same _sessionActive unused-field flag that's been there since
v1.0.0).

Outstanding from the deep review (queued):
  B1 — auth on POST /invoices (contract change vs current
       intentionally-anonymous integration endpoint; needs operator
       decision before flipping)
  B3 — persistent webhook-delivery table + retry worker
       (next-biggest reliability win; ~3-4h of work — migration +
       new HostedService + retry policy)
  B4 full — SettlementWebhookSent column + transactional guard
       so the Status + Monitor races don't double-fire even in
       error paths
  P1 — IPaymentMethodHandler integration so Grin lives in BTCPay's
       main invoice subsystem (the #1 publish-readiness blocker;
       multi-day rework)
  P2 — .github/workflows/dotnet.yml for CI
  P9 — SSRF guard on WebhookUrl / NodeApiUrl / OwnerApiUrl at save
  P10 — proper icon (512×512) + checkout screenshots for plugin-
       store listing
2026-06-15 09:49:54 -04:00
2026-05-25 12:42:41 -04:00
2023-01-25 17:37:21 +01:00
2023-01-25 17:37:21 +01:00

BTCPay Server Grin Plugin

Accept Grin payments in your BTCPay Server store.

Grin is a privacy-preserving cryptocurrency using MimbleWimble. Unlike Bitcoin, Grin transactions are interactive — both sender and receiver must exchange data (slatepacks) to build a transaction. This plugin manages that exchange through a checkout UI.

How It Works

  1. Merchant configures their grin-wallet connection in BTCPay store settings
  2. Customer creates an invoice (via API or storefront integration)
  3. Checkout page shows a slatepack message (with QR code) for the customer to process in their wallet
  4. Customer pastes their wallet's response slatepack back into the checkout page
  5. Plugin finalizes and broadcasts the transaction
  6. Background service monitors confirmations and marks the invoice complete
Customer                    BTCPay (this plugin)              grin-wallet
   |                              |                               |
   |  --- create invoice -------> |  --- issue_invoice_tx ------> |
   |  <-- slatepack S1 ---------  |  <-- slatepack S1 ----------- |
   |                              |                               |
   |  (process S1 in wallet)      |                               |
   |                              |                               |
   |  --- paste response S2 ----> |  --- finalize_tx (S2) ------> |
   |                              |  --- post_tx ---------------> |
   |  <-- "payment broadcast" --- |                               |
   |                              |                               |
   |                            [monitor service polls every 30s] |
   |                              |  --- retrieve_txs ----------> |
   |                              |  --- node_height -----------> |
   |  <-- "confirmed" ----------- |  (confirmations >= threshold) |

Requirements

  • BTCPay Server v2.3.9+ (.NET 10)
  • PostgreSQL (used by BTCPay)
  • A running grin-wallet instance with the Owner API enabled
  • A synced Grin node (the wallet connects to it)

grin-wallet Setup

The plugin communicates with grin-wallet's Owner API using v3 encrypted JSON-RPC (ECDH key exchange + AES-256-GCM).

# Start the owner API (default port 3420)
grin-wallet owner_api

You'll need three things from your wallet for plugin configuration:

  • Owner API URL: default http://127.0.0.1:3420
  • Wallet password: the password used to open/unlock the wallet
  • API secret: contents of .owner_api_secret in your grin-wallet data directory

Installation

Coming soon — the plugin will be published to the BTCPay plugin directory.

Manual / Development

git clone https://github.com/Such-Software/btcpayserver-grin-plugin.git
cd btcpayserver-grin-plugin
git submodule update --init --recursive

# Build the plugin (.NET 10 SDK required)
dotnet build BTCPayServer.Plugins.Grin/BTCPayServer.Plugins.Grin.csproj

# Run the tests
dotnet test BTCPayServer.Plugins.Grin.Tests/BTCPayServer.Plugins.Grin.Tests.csproj

# Run BTCPay with the plugin loaded
export BTCPAY_DEBUG_PLUGINS="$(pwd)/BTCPayServer.Plugins.Grin/bin/Debug/net10.0/BTCPayServer.Plugins.Grin.dll"
dotnet run --project btcpayserver/BTCPayServer --no-launch-profile

Set your standard BTCPay environment variables (BTCPAY_NETWORK, BTCPAY_POSTGRES, BTCPAY_BTCEXPLORERURL, etc.) as needed.

Configuration

  1. In BTCPay, go to your store's settings
  2. Click Grin in the left sidebar
  3. Enter your wallet's Owner API URL, password, and API secret
  4. Optionally enter your Grin node's API URL for detailed sync status monitoring (shows sync percentage and phase in the BTCPay footer panel)
  5. Set minimum confirmations (default: 10, roughly 10 minutes)
  6. Check Enable Grin Payments and save
  7. Click Test Connection to verify

See SETUP.md for detailed deployment instructions including Docker networking with socat proxies.

Creating Invoices

Invoices are created via the plugin's REST API:

curl -X POST "http://localhost:23000/stores/{storeId}/plugins/grin/invoices" \
  -H "Content-Type: application/json" \
  -d '{"amount": 1.5, "orderId": "order-123"}'

Response:

{
  "invoiceId": "a1b2c3d4e5f6",
  "checkoutUrl": "http://localhost:23000/stores/{storeId}/plugins/grin/checkout/a1b2c3d4e5f6",
  "amount": 1.5,
  "amountNanogrin": 1500000000,
  "txSlateId": "...",
  "slatepackAddress": "grin1...",
  "status": "Pending"
}

Direct the customer to the checkoutUrl to complete payment.

Invoice Lifecycle

Status Description
Pending Invoice created, slatepack issued, awaiting customer response
AwaitingResponse Reserved for future use
Broadcast Transaction finalized and broadcast to the network
Confirmed Transaction has enough confirmations (per store setting)
Expired No response within 24 hours; wallet transaction cancelled automatically

The background monitor service polls every 30 seconds to update confirmation counts and expire stale invoices.

USD Pricing

The checkout page shows an approximate USD value alongside the Grin amount. The price is fetched from Gate.io's spot API and cached for 2 minutes. Sub-cent amounts display as "< $0.01".

A GrinRateProvider is also registered with BTCPay's rate engine, making GRIN pairs available for rate rules and scripting.

Architecture

BTCPayServer.Plugins.Grin/
├── Plugin.cs                          # Service registration, nav extension
├── PluginMigrationRunner.cs           # EF Core migrations on startup
├── GrinRPCClient.cs                   # v3 encrypted Owner API client
├── Controllers/
│   ├── UIGrinController.cs            # Settings UI (store admin)
│   └── GrinCheckoutController.cs      # Checkout flow + REST API
├── Data/
│   ├── GrinDbContext.cs               # Plugin's own DB context
│   ├── GrinInvoice.cs                 # Invoice model
│   └── GrinStoreSettings.cs           # Per-store wallet config
├── Services/
│   ├── GrinService.cs                 # Business logic, price fetching
│   ├── GrinRPCProvider.cs             # Per-store RPC client cache
│   ├── GrinRateProvider.cs            # BTCPay rate engine integration
│   ├── GrinPaymentMonitorService.cs   # Background confirmation tracker
│   ├── GrinSyncService.cs            # Node sync status polling (30s)
│   ├── GrinSyncSummaryProvider.cs    # BTCPay footer panel integration
│   └── GrinDbContextFactory.cs        # DB context factory
├── Views/
│   ├── UIGrin/Settings.cshtml         # Store settings + invoice list
│   ├── GrinCheckout/Checkout.cshtml   # Payment page
│   ├── GrinCheckout/CheckoutComplete.cshtml
│   ├── GrinCheckout/CheckoutExpired.cshtml
│   ├── Shared/GrinNav.cshtml          # Sidebar nav item
│   └── Shared/Grin/GrinSyncSummary.cshtml  # Footer sync panel
├── Migrations/                        # EF Core migrations
└── Resources/img/                     # Grin logo

Each BTCPay store connects to its own grin-wallet instance, similar to how Lightning works — the plugin doesn't hold keys, the merchant runs their own wallet.

Trust Model

Like Lightning in BTCPay, this plugin requires the merchant to run their own grin-wallet. The wallet password and API secret are stored in the BTCPay database. This means:

  • Self-hosted BTCPay: Fully self-custodial. You control both the server and the wallet.
  • Third-party BTCPay: The server operator has access to your wallet credentials. This is the same trust model as Lightning — if you don't run the server, you're trusting the operator.

Grin's interactive transaction model requires private keys for receiving, so there's no equivalent to Bitcoin's xpub-based watch-only wallets. True non-custodial operation requires self-hosting.

Security Notes

  • Wallet credentials (password, API secret, webhook secret) are stored in the plugin's PostgreSQL tables, encrypted at rest via ASP.NET Core's IDataProtector (v1.0.10+). Key ring lives in BTCPay's existing DataProtection directory — back it up.
  • The Owner API connection uses v3 encrypted JSON-RPC (ECDH + AES-256-GCM) — even over plaintext HTTP, the RPC payload is encrypted
  • The plugin never holds private keys; all signing happens in grin-wallet
  • Invoice slatepack exchange happens over HTTPS (or whatever your BTCPay instance uses)
  • Error messages shown to customers are generic — internal RPC errors are logged server-side only
  • Chain-reorg detection (v1.0.10+) re-checks confirmed invoices for up to 2 hours after settlement and fires an InvoiceInvalid webhook if the payment is later orphaned

For the full security policy + responsible disclosure process, see SECURITY.md.

Limitations & Known Issues

This release is labelled "ready for community testing" — production- ready for early adopters, with a short list of known gaps:

  • QR code on checkout is hard to scan with mobile cameras. The slatepack payload is 5001500 chars, producing a Version 30+ QR with ~1px modules at default zoom. Workaround: customers paste the slatepack manually. Fix (animated multi-frame QR via UR / BBQr) is on the roadmap for the next release.
  • Webhook delivery has no retry queue. If your consumer is unreachable for 30s+ around a confirmation event, the notification is logged-and-dropped. Acceptable for low-volume stores monitoring their logs; high-volume operators should wait for the retry feature.
  • Integration tests run against pure helpers (reorg decision, HMAC signature, encrypted columns). A real grin-wallet integration test isn't in CI yet — manual end-to-end is documented in SETUP.md under "Accept your first payment."

See CHANGELOG.md for the full release history and a detailed view of what shipped when.

Running grin-wallet as a Service

See contrib/systemd/ for example systemd unit files for running the Grin node and wallet as background services.

Contributing

PRs welcome. Bugs, feature requests, and design discussion go in GitHub issues. Security issues go via email — see SECURITY.md. The full contribution guide lives in CONTRIBUTING.md.

Quick PR checklist:

  • dotnet build clean
  • dotnet test green (all 23+ tests)
  • CHANGELOG.md updated under the current version
  • One logical change per PR

License

MIT

Description
BTCPayServer plugin in for grin cryptocurrency
Readme MIT 330 KiB
Languages
C# 81.5%
HTML 18.5%