@did-btcr2/bitcoin 0.3.4 → 0.5.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/dist/cjs/client/http.js +9 -0
- package/dist/cjs/client/http.js.map +1 -0
- package/dist/cjs/client/rest/address.js +20 -28
- package/dist/cjs/client/rest/address.js.map +1 -1
- package/dist/cjs/client/rest/block.js +23 -22
- package/dist/cjs/client/rest/block.js.map +1 -1
- package/dist/cjs/client/rest/index.js +31 -84
- package/dist/cjs/client/rest/index.js.map +1 -1
- package/dist/cjs/client/rest/protocol.js +129 -0
- package/dist/cjs/client/rest/protocol.js.map +1 -0
- package/dist/cjs/client/rest/transaction.js +19 -21
- package/dist/cjs/client/rest/transaction.js.map +1 -1
- package/dist/cjs/client/rpc/index.js +79 -337
- package/dist/cjs/client/rpc/index.js.map +1 -1
- package/dist/cjs/client/rpc/interface.js +1 -6
- package/dist/cjs/client/rpc/interface.js.map +1 -1
- package/dist/cjs/client/rpc/json-rpc.js +40 -63
- package/dist/cjs/client/rpc/json-rpc.js.map +1 -1
- package/dist/cjs/client/rpc/protocol.js +134 -0
- package/dist/cjs/client/rpc/protocol.js.map +1 -0
- package/dist/cjs/connection.js +104 -0
- package/dist/cjs/connection.js.map +1 -0
- package/dist/cjs/constants.js +16 -5
- package/dist/cjs/constants.js.map +1 -1
- package/dist/cjs/errors.js.map +1 -1
- package/dist/cjs/index.js +21 -13
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/types.js +5 -55
- package/dist/cjs/types.js.map +1 -1
- package/dist/esm/client/http.js +9 -0
- package/dist/esm/client/http.js.map +1 -0
- package/dist/esm/client/rest/address.js +20 -28
- package/dist/esm/client/rest/address.js.map +1 -1
- package/dist/esm/client/rest/block.js +23 -22
- package/dist/esm/client/rest/block.js.map +1 -1
- package/dist/esm/client/rest/index.js +31 -84
- package/dist/esm/client/rest/index.js.map +1 -1
- package/dist/esm/client/rest/protocol.js +129 -0
- package/dist/esm/client/rest/protocol.js.map +1 -0
- package/dist/esm/client/rest/transaction.js +19 -21
- package/dist/esm/client/rest/transaction.js.map +1 -1
- package/dist/esm/client/rpc/index.js +79 -337
- package/dist/esm/client/rpc/index.js.map +1 -1
- package/dist/esm/client/rpc/interface.js +1 -6
- package/dist/esm/client/rpc/interface.js.map +1 -1
- package/dist/esm/client/rpc/json-rpc.js +40 -63
- package/dist/esm/client/rpc/json-rpc.js.map +1 -1
- package/dist/esm/client/rpc/protocol.js +134 -0
- package/dist/esm/client/rpc/protocol.js.map +1 -0
- package/dist/esm/connection.js +104 -0
- package/dist/esm/connection.js.map +1 -0
- package/dist/esm/constants.js +16 -5
- package/dist/esm/constants.js.map +1 -1
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.js +21 -13
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/types.js +5 -55
- package/dist/esm/types.js.map +1 -1
- package/dist/types/client/http.d.ts +22 -0
- package/dist/types/client/http.d.ts.map +1 -0
- package/dist/types/client/rest/address.d.ts +17 -19
- package/dist/types/client/rest/address.d.ts.map +1 -1
- package/dist/types/client/rest/block.d.ts +25 -16
- package/dist/types/client/rest/block.d.ts.map +1 -1
- package/dist/types/client/rest/index.d.ts +21 -51
- package/dist/types/client/rest/index.d.ts.map +1 -1
- package/dist/types/client/rest/protocol.d.ts +59 -0
- package/dist/types/client/rest/protocol.d.ts.map +1 -0
- package/dist/types/client/rest/transaction.d.ts +17 -17
- package/dist/types/client/rest/transaction.d.ts.map +1 -1
- package/dist/types/client/rpc/index.d.ts +112 -253
- package/dist/types/client/rpc/index.d.ts.map +1 -1
- package/dist/types/client/rpc/interface.d.ts +42 -6
- package/dist/types/client/rpc/interface.d.ts.map +1 -1
- package/dist/types/client/rpc/json-rpc.d.ts +21 -18
- package/dist/types/client/rpc/json-rpc.d.ts.map +1 -1
- package/dist/types/client/rpc/protocol.d.ts +83 -0
- package/dist/types/client/rpc/protocol.d.ts.map +1 -0
- package/dist/types/client/utils.d.ts +1 -0
- package/dist/types/connection.d.ts +82 -0
- package/dist/types/connection.d.ts.map +1 -0
- package/dist/types/constants.d.ts +17 -5
- package/dist/types/constants.d.ts.map +1 -1
- package/dist/types/errors.d.ts +5 -3
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/index.d.ts +21 -13
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/network.d.ts +1 -0
- package/dist/types/types.d.ts +99 -696
- package/dist/types/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/client/http.ts +28 -0
- package/src/client/rest/address.ts +24 -30
- package/src/client/rest/block.ts +29 -26
- package/src/client/rest/index.ts +38 -94
- package/src/client/rest/protocol.ts +153 -0
- package/src/client/rest/transaction.ts +23 -23
- package/src/client/rpc/index.ts +146 -379
- package/src/client/rpc/interface.ts +88 -53
- package/src/client/rpc/json-rpc.ts +53 -60
- package/src/client/rpc/protocol.ts +160 -0
- package/src/connection.ts +136 -0
- package/src/constants.ts +17 -5
- package/src/errors.ts +10 -3
- package/src/index.ts +81 -13
- package/src/types.ts +332 -972
- package/dist/cjs/bitcoin.js +0 -105
- package/dist/cjs/bitcoin.js.map +0 -1
- package/dist/esm/bitcoin.js +0 -105
- package/dist/esm/bitcoin.js.map +0 -1
- package/dist/types/bitcoin.d.ts +0 -60
- package/dist/types/bitcoin.d.ts.map +0 -1
- package/src/bitcoin.ts +0 -149
|
@@ -3,63 +3,98 @@ import {
|
|
|
3
3
|
ChainInfo,
|
|
4
4
|
CreateRawTxInputs,
|
|
5
5
|
CreateRawTxOutputs,
|
|
6
|
-
|
|
6
|
+
DerivedAddresses,
|
|
7
7
|
GetBlockParams,
|
|
8
|
-
|
|
8
|
+
ListTransactionsParams,
|
|
9
|
+
ListTransactionsResult,
|
|
10
|
+
RawTransactionV2,
|
|
11
|
+
SignedRawTx,
|
|
12
|
+
UnspentTxInfo
|
|
9
13
|
} from '../../types.js';
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
|
-
* General interface for a Bitcoin Core RPC client
|
|
13
|
-
* @name BitcoinRpcClient
|
|
14
|
-
* @type {BitcoinRpcClient}
|
|
16
|
+
* General interface for a Bitcoin Core RPC client.
|
|
15
17
|
*/
|
|
16
18
|
export interface BitcoinRpcClient {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
verifyMessage(address: string, signature: string, message: string): Promise<boolean>;
|
|
56
|
-
|
|
57
|
-
signMessage(address: string, message: string): Promise<string>;
|
|
58
|
-
}
|
|
19
|
+
/** Gets detailed information about a specific block. */
|
|
20
|
+
getBlock({ blockhash, height, verbosity }: GetBlockParams): Promise<BlockResponse | undefined>;
|
|
21
|
+
|
|
22
|
+
/** Returns the number of blocks in the longest blockchain. */
|
|
23
|
+
getBlockCount(): Promise<number>;
|
|
24
|
+
|
|
25
|
+
/** Gets the hash of a block at a given height. */
|
|
26
|
+
getBlockHash(height: number): Promise<string>;
|
|
27
|
+
|
|
28
|
+
/** Retrieves general blockchain state info. */
|
|
29
|
+
getBlockchainInfo(): Promise<ChainInfo>;
|
|
30
|
+
|
|
31
|
+
/** Signs a raw transaction with the wallet's private keys. */
|
|
32
|
+
signRawTransaction(hexstring: string): Promise<SignedRawTx>;
|
|
33
|
+
|
|
34
|
+
/** Sends a raw transaction hex to the Bitcoin network. */
|
|
35
|
+
sendRawTransaction(hexstring: string, maxfeerate?: number | string, maxBurnAmount?: number | string): Promise<string>;
|
|
36
|
+
|
|
37
|
+
/** Signs and sends a raw transaction in one step. */
|
|
38
|
+
signAndSendRawTransaction(hexstring: string): Promise<string>
|
|
39
|
+
|
|
40
|
+
/** Creates, signs, and sends a raw transaction in one step. */
|
|
41
|
+
createSignSendRawTransaction(inputs: CreateRawTxInputs[], outputs: CreateRawTxOutputs[]): Promise<string>;
|
|
42
|
+
|
|
43
|
+
/** Lists transactions in the wallet. */
|
|
44
|
+
listTransactions(params: ListTransactionsParams): Promise<ListTransactionsResult>;
|
|
45
|
+
|
|
46
|
+
/** Creates a raw transaction spending specified inputs to specified outputs. */
|
|
47
|
+
createRawTransaction(inputs: CreateRawTxInputs[], outputs: CreateRawTxOutputs[], locktime?: number, replacable?: boolean): Promise<string>;
|
|
48
|
+
|
|
49
|
+
/** Derives addresses from a descriptor. */
|
|
50
|
+
deriveAddresses(descriptor: string, range?: Array<number>): Promise<Array<DerivedAddresses>>;
|
|
51
|
+
|
|
52
|
+
/** Mines a specified number of blocks to a given address. */
|
|
53
|
+
generateToAddress(nblocks: number, address: string): Promise<string[]>;
|
|
54
|
+
|
|
55
|
+
/** Gets the wallet's balance. */
|
|
56
|
+
getBalance(): Promise<number>;
|
|
59
57
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
/** Gets a new Bitcoin address for receiving payments. */
|
|
59
|
+
getNewAddress(addressType: string, label?: string): Promise<string>;
|
|
60
|
+
|
|
61
|
+
/** Lists unspent transaction outputs in the wallet. */
|
|
62
|
+
listUnspent(params: { minconf?: number; maxconf?: number; address?: string[]; include_unsafe?: boolean; }): Promise<UnspentTxInfo[]>
|
|
63
|
+
|
|
64
|
+
/** Creates a raw transaction spending specified inputs to specified outputs. */
|
|
65
|
+
createRawTransaction(inputs: CreateRawTxInputs[], outputs: CreateRawTxOutputs[], locktime?: number, replacable?: boolean): Promise<string>;
|
|
66
|
+
|
|
67
|
+
/** Returns the number of blocks in the longest blockchain. */
|
|
68
|
+
getBlockCount(): Promise<number>;
|
|
69
|
+
|
|
70
|
+
/** Gets the hash of a block at a given height. */
|
|
71
|
+
getBlockHash(height: number): Promise<string>;
|
|
72
|
+
|
|
73
|
+
/** Gets detailed information about a specific block. */
|
|
74
|
+
getBlock({ blockhash, height, verbosity }: GetBlockParams): Promise<BlockResponse | undefined>
|
|
75
|
+
|
|
76
|
+
/** Retrieves general blockchain state info. */
|
|
77
|
+
getBlockchainInfo(): Promise<ChainInfo>;
|
|
78
|
+
|
|
79
|
+
/** Gets a new Bitcoin address for receiving payments. */
|
|
80
|
+
getNewAddress(account?: string): Promise<string>;
|
|
81
|
+
|
|
82
|
+
/** Sends raw transaction hex to the Bitcoin network. */
|
|
83
|
+
sendRawTransaction(
|
|
84
|
+
hexstring: string,
|
|
85
|
+
maxfeerate?: number | string,
|
|
86
|
+
maxBurnAmount?: number | string
|
|
87
|
+
): Promise<string>;
|
|
88
|
+
|
|
89
|
+
/** Sends bitcoins to a specified address. */
|
|
90
|
+
sendToAddress(address: string, amount: number): Promise<RawTransactionV2>;
|
|
91
|
+
|
|
92
|
+
/** Verifies a signed message. */
|
|
93
|
+
verifyMessage(address: string, signature: string, message: string): Promise<boolean>;
|
|
94
|
+
|
|
95
|
+
/** Sign a message with the private key of an address. */
|
|
96
|
+
signMessage(address: string, message: string): Promise<string>;
|
|
97
|
+
|
|
98
|
+
/** Mines a specified number of blocks to a given address. */
|
|
99
|
+
generateToAddress(nblocks: number, address: string): Promise<string[]>;
|
|
100
|
+
}
|
|
@@ -1,81 +1,74 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { BitcoinRpcError } from '../../errors.js';
|
|
2
|
+
import { RpcConfig } from '../../types.js';
|
|
3
|
+
import { HttpExecutor, defaultHttpExecutor } from '../http.js';
|
|
4
|
+
import { safeText } from '../utils.js';
|
|
5
|
+
import { JsonRpcProtocol } from './protocol.js';
|
|
3
6
|
|
|
4
7
|
export class JsonRpcTransport {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
/**
|
|
9
|
+
* The sans-I/O protocol layer. Use this to build {@link HttpRequest}
|
|
10
|
+
* descriptors without performing any I/O.
|
|
11
|
+
*/
|
|
12
|
+
readonly protocol: JsonRpcProtocol;
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
this.url = (cfg.host || 'http://127.0.0.1:8332').replace(/\/+$/, '');
|
|
14
|
+
private readonly execute: HttpExecutor;
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
this.url = u.toString().replace(/\/+$/, '');
|
|
21
|
-
}
|
|
22
|
-
} catch(error: any) {
|
|
23
|
-
console.error(`Invalid URL in Bitcoin RPC config: ${this.url}`, error);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
16
|
+
constructor(cfg: RpcConfig, executor?: HttpExecutor) {
|
|
17
|
+
this.protocol = new JsonRpcProtocol(cfg);
|
|
18
|
+
this.execute = executor ?? defaultHttpExecutor;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** @internal Expose URL for tests that inspect transport state. */
|
|
22
|
+
get url(): string {
|
|
23
|
+
return this.protocol.url;
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
/**
|
|
29
|
-
*
|
|
30
|
-
* @param {BatchOption[] | { method: string; parameters?: any[] }} batch A single RPC call or an array of calls to be made.
|
|
31
|
-
* @returns {Promise<any[] | any>} The result of the RPC call(s).
|
|
32
|
-
* @example
|
|
27
|
+
* Execute a single JSON-RPC call.
|
|
33
28
|
*/
|
|
34
|
-
async
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
29
|
+
async call(method: string, params: unknown[] = []): Promise<unknown> {
|
|
30
|
+
const request = this.protocol.buildRequest(method, params);
|
|
31
|
+
const res = await this.execute(request);
|
|
32
|
+
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
const text = await safeText(res);
|
|
35
|
+
throw new BitcoinRpcError(
|
|
36
|
+
'HTTP_ERROR',
|
|
37
|
+
res.status,
|
|
38
|
+
text || `${res.status} ${res.statusText}`,
|
|
39
|
+
{ method }
|
|
40
|
+
);
|
|
43
41
|
}
|
|
42
|
+
|
|
43
|
+
const payload = await res.json() as { result?: unknown; error?: { code: number; message: string } };
|
|
44
|
+
return this.protocol.parseResponse(payload, method);
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* @param {any[]} params The parameters for the RPC method.
|
|
50
|
-
* @returns {Promise<any>} The result of the RPC call.
|
|
48
|
+
* Execute a JSON-RPC batch in a single HTTP request.
|
|
49
|
+
* Returns results in the same order as the input calls.
|
|
51
50
|
*/
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
async batch(calls: Array<{ method: string; params: unknown[] }>): Promise<unknown[]> {
|
|
52
|
+
if (calls.length === 0) return [];
|
|
53
|
+
if (calls.length === 1) {
|
|
54
|
+
const result = await this.call(calls[0].method, calls[0].params);
|
|
55
|
+
return [result];
|
|
56
|
+
}
|
|
54
57
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
headers : {
|
|
58
|
-
'Content-Type' : 'application/json',
|
|
59
|
-
...(this.authHeader ? { Authorization: this.authHeader } : {})
|
|
60
|
-
},
|
|
61
|
-
body : JSON.stringify(body)
|
|
62
|
-
});
|
|
58
|
+
const request = this.protocol.buildBatchRequest(calls);
|
|
59
|
+
const res = await this.execute(request);
|
|
63
60
|
|
|
64
61
|
if (!res.ok) {
|
|
65
62
|
const text = await safeText(res);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
throw new BitcoinRpcError(
|
|
64
|
+
'HTTP_ERROR',
|
|
65
|
+
res.status,
|
|
66
|
+
text || `${res.status} ${res.statusText}`,
|
|
67
|
+
{ methods: calls.map(c => c.method) }
|
|
68
|
+
);
|
|
70
69
|
}
|
|
71
70
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
const err = new Error(payload.error.message);
|
|
75
|
-
(err as any).code = payload.error.code;
|
|
76
|
-
(err as any).rpc = true;
|
|
77
|
-
throw err;
|
|
78
|
-
}
|
|
79
|
-
return payload.result;
|
|
71
|
+
const payloads = await res.json() as Array<{ id: number; result?: unknown; error?: { code: number; message: string } }>;
|
|
72
|
+
return this.protocol.parseBatchResponse(payloads, calls);
|
|
80
73
|
}
|
|
81
74
|
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { BitcoinRpcError } from '../../errors.js';
|
|
2
|
+
import { RpcConfig } from '../../types.js';
|
|
3
|
+
import { HttpRequest } from '../http.js';
|
|
4
|
+
import { toBase64 } from '../utils.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Sans-I/O JSON-RPC protocol for Bitcoin Core.
|
|
8
|
+
*
|
|
9
|
+
* Builds {@link HttpRequest} descriptors for JSON-RPC method calls and
|
|
10
|
+
* provides response parsing — without performing any I/O.
|
|
11
|
+
*
|
|
12
|
+
* **Security note:** Built requests include an `Authorization` header when
|
|
13
|
+
* credentials are configured. Do not log or persist {@link HttpRequest}
|
|
14
|
+
* objects without redacting the `Authorization` header.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* const protocol = new JsonRpcProtocol({
|
|
19
|
+
* host: 'http://localhost:18443',
|
|
20
|
+
* username: 'user',
|
|
21
|
+
* password: 'pass',
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* // Build a request descriptor (no I/O)
|
|
25
|
+
* const req = protocol.buildRequest('getblockcount', []);
|
|
26
|
+
*
|
|
27
|
+
* // Execute with any HTTP client
|
|
28
|
+
* const res = await fetch(req.url, req);
|
|
29
|
+
* const json = await res.json();
|
|
30
|
+
*
|
|
31
|
+
* // Parse the JSON-RPC response (throws on errors)
|
|
32
|
+
* const blockCount = protocol.parseResponse(json, 'getblockcount', []);
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export class JsonRpcProtocol {
|
|
36
|
+
readonly url: string;
|
|
37
|
+
|
|
38
|
+
/** Whether this protocol instance has credentials configured. */
|
|
39
|
+
readonly hasAuth: boolean;
|
|
40
|
+
|
|
41
|
+
private readonly _headers: Record<string, string>;
|
|
42
|
+
private _id = 0;
|
|
43
|
+
|
|
44
|
+
constructor(cfg: RpcConfig) {
|
|
45
|
+
let url = (cfg.host || 'http://127.0.0.1:8332').replace(/\/+$/, '');
|
|
46
|
+
let authHeader: string | undefined;
|
|
47
|
+
|
|
48
|
+
if (cfg.username && cfg.password) {
|
|
49
|
+
authHeader = `Basic ${toBase64(`${cfg.username}:${cfg.password}`)}`;
|
|
50
|
+
} else {
|
|
51
|
+
try {
|
|
52
|
+
const u = new URL(url);
|
|
53
|
+
if (u.username || u.password) {
|
|
54
|
+
authHeader = `Basic ${toBase64(`${decodeURIComponent(u.username)}:${decodeURIComponent(u.password)}`)}`;
|
|
55
|
+
u.username = ''; u.password = '';
|
|
56
|
+
url = u.toString().replace(/\/+$/, '');
|
|
57
|
+
}
|
|
58
|
+
} catch (error: unknown) {
|
|
59
|
+
console.error(`Invalid URL in Bitcoin RPC config: ${url}`, error);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.url = url;
|
|
64
|
+
this.hasAuth = authHeader !== undefined;
|
|
65
|
+
this._headers = {
|
|
66
|
+
'Content-Type' : 'application/json',
|
|
67
|
+
...(authHeader ? { Authorization: authHeader } : {}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Build an {@link HttpRequest} for a JSON-RPC method call.
|
|
73
|
+
*/
|
|
74
|
+
buildRequest(method: string, params: unknown[]): HttpRequest {
|
|
75
|
+
const body = { jsonrpc: '2.0', id: ++this._id, method, params };
|
|
76
|
+
return {
|
|
77
|
+
url : this.url,
|
|
78
|
+
method : 'POST',
|
|
79
|
+
headers : { ...this._headers },
|
|
80
|
+
body : JSON.stringify(body),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Build an {@link HttpRequest} for a JSON-RPC batch call.
|
|
86
|
+
* Sends all calls in a single HTTP request per the JSON-RPC 2.0 spec.
|
|
87
|
+
*/
|
|
88
|
+
buildBatchRequest(calls: Array<{ method: string; params: unknown[] }>): HttpRequest {
|
|
89
|
+
const body = calls.map(c => ({
|
|
90
|
+
jsonrpc : '2.0',
|
|
91
|
+
id : ++this._id,
|
|
92
|
+
method : c.method,
|
|
93
|
+
params : c.params,
|
|
94
|
+
}));
|
|
95
|
+
return {
|
|
96
|
+
url : this.url,
|
|
97
|
+
method : 'POST',
|
|
98
|
+
headers : { ...this._headers },
|
|
99
|
+
body : JSON.stringify(body),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Parse a JSON-RPC response payload, throwing {@link BitcoinRpcError}
|
|
105
|
+
* if the response contains an error.
|
|
106
|
+
*/
|
|
107
|
+
parseResponse(
|
|
108
|
+
payload: { result?: unknown; error?: { code: number; message: string } },
|
|
109
|
+
method: string,
|
|
110
|
+
): unknown {
|
|
111
|
+
if (payload.error) {
|
|
112
|
+
throw new BitcoinRpcError(
|
|
113
|
+
'RPC_ERROR',
|
|
114
|
+
payload.error.code,
|
|
115
|
+
payload.error.message,
|
|
116
|
+
{ method }
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return payload.result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Parse a JSON-RPC batch response payload.
|
|
124
|
+
* Returns results in the same order as the original calls.
|
|
125
|
+
*/
|
|
126
|
+
parseBatchResponse(
|
|
127
|
+
payloads: Array<{ id: number; result?: unknown; error?: { code: number; message: string } }>,
|
|
128
|
+
calls: Array<{ method: string; params: unknown[] }>,
|
|
129
|
+
): unknown[] {
|
|
130
|
+
const byId = new Map(payloads.map(p => [p.id, p]));
|
|
131
|
+
// Batch responses may arrive out of order; re-sort by sequential id.
|
|
132
|
+
// IDs were assigned as (_id - calls.length + 1) .. _id
|
|
133
|
+
const startId = this._id - calls.length + 1;
|
|
134
|
+
|
|
135
|
+
return calls.map((call, i) => {
|
|
136
|
+
const payload = byId.get(startId + i);
|
|
137
|
+
if (!payload) {
|
|
138
|
+
throw new BitcoinRpcError(
|
|
139
|
+
'RPC_ERROR',
|
|
140
|
+
-1,
|
|
141
|
+
`Missing response for batch call ${call.method} (id ${startId + i})`,
|
|
142
|
+
{ method: call.method }
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return this.parseResponse(payload, call.method);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Return a copy of the headers with the Authorization value redacted.
|
|
151
|
+
* Use this for logging or debugging.
|
|
152
|
+
*/
|
|
153
|
+
redactedHeaders(): Record<string, string> {
|
|
154
|
+
const copy = { ...this._headers };
|
|
155
|
+
if (copy.Authorization) {
|
|
156
|
+
copy.Authorization = 'Basic [REDACTED]';
|
|
157
|
+
}
|
|
158
|
+
return copy;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { MethodError } from '@did-btcr2/common';
|
|
2
|
+
import { networks } from 'bitcoinjs-lib';
|
|
3
|
+
import { HttpExecutor } from './client/http.js';
|
|
4
|
+
import { BitcoinRestClient } from './client/rest/index.js';
|
|
5
|
+
import { BitcoinCoreRpcClient } from './client/rpc/index.js';
|
|
6
|
+
import { getNetwork } from './network.js';
|
|
7
|
+
import { NetworkName, RestConfig, RpcConfig } from './types.js';
|
|
8
|
+
import { DEFAULT_BITCOIN_NETWORK_CONFIG } from './constants.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Options for creating a BitcoinConnection.
|
|
12
|
+
*/
|
|
13
|
+
export type BitcoinConnectionOptions = {
|
|
14
|
+
network: NetworkName;
|
|
15
|
+
rest: RestConfig;
|
|
16
|
+
rpc?: RpcConfig;
|
|
17
|
+
/** Optional HTTP executor for sans-I/O usage. Defaults to global `fetch`. */
|
|
18
|
+
executor?: HttpExecutor;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Represents a connection to a single Bitcoin network.
|
|
23
|
+
* Holds the REST and optional RPC clients for that network.
|
|
24
|
+
*
|
|
25
|
+
* The underlying clients use a sans-I/O protocol layer that separates
|
|
26
|
+
* request construction from HTTP execution. By default, requests are
|
|
27
|
+
* executed via the global `fetch` function. Supply a custom
|
|
28
|
+
* {@link HttpExecutor} to use any HTTP client.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* // Quick setup with defaults (uses fetch)
|
|
33
|
+
* const btc = BitcoinConnection.forNetwork('regtest');
|
|
34
|
+
*
|
|
35
|
+
* // With a custom HTTP executor
|
|
36
|
+
* const btc = BitcoinConnection.forNetwork('testnet4', {
|
|
37
|
+
* rest: { host: 'https://my-mempool/api' },
|
|
38
|
+
* executor: myCustomExecutor,
|
|
39
|
+
* });
|
|
40
|
+
*
|
|
41
|
+
* // Direct usage
|
|
42
|
+
* const tx = await btc.rest.transaction.get(txid);
|
|
43
|
+
* const block = await btc.rpc?.getBlock({ height: 100 });
|
|
44
|
+
*
|
|
45
|
+
* // Sans-I/O: build requests without performing I/O
|
|
46
|
+
* const req = btc.rest.protocol.getTx(txid);
|
|
47
|
+
* const res = await myHttpClient.execute(req);
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export class BitcoinConnection {
|
|
51
|
+
/** The network this connection targets. */
|
|
52
|
+
readonly name: NetworkName;
|
|
53
|
+
|
|
54
|
+
/** REST client (Esplora API). */
|
|
55
|
+
readonly rest: BitcoinRestClient;
|
|
56
|
+
|
|
57
|
+
/** RPC client (Bitcoin Core). May be undefined if not configured. */
|
|
58
|
+
readonly rpc?: BitcoinCoreRpcClient;
|
|
59
|
+
|
|
60
|
+
/** bitcoinjs-lib network data (for address derivation, PSBT signing, etc.). */
|
|
61
|
+
readonly data: networks.Network;
|
|
62
|
+
|
|
63
|
+
constructor(options: BitcoinConnectionOptions) {
|
|
64
|
+
this.name = options.network;
|
|
65
|
+
this.rest = new BitcoinRestClient(options.rest, options.executor);
|
|
66
|
+
this.rpc = options.rpc ? new BitcoinCoreRpcClient(options.rpc, options.executor) : undefined;
|
|
67
|
+
this.data = getNetwork(options.network);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Create a connection for a single network with optional REST/RPC endpoint overrides.
|
|
72
|
+
* Merges overrides on top of defaults from DEFAULT_BITCOIN_NETWORK_CONFIG.
|
|
73
|
+
* Does not read environment variables.
|
|
74
|
+
*
|
|
75
|
+
* @param network The network name (e.g., 'regtest', 'testnet4', 'bitcoin').
|
|
76
|
+
* @param overrides Optional endpoint and executor overrides.
|
|
77
|
+
* @returns A BitcoinConnection for the requested network.
|
|
78
|
+
*/
|
|
79
|
+
static forNetwork(
|
|
80
|
+
network: NetworkName,
|
|
81
|
+
overrides?: { rest?: Partial<RestConfig>; rpc?: RpcConfig; executor?: HttpExecutor }
|
|
82
|
+
): BitcoinConnection {
|
|
83
|
+
const defaults = DEFAULT_BITCOIN_NETWORK_CONFIG[network];
|
|
84
|
+
|
|
85
|
+
if (!defaults) {
|
|
86
|
+
throw new MethodError(
|
|
87
|
+
`Unknown network '${network}'. Available: bitcoin, testnet3, testnet4, signet, mutinynet, regtest`,
|
|
88
|
+
'UNKNOWN_NETWORK',
|
|
89
|
+
{ network }
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const restCfg: RestConfig = { ...defaults.rest, ...overrides?.rest };
|
|
94
|
+
|
|
95
|
+
const hasRpc = defaults.rpc !== undefined || overrides?.rpc !== undefined;
|
|
96
|
+
const rpcCfg = hasRpc
|
|
97
|
+
? { ...defaults.rpc, ...overrides?.rpc } as RpcConfig
|
|
98
|
+
: undefined;
|
|
99
|
+
|
|
100
|
+
return new BitcoinConnection({
|
|
101
|
+
network,
|
|
102
|
+
rest : restCfg,
|
|
103
|
+
rpc : rpcCfg,
|
|
104
|
+
executor : overrides?.executor,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Converts Bitcoin (BTC) to satoshis.
|
|
110
|
+
* Uses string-based arithmetic to avoid floating-point precision errors.
|
|
111
|
+
* @throws {RangeError} If the value has more than 8 decimal places.
|
|
112
|
+
*/
|
|
113
|
+
static btcToSats(btc: number): number {
|
|
114
|
+
const str = btc.toFixed(8);
|
|
115
|
+
const [whole, frac] = str.split('.');
|
|
116
|
+
// Verify no precision beyond 8 decimals was lost
|
|
117
|
+
if (Math.abs(btc - Number(str)) > Number.EPSILON) {
|
|
118
|
+
throw new RangeError(`BTC value ${btc} exceeds 8 decimal places of precision`);
|
|
119
|
+
}
|
|
120
|
+
return Number(whole) * 1e8 + Number(frac);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Converts satoshis to Bitcoin (BTC).
|
|
125
|
+
* Uses string-based arithmetic to avoid floating-point precision errors.
|
|
126
|
+
* @param sats Must be a non-negative integer.
|
|
127
|
+
*/
|
|
128
|
+
static satsToBtc(sats: number): number {
|
|
129
|
+
const negative = sats < 0;
|
|
130
|
+
const abs = Math.abs(sats);
|
|
131
|
+
const whole = Math.floor(abs / 1e8);
|
|
132
|
+
const frac = abs % 1e8;
|
|
133
|
+
const result = Number(`${whole}.${frac.toString().padStart(8, '0')}`);
|
|
134
|
+
return negative ? -result : result;
|
|
135
|
+
}
|
|
136
|
+
}
|
package/src/constants.ts
CHANGED
|
@@ -4,6 +4,23 @@ export const COINBASE_MATURITY_DELAY = 100;
|
|
|
4
4
|
export const DEFAULT_BLOCK_CONFIRMATIONS = 7;
|
|
5
5
|
export const TXIN_WITNESS_COINBASE = '0000000000000000000000000000000000000000000000000000000000000000';
|
|
6
6
|
export const GENESIS_TX_ID = '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Default endpoint configuration per Bitcoin network.
|
|
10
|
+
*
|
|
11
|
+
* **Regtest RPC:** Credentials are intentionally omitted — callers must
|
|
12
|
+
* provide `username` and `password` via overrides or explicit config.
|
|
13
|
+
* This prevents accidentally using hardcoded credentials in non-local
|
|
14
|
+
* environments.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* // Provide credentials explicitly
|
|
19
|
+
* BitcoinConnection.forNetwork('regtest', {
|
|
20
|
+
* rpc: { username: 'polaruser', password: 'polarpass' },
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
7
24
|
export const DEFAULT_BITCOIN_NETWORK_CONFIG = {
|
|
8
25
|
bitcoin : {
|
|
9
26
|
rpc : undefined,
|
|
@@ -27,13 +44,8 @@ export const DEFAULT_BITCOIN_NETWORK_CONFIG = {
|
|
|
27
44
|
},
|
|
28
45
|
regtest : {
|
|
29
46
|
rpc : {
|
|
30
|
-
network : 'regtest',
|
|
31
47
|
host : 'http://localhost:18443',
|
|
32
|
-
port : 18443,
|
|
33
|
-
username : 'polaruser',
|
|
34
|
-
password : 'polarpass',
|
|
35
48
|
allowDefaultWallet : true,
|
|
36
|
-
version : '28.1.0',
|
|
37
49
|
},
|
|
38
50
|
rest : { host: 'http://localhost:3000' }
|
|
39
51
|
},
|
package/src/errors.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
+
export type RpcErrorType =
|
|
2
|
+
| 'HTTP_ERROR'
|
|
3
|
+
| 'RPC_ERROR'
|
|
4
|
+
| 'INVALID_PARAMS_GET_BLOCK'
|
|
5
|
+
| 'SIGNING_INCOMPLETE'
|
|
6
|
+
| 'UNKNOWN_ERROR';
|
|
7
|
+
|
|
1
8
|
/**
|
|
2
9
|
* Custom Error class for handling Bitcoin RPC errors.
|
|
3
10
|
*/
|
|
4
11
|
export class BitcoinRpcError extends Error {
|
|
5
|
-
public readonly type:
|
|
12
|
+
public readonly type: RpcErrorType;
|
|
6
13
|
public readonly code: number;
|
|
7
|
-
public readonly data?:
|
|
8
|
-
constructor(type:
|
|
14
|
+
public readonly data?: unknown;
|
|
15
|
+
constructor(type: RpcErrorType, code: number, message: string, data?: unknown) {
|
|
9
16
|
super(message);
|
|
10
17
|
this.type = type;
|
|
11
18
|
this.code = code;
|