@dynamic-labs/bitcoin 4.52.5 → 4.53.1
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/CHANGELOG.md +18 -0
- package/package.cjs +1 -1
- package/package.js +1 -1
- package/package.json +11 -6
- package/src/connectors/BitcoinSatsConnectConnector/BitcoinSatsConnectConnector.js +1 -1
- package/src/connectors/BitcoinWalletConnector.cjs +8 -0
- package/src/connectors/BitcoinWalletConnector.d.ts +1 -0
- package/src/connectors/BitcoinWalletConnector.js +10 -2
- package/src/connectors/DynamicWaasBitcoinConnector/DynamicWaasBitcoinConnector.cjs +464 -0
- package/src/connectors/DynamicWaasBitcoinConnector/DynamicWaasBitcoinConnector.d.ts +314 -0
- package/src/connectors/DynamicWaasBitcoinConnector/DynamicWaasBitcoinConnector.js +460 -0
- package/src/connectors/DynamicWaasBitcoinConnector/index.d.ts +1 -0
- package/src/connectors/index.d.ts +1 -0
- package/src/const.cjs +13 -0
- package/src/const.d.ts +6 -0
- package/src/const.js +8 -1
- package/src/index.cjs +9 -0
- package/src/index.d.ts +2 -2
- package/src/index.js +6 -0
- package/src/services/MempoolApiService.cjs +127 -0
- package/src/services/MempoolApiService.d.ts +42 -0
- package/src/services/MempoolApiService.js +123 -0
- package/src/services/PsbtBuilderService.cjs +132 -0
- package/src/services/PsbtBuilderService.d.ts +27 -0
- package/src/services/PsbtBuilderService.js +124 -0
- package/src/types.d.ts +43 -0
- package/src/utils/btcToSatoshis/btcToSatoshis.cjs +24 -0
- package/src/utils/btcToSatoshis/btcToSatoshis.d.ts +7 -0
- package/src/utils/btcToSatoshis/btcToSatoshis.js +20 -0
- package/src/utils/btcToSatoshis/index.d.ts +1 -0
- package/src/utils/fetchSatsConnectConnectors/fetchSatsConnectConnectors.cjs +1 -0
- package/src/utils/fetchSatsConnectConnectors/fetchSatsConnectConnectors.js +1 -0
- package/src/utils/getBitcoinNetwork/getBitcoinNetwork.cjs +15 -0
- package/src/utils/getBitcoinNetwork/getBitcoinNetwork.d.ts +7 -0
- package/src/utils/getBitcoinNetwork/getBitcoinNetwork.js +11 -0
- package/src/utils/getBitcoinNetwork/index.d.ts +1 -0
- package/src/utils/index.d.ts +3 -0
- package/src/utils/psbtParser/PsbtParser.cjs +185 -0
- package/src/utils/psbtParser/PsbtParser.d.ts +63 -0
- package/src/utils/psbtParser/PsbtParser.js +181 -0
- package/src/utils/psbtParser/index.d.ts +1 -0
- package/src/wallet/BitcoinWallet.cjs +11 -0
- package/src/wallet/BitcoinWallet.d.ts +6 -0
- package/src/wallet/BitcoinWallet.js +11 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
5
|
+
|
|
6
|
+
var _tslib = require('../../_virtual/_tslib.cjs');
|
|
7
|
+
var logger$1 = require('@dynamic-labs/logger');
|
|
8
|
+
var utils = require('@dynamic-labs/utils');
|
|
9
|
+
require('sats-connect');
|
|
10
|
+
require('bitcoinjs-lib');
|
|
11
|
+
require('@dynamic-labs/wallet-connector-core');
|
|
12
|
+
require('@dynamic-labs/wallet-book');
|
|
13
|
+
require('@dynamic-labs/sdk-api-core');
|
|
14
|
+
require('@wallet-standard/app');
|
|
15
|
+
var getMempoolApiUrl = require('../utils/getMempoolApiUrl.cjs');
|
|
16
|
+
var _const = require('../const.cjs');
|
|
17
|
+
require('jsontokens');
|
|
18
|
+
require('../connectors/DynamicWaasBitcoinConnector/DynamicWaasBitcoinConnector.cjs');
|
|
19
|
+
|
|
20
|
+
const logger = new logger$1.Logger('MempoolApiService');
|
|
21
|
+
/**
|
|
22
|
+
* Service for interacting with the mempool.space API
|
|
23
|
+
*/
|
|
24
|
+
class MempoolApiService {
|
|
25
|
+
/**
|
|
26
|
+
* Gets the base URL for the mempool API based on the address network
|
|
27
|
+
* @param address - Bitcoin address to determine network
|
|
28
|
+
* @returns The mempool API base URL
|
|
29
|
+
*/
|
|
30
|
+
getBaseUrl(address) {
|
|
31
|
+
return getMempoolApiUrl.getMempoolApiUrl(address);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Gets UTXOs for a Bitcoin address from mempool.space API
|
|
35
|
+
* @param address - The Bitcoin address to get UTXOs for
|
|
36
|
+
* @returns Array of UTXOs
|
|
37
|
+
* @throws {DynamicError} If fetching UTXOs fails
|
|
38
|
+
*/
|
|
39
|
+
getUTXOs(address) {
|
|
40
|
+
return _tslib.__awaiter(this, void 0, void 0, function* () {
|
|
41
|
+
try {
|
|
42
|
+
const baseUrl = this.getBaseUrl(address);
|
|
43
|
+
const response = yield fetch(`${baseUrl}/address/${address}/utxo`);
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
throw new utils.DynamicError(`Failed to fetch UTXOs: ${response.statusText}`);
|
|
46
|
+
}
|
|
47
|
+
return response.json();
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
logger.error('Error fetching UTXOs:', error);
|
|
51
|
+
throw new utils.DynamicError('Failed to fetch UTXOs');
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Gets fee recommendations from mempool.space API
|
|
57
|
+
* @param address - The Bitcoin address to determine the network from
|
|
58
|
+
* @returns Fee recommendation data
|
|
59
|
+
* @throws {Error} If fetching fee recommendations fails
|
|
60
|
+
*/
|
|
61
|
+
getFeeRecommendations(address) {
|
|
62
|
+
return _tslib.__awaiter(this, void 0, void 0, function* () {
|
|
63
|
+
const baseUrl = this.getBaseUrl(address);
|
|
64
|
+
const url = `${baseUrl}/v1/fees/recommended`;
|
|
65
|
+
const response = yield fetch(url);
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new Error('Failed to fetch fee estimate');
|
|
68
|
+
}
|
|
69
|
+
return response.json();
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Estimates transaction fees based on number of inputs and outputs
|
|
74
|
+
* @param address - The Bitcoin address to determine the network from
|
|
75
|
+
* @param numInputs - Number of transaction inputs
|
|
76
|
+
* @param numOutputs - Number of transaction outputs
|
|
77
|
+
* @returns Estimated fee in satoshis
|
|
78
|
+
*/
|
|
79
|
+
estimateTransactionFee(address, numInputs, numOutputs) {
|
|
80
|
+
return _tslib.__awaiter(this, void 0, void 0, function* () {
|
|
81
|
+
try {
|
|
82
|
+
const feeData = yield this.getFeeRecommendations(address);
|
|
83
|
+
const feePerByte = feeData.hourFee || feeData.economyFee || 1;
|
|
84
|
+
const estimatedFee = feePerByte *
|
|
85
|
+
(_const.INPUT_BYTE_SIZE_UPPER_BOUND * numInputs +
|
|
86
|
+
_const.OUTPUT_BYTE_SIZE_UPPER_BOUND * numOutputs) +
|
|
87
|
+
_const.MIN_RELAY_FEE;
|
|
88
|
+
return estimatedFee;
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
logger.error('Error fetching fee estimate:', error);
|
|
92
|
+
// Return a conservative default fee estimate
|
|
93
|
+
return _const.DEFAULT_FEE_ESTIMATE;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Sends a raw Bitcoin transaction to the mempool
|
|
99
|
+
* @param address - The Bitcoin address to determine the network from
|
|
100
|
+
* @param rawTransaction - The raw transaction in hex format
|
|
101
|
+
* @returns The transaction ID
|
|
102
|
+
* @throws {DynamicError} If no transaction is specified or sending fails
|
|
103
|
+
*/
|
|
104
|
+
sendRawTransaction(address, rawTransaction) {
|
|
105
|
+
return _tslib.__awaiter(this, void 0, void 0, function* () {
|
|
106
|
+
if (!rawTransaction) {
|
|
107
|
+
throw new utils.DynamicError('No transaction specified!');
|
|
108
|
+
}
|
|
109
|
+
const baseUrl = this.getBaseUrl(address);
|
|
110
|
+
const response = yield fetch(`${baseUrl}/tx`, {
|
|
111
|
+
body: rawTransaction,
|
|
112
|
+
headers: {
|
|
113
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
114
|
+
},
|
|
115
|
+
method: 'POST',
|
|
116
|
+
});
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
const error = yield response.text();
|
|
119
|
+
logger.debug(`sendRawTransaction - response not ok: ${JSON.stringify(error)}`);
|
|
120
|
+
throw new utils.DynamicError('sendRawTransaction - failed to send transaction');
|
|
121
|
+
}
|
|
122
|
+
return response.text();
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
exports.MempoolApiService = MempoolApiService;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { FeeRecommendations, UTXO } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Service for interacting with the mempool.space API
|
|
4
|
+
*/
|
|
5
|
+
export declare class MempoolApiService {
|
|
6
|
+
/**
|
|
7
|
+
* Gets the base URL for the mempool API based on the address network
|
|
8
|
+
* @param address - Bitcoin address to determine network
|
|
9
|
+
* @returns The mempool API base URL
|
|
10
|
+
*/
|
|
11
|
+
private getBaseUrl;
|
|
12
|
+
/**
|
|
13
|
+
* Gets UTXOs for a Bitcoin address from mempool.space API
|
|
14
|
+
* @param address - The Bitcoin address to get UTXOs for
|
|
15
|
+
* @returns Array of UTXOs
|
|
16
|
+
* @throws {DynamicError} If fetching UTXOs fails
|
|
17
|
+
*/
|
|
18
|
+
getUTXOs(address: string): Promise<UTXO[]>;
|
|
19
|
+
/**
|
|
20
|
+
* Gets fee recommendations from mempool.space API
|
|
21
|
+
* @param address - The Bitcoin address to determine the network from
|
|
22
|
+
* @returns Fee recommendation data
|
|
23
|
+
* @throws {Error} If fetching fee recommendations fails
|
|
24
|
+
*/
|
|
25
|
+
getFeeRecommendations(address: string): Promise<FeeRecommendations>;
|
|
26
|
+
/**
|
|
27
|
+
* Estimates transaction fees based on number of inputs and outputs
|
|
28
|
+
* @param address - The Bitcoin address to determine the network from
|
|
29
|
+
* @param numInputs - Number of transaction inputs
|
|
30
|
+
* @param numOutputs - Number of transaction outputs
|
|
31
|
+
* @returns Estimated fee in satoshis
|
|
32
|
+
*/
|
|
33
|
+
estimateTransactionFee(address: string, numInputs: number, numOutputs: number): Promise<number>;
|
|
34
|
+
/**
|
|
35
|
+
* Sends a raw Bitcoin transaction to the mempool
|
|
36
|
+
* @param address - The Bitcoin address to determine the network from
|
|
37
|
+
* @param rawTransaction - The raw transaction in hex format
|
|
38
|
+
* @returns The transaction ID
|
|
39
|
+
* @throws {DynamicError} If no transaction is specified or sending fails
|
|
40
|
+
*/
|
|
41
|
+
sendRawTransaction(address: string, rawTransaction: string): Promise<string>;
|
|
42
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import { __awaiter } from '../../_virtual/_tslib.js';
|
|
3
|
+
import { Logger } from '@dynamic-labs/logger';
|
|
4
|
+
import { DynamicError } from '@dynamic-labs/utils';
|
|
5
|
+
import 'sats-connect';
|
|
6
|
+
import 'bitcoinjs-lib';
|
|
7
|
+
import '@dynamic-labs/wallet-connector-core';
|
|
8
|
+
import '@dynamic-labs/wallet-book';
|
|
9
|
+
import '@dynamic-labs/sdk-api-core';
|
|
10
|
+
import '@wallet-standard/app';
|
|
11
|
+
import { getMempoolApiUrl } from '../utils/getMempoolApiUrl.js';
|
|
12
|
+
import { INPUT_BYTE_SIZE_UPPER_BOUND, OUTPUT_BYTE_SIZE_UPPER_BOUND, MIN_RELAY_FEE, DEFAULT_FEE_ESTIMATE } from '../const.js';
|
|
13
|
+
import 'jsontokens';
|
|
14
|
+
import '../connectors/DynamicWaasBitcoinConnector/DynamicWaasBitcoinConnector.js';
|
|
15
|
+
|
|
16
|
+
const logger = new Logger('MempoolApiService');
|
|
17
|
+
/**
|
|
18
|
+
* Service for interacting with the mempool.space API
|
|
19
|
+
*/
|
|
20
|
+
class MempoolApiService {
|
|
21
|
+
/**
|
|
22
|
+
* Gets the base URL for the mempool API based on the address network
|
|
23
|
+
* @param address - Bitcoin address to determine network
|
|
24
|
+
* @returns The mempool API base URL
|
|
25
|
+
*/
|
|
26
|
+
getBaseUrl(address) {
|
|
27
|
+
return getMempoolApiUrl(address);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Gets UTXOs for a Bitcoin address from mempool.space API
|
|
31
|
+
* @param address - The Bitcoin address to get UTXOs for
|
|
32
|
+
* @returns Array of UTXOs
|
|
33
|
+
* @throws {DynamicError} If fetching UTXOs fails
|
|
34
|
+
*/
|
|
35
|
+
getUTXOs(address) {
|
|
36
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
37
|
+
try {
|
|
38
|
+
const baseUrl = this.getBaseUrl(address);
|
|
39
|
+
const response = yield fetch(`${baseUrl}/address/${address}/utxo`);
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
throw new DynamicError(`Failed to fetch UTXOs: ${response.statusText}`);
|
|
42
|
+
}
|
|
43
|
+
return response.json();
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
logger.error('Error fetching UTXOs:', error);
|
|
47
|
+
throw new DynamicError('Failed to fetch UTXOs');
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Gets fee recommendations from mempool.space API
|
|
53
|
+
* @param address - The Bitcoin address to determine the network from
|
|
54
|
+
* @returns Fee recommendation data
|
|
55
|
+
* @throws {Error} If fetching fee recommendations fails
|
|
56
|
+
*/
|
|
57
|
+
getFeeRecommendations(address) {
|
|
58
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
59
|
+
const baseUrl = this.getBaseUrl(address);
|
|
60
|
+
const url = `${baseUrl}/v1/fees/recommended`;
|
|
61
|
+
const response = yield fetch(url);
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
throw new Error('Failed to fetch fee estimate');
|
|
64
|
+
}
|
|
65
|
+
return response.json();
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Estimates transaction fees based on number of inputs and outputs
|
|
70
|
+
* @param address - The Bitcoin address to determine the network from
|
|
71
|
+
* @param numInputs - Number of transaction inputs
|
|
72
|
+
* @param numOutputs - Number of transaction outputs
|
|
73
|
+
* @returns Estimated fee in satoshis
|
|
74
|
+
*/
|
|
75
|
+
estimateTransactionFee(address, numInputs, numOutputs) {
|
|
76
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
77
|
+
try {
|
|
78
|
+
const feeData = yield this.getFeeRecommendations(address);
|
|
79
|
+
const feePerByte = feeData.hourFee || feeData.economyFee || 1;
|
|
80
|
+
const estimatedFee = feePerByte *
|
|
81
|
+
(INPUT_BYTE_SIZE_UPPER_BOUND * numInputs +
|
|
82
|
+
OUTPUT_BYTE_SIZE_UPPER_BOUND * numOutputs) +
|
|
83
|
+
MIN_RELAY_FEE;
|
|
84
|
+
return estimatedFee;
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
logger.error('Error fetching fee estimate:', error);
|
|
88
|
+
// Return a conservative default fee estimate
|
|
89
|
+
return DEFAULT_FEE_ESTIMATE;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Sends a raw Bitcoin transaction to the mempool
|
|
95
|
+
* @param address - The Bitcoin address to determine the network from
|
|
96
|
+
* @param rawTransaction - The raw transaction in hex format
|
|
97
|
+
* @returns The transaction ID
|
|
98
|
+
* @throws {DynamicError} If no transaction is specified or sending fails
|
|
99
|
+
*/
|
|
100
|
+
sendRawTransaction(address, rawTransaction) {
|
|
101
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
102
|
+
if (!rawTransaction) {
|
|
103
|
+
throw new DynamicError('No transaction specified!');
|
|
104
|
+
}
|
|
105
|
+
const baseUrl = this.getBaseUrl(address);
|
|
106
|
+
const response = yield fetch(`${baseUrl}/tx`, {
|
|
107
|
+
body: rawTransaction,
|
|
108
|
+
headers: {
|
|
109
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
110
|
+
},
|
|
111
|
+
method: 'POST',
|
|
112
|
+
});
|
|
113
|
+
if (!response.ok) {
|
|
114
|
+
const error = yield response.text();
|
|
115
|
+
logger.debug(`sendRawTransaction - response not ok: ${JSON.stringify(error)}`);
|
|
116
|
+
throw new DynamicError('sendRawTransaction - failed to send transaction');
|
|
117
|
+
}
|
|
118
|
+
return response.text();
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export { MempoolApiService };
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
5
|
+
|
|
6
|
+
var _tslib = require('../../_virtual/_tslib.cjs');
|
|
7
|
+
var ecc = require('@bitcoinerlab/secp256k1');
|
|
8
|
+
var ecpair = require('ecpair');
|
|
9
|
+
var bitcoinjsLib = require('bitcoinjs-lib');
|
|
10
|
+
var logger$1 = require('@dynamic-labs/logger');
|
|
11
|
+
var utils = require('@dynamic-labs/utils');
|
|
12
|
+
var _const = require('../const.cjs');
|
|
13
|
+
var getBitcoinNetwork = require('../utils/getBitcoinNetwork/getBitcoinNetwork.cjs');
|
|
14
|
+
|
|
15
|
+
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
16
|
+
|
|
17
|
+
var ecc__default = /*#__PURE__*/_interopDefaultLegacy(ecc);
|
|
18
|
+
|
|
19
|
+
const logger = new logger$1.Logger('PsbtBuilderService');
|
|
20
|
+
/**
|
|
21
|
+
* Service for building Bitcoin PSBTs
|
|
22
|
+
*/
|
|
23
|
+
class PsbtBuilderService {
|
|
24
|
+
constructor(mempoolApiService) {
|
|
25
|
+
this.mempoolApiService = mempoolApiService;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Builds a PSBT for a Bitcoin transaction with real UTXOs
|
|
29
|
+
* @param options - Options for building the PSBT
|
|
30
|
+
* @returns A PSBT in Base64 format
|
|
31
|
+
* @throws {DynamicError} If insufficient funds, no UTXOs, or other errors
|
|
32
|
+
*/
|
|
33
|
+
buildPsbt(options) {
|
|
34
|
+
return _tslib.__awaiter(this, void 0, void 0, function* () {
|
|
35
|
+
const { accountAddress, recipientAddress, amountInSatoshis, publicKeyHex, network, } = options;
|
|
36
|
+
if (amountInSatoshis <= BigInt(0)) {
|
|
37
|
+
throw new utils.DynamicError('Amount must be greater than 0');
|
|
38
|
+
}
|
|
39
|
+
// Get UTXOs for the account address
|
|
40
|
+
const utxos = yield this.mempoolApiService.getUTXOs(accountAddress);
|
|
41
|
+
if (utxos.length === 0) {
|
|
42
|
+
throw new utils.DynamicError('No UTXOs found for this address');
|
|
43
|
+
}
|
|
44
|
+
// Get public key for creating output scripts
|
|
45
|
+
const publicKeyBuffer = Buffer.from(publicKeyHex, 'hex');
|
|
46
|
+
// Use ECPair to ensure the public key is properly formatted for bitcoinjs-lib
|
|
47
|
+
const ECPair = ecpair.ECPairFactory(ecc__default["default"]);
|
|
48
|
+
const publicKeyPair = ECPair.fromPublicKey(publicKeyBuffer, {
|
|
49
|
+
compressed: true,
|
|
50
|
+
});
|
|
51
|
+
// Calculate total available
|
|
52
|
+
const totalToSpend = utxos.reduce((total, utxo) => total + utxo.value, 0);
|
|
53
|
+
// Initial fee estimate with 1 output (recipient only)
|
|
54
|
+
let feeEstimate = yield this.mempoolApiService.estimateTransactionFee(accountAddress, utxos.length, 1);
|
|
55
|
+
// Calculate change amount with 1-output fee estimate
|
|
56
|
+
let maxToSpend = totalToSpend - feeEstimate;
|
|
57
|
+
let changeAmount = BigInt(maxToSpend) - amountInSatoshis;
|
|
58
|
+
// If change will be above dust limit, re-estimate fees for 2 outputs
|
|
59
|
+
if (changeAmount > 0 && Number(changeAmount) >= _const.DUST_LIMIT) {
|
|
60
|
+
feeEstimate = yield this.mempoolApiService.estimateTransactionFee(accountAddress, utxos.length, 2);
|
|
61
|
+
maxToSpend = totalToSpend - feeEstimate;
|
|
62
|
+
changeAmount = BigInt(maxToSpend) - amountInSatoshis;
|
|
63
|
+
}
|
|
64
|
+
if (maxToSpend < Number(amountInSatoshis)) {
|
|
65
|
+
const amountInSatoshisNumber = Number(amountInSatoshis);
|
|
66
|
+
throw new utils.DynamicError(`Insufficient funds. Available: ${maxToSpend / _const.SATOSHIS_PER_BTC} BTC (${maxToSpend} satoshis), Required: ${amountInSatoshisNumber / _const.SATOSHIS_PER_BTC} BTC (${amountInSatoshisNumber} satoshis)`);
|
|
67
|
+
}
|
|
68
|
+
// Create PSBT
|
|
69
|
+
const psbt = new bitcoinjsLib.Psbt({ network });
|
|
70
|
+
// Add inputs from UTXOs (only supporting native SegWit P2WPKH)
|
|
71
|
+
for (const utxo of utxos) {
|
|
72
|
+
// SegWit (P2WPKH) addresses
|
|
73
|
+
// For SegWit, we need the public key to construct the witness output script
|
|
74
|
+
const outputScript = bitcoinjsLib.payments.p2wpkh({
|
|
75
|
+
network,
|
|
76
|
+
pubkey: publicKeyPair.publicKey,
|
|
77
|
+
}).output;
|
|
78
|
+
if (!outputScript) {
|
|
79
|
+
throw new utils.DynamicError('Failed to create segwit output script');
|
|
80
|
+
}
|
|
81
|
+
// Convert txid from hex string to Buffer and reverse it (Bitcoin uses little-endian)
|
|
82
|
+
// The txid from the API is in big-endian format, but bitcoinjs-lib expects little-endian
|
|
83
|
+
const txidBuffer = Buffer.from(utxo.txid, 'hex').reverse();
|
|
84
|
+
psbt.addInput({
|
|
85
|
+
hash: txidBuffer,
|
|
86
|
+
index: utxo.vout,
|
|
87
|
+
witnessUtxo: {
|
|
88
|
+
script: outputScript,
|
|
89
|
+
value: utxo.value,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// Add recipient output
|
|
94
|
+
const amountInSatoshisNumber = Number(amountInSatoshis);
|
|
95
|
+
if (amountInSatoshisNumber < _const.DUST_LIMIT) {
|
|
96
|
+
throw new utils.DynamicError(`Amount is below dust limit of ${_const.DUST_LIMIT} satoshis (${_const.DUST_LIMIT / _const.SATOSHIS_PER_BTC} BTC)`);
|
|
97
|
+
}
|
|
98
|
+
psbt.addOutput({
|
|
99
|
+
script: bitcoinjsLib.address.toOutputScript(recipientAddress, network),
|
|
100
|
+
value: amountInSatoshisNumber,
|
|
101
|
+
});
|
|
102
|
+
// Add change output if needed
|
|
103
|
+
const changeAmountNumber = Number(changeAmount);
|
|
104
|
+
if (changeAmount > 0 && changeAmountNumber >= _const.DUST_LIMIT) {
|
|
105
|
+
psbt.addOutput({
|
|
106
|
+
script: bitcoinjsLib.address.toOutputScript(accountAddress, network),
|
|
107
|
+
value: changeAmountNumber,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
logger.debug(`buildPsbt created PSBT for recipientAddress: ${recipientAddress}, amount: ${amountInSatoshisNumber} satoshis (${amountInSatoshisNumber / _const.SATOSHIS_PER_BTC} BTC), change: ${changeAmountNumber} satoshis`);
|
|
111
|
+
return psbt.toBase64();
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Helper method to create BuildPsbtOptions from transaction and account details
|
|
116
|
+
* @param accountAddress - The account address
|
|
117
|
+
* @param transaction - The Bitcoin transaction
|
|
118
|
+
* @param publicKeyHex - The public key in hex format
|
|
119
|
+
* @returns BuildPsbtOptions
|
|
120
|
+
*/
|
|
121
|
+
static createBuildOptions(accountAddress, transaction, publicKeyHex) {
|
|
122
|
+
return {
|
|
123
|
+
accountAddress,
|
|
124
|
+
amountInSatoshis: transaction.amount,
|
|
125
|
+
network: getBitcoinNetwork.getBitcoinNetwork(accountAddress),
|
|
126
|
+
publicKeyHex,
|
|
127
|
+
recipientAddress: transaction.recipientAddress,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
exports.PsbtBuilderService = PsbtBuilderService;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { BuildPsbtOptions } from '../types';
|
|
2
|
+
import { MempoolApiService } from './MempoolApiService';
|
|
3
|
+
/**
|
|
4
|
+
* Service for building Bitcoin PSBTs
|
|
5
|
+
*/
|
|
6
|
+
export declare class PsbtBuilderService {
|
|
7
|
+
private mempoolApiService;
|
|
8
|
+
constructor(mempoolApiService: MempoolApiService);
|
|
9
|
+
/**
|
|
10
|
+
* Builds a PSBT for a Bitcoin transaction with real UTXOs
|
|
11
|
+
* @param options - Options for building the PSBT
|
|
12
|
+
* @returns A PSBT in Base64 format
|
|
13
|
+
* @throws {DynamicError} If insufficient funds, no UTXOs, or other errors
|
|
14
|
+
*/
|
|
15
|
+
buildPsbt(options: BuildPsbtOptions): Promise<string>;
|
|
16
|
+
/**
|
|
17
|
+
* Helper method to create BuildPsbtOptions from transaction and account details
|
|
18
|
+
* @param accountAddress - The account address
|
|
19
|
+
* @param transaction - The Bitcoin transaction
|
|
20
|
+
* @param publicKeyHex - The public key in hex format
|
|
21
|
+
* @returns BuildPsbtOptions
|
|
22
|
+
*/
|
|
23
|
+
static createBuildOptions(accountAddress: string, transaction: {
|
|
24
|
+
amount: bigint;
|
|
25
|
+
recipientAddress: string;
|
|
26
|
+
}, publicKeyHex: string): BuildPsbtOptions;
|
|
27
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import { __awaiter } from '../../_virtual/_tslib.js';
|
|
3
|
+
import ecc from '@bitcoinerlab/secp256k1';
|
|
4
|
+
import { ECPairFactory } from 'ecpair';
|
|
5
|
+
import { Psbt, payments, address } from 'bitcoinjs-lib';
|
|
6
|
+
import { Logger } from '@dynamic-labs/logger';
|
|
7
|
+
import { DynamicError } from '@dynamic-labs/utils';
|
|
8
|
+
import { DUST_LIMIT, SATOSHIS_PER_BTC } from '../const.js';
|
|
9
|
+
import { getBitcoinNetwork } from '../utils/getBitcoinNetwork/getBitcoinNetwork.js';
|
|
10
|
+
|
|
11
|
+
const logger = new Logger('PsbtBuilderService');
|
|
12
|
+
/**
|
|
13
|
+
* Service for building Bitcoin PSBTs
|
|
14
|
+
*/
|
|
15
|
+
class PsbtBuilderService {
|
|
16
|
+
constructor(mempoolApiService) {
|
|
17
|
+
this.mempoolApiService = mempoolApiService;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Builds a PSBT for a Bitcoin transaction with real UTXOs
|
|
21
|
+
* @param options - Options for building the PSBT
|
|
22
|
+
* @returns A PSBT in Base64 format
|
|
23
|
+
* @throws {DynamicError} If insufficient funds, no UTXOs, or other errors
|
|
24
|
+
*/
|
|
25
|
+
buildPsbt(options) {
|
|
26
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
27
|
+
const { accountAddress, recipientAddress, amountInSatoshis, publicKeyHex, network, } = options;
|
|
28
|
+
if (amountInSatoshis <= BigInt(0)) {
|
|
29
|
+
throw new DynamicError('Amount must be greater than 0');
|
|
30
|
+
}
|
|
31
|
+
// Get UTXOs for the account address
|
|
32
|
+
const utxos = yield this.mempoolApiService.getUTXOs(accountAddress);
|
|
33
|
+
if (utxos.length === 0) {
|
|
34
|
+
throw new DynamicError('No UTXOs found for this address');
|
|
35
|
+
}
|
|
36
|
+
// Get public key for creating output scripts
|
|
37
|
+
const publicKeyBuffer = Buffer.from(publicKeyHex, 'hex');
|
|
38
|
+
// Use ECPair to ensure the public key is properly formatted for bitcoinjs-lib
|
|
39
|
+
const ECPair = ECPairFactory(ecc);
|
|
40
|
+
const publicKeyPair = ECPair.fromPublicKey(publicKeyBuffer, {
|
|
41
|
+
compressed: true,
|
|
42
|
+
});
|
|
43
|
+
// Calculate total available
|
|
44
|
+
const totalToSpend = utxos.reduce((total, utxo) => total + utxo.value, 0);
|
|
45
|
+
// Initial fee estimate with 1 output (recipient only)
|
|
46
|
+
let feeEstimate = yield this.mempoolApiService.estimateTransactionFee(accountAddress, utxos.length, 1);
|
|
47
|
+
// Calculate change amount with 1-output fee estimate
|
|
48
|
+
let maxToSpend = totalToSpend - feeEstimate;
|
|
49
|
+
let changeAmount = BigInt(maxToSpend) - amountInSatoshis;
|
|
50
|
+
// If change will be above dust limit, re-estimate fees for 2 outputs
|
|
51
|
+
if (changeAmount > 0 && Number(changeAmount) >= DUST_LIMIT) {
|
|
52
|
+
feeEstimate = yield this.mempoolApiService.estimateTransactionFee(accountAddress, utxos.length, 2);
|
|
53
|
+
maxToSpend = totalToSpend - feeEstimate;
|
|
54
|
+
changeAmount = BigInt(maxToSpend) - amountInSatoshis;
|
|
55
|
+
}
|
|
56
|
+
if (maxToSpend < Number(amountInSatoshis)) {
|
|
57
|
+
const amountInSatoshisNumber = Number(amountInSatoshis);
|
|
58
|
+
throw new DynamicError(`Insufficient funds. Available: ${maxToSpend / SATOSHIS_PER_BTC} BTC (${maxToSpend} satoshis), Required: ${amountInSatoshisNumber / SATOSHIS_PER_BTC} BTC (${amountInSatoshisNumber} satoshis)`);
|
|
59
|
+
}
|
|
60
|
+
// Create PSBT
|
|
61
|
+
const psbt = new Psbt({ network });
|
|
62
|
+
// Add inputs from UTXOs (only supporting native SegWit P2WPKH)
|
|
63
|
+
for (const utxo of utxos) {
|
|
64
|
+
// SegWit (P2WPKH) addresses
|
|
65
|
+
// For SegWit, we need the public key to construct the witness output script
|
|
66
|
+
const outputScript = payments.p2wpkh({
|
|
67
|
+
network,
|
|
68
|
+
pubkey: publicKeyPair.publicKey,
|
|
69
|
+
}).output;
|
|
70
|
+
if (!outputScript) {
|
|
71
|
+
throw new DynamicError('Failed to create segwit output script');
|
|
72
|
+
}
|
|
73
|
+
// Convert txid from hex string to Buffer and reverse it (Bitcoin uses little-endian)
|
|
74
|
+
// The txid from the API is in big-endian format, but bitcoinjs-lib expects little-endian
|
|
75
|
+
const txidBuffer = Buffer.from(utxo.txid, 'hex').reverse();
|
|
76
|
+
psbt.addInput({
|
|
77
|
+
hash: txidBuffer,
|
|
78
|
+
index: utxo.vout,
|
|
79
|
+
witnessUtxo: {
|
|
80
|
+
script: outputScript,
|
|
81
|
+
value: utxo.value,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
// Add recipient output
|
|
86
|
+
const amountInSatoshisNumber = Number(amountInSatoshis);
|
|
87
|
+
if (amountInSatoshisNumber < DUST_LIMIT) {
|
|
88
|
+
throw new DynamicError(`Amount is below dust limit of ${DUST_LIMIT} satoshis (${DUST_LIMIT / SATOSHIS_PER_BTC} BTC)`);
|
|
89
|
+
}
|
|
90
|
+
psbt.addOutput({
|
|
91
|
+
script: address.toOutputScript(recipientAddress, network),
|
|
92
|
+
value: amountInSatoshisNumber,
|
|
93
|
+
});
|
|
94
|
+
// Add change output if needed
|
|
95
|
+
const changeAmountNumber = Number(changeAmount);
|
|
96
|
+
if (changeAmount > 0 && changeAmountNumber >= DUST_LIMIT) {
|
|
97
|
+
psbt.addOutput({
|
|
98
|
+
script: address.toOutputScript(accountAddress, network),
|
|
99
|
+
value: changeAmountNumber,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
logger.debug(`buildPsbt created PSBT for recipientAddress: ${recipientAddress}, amount: ${amountInSatoshisNumber} satoshis (${amountInSatoshisNumber / SATOSHIS_PER_BTC} BTC), change: ${changeAmountNumber} satoshis`);
|
|
103
|
+
return psbt.toBase64();
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Helper method to create BuildPsbtOptions from transaction and account details
|
|
108
|
+
* @param accountAddress - The account address
|
|
109
|
+
* @param transaction - The Bitcoin transaction
|
|
110
|
+
* @param publicKeyHex - The public key in hex format
|
|
111
|
+
* @returns BuildPsbtOptions
|
|
112
|
+
*/
|
|
113
|
+
static createBuildOptions(accountAddress, transaction, publicKeyHex) {
|
|
114
|
+
return {
|
|
115
|
+
accountAddress,
|
|
116
|
+
amountInSatoshis: transaction.amount,
|
|
117
|
+
network: getBitcoinNetwork(accountAddress),
|
|
118
|
+
publicKeyHex,
|
|
119
|
+
recipientAddress: transaction.recipientAddress,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export { PsbtBuilderService };
|
package/src/types.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { BitcoinProvider, InputToSign as SatsConnectInputToSign } from 'sats-connect';
|
|
2
2
|
import { SignPsbtResponse as BtcKitSignPsbtResponse } from '@btckit/types';
|
|
3
3
|
import { IdentifierString, WalletAccount } from '@wallet-standard/base';
|
|
4
|
+
import type { Network } from 'bitcoinjs-lib';
|
|
5
|
+
import type { WalletUiUtils } from '@dynamic-labs/types';
|
|
6
|
+
import type { InternalWalletConnector } from '@dynamic-labs/wallet-connector-core';
|
|
4
7
|
import type { WalletAdditionalAddress } from '@dynamic-labs/sdk-api-core';
|
|
8
|
+
import type { BitcoinWalletConnectorOpts } from './connectors';
|
|
5
9
|
import { SATSCONNECT_FEATURE } from './const';
|
|
6
10
|
export type ErrorWithCode = Error & {
|
|
7
11
|
code: string;
|
|
@@ -137,4 +141,43 @@ interface ProviderBalance {
|
|
|
137
141
|
export interface ProviderWithBalance {
|
|
138
142
|
getBalance: () => Promise<ProviderBalance>;
|
|
139
143
|
}
|
|
144
|
+
export type DynamicWaasBitcoinConnectorProps = BitcoinWalletConnectorOpts & {
|
|
145
|
+
walletUiUtils: WalletUiUtils<InternalWalletConnector>;
|
|
146
|
+
};
|
|
147
|
+
export interface UTXO {
|
|
148
|
+
txid: string;
|
|
149
|
+
vout: number;
|
|
150
|
+
value: number;
|
|
151
|
+
status?: {
|
|
152
|
+
confirmed: boolean;
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
export interface FeeRecommendations {
|
|
156
|
+
fastestFee?: number;
|
|
157
|
+
halfHourFee?: number;
|
|
158
|
+
hourFee?: number;
|
|
159
|
+
economyFee?: number;
|
|
160
|
+
minimumFee?: number;
|
|
161
|
+
}
|
|
162
|
+
export interface ParsedTransactionInput {
|
|
163
|
+
txid: string;
|
|
164
|
+
vout: number;
|
|
165
|
+
address?: string;
|
|
166
|
+
value?: number;
|
|
167
|
+
}
|
|
168
|
+
export interface ParsedTransactionOutput {
|
|
169
|
+
address?: string;
|
|
170
|
+
value: number;
|
|
171
|
+
}
|
|
172
|
+
export interface ParsedTransaction {
|
|
173
|
+
inputs: ParsedTransactionInput[];
|
|
174
|
+
outputs: ParsedTransactionOutput[];
|
|
175
|
+
}
|
|
176
|
+
export interface BuildPsbtOptions {
|
|
177
|
+
accountAddress: string;
|
|
178
|
+
recipientAddress: string;
|
|
179
|
+
amountInSatoshis: bigint;
|
|
180
|
+
publicKeyHex: string;
|
|
181
|
+
network: Network;
|
|
182
|
+
}
|
|
140
183
|
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
5
|
+
|
|
6
|
+
var utils = require('@dynamic-labs/utils');
|
|
7
|
+
var _const = require('../../const.cjs');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Helper function to convert BTC amount to satoshis
|
|
11
|
+
* @param btcAmount - The Bitcoin amount to convert (number)
|
|
12
|
+
* @returns The amount in satoshis as a BigInt
|
|
13
|
+
* @throws {DynamicError} If the amount is negative
|
|
14
|
+
*/
|
|
15
|
+
const btcToSatoshis = (btcAmount) => {
|
|
16
|
+
if (btcAmount < 0) {
|
|
17
|
+
throw new utils.DynamicError('BTC amount cannot be negative');
|
|
18
|
+
}
|
|
19
|
+
// Convert to satoshis and return as BigInt
|
|
20
|
+
const satoshis = Math.floor(btcAmount * _const.SATOSHIS_PER_BTC);
|
|
21
|
+
return BigInt(satoshis);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
exports.btcToSatoshis = btcToSatoshis;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper function to convert BTC amount to satoshis
|
|
3
|
+
* @param btcAmount - The Bitcoin amount to convert (number)
|
|
4
|
+
* @returns The amount in satoshis as a BigInt
|
|
5
|
+
* @throws {DynamicError} If the amount is negative
|
|
6
|
+
*/
|
|
7
|
+
export declare const btcToSatoshis: (btcAmount: number) => bigint;
|