@aquarian-metals/coin-moebius-bitcoin 0.0.0-stage → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +168 -2
- package/dist/amounts.d.ts +13 -0
- package/dist/amounts.d.ts.map +1 -0
- package/dist/amounts.js +18 -0
- package/dist/amounts.js.map +1 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +172 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +306 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +852 -0
- package/dist/server.js.map +1 -0
- package/package.json +62 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 aquarian-metals
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,169 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @aquarian-metals/coin-moebius-bitcoin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Self-hosted Bitcoin provider for **[Coin Moebius](https://github.com/aquarian-metals/coin-moebius)**. No custodial third-party gateway, no API keys to a payment processor, no merchant-tax-form-collecting middleman. The merchant runs `bitcoind`, this package supplies everything else.
|
|
4
|
+
|
|
5
|
+
Two entries in one package:
|
|
6
|
+
|
|
7
|
+
- `@aquarian-metals/coin-moebius-bitcoin` — **browser** entry. Modal-rendering provider that posts to your own checkout endpoint and shows the buyer a fresh Bitcoin address, the exact amount, and a `bitcoin:` link.
|
|
8
|
+
- `@aquarian-metals/coin-moebius-bitcoin/server` — **server** entry. `createBitcoinCreator` (mints a labeled address per checkout), `createBitcoinVerifier` (validates indexer webhooks), `createBitcoinIndexer` (the long-running chain watcher). **Never import this from browser code.**
|
|
9
|
+
|
|
10
|
+
## Why a self-hosted Bitcoin provider
|
|
11
|
+
|
|
12
|
+
Custodial gateways (Cryptomus, NOWPayments, BTCPay-as-a-service) ask you to trust them with your money, your KYC, and your geo-availability. Self-hosting Bitcoin gives you:
|
|
13
|
+
|
|
14
|
+
- **No counterparty.** Funds land directly in a wallet you control. No "gateway froze our account" thread on r/Bitcoin ends with your store.
|
|
15
|
+
- **No third-party API keys.** Nothing on a vendor's dashboard can be revoked from under you.
|
|
16
|
+
- **No geo limits.** Cryptomus is geo-blocked in the US; NOWPayments is fine but is a custodian. Self-hosted has no political surface.
|
|
17
|
+
- **No fee skim.** The only fee is the Bitcoin network fee, which the buyer pays.
|
|
18
|
+
|
|
19
|
+
The trade-off is operational: you (or your VPS) run a Bitcoin node. This package makes the wiring on top of that as small as possible.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @aquarian-metals/coin-moebius-bitcoin
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The server entry uses Web Crypto (`crypto.subtle`) for HMAC and `fetch` for the node's JSON-RPC — works in Node 18+, Cloudflare Workers, Deno, and Bun. No additional dependency.
|
|
28
|
+
|
|
29
|
+
## Use — browser
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { createBitcoinProvider } from '@aquarian-metals/coin-moebius-bitcoin';
|
|
33
|
+
import { createPaymentManager } from '@aquarian-metals/coin-moebius';
|
|
34
|
+
|
|
35
|
+
const payments = createPaymentManager({
|
|
36
|
+
providers: [
|
|
37
|
+
createBitcoinProvider({
|
|
38
|
+
checkoutEndpoint: '/api/checkout/bitcoin',
|
|
39
|
+
statusEndpoint: '/api/payment-status',
|
|
40
|
+
}),
|
|
41
|
+
],
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
document.getElementById('buy-with-bitcoin')?.addEventListener('click', () => {
|
|
45
|
+
payments.initiate({ productId: 'ebook-42', amount: 19.99, currency: 'USD' });
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Use — server (overview)
|
|
50
|
+
|
|
51
|
+
Three serverless functions / handlers, plus one long-running indexer:
|
|
52
|
+
|
|
53
|
+
1. `POST /api/checkout/bitcoin` — the browser posts here; you call `createBitcoinCreator(...)` and return its result.
|
|
54
|
+
2. `POST /api/payment-webhook` — the **indexer** posts here; you call `verifierRegistry.verify(...)` and fulfill orders on `'success'`.
|
|
55
|
+
3. `GET /api/payment-status?paymentId=…` — the browser polls here; you return the current `PaymentRecord` from your store.
|
|
56
|
+
4. **The indexer process** — see "Running the indexer" below.
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
// /api/checkout/bitcoin
|
|
60
|
+
import { createBitcoinCreator } from '@aquarian-metals/coin-moebius-bitcoin/server';
|
|
61
|
+
import { myProductionStore } from './store.js';
|
|
62
|
+
|
|
63
|
+
const create = createBitcoinCreator({
|
|
64
|
+
rpcUrl: process.env.BITCOIN_RPC_URL,
|
|
65
|
+
wallet: 'shop',
|
|
66
|
+
rpcUser: process.env.BITCOIN_RPC_USER,
|
|
67
|
+
rpcPassword: process.env.BITCOIN_RPC_PASSWORD,
|
|
68
|
+
store: myProductionStore,
|
|
69
|
+
btcPerUnit: async (currency) => {
|
|
70
|
+
// You decide where prices come from — CoinGecko, Kraken ticker,
|
|
71
|
+
// pinned constant. Coin Moebius does not call any oracle.
|
|
72
|
+
return 1 / (await fetchBitcoinPriceFromMyOracle(currency));
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export default async (req) => {
|
|
77
|
+
const { productId, metadata } = await req.json();
|
|
78
|
+
// Price from your own catalog, never from the request: the browser is the
|
|
79
|
+
// buyer's, and a buyer can send any amount they like.
|
|
80
|
+
const price = await priceOf(productId);
|
|
81
|
+
if (!price) return new Response('Unknown product', { status: 404 });
|
|
82
|
+
return Response.json(await create({ productId, ...price, metadata }));
|
|
83
|
+
};
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
// /api/payment-webhook
|
|
88
|
+
import { createVerifierRegistry } from '@aquarian-metals/coin-moebius-server';
|
|
89
|
+
import { createBitcoinVerifier } from '@aquarian-metals/coin-moebius-bitcoin/server';
|
|
90
|
+
|
|
91
|
+
const verifiers = createVerifierRegistry();
|
|
92
|
+
verifiers.register('bitcoin', createBitcoinVerifier({ hmacSecret: process.env.BITCOIN_HMAC_SECRET }).verify);
|
|
93
|
+
|
|
94
|
+
export default async (req) => {
|
|
95
|
+
const result = await verifiers.verify(await req.text(), Object.fromEntries(req.headers));
|
|
96
|
+
if (result?.status === 'success') {
|
|
97
|
+
// Fulfill the order. This handler MUST be idempotent — like every
|
|
98
|
+
// webhook receiver, you may get a duplicate.
|
|
99
|
+
}
|
|
100
|
+
return new Response('', { status: 200 });
|
|
101
|
+
};
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Running the indexer
|
|
105
|
+
|
|
106
|
+
The indexer is the long-running process that watches the node's wallet and converts on-chain events into webhook posts. **It must run inside the same private network as `bitcoind`.** It never accepts inbound network traffic; it only makes outbound calls to the node's RPC (private network) and to your webhook endpoint (HTTPS).
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
// indexer.js
|
|
110
|
+
import { createBitcoinIndexer } from '@aquarian-metals/coin-moebius-bitcoin/server';
|
|
111
|
+
import { myProductionStore } from './store.js';
|
|
112
|
+
|
|
113
|
+
const indexer = createBitcoinIndexer({
|
|
114
|
+
rpcUrl: process.env.BITCOIN_RPC_URL,
|
|
115
|
+
wallet: 'shop',
|
|
116
|
+
rpcUser: process.env.BITCOIN_RPC_USER,
|
|
117
|
+
rpcPassword: process.env.BITCOIN_RPC_PASSWORD,
|
|
118
|
+
store: myProductionStore,
|
|
119
|
+
webhookUrl: process.env.BITCOIN_WEBHOOK_URL,
|
|
120
|
+
hmacSecret: process.env.BITCOIN_HMAC_SECRET,
|
|
121
|
+
requiredConfirmations: 1,
|
|
122
|
+
pollIntervalMs: 30_000,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const stop = indexer.start();
|
|
126
|
+
process.on('SIGTERM', stop);
|
|
127
|
+
process.on('SIGINT', stop);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Why downtime is cheap
|
|
131
|
+
|
|
132
|
+
The indexer is **catch-up by design**. If it's offline for an hour, the next tick queries the node for the missed block range and emits the webhooks then. Buyers see a stale "pending" during the outage; nothing is lost, nothing requires manual reconciliation.
|
|
133
|
+
|
|
134
|
+
Compared to a hosted gateway, where downtime on the gateway's end means missed events and refund tickets, your operational SLA for self-hosted Bitcoin is "eventually consistent within a few minutes." That's a much easier bar to meet than "five-nines uptime," and it's the property that makes self-hosting realistic for indie merchants who don't run an ops team.
|
|
135
|
+
|
|
136
|
+
## Node setup notes
|
|
137
|
+
|
|
138
|
+
The minimum the merchant needs running, in production:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# bitcoin.conf
|
|
142
|
+
server=1
|
|
143
|
+
prune=550
|
|
144
|
+
rpcbind=127.0.0.1
|
|
145
|
+
rpcallowip=127.0.0.1
|
|
146
|
+
rpcuser=shop
|
|
147
|
+
rpcpassword=<long random password>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# A watch-only wallet holding only your public descriptor.
|
|
152
|
+
bitcoin-cli -named createwallet wallet_name=shop disable_private_keys=true blank=true load_on_startup=true
|
|
153
|
+
bitcoin-cli -rpcwallet=shop importdescriptors '[{"desc":"wpkh([…]xpub…/0/*)#…","active":true,"range":[0,999],"timestamp":"now"}]'
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Never expose the node's RPC to the public internet.** See [`docs/self-hosted-bitcoin.md`](https://github.com/aquarian-metals/coin-moebius/blob/main/docs/self-hosted-bitcoin.md) for the full walkthrough, including where the descriptor comes from, a regtest recipe for trying it without real money, and troubleshooting.
|
|
157
|
+
|
|
158
|
+
## Related
|
|
159
|
+
|
|
160
|
+
- For self-hosted Monero or Zano, see `@aquarian-metals/coin-moebius-monero` and `@aquarian-metals/coin-moebius-zano`.
|
|
161
|
+
- For custodial crypto on a hosted gateway, see `@aquarian-metals/coin-moebius-nowpayments`.
|
|
162
|
+
- For fiat (Stripe), see `@aquarian-metals/coin-moebius-stripe`.
|
|
163
|
+
- For Goldbacks, cash, check, or barter, see `@aquarian-metals/coin-moebius-manual`.
|
|
164
|
+
|
|
165
|
+
See the [main README](https://github.com/aquarian-metals/coin-moebius#readme) for the full quick-start, and `examples/static-site-demo/bitcoin/` for a copy-paste deployment.
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Bitcoin carries eight decimal places. One satoshi is 10^-8 BTC. */
|
|
2
|
+
export declare const BTC_DECIMALS = 8;
|
|
3
|
+
/**
|
|
4
|
+
* Satoshis to an exact decimal string of BTC with no trailing zeros.
|
|
5
|
+
*
|
|
6
|
+
* This is the only honest way to write an amount for a buyer. The satoshi
|
|
7
|
+
* integer is what the invoice actually requires; a float built from it can
|
|
8
|
+
* print binary noise or slide into exponent form, and a buyer who types what
|
|
9
|
+
* they read would then underpay. Lives here because both the browser modal
|
|
10
|
+
* and the server creator have to print the same number.
|
|
11
|
+
*/
|
|
12
|
+
export declare function formatAtomic(atomic: bigint): string;
|
|
13
|
+
//# sourceMappingURL=amounts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"amounts.d.ts","sourceRoot":"","sources":["../src/amounts.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,eAAO,MAAM,YAAY,IAAI,CAAC;AAE9B;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAKnD"}
|
package/dist/amounts.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Bitcoin carries eight decimal places. One satoshi is 10^-8 BTC. */
|
|
2
|
+
export const BTC_DECIMALS = 8;
|
|
3
|
+
/**
|
|
4
|
+
* Satoshis to an exact decimal string of BTC with no trailing zeros.
|
|
5
|
+
*
|
|
6
|
+
* This is the only honest way to write an amount for a buyer. The satoshi
|
|
7
|
+
* integer is what the invoice actually requires; a float built from it can
|
|
8
|
+
* print binary noise or slide into exponent form, and a buyer who types what
|
|
9
|
+
* they read would then underpay. Lives here because both the browser modal
|
|
10
|
+
* and the server creator have to print the same number.
|
|
11
|
+
*/
|
|
12
|
+
export function formatAtomic(atomic) {
|
|
13
|
+
const digits = atomic.toString().padStart(BTC_DECIMALS + 1, '0');
|
|
14
|
+
const whole = digits.slice(0, digits.length - BTC_DECIMALS);
|
|
15
|
+
const fraction = digits.slice(digits.length - BTC_DECIMALS).replace(/0+$/, '');
|
|
16
|
+
return fraction.length > 0 ? `${whole}.${fraction}` : whole;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=amounts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"amounts.js","sourceRoot":"","sources":["../src/amounts.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC;AAE9B;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAAC,MAAc;IAC1C,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,YAAY,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;IACjE,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,GAAG,YAAY,CAAC,CAAC;IAC5D,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,YAAY,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC/E,OAAO,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,IAAI,QAAQ,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;AAC7D,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bitcoin client-side provider for Coin Moebius.
|
|
3
|
+
*
|
|
4
|
+
* Self-hosted flow. The browser POSTs to a checkout endpoint on the
|
|
5
|
+
* merchant's own backend, which asks the merchant's Bitcoin Core wallet for
|
|
6
|
+
* a fresh receiving address and returns payment instructions. The buyer
|
|
7
|
+
* sees those instructions in a modal and pays from their own wallet. The
|
|
8
|
+
* merchant's separately running indexer watches the wallet and posts a
|
|
9
|
+
* webhook when the payment confirms.
|
|
10
|
+
*
|
|
11
|
+
* import { createBitcoinProvider } from '@aquarian-metals/coin-moebius-bitcoin';
|
|
12
|
+
* const bitcoin = createBitcoinProvider({
|
|
13
|
+
* checkoutEndpoint: '/api/checkout/bitcoin',
|
|
14
|
+
* statusEndpoint: '/api/payment-status',
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* const manager = createPaymentManager({ providers: [bitcoin] });
|
|
18
|
+
* await manager.initiate({ productId: 'pro', amount: 19.99, currency: 'USD' });
|
|
19
|
+
*/
|
|
20
|
+
import type { PaymentProvider } from '@aquarian-metals/coin-moebius-core';
|
|
21
|
+
export { BTC_DECIMALS, formatAtomic } from './amounts.js';
|
|
22
|
+
/**
|
|
23
|
+
* Shape of the payment instructions presented to the buyer. A custom
|
|
24
|
+
* `renderModal` receives this and is responsible for displaying it.
|
|
25
|
+
*/
|
|
26
|
+
export interface BitcoinInstructions {
|
|
27
|
+
paymentId: string;
|
|
28
|
+
/** Bitcoin address minted for this checkout. Never handed to a second checkout. */
|
|
29
|
+
address: string;
|
|
30
|
+
/** Exact amount to send, in satoshis, as a string to avoid float precision loss. */
|
|
31
|
+
atomicAmount: string;
|
|
32
|
+
/** Same amount in BTC, for human display. */
|
|
33
|
+
btcAmount: number;
|
|
34
|
+
/** `bitcoin:address?amount=…` link (BIP 21) suitable for a QR code or a wallet deep link. */
|
|
35
|
+
uri: string;
|
|
36
|
+
/** Epoch milliseconds. After this, the merchant's indexer will mark the payment failed if unpaid. */
|
|
37
|
+
expiresAt: number;
|
|
38
|
+
}
|
|
39
|
+
/** Client-side config. All secrets stay server-side. */
|
|
40
|
+
export interface BitcoinProviderConfig {
|
|
41
|
+
/**
|
|
42
|
+
* Full URL (or relative path) of the checkout endpoint that calls
|
|
43
|
+
* `createBitcoinCreator(...)` on the server and returns
|
|
44
|
+
* {@link BitcoinInstructions}. Defaults to `/api/checkout/bitcoin`.
|
|
45
|
+
*/
|
|
46
|
+
checkoutEndpoint?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Optional URL (or relative path) of a status endpoint that returns the
|
|
49
|
+
* current `PaymentRecord` for a `paymentId`. When provided, the provider
|
|
50
|
+
* attaches it to the pending result's metadata so the caller can hand it
|
|
51
|
+
* to `payments.subscribeToStatus(paymentId, …)`.
|
|
52
|
+
*/
|
|
53
|
+
statusEndpoint?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Optional custom modal renderer. Receives the buyer-facing instructions
|
|
56
|
+
* and a `close()` callback. Must return a cleanup function that removes
|
|
57
|
+
* the modal from the DOM.
|
|
58
|
+
*
|
|
59
|
+
* When omitted, a minimal inline-styled modal shows the address, amount,
|
|
60
|
+
* and the `bitcoin:` link. The default is a working fallback, not a
|
|
61
|
+
* styling target.
|
|
62
|
+
*/
|
|
63
|
+
renderModal?: (instructions: BitcoinInstructions, callbacks: {
|
|
64
|
+
onClose: () => void;
|
|
65
|
+
}) => () => void;
|
|
66
|
+
/** Optional `fetch` override. Defaults to global `fetch`. */
|
|
67
|
+
fetcher?: typeof fetch;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Build a `PaymentProvider` registered as `id: 'bitcoin'`. Calls `onPending`
|
|
71
|
+
* once the buyer-facing modal is shown; never calls `onSuccess` directly,
|
|
72
|
+
* because chain confirmations land asynchronously through the indexer, the
|
|
73
|
+
* webhook, and the merchant's status store. Call
|
|
74
|
+
* `manager.subscribeToStatus(paymentId, …)` after `initiate` resolves to be
|
|
75
|
+
* told when the payment settles.
|
|
76
|
+
*/
|
|
77
|
+
export declare function createBitcoinProvider(config?: BitcoinProviderConfig): PaymentProvider;
|
|
78
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,eAAe,EAAmB,MAAM,oCAAoC,CAAC;AAI3F,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE1D;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,mFAAmF;IACnF,OAAO,EAAE,MAAM,CAAC;IAChB,oFAAoF;IACpF,YAAY,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,6FAA6F;IAC7F,GAAG,EAAE,MAAM,CAAC;IACZ,qGAAqG;IACrG,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,wDAAwD;AACxD,MAAM,WAAW,qBAAqB;IACrC;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,CACb,YAAY,EAAE,mBAAmB,EACjC,SAAS,EAAE;QAAE,OAAO,EAAE,MAAM,IAAI,CAAA;KAAE,KAC9B,MAAM,IAAI,CAAC;IAChB,6DAA6D;IAC7D,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,GAAE,qBAA0B,GAAG,eAAe,CAsCzF"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { formatAtomic } from './amounts.js';
|
|
2
|
+
export { BTC_DECIMALS, formatAtomic } from './amounts.js';
|
|
3
|
+
/**
|
|
4
|
+
* Build a `PaymentProvider` registered as `id: 'bitcoin'`. Calls `onPending`
|
|
5
|
+
* once the buyer-facing modal is shown; never calls `onSuccess` directly,
|
|
6
|
+
* because chain confirmations land asynchronously through the indexer, the
|
|
7
|
+
* webhook, and the merchant's status store. Call
|
|
8
|
+
* `manager.subscribeToStatus(paymentId, …)` after `initiate` resolves to be
|
|
9
|
+
* told when the payment settles.
|
|
10
|
+
*/
|
|
11
|
+
export function createBitcoinProvider(config = {}) {
|
|
12
|
+
const checkoutEndpoint = config.checkoutEndpoint ?? '/api/checkout/bitcoin';
|
|
13
|
+
const fetcher = config.fetcher ?? globalThis.fetch.bind(globalThis);
|
|
14
|
+
const renderer = config.renderModal ?? defaultRenderModal;
|
|
15
|
+
return {
|
|
16
|
+
id: 'bitcoin',
|
|
17
|
+
name: 'Bitcoin',
|
|
18
|
+
async initiate(options, callbacks) {
|
|
19
|
+
try {
|
|
20
|
+
const instructions = await fetchInstructions(fetcher, checkoutEndpoint, options);
|
|
21
|
+
const cleanup = renderer(instructions, {
|
|
22
|
+
onClose: () => cleanup(),
|
|
23
|
+
});
|
|
24
|
+
callbacks.onPending?.({
|
|
25
|
+
status: 'pending',
|
|
26
|
+
paymentId: instructions.paymentId,
|
|
27
|
+
provider: 'bitcoin',
|
|
28
|
+
amount: options.amount,
|
|
29
|
+
currency: options.currency,
|
|
30
|
+
metadata: {
|
|
31
|
+
...(options.metadata ?? {}),
|
|
32
|
+
address: instructions.address,
|
|
33
|
+
atomicAmount: instructions.atomicAmount,
|
|
34
|
+
btcAmount: instructions.btcAmount,
|
|
35
|
+
uri: instructions.uri,
|
|
36
|
+
expiresAt: instructions.expiresAt,
|
|
37
|
+
statusEndpoint: config.statusEndpoint,
|
|
38
|
+
},
|
|
39
|
+
timestamp: Date.now(),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
callbacks.onError(err instanceof Error ? err : new Error(String(err)));
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const ALLOWED_URI_SCHEMES = new Set(['bitcoin:']);
|
|
49
|
+
function validateUri(uri) {
|
|
50
|
+
let parsed;
|
|
51
|
+
try {
|
|
52
|
+
parsed = new URL(uri);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
throw new Error('coin-moebius/bitcoin: checkout response contains an invalid URI');
|
|
56
|
+
}
|
|
57
|
+
if (!ALLOWED_URI_SCHEMES.has(parsed.protocol)) {
|
|
58
|
+
throw new Error(`coin-moebius/bitcoin: URI scheme "${parsed.protocol}" is not allowed (expected bitcoin:)`);
|
|
59
|
+
}
|
|
60
|
+
return uri;
|
|
61
|
+
}
|
|
62
|
+
async function fetchInstructions(fetcher, endpoint, options) {
|
|
63
|
+
const response = await fetcher(endpoint, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: { 'Content-Type': 'application/json' },
|
|
66
|
+
body: JSON.stringify({
|
|
67
|
+
productId: options.productId,
|
|
68
|
+
amount: options.amount,
|
|
69
|
+
currency: options.currency,
|
|
70
|
+
metadata: options.metadata,
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
throw new Error(`coin-moebius/bitcoin: checkout endpoint responded ${response.status}`);
|
|
75
|
+
}
|
|
76
|
+
const payload = (await response.json());
|
|
77
|
+
// Check every field the modal renders, not just the ones it routes on. A
|
|
78
|
+
// missing number is not a blank on the screen, it is the word "undefined"
|
|
79
|
+
// where an amount should be, or "NaN minutes" on the expiry line, and the
|
|
80
|
+
// buyer has no way to tell that from a real instruction.
|
|
81
|
+
if (typeof payload.paymentId !== 'string' ||
|
|
82
|
+
typeof payload.address !== 'string' ||
|
|
83
|
+
typeof payload.atomicAmount !== 'string' ||
|
|
84
|
+
!/^\d+$/.test(payload.atomicAmount) ||
|
|
85
|
+
typeof payload.uri !== 'string' ||
|
|
86
|
+
!Number.isFinite(payload.btcAmount) ||
|
|
87
|
+
!Number.isFinite(payload.expiresAt)) {
|
|
88
|
+
throw new Error('coin-moebius/bitcoin: checkout response missing required fields (paymentId, address, atomicAmount, btcAmount, uri, expiresAt)');
|
|
89
|
+
}
|
|
90
|
+
payload.uri = validateUri(payload.uri);
|
|
91
|
+
return payload;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Minimal default modal. Names the network on the line that matters, because
|
|
95
|
+
* bitcoin sent over Lightning or as a wrapped coin on another chain never
|
|
96
|
+
* reaches this address. Uses inline styles to survive arbitrary host-page
|
|
97
|
+
* CSS. No QR rendering, to keep the browser bundle small; render your own
|
|
98
|
+
* inside a custom `renderModal`.
|
|
99
|
+
*/
|
|
100
|
+
function defaultRenderModal(instructions, callbacks) {
|
|
101
|
+
const overlay = document.createElement('div');
|
|
102
|
+
overlay.setAttribute('role', 'dialog');
|
|
103
|
+
overlay.setAttribute('aria-modal', 'true');
|
|
104
|
+
overlay.setAttribute('aria-labelledby', 'cm-bitcoin-title');
|
|
105
|
+
overlay.style.cssText = [
|
|
106
|
+
'position:fixed',
|
|
107
|
+
'inset:0',
|
|
108
|
+
'z-index:2147483647',
|
|
109
|
+
'background:rgba(0,0,0,0.5)',
|
|
110
|
+
'display:flex',
|
|
111
|
+
'align-items:center',
|
|
112
|
+
'justify-content:center',
|
|
113
|
+
'padding:16px',
|
|
114
|
+
'font-family:system-ui,-apple-system,sans-serif',
|
|
115
|
+
].join(';');
|
|
116
|
+
const card = document.createElement('div');
|
|
117
|
+
card.style.cssText = [
|
|
118
|
+
'background:#fff',
|
|
119
|
+
'color:#1a1a1a',
|
|
120
|
+
'border-radius:8px',
|
|
121
|
+
'padding:24px',
|
|
122
|
+
'max-width:520px',
|
|
123
|
+
'width:100%',
|
|
124
|
+
'box-shadow:0 20px 60px rgba(0,0,0,0.2)',
|
|
125
|
+
].join(';');
|
|
126
|
+
const expiresIn = Math.max(0, Math.round((instructions.expiresAt - Date.now()) / 1000 / 60));
|
|
127
|
+
// Print the satoshi integer, not the float beside it. The satoshi value is
|
|
128
|
+
// what settles the invoice, and it is the only form that cannot show the
|
|
129
|
+
// buyer binary noise or exponent notation in place of a payable number.
|
|
130
|
+
const amount = `${escapeHtml(formatAtomic(BigInt(instructions.atomicAmount)))} BTC`;
|
|
131
|
+
card.innerHTML = `
|
|
132
|
+
<h2 id="cm-bitcoin-title" style="margin:0 0 8px;font-size:1.25rem;">Pay with Bitcoin</h2>
|
|
133
|
+
<p style="margin:0 0 16px;color:#555;">Send <strong>exactly</strong> ${amount} to the address below. Send only on the Bitcoin network. Expires in ~${expiresIn} minutes.</p>
|
|
134
|
+
<div style="background:#f5f5f5;padding:16px;border-radius:4px;margin-bottom:16px;font-family:ui-monospace,monospace;font-size:0.85rem;word-break:break-all;">
|
|
135
|
+
<div style="margin-bottom:8px;"><strong>Address:</strong></div>
|
|
136
|
+
<div style="user-select:all;">${escapeHtml(instructions.address)}</div>
|
|
137
|
+
<div style="margin-top:12px;"><strong>Amount:</strong> ${amount}</div>
|
|
138
|
+
<div style="margin-top:8px;"><strong>Wallet link:</strong></div>
|
|
139
|
+
<div><a href="${escapeHtml(instructions.uri)}" style="color:#1a73e8;word-break:break-all;">${escapeHtml(instructions.uri)}</a></div>
|
|
140
|
+
</div>
|
|
141
|
+
<div style="display:flex;gap:8px;justify-content:flex-end;">
|
|
142
|
+
<button type="button" data-action="close" style="padding:8px 16px;border:none;background:#1a1a1a;color:#fff;border-radius:4px;cursor:pointer;font:inherit;">Done</button>
|
|
143
|
+
</div>
|
|
144
|
+
`;
|
|
145
|
+
overlay.appendChild(card);
|
|
146
|
+
document.body.appendChild(overlay);
|
|
147
|
+
const previouslyFocused = document.activeElement;
|
|
148
|
+
const closeBtn = card.querySelector('[data-action="close"]');
|
|
149
|
+
closeBtn?.focus();
|
|
150
|
+
closeBtn?.addEventListener('click', () => callbacks.onClose());
|
|
151
|
+
const onKey = (e) => {
|
|
152
|
+
if (e.key === 'Escape')
|
|
153
|
+
callbacks.onClose();
|
|
154
|
+
};
|
|
155
|
+
document.addEventListener('keydown', onKey);
|
|
156
|
+
return () => {
|
|
157
|
+
document.removeEventListener('keydown', onKey);
|
|
158
|
+
overlay.remove();
|
|
159
|
+
previouslyFocused?.focus();
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function escapeHtml(s) {
|
|
163
|
+
const map = {
|
|
164
|
+
'&': '&',
|
|
165
|
+
'<': '<',
|
|
166
|
+
'>': '>',
|
|
167
|
+
'"': '"',
|
|
168
|
+
"'": ''',
|
|
169
|
+
};
|
|
170
|
+
return s.replace(/[&<>"']/g, (c) => map[c] ?? c);
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|