This plugin extends BTCPay Server to enable users to receive payments via Dash on the Evolution Platform — both the shielded pool (Orchard / ZK) and unshielded Platform Addresses (DIP-17).
Warning
This plugin shares a single Dash Evolution wallet across all the stores in the BTCPay Server instance. Use this plugin only if you are not sharing your instance.
- Shielded payments (Orchard / ZK protocol) — privacy-preserving Dash receives
- Platform Addresses (DIP-17) — unshielded Evolution Platform credits
- Single payment method code (
DASHE) — the handler multiplexes internally between shielded and Platform Address transports - Native FFI integration — uses the
platform-wallet-ffiRust library for wallet operations - Automatic invoice settlement — background sync service detects incoming payments and marks invoices paid
Unlike transparent Dash (which uses NBXplorer), Evolution Dash uses:
- Rust FFI (
libplatform_wallet_ffi) for shielded note scanning and Platform Address sync - DAPI (Dash Platform API) for blockchain state — no local node required
- Bech32m addresses (BIP-350, DIP-0018) for both receives — shielded:
dash1…(type 0x10 + 43-byte Orchard payload); Transparent (DIP-17):dash1…(type 0xb0 + 20-byte hash160). The base58X…alias of a Transparent address is deliberately NOT shown: it is also a valid L1 P2PKH address and wallets send those on-chain, where the Platform (BLAST) sync cannot see them - One prompt, two addresses — every DASHE invoice carries a unique Transparent address AND a unique shielded (diversified) address; the checkout renders them as tabs and the prompt's
destinationis the default tab's address (Transparent when both transports allocated unique addresses)
- BTCPay Server 2.3.7 or later
- The native library
libplatform_wallet_ffi.so(Linux) orplatform_wallet_ffi.dll(Windows)
The plugin requires the platform-wallet-ffi native library. You can either:
Option A: Build from source (recommended for development)
Use the fork branch sansbankdao/platform@shielded-address-at — it adds the two FFI exports the plugin needs for per-invoice addresses (platform_wallet_manager_shielded_address_at, platform_address_wallet_next_unused_receive_address) on top of upstream dashpay/platform. Without them the plugin still runs, but invoices fall back to the shared default shielded address and the Transparent tab is hidden.
git clone https://github.com/sansbankdao/platform -b shielded-address-at
cd platform
cargo build --release --features shielded -p platform-wallet-ffi
# Set the environment variable:
export DASHE_NATIVE_LIB=/path/to/platform/target/release/libplatform_wallet_ffi.soOption B: Download pre-built binary (when available)
# Place in the plugin's runtimes/<rid>/native/ directory
mkdir -p Plugins/DashEvolution/runtimes/linux-x64/native/
# Download libplatform_wallet_ffi.so to that directory-
Build the plugin:
dotnet build Plugins/DashEvolution/BTCPayServer.Plugins.DashEvolution.csproj -c Release
-
Copy the output to your BTCPay Server plugins directory
-
Configure the plugin via environment variables:
export DashEvolution__WalletIdHex="your-64-char-hex-wallet-id" export DashEvolution__Mnemonic="your 24 word mnemonic phrase" export DashEvolution__DapiAddresses="https://45.135.180.70:443" export DashEvolution__Mainnet="true" export DashEvolution__SyncIntervalSeconds="15"
-
Restart BTCPay Server
This is a complete, step-by-step guide to a production deployment on a brand-new VPS (Ubuntu/Docker), verified end-to-end. Bring your own reverse proxy (Cloudflare Tunnel, nginx, …).
- A small VPS is fine (2 vCPU / 4 GB RAM / 30 GB disk is sufficient — BTCPay + the plugin add ~2.8 GB RSS over Docker).
- Install Docker and the .NET 10 SDK (only needed to build the plugin and
.btcpay). - Create a dedicated Docker network and a Postgres container:
docker network create btcpay-network
docker run -d --name btcpay-pg --network btcpay-network --restart unless-stopped \
-e POSTGRES_PASSWORD='<postgres-password>' -p 127.0.0.1:5432:5432 postgres:16
docker exec btcpay-pg psql -U postgres -c "CREATE DATABASE btcpayserver;"git clone https://github.com/sansbankdao/btcpay-dash-evolution.git && cd btcpay-dash-evolution
dotnet build Plugins/DashEvolution/BTCPayServer.Plugins.DashEvolution.csproj -c Release
OUT=Plugins/DashEvolution/bin/Release/net10.0
mkdir -p ~/btcpay-docker/plugins/BTCPayServer.Plugins.DashEvolution
cp $OUT/BTCPayServer.Plugins.DashEvolution.dll ~/btcpay-docker/plugins/BTCPayServer.Plugins.DashEvolution/
cp $OUT/BTCPayServer.Plugins.DashEvolution.deps.json ~/btcpay-docker/plugins/BTCPayServer.Plugins.DashEvolution/
cp Plugins/DashEvolution/BTCPayServer.Plugins.DashEvolution.json ~/btcpay-docker/plugins/BTCPayServer.Plugins.DashEvolution/
# Package the distributable .btcpay archive too (manifest + dll + deps.json at the zip root):
STAGE=$(mktemp -d)
cp $OUT/BTCPayServer.Plugins.DashEvolution.dll $OUT/BTCPayServer.Plugins.DashEvolution.deps.json $STAGE/
cp Plugins/DashEvolution/BTCPayServer.Plugins.DashEvolution.json $STAGE/
(cd $STAGE && zip ~/btcpay-docker/plugins/BTCPayServer.Plugins.DashEvolution.btcpay ./*) && rm -rf $STAGEThe plugin must be present as an EXTRACTED directory named by its
Identifier(BTCPayServer.Plugins.DashEvolution/, containingBTCPayServer.Plugins.DashEvolution.json
- the dll + deps.json). A
.btcpayfile alone is NOT loaded by the packaged base image. Place both in~/btcpay-docker/plugins/.
mkdir -p ~/Workspace/native
cp libplatform_wallet_ffi.so ~/Workspace/native/ # see "Native Library Setup" aboveCreate ~/btcpay-docker/docker-compose.yml:
version: "3.7"
services:
btcpayserver:
image: btcpayserver/btcpayserver:2.3.7 # check https://hub.docker.com/r/btcpayserver/btcpayserver/tags for the current release
restart: unless-stopped
ports:
- "127.0.0.1:14142:23000"
environment:
BTCPAY_HOST: your-domain.example
BTCPAY_CHAINS: btc # see note below — do NOT put dashe here
BTCPAY_POSTGRES: Host=btcpay-pg;Port=5432;Database=btcpayserver;Username=postgres;Password=<postgres-password>
BTCPAY_CHEATMODE: "false"
BTCPAY_BIND: 0.0.0.0 # required: the packaged image otherwise binds to localhost only
DASHE_NATIVE_LIB: /native/libplatform_wallet_ffi.so
DashEvolution__DapiAddresses: https://<mainnet-dapi-node>:443,https://<another>:443 # see note below
DashEvolution__Mainnet: "true"
DashEvolution__Mnemonic: <your 24-word BIP-39 mnemonic>
DashEvolution__AccountIndex: "0"
DashEvolution__ShieldedDbPath: /data/dash_shielded.sqlite
DashEvolution__SyncIntervalSeconds: "15"
volumes:
- btcpay_data:/data
- ./plugins:/root/.btcpayserver/Plugins
- ~/Workspace/native:/native:ro
networks:
- btcpay-network
networks:
btcpay-network:
external: true
volumes:
btcpay_data:Critical gotchas (all verified on a fresh host):
BTCPAY_CHAINS: btc— the packaged base image does not knowdasheand exits withInvalid chains "dashe"if you set it. The plugin registersDASHEat startup; the log then showsSupported chains: BTC,DASHE.BTCPAY_BIND: 0.0.0.0— without it the container only listens on localhost from its own perspective and nothing reaches port 14142 (connection refused).- Do NOT set
DashEvolution__WalletIdHex. The wallet id is derived from the mnemonic and auto-created (Auto-created DashEvolution wallet id …in the log). Setting it manually with a mismatched value throwsWalletIdHex mismatchat startup. - Secrets: keep the mnemonic out of git — either put this file in a private, gitignored
directory, or split secrets into an un-tracked
docker-compose.override.yml/.envfile. DapiAddresses— the IPs above are placeholders. Fetch the current enabled-node list fromhttps://quorums.mainnet.networks.dash.org/masternodesand pick ~10-20 enabled mainnet nodes exposing their Platform DAPI on port 443, formatted ashttps://<ip>:443, comma-separated.8333 connection refused/ NBXplorer errors in the log are harmless — the packaged image tries to SPV-sync BTC headers and there is no Bitcoin Core on this box.DASHEis unaffected.
cd ~/btcpay-docker && docker compose up -d
docker logs -f <project>-btcpayserver-1 2>&1 | grep -aE "Running plugin|Supported chains|sync started|Baseline seeded|Auto-created"Expected lines (in order): Running plugin BTCPayServer.Plugins.DashEvolution - 1.0.0.0 →
Supported chains: BTC,DASHE → Auto-created DashEvolution wallet id … →
DashEvolution sync started for wallet … → Baseline seeded on first sync pass: balance=… (no invoice matching on first pass). A New invoice created now survives restarts (see
"Restart behavior" in Limitations).
If the app never listens on 14142, read the full log, not only ERR entries — a
wallet-not-configured FATAL means the mnemonic env was missing/misparsed.
The initial admin cannot log in via Greenfield basic auth out of the box when created through the API — three documented gaps require one-time SQL fixes:
# 1) Create the user (allowed while the instance has no admin):
curl -s -X POST http://127.0.0.1:14142/api/v1/users \
-H "Content-Type: application/json" \
-d '{"email":"you@example.org","password":"<strong-password>"}'
# 2) Fix the three API-creation gaps (EmailConfirmed, basic-auth flag, ServerAdmin role)
# — substitute the user id returned above:
cat > /tmp/fix_admin.sql <<EOF
UPDATE "AspNetUsers" SET "Blob2" = jsonb_set(COALESCE("Blob2",'{}'::jsonb), '{allowGreenfieldBasicAuth}', 'true'::jsonb) WHERE "Email" = 'you@example.org';
UPDATE "AspNetUsers" SET "EmailConfirmed" = true WHERE "Email" = 'you@example.org';
UPDATE "AspNetUsers" SET "Blob2" = jsonb_set("Blob2", '{showInvoiceStatusChangeWarning}', 'true'::jsonb) WHERE "Email" = 'you@example.org';
INSERT INTO "AspNetRoles" ("Id","Name","NormalizedName","ConcurrencyStamp")
SELECT 'ServerAdmin','ServerAdmin','SERVERADMIN', gen_random_uuid()::text
WHERE NOT EXISTS (SELECT 1 FROM "AspNetRoles" WHERE "Name"='ServerAdmin');
INSERT INTO "AspNetUserRoles" ("UserId","RoleId")
SELECT '<user-id>', 'ServerAdmin'
WHERE NOT EXISTS (SELECT 1 FROM "AspNetUserRoles" WHERE "UserId"='<user-id>' AND "RoleId"='ServerAdmin');
-- If login was attempted before these fixes, "FirstRun" may be stuck true:
DELETE FROM "Settings" WHERE "Id" = 'BTCPayServer.Services.PoliciesSettings' AND "Value"::jsonb ->> 'FirstRun' = 'true';
EOF
docker cp /tmp/fix_admin.sql btcpay-pg:/tmp/ && docker exec btcpay-pg psql -U postgres -d btcpayserver -f /tmp/fix_admin.sqlThis is a BTCPay Server limitation, not a plugin issue: the Greenfield
POST /api/v1/usersendpoint creates the user withEmailConfirmed=false,allowGreenfieldBasicAuthunset, and NO roles — the first admin must be registered through the MVC UI to avoid this, or fixed with the SQL above.
SID=$(curl -s -X POST http://127.0.0.1:14142/api/v1/stores \
-u 'you@example.org:<strong-password>' -H "Content-Type: application/json" \
-d '{"name":"My Store","defaultCurrency":"USD","invoiceExpiration":54000,"paymentTolerance":2.0}' \
| python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")invoiceExpirationis in SECONDS over the API (54000 = 15 hours).monitoringExpirationlikewise.paymentTolerancepercent: shielded receives arrive slightly under the invoice amount (the sender-side Orchard fee varies), so use2.0for the demo. Must be set BEFORE the invoices you intend to settle — existing invoices keep the tolerance baked in at creation.
The Greenfield endpoint PUT /api/v1/stores/{storeId}/payment-methods/DASHE-CHAIN currently
rejects new payment-method ids for a store that has never had one (verified: paymentmethod-not-found). The only working method on a fresh store is a direct DB write (no BTCPay restart needed — the next invoice picks it up):
docker exec btcpay-pg psql -U postgres -d btcpayserver -c \
"UPDATE \"Stores\" SET \"DerivationStrategies\" = '{\"DASHE-CHAIN\": {\"walletIdHex\": \"<64-hex wallet id from the startup log>\"}}' WHERE \"Id\" = '$SID';"Confirm with a test invoice (POST /api/v1/stores/$SID/invoices {"amount":0.25,"currency":"USD"})
and read back paymentPrompts — DASHE-CHAIN should appear with the shielded address and a
quoted rate.
Any reverse proxy works. With Cloudflare Tunnel (dashboard-created token tunnel):
cloudflared service install <token> with the public hostname routed to http://localhost:14142.
Then browse https://your-domain.example/i/<invoiceId> — the checkout shows the shielded
address as a QR code and the wallet identity string.
| Environment Variable | Description | Default |
|---|---|---|
DashEvolution__WalletIdHex |
64-character hex wallet ID. Omit it — the wallet id is derived from the mnemonic and auto-created; setting a mismatched value fails startup | auto |
DashEvolution__Mnemonic |
24-word BIP-39 mnemonic phrase | required |
DashEvolution__DapiAddresses |
Comma-separated DAPI endpoint URLs | https://45.135.180.70:443 |
DashEvolution__Mainnet |
true for mainnet, false for testnet |
true |
DashEvolution__AccountIndex |
Account index for HD derivation | 0 |
DashEvolution__ShieldedDbPath |
Path to shielded SQLite database | dash_shielded.sqlite |
DashEvolution__SyncIntervalSeconds |
How often to sync (seconds) | 15 |
DASHE_NATIVE_LIB |
Absolute path to native library | auto-detect |
Once deployed (see "Deploy on a fresh VPS with Docker" above):
- Present the checkout (
/i/<invoiceId>) — the DASHE payment panel shows a Transparent Address | Shielded Address tab switcher with a per-invoice-unique address on each transport - The customer pays from any Dash Evolution wallet; the invoice settles automatically within ~15-30 s of the payment reaching the network
- Transparent addresses accept Platform credits transfers only. They are shown in the DIP-0018
dash1…bech32m form so wallets route them to Platform; the equivalentX…base58 form would be accepted by wallets as an L1 address and such on-chain payments are NOT detected by this plugin (BLAST watches Platform state, not L1 UTXOs). The checkout shows a warning under the Transparent tab. The Shielded tab has no such ambiguity.
- Per-invoice addresses on both transports — Transparent (DIP-17) invoices allocate a fresh gap-limit-aware address per invoice (via the fork's
platform_address_wallet_next_unused_receive_address), so Transparent payments attribute to exactly one invoice. Shielded invoices allocate a per-invoice diversified address (via the fork'splatform_wallet_manager_shielded_address_at) and detection is IVK-based (diversifier-agnostic); a shielded delta is still booked to the most recent unpaid DASHE invoice, so concurrent unpaid shielded invoices are not individually matchable (per-note FFI still needed). - Restart behavior — the wallet baseline is seeded on the FIRST sync pass after process start and no invoice is marked on that pass, so
docker restartis safe. A payment received while the process is down is NOT matched after boot (full catch-up needs persisted baselines — planned). - Authorized viewing key not yet implemented — outgoing spends are not reflected in BTCPay's view of the wallet balance (receive-only view).
dash:payment-link UX — thedash:URI works for copy-paste, but mobile wallet deep-links for bech32m shielded addresses are pending a wallet-side fix. Use the QR / copy button today.- For plugin developers: a plugin-shipped checkout partial MUST declare its own
_ViewImports.cshtmlwith@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers,@addTagHelper *, BTCPayServerand@addTagHelper *, BTCPayServer.Abstractions— BTCPay's Content-Security-Policy blocks any inline<script>that was not nonced by the TagHelpers carried in via_ViewImports. (The host app's_ViewImports.cshtmldoes NOT flow into plugin projects at compile time — this is exactly the bug that produced a seemingly-empty payment box in 1.0.0.)
# Clone with submodules
git clone --recurse-submodules https://github.com/sansbankdao/btcpay-dash-evolution
cd btcpay-dash-evolution
# Build
dotnet build Plugins/DashEvolution/BTCPayServer.Plugins.DashEvolution.csprojPlugins/DashEvolution/
├── DashEvolutionPlugin.cs # Plugin entry point
├── DashEvolutionNetwork.cs # Network definition (BTCPayNetworkBase)
├── DashEvolutionPaymentMethodHandler.cs # Payment method handler
├── DashEvolutionSyncService.cs # Background sync service
├── DashEvolutionSyncOptions.cs # Configuration options
├── DashEvolutionMnemonicResolver.cs # Native mnemonic resolver
├── DashEvolutionNativeRegistration.cs # Native library loader
├── Bech32m.cs # Bech32m address encoding
├── Native/
│ ├── PlatformWalletFFI.cs # Shielded wallet P/Invoke
│ ├── PlatformWalletManagerFFI.cs # SDK/manager P/Invoke
│ └── PlatformAddressFFI.cs # Platform Address P/Invoke
└── Views/
└── DashEvolutionMethodCheckout.cshtml # Checkout UI
MIT
- Built on the Dash Platform Rust SDK
- Follows the BTCPay Server plugin architecture established by the Monero and Zcash plugins