@dynamic-labs/bitcoin 4.53.1 → 4.53.2
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 +16 -0
- package/package.cjs +1 -1
- package/package.js +1 -1
- package/package.json +10 -10
- package/src/connectors/DynamicWaasBitcoinConnector/DynamicWaasBitcoinConnector.cjs +10 -11
- package/src/connectors/DynamicWaasBitcoinConnector/DynamicWaasBitcoinConnector.d.ts +30 -10
- package/src/connectors/DynamicWaasBitcoinConnector/DynamicWaasBitcoinConnector.js +10 -11
- package/src/const.cjs +12 -4
- package/src/const.d.ts +4 -2
- package/src/const.js +9 -3
- package/src/index.d.ts +1 -1
- package/src/services/MempoolApiService.cjs +31 -8
- package/src/services/MempoolApiService.d.ts +3 -2
- package/src/services/MempoolApiService.js +32 -9
- package/src/services/PsbtBuilderService.cjs +195 -65
- package/src/services/PsbtBuilderService.d.ts +66 -2
- package/src/services/PsbtBuilderService.js +197 -67
- package/src/types.d.ts +18 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
|
|
2
|
+
### [4.53.2](https://github.com/dynamic-labs/dynamic-auth/compare/v4.53.1...v4.53.2) (2026-01-16)
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
* add iCloud backup functionality ([#10219](https://github.com/dynamic-labs/dynamic-auth/issues/10219)) ([44e95e5](https://github.com/dynamic-labs/dynamic-auth/commit/44e95e5a5dc99f83918a382ab1c69d452359c346))
|
|
8
|
+
* update PSBT building with Largest-First UTXO selection and fee priorities (high/medium/low) ([#10227](https://github.com/dynamic-labs/dynamic-auth/issues/10227)) ([94c5f5c](https://github.com/dynamic-labs/dynamic-auth/commit/94c5f5cb97432bb97374b754cb95bc23290dd184))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* allow late registered solana wallets to appear in the wallet list ([#10224](https://github.com/dynamic-labs/dynamic-auth/issues/10224)) ([2f418d4](https://github.com/dynamic-labs/dynamic-auth/commit/2f418d4f3e0543bdd829a45807329f89da1e41a7))
|
|
14
|
+
* correctly show app name in wallet app for wallet connect evm connections ([#10218](https://github.com/dynamic-labs/dynamic-auth/issues/10218)) ([fec0009](https://github.com/dynamic-labs/dynamic-auth/commit/fec0009177439baa67015050b4ad799119615f4f))
|
|
15
|
+
* destructure wallets from getWalletStandardWallets before calling find ([#10234](https://github.com/dynamic-labs/dynamic-auth/issues/10234)) ([b59617a](https://github.com/dynamic-labs/dynamic-auth/commit/b59617a8579b9c7d8a5f744a2ea9363ccb4aee58))
|
|
16
|
+
* **react-native:** filter connectors by enabled chains and add chain parameter to connectWallet ([#10230](https://github.com/dynamic-labs/dynamic-auth/issues/10230)) ([a2bbd03](https://github.com/dynamic-labs/dynamic-auth/commit/a2bbd03ece52950711d2eda18cb2345df15710dd))
|
|
17
|
+
|
|
2
18
|
### [4.53.1](https://github.com/dynamic-labs/dynamic-auth/compare/v4.53.0...v4.53.1) (2026-01-14)
|
|
3
19
|
|
|
4
20
|
## [4.53.0](https://github.com/dynamic-labs/dynamic-auth/compare/v4.52.5...v4.53.0) (2026-01-13)
|
package/package.cjs
CHANGED
package/package.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dynamic-labs/bitcoin",
|
|
3
|
-
"version": "4.53.
|
|
3
|
+
"version": "4.53.2",
|
|
4
4
|
"description": "A React SDK for implementing wallet web3 authentication and authorization to your website.",
|
|
5
5
|
"author": "Dynamic Labs, Inc.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,23 +18,23 @@
|
|
|
18
18
|
},
|
|
19
19
|
"homepage": "https://www.dynamic.xyz/",
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@dynamic-labs-wallet/browser-wallet-client": "0.0.
|
|
21
|
+
"@dynamic-labs-wallet/browser-wallet-client": "0.0.245",
|
|
22
22
|
"@bitcoinerlab/secp256k1": "1.1.1",
|
|
23
23
|
"@btckit/types": "0.0.19",
|
|
24
|
-
"@dynamic-labs/sdk-api-core": "0.0.
|
|
24
|
+
"@dynamic-labs/sdk-api-core": "0.0.855",
|
|
25
25
|
"@wallet-standard/app": "1.0.1",
|
|
26
26
|
"@wallet-standard/base": "1.0.1",
|
|
27
27
|
"bitcoinjs-lib": "6.1.5",
|
|
28
28
|
"ecpair": "2.1.0",
|
|
29
29
|
"sats-connect": "4.2.0",
|
|
30
30
|
"jsontokens": "4.0.1",
|
|
31
|
-
"@dynamic-labs/assert-package-version": "4.53.
|
|
32
|
-
"@dynamic-labs/logger": "4.53.
|
|
33
|
-
"@dynamic-labs/types": "4.53.
|
|
34
|
-
"@dynamic-labs/utils": "4.53.
|
|
35
|
-
"@dynamic-labs/waas": "4.53.
|
|
36
|
-
"@dynamic-labs/wallet-book": "4.53.
|
|
37
|
-
"@dynamic-labs/wallet-connector-core": "4.53.
|
|
31
|
+
"@dynamic-labs/assert-package-version": "4.53.2",
|
|
32
|
+
"@dynamic-labs/logger": "4.53.2",
|
|
33
|
+
"@dynamic-labs/types": "4.53.2",
|
|
34
|
+
"@dynamic-labs/utils": "4.53.2",
|
|
35
|
+
"@dynamic-labs/waas": "4.53.2",
|
|
36
|
+
"@dynamic-labs/wallet-book": "4.53.2",
|
|
37
|
+
"@dynamic-labs/wallet-connector-core": "4.53.2",
|
|
38
38
|
"eventemitter3": "5.0.1"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {}
|
|
@@ -128,11 +128,15 @@ class DynamicWaasBitcoinConnector extends waas.withDynamicWaas(BitcoinWalletConn
|
|
|
128
128
|
});
|
|
129
129
|
}
|
|
130
130
|
/**
|
|
131
|
-
* Signs a Partially Signed Bitcoin Transaction (PSBT)
|
|
132
|
-
*
|
|
133
|
-
*
|
|
131
|
+
* Signs a Partially Signed Bitcoin Transaction (PSBT) for embedded wallets
|
|
132
|
+
*
|
|
133
|
+
* Embedded wallets only support PSBT format and automatically sign all inputs
|
|
134
|
+
* that belong to the wallet address. Always uses SIGHASH_ALL (0x01).
|
|
135
|
+
*
|
|
136
|
+
* @param request - The PSBT signing request. Only unsignedPsbtBase64 is required.
|
|
137
|
+
* @returns The signed (but not finalized) PSBT response
|
|
134
138
|
* @throws {DynamicError} If active account address is not set
|
|
135
|
-
* @throws {DynamicError} If signed session ID is not available
|
|
139
|
+
* @throws {DynamicError} If signed session ID is not available
|
|
136
140
|
*/
|
|
137
141
|
signPsbt(request) {
|
|
138
142
|
return _tslib.__awaiter(this, void 0, void 0, function* () {
|
|
@@ -148,9 +152,6 @@ class DynamicWaasBitcoinConnector extends waas.withDynamicWaas(BitcoinWalletConn
|
|
|
148
152
|
const mfaToken = yield ((_b = this.getMfaToken) === null || _b === void 0 ? void 0 : _b.call(this, {
|
|
149
153
|
mfaAction: sdkApiCore.MFAAction.WalletWaasSign,
|
|
150
154
|
}));
|
|
151
|
-
if (request.signature && request.signature.length > 0) {
|
|
152
|
-
throw new utils.DynamicError('Signature is not supported for waas at the moment');
|
|
153
|
-
}
|
|
154
155
|
const signedTransaction = yield walletClient.signTransaction({
|
|
155
156
|
authToken: (_c = this.getAuthToken) === null || _c === void 0 ? void 0 : _c.call(this),
|
|
156
157
|
mfaToken,
|
|
@@ -192,9 +193,7 @@ class DynamicWaasBitcoinConnector extends waas.withDynamicWaas(BitcoinWalletConn
|
|
|
192
193
|
// Step 1: Build the PSBT
|
|
193
194
|
const unsignedPsbt = yield this.buildPsbt(transaction);
|
|
194
195
|
// Step 2: Sign the PSBT
|
|
195
|
-
// SIGHASH_ALL (0x01) is the most common sighash type for Bitcoin transactions (Eventually can be configurable)
|
|
196
196
|
const signedPsbtResponse = yield this.signPsbt({
|
|
197
|
-
allowedSighash: [0x01],
|
|
198
197
|
unsignedPsbtBase64: unsignedPsbt,
|
|
199
198
|
});
|
|
200
199
|
if (!signedPsbtResponse) {
|
|
@@ -337,7 +336,7 @@ class DynamicWaasBitcoinConnector extends waas.withDynamicWaas(BitcoinWalletConn
|
|
|
337
336
|
}
|
|
338
337
|
/**
|
|
339
338
|
* Builds a PSBT for a Bitcoin transaction with real UTXOs
|
|
340
|
-
* @param transaction -
|
|
339
|
+
* @param transaction - Bitcoin transaction with recipient address, amount in satoshis, and optional fee priority
|
|
341
340
|
* @returns A PSBT in Base64 format
|
|
342
341
|
* @throws {DynamicError} If no active account address, insufficient funds, or other errors
|
|
343
342
|
*/
|
|
@@ -347,7 +346,7 @@ class DynamicWaasBitcoinConnector extends waas.withDynamicWaas(BitcoinWalletConn
|
|
|
347
346
|
throw new utils.DynamicError('Active account address is required');
|
|
348
347
|
}
|
|
349
348
|
const publicKeyHex = yield this.getPublicKey();
|
|
350
|
-
const buildOptions = PsbtBuilderService.PsbtBuilderService.createBuildOptions(this.activeAccountAddress, transaction, publicKeyHex);
|
|
349
|
+
const buildOptions = PsbtBuilderService.PsbtBuilderService.createBuildOptions(this.activeAccountAddress, transaction, publicKeyHex, transaction.feePriority || 'medium');
|
|
351
350
|
return this.psbtBuilderService.buildPsbt(buildOptions);
|
|
352
351
|
});
|
|
353
352
|
}
|
|
@@ -2,8 +2,8 @@ import { BitcoinConfig, BitcoinNetwork } from '@dynamic-labs-wallet/browser-wall
|
|
|
2
2
|
import { JwtVerifiedCredential, MFAAction, SignMessageContext } from '@dynamic-labs/sdk-api-core';
|
|
3
3
|
import { Logger } from '@dynamic-labs/logger';
|
|
4
4
|
import { WalletUiUtils } from '@dynamic-labs/types';
|
|
5
|
-
import { IDynamicWaasConnector, InternalWalletConnector, Chain,
|
|
6
|
-
import { BitcoinTransaction } from '../../types';
|
|
5
|
+
import { IDynamicWaasConnector, InternalWalletConnector, Chain, BitcoinSignPsbtResponse } from '@dynamic-labs/wallet-connector-core';
|
|
6
|
+
import { BitcoinTransaction, EmbeddedWalletSignPsbtRequest } from '../../types';
|
|
7
7
|
import { BitcoinWalletConnector } from '../BitcoinWalletConnector';
|
|
8
8
|
import type { ParsedTransaction, DynamicWaasBitcoinConnectorProps } from '../../types';
|
|
9
9
|
declare const DynamicWaasBitcoinConnector_base: (abstract new (...args: any[]) => {
|
|
@@ -35,7 +35,10 @@ declare const DynamicWaasBitcoinConnector_base: (abstract new (...args: any[]) =
|
|
|
35
35
|
setBaseApiUrl(baseApiUrl: string): void;
|
|
36
36
|
setBaseClientKeysharesRelayApiUrl(baseClientKeysharesRelayApiUrl?: string | undefined): void;
|
|
37
37
|
setRelayUrl(relayUrl: string): void;
|
|
38
|
-
setGetSignedSessionIdFunction(getSignedSessionId: () => Promise<string>): void;
|
|
38
|
+
setGetSignedSessionIdFunction(getSignedSessionId: () => Promise<string>): void; /**
|
|
39
|
+
* The primary/active verified credential (first from the filtered array)
|
|
40
|
+
* This is used for the active account address
|
|
41
|
+
*/
|
|
39
42
|
delegateKeyShares({ accountAddress, password, }: {
|
|
40
43
|
accountAddress: string;
|
|
41
44
|
password?: string | undefined;
|
|
@@ -56,7 +59,11 @@ declare const DynamicWaasBitcoinConnector_base: (abstract new (...args: any[]) =
|
|
|
56
59
|
privateKey: string;
|
|
57
60
|
thresholdSignatureScheme?: string | undefined;
|
|
58
61
|
publicAddressCheck?: string | undefined;
|
|
59
|
-
addressType?: string | undefined;
|
|
62
|
+
addressType?: string | undefined; /**
|
|
63
|
+
* Override setVerifiedCredentials to filter and set Bitcoin WaaS credentials
|
|
64
|
+
* Filters for credentials with walletName === 'dynamicwaas' and chain === 'bip122'
|
|
65
|
+
* The base class already has verifiedCredentials property, so we just filter and set it
|
|
66
|
+
*/
|
|
60
67
|
}): Promise<void>;
|
|
61
68
|
exportPrivateKey({ accountAddress, displayContainer, password, }?: {
|
|
62
69
|
accountAddress?: string | undefined;
|
|
@@ -78,6 +85,15 @@ declare const DynamicWaasBitcoinConnector_base: (abstract new (...args: any[]) =
|
|
|
78
85
|
accountAddress: string;
|
|
79
86
|
password?: string | undefined;
|
|
80
87
|
}): Promise<void>;
|
|
88
|
+
backupKeySharesToICloud({ accountAddress, password, }: {
|
|
89
|
+
accountAddress: string;
|
|
90
|
+
password?: string | undefined;
|
|
91
|
+
}): Promise<void>;
|
|
92
|
+
displayICloudSignIn({ displayContainer, }: {
|
|
93
|
+
displayContainer: HTMLElement;
|
|
94
|
+
}): Promise<void>;
|
|
95
|
+
hideICloudSignIn(): Promise<void>;
|
|
96
|
+
isICloudAuthenticated(): Promise<boolean>;
|
|
81
97
|
refreshWalletAccountShares({ accountAddress, password, }: {
|
|
82
98
|
accountAddress: string;
|
|
83
99
|
password?: string | undefined;
|
|
@@ -169,13 +185,17 @@ export declare class DynamicWaasBitcoinConnector extends DynamicWaasBitcoinConne
|
|
|
169
185
|
*/
|
|
170
186
|
signMessage(message: string): Promise<string>;
|
|
171
187
|
/**
|
|
172
|
-
* Signs a Partially Signed Bitcoin Transaction (PSBT)
|
|
173
|
-
*
|
|
174
|
-
*
|
|
188
|
+
* Signs a Partially Signed Bitcoin Transaction (PSBT) for embedded wallets
|
|
189
|
+
*
|
|
190
|
+
* Embedded wallets only support PSBT format and automatically sign all inputs
|
|
191
|
+
* that belong to the wallet address. Always uses SIGHASH_ALL (0x01).
|
|
192
|
+
*
|
|
193
|
+
* @param request - The PSBT signing request. Only unsignedPsbtBase64 is required.
|
|
194
|
+
* @returns The signed (but not finalized) PSBT response
|
|
175
195
|
* @throws {DynamicError} If active account address is not set
|
|
176
|
-
* @throws {DynamicError} If signed session ID is not available
|
|
196
|
+
* @throws {DynamicError} If signed session ID is not available
|
|
177
197
|
*/
|
|
178
|
-
signPsbt(request:
|
|
198
|
+
signPsbt(request: EmbeddedWalletSignPsbtRequest): Promise<BitcoinSignPsbtResponse>;
|
|
179
199
|
/**
|
|
180
200
|
* Sends a raw Bitcoin transaction to the mempool
|
|
181
201
|
* @param rawTransaction - The raw transaction in hex format
|
|
@@ -258,7 +278,7 @@ export declare class DynamicWaasBitcoinConnector extends DynamicWaasBitcoinConne
|
|
|
258
278
|
}): Promise<string>;
|
|
259
279
|
/**
|
|
260
280
|
* Builds a PSBT for a Bitcoin transaction with real UTXOs
|
|
261
|
-
* @param transaction -
|
|
281
|
+
* @param transaction - Bitcoin transaction with recipient address, amount in satoshis, and optional fee priority
|
|
262
282
|
* @returns A PSBT in Base64 format
|
|
263
283
|
* @throws {DynamicError} If no active account address, insufficient funds, or other errors
|
|
264
284
|
*/
|
|
@@ -124,11 +124,15 @@ class DynamicWaasBitcoinConnector extends withDynamicWaas(BitcoinWalletConnector
|
|
|
124
124
|
});
|
|
125
125
|
}
|
|
126
126
|
/**
|
|
127
|
-
* Signs a Partially Signed Bitcoin Transaction (PSBT)
|
|
128
|
-
*
|
|
129
|
-
*
|
|
127
|
+
* Signs a Partially Signed Bitcoin Transaction (PSBT) for embedded wallets
|
|
128
|
+
*
|
|
129
|
+
* Embedded wallets only support PSBT format and automatically sign all inputs
|
|
130
|
+
* that belong to the wallet address. Always uses SIGHASH_ALL (0x01).
|
|
131
|
+
*
|
|
132
|
+
* @param request - The PSBT signing request. Only unsignedPsbtBase64 is required.
|
|
133
|
+
* @returns The signed (but not finalized) PSBT response
|
|
130
134
|
* @throws {DynamicError} If active account address is not set
|
|
131
|
-
* @throws {DynamicError} If signed session ID is not available
|
|
135
|
+
* @throws {DynamicError} If signed session ID is not available
|
|
132
136
|
*/
|
|
133
137
|
signPsbt(request) {
|
|
134
138
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -144,9 +148,6 @@ class DynamicWaasBitcoinConnector extends withDynamicWaas(BitcoinWalletConnector
|
|
|
144
148
|
const mfaToken = yield ((_b = this.getMfaToken) === null || _b === void 0 ? void 0 : _b.call(this, {
|
|
145
149
|
mfaAction: MFAAction.WalletWaasSign,
|
|
146
150
|
}));
|
|
147
|
-
if (request.signature && request.signature.length > 0) {
|
|
148
|
-
throw new DynamicError('Signature is not supported for waas at the moment');
|
|
149
|
-
}
|
|
150
151
|
const signedTransaction = yield walletClient.signTransaction({
|
|
151
152
|
authToken: (_c = this.getAuthToken) === null || _c === void 0 ? void 0 : _c.call(this),
|
|
152
153
|
mfaToken,
|
|
@@ -188,9 +189,7 @@ class DynamicWaasBitcoinConnector extends withDynamicWaas(BitcoinWalletConnector
|
|
|
188
189
|
// Step 1: Build the PSBT
|
|
189
190
|
const unsignedPsbt = yield this.buildPsbt(transaction);
|
|
190
191
|
// Step 2: Sign the PSBT
|
|
191
|
-
// SIGHASH_ALL (0x01) is the most common sighash type for Bitcoin transactions (Eventually can be configurable)
|
|
192
192
|
const signedPsbtResponse = yield this.signPsbt({
|
|
193
|
-
allowedSighash: [0x01],
|
|
194
193
|
unsignedPsbtBase64: unsignedPsbt,
|
|
195
194
|
});
|
|
196
195
|
if (!signedPsbtResponse) {
|
|
@@ -333,7 +332,7 @@ class DynamicWaasBitcoinConnector extends withDynamicWaas(BitcoinWalletConnector
|
|
|
333
332
|
}
|
|
334
333
|
/**
|
|
335
334
|
* Builds a PSBT for a Bitcoin transaction with real UTXOs
|
|
336
|
-
* @param transaction -
|
|
335
|
+
* @param transaction - Bitcoin transaction with recipient address, amount in satoshis, and optional fee priority
|
|
337
336
|
* @returns A PSBT in Base64 format
|
|
338
337
|
* @throws {DynamicError} If no active account address, insufficient funds, or other errors
|
|
339
338
|
*/
|
|
@@ -343,7 +342,7 @@ class DynamicWaasBitcoinConnector extends withDynamicWaas(BitcoinWalletConnector
|
|
|
343
342
|
throw new DynamicError('Active account address is required');
|
|
344
343
|
}
|
|
345
344
|
const publicKeyHex = yield this.getPublicKey();
|
|
346
|
-
const buildOptions = PsbtBuilderService.createBuildOptions(this.activeAccountAddress, transaction, publicKeyHex);
|
|
345
|
+
const buildOptions = PsbtBuilderService.createBuildOptions(this.activeAccountAddress, transaction, publicKeyHex, transaction.feePriority || 'medium');
|
|
347
346
|
return this.psbtBuilderService.buildPsbt(buildOptions);
|
|
348
347
|
});
|
|
349
348
|
}
|
package/src/const.cjs
CHANGED
|
@@ -12,20 +12,28 @@ const MEMPOOL_API_URL_TESTNET = 'https://mempool.space/testnet/api';
|
|
|
12
12
|
// WaaS Bitcoin constants
|
|
13
13
|
const SATOSHIS_PER_BTC = 100000000;
|
|
14
14
|
const DUST_LIMIT = 546; // Bitcoin's dust limit in satoshis
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
// Accurate vSize constants for Native SegWit (P2WPKH) transactions
|
|
16
|
+
// These are used for precise fee estimation
|
|
17
|
+
const VSIZE_OVERHEAD = 10.5; // Base transaction overhead in vBytes
|
|
18
|
+
const VSIZE_INPUT_P2WPKH = 68; // Each P2WPKH input in vBytes
|
|
19
|
+
const VSIZE_OUTPUT_P2WPKH = 31; // Each P2WPKH output in vBytes
|
|
17
20
|
const MIN_RELAY_FEE = 111;
|
|
18
21
|
const DEFAULT_FEE_ESTIMATE = 1000; // Conservative default fee estimate in satoshis
|
|
22
|
+
// RBF (Replace-By-Fee) sequence number
|
|
23
|
+
// 0xfffffffd = 4294967293 (enables RBF, not final)
|
|
24
|
+
const RBF_SEQUENCE = 0xfffffffd;
|
|
19
25
|
|
|
20
26
|
exports.BTCKIT_INTERFACE = BTCKIT_INTERFACE;
|
|
21
27
|
exports.DEFAULT_FEE_ESTIMATE = DEFAULT_FEE_ESTIMATE;
|
|
22
28
|
exports.DUST_LIMIT = DUST_LIMIT;
|
|
23
29
|
exports.HTTP_STATUS_NOT_FOUND = HTTP_STATUS_NOT_FOUND;
|
|
24
30
|
exports.HTTP_STATUS_TOO_MANY_REQUESTS = HTTP_STATUS_TOO_MANY_REQUESTS;
|
|
25
|
-
exports.INPUT_BYTE_SIZE_UPPER_BOUND = INPUT_BYTE_SIZE_UPPER_BOUND;
|
|
26
31
|
exports.MEMPOOL_API_URL = MEMPOOL_API_URL;
|
|
27
32
|
exports.MEMPOOL_API_URL_TESTNET = MEMPOOL_API_URL_TESTNET;
|
|
28
33
|
exports.MIN_RELAY_FEE = MIN_RELAY_FEE;
|
|
29
|
-
exports.
|
|
34
|
+
exports.RBF_SEQUENCE = RBF_SEQUENCE;
|
|
30
35
|
exports.SATOSHIS_PER_BTC = SATOSHIS_PER_BTC;
|
|
31
36
|
exports.SATSCONNECT_FEATURE = SATSCONNECT_FEATURE;
|
|
37
|
+
exports.VSIZE_INPUT_P2WPKH = VSIZE_INPUT_P2WPKH;
|
|
38
|
+
exports.VSIZE_OUTPUT_P2WPKH = VSIZE_OUTPUT_P2WPKH;
|
|
39
|
+
exports.VSIZE_OVERHEAD = VSIZE_OVERHEAD;
|
package/src/const.d.ts
CHANGED
|
@@ -7,7 +7,9 @@ export declare const MEMPOOL_API_URL = "https://mempool.space/api";
|
|
|
7
7
|
export declare const MEMPOOL_API_URL_TESTNET = "https://mempool.space/testnet/api";
|
|
8
8
|
export declare const SATOSHIS_PER_BTC = 100000000;
|
|
9
9
|
export declare const DUST_LIMIT = 546;
|
|
10
|
-
export declare const
|
|
11
|
-
export declare const
|
|
10
|
+
export declare const VSIZE_OVERHEAD = 10.5;
|
|
11
|
+
export declare const VSIZE_INPUT_P2WPKH = 68;
|
|
12
|
+
export declare const VSIZE_OUTPUT_P2WPKH = 31;
|
|
12
13
|
export declare const MIN_RELAY_FEE = 111;
|
|
13
14
|
export declare const DEFAULT_FEE_ESTIMATE = 1000;
|
|
15
|
+
export declare const RBF_SEQUENCE = 4294967293;
|
package/src/const.js
CHANGED
|
@@ -8,9 +8,15 @@ const MEMPOOL_API_URL_TESTNET = 'https://mempool.space/testnet/api';
|
|
|
8
8
|
// WaaS Bitcoin constants
|
|
9
9
|
const SATOSHIS_PER_BTC = 100000000;
|
|
10
10
|
const DUST_LIMIT = 546; // Bitcoin's dust limit in satoshis
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
// Accurate vSize constants for Native SegWit (P2WPKH) transactions
|
|
12
|
+
// These are used for precise fee estimation
|
|
13
|
+
const VSIZE_OVERHEAD = 10.5; // Base transaction overhead in vBytes
|
|
14
|
+
const VSIZE_INPUT_P2WPKH = 68; // Each P2WPKH input in vBytes
|
|
15
|
+
const VSIZE_OUTPUT_P2WPKH = 31; // Each P2WPKH output in vBytes
|
|
13
16
|
const MIN_RELAY_FEE = 111;
|
|
14
17
|
const DEFAULT_FEE_ESTIMATE = 1000; // Conservative default fee estimate in satoshis
|
|
18
|
+
// RBF (Replace-By-Fee) sequence number
|
|
19
|
+
// 0xfffffffd = 4294967293 (enables RBF, not final)
|
|
20
|
+
const RBF_SEQUENCE = 0xfffffffd;
|
|
15
21
|
|
|
16
|
-
export { BTCKIT_INTERFACE, DEFAULT_FEE_ESTIMATE, DUST_LIMIT, HTTP_STATUS_NOT_FOUND, HTTP_STATUS_TOO_MANY_REQUESTS,
|
|
22
|
+
export { BTCKIT_INTERFACE, DEFAULT_FEE_ESTIMATE, DUST_LIMIT, HTTP_STATUS_NOT_FOUND, HTTP_STATUS_TOO_MANY_REQUESTS, MEMPOOL_API_URL, MEMPOOL_API_URL_TESTNET, MIN_RELAY_FEE, RBF_SEQUENCE, SATOSHIS_PER_BTC, SATSCONNECT_FEATURE, VSIZE_INPUT_P2WPKH, VSIZE_OUTPUT_P2WPKH, VSIZE_OVERHEAD };
|
package/src/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { OkxConnector } from './connectors';
|
|
2
2
|
export { BitcoinWalletConnector, type BitcoinWalletConnectorOpts, } from './connectors';
|
|
3
|
-
export type { BitcoinTransaction, BitcoinSignProtocol, BitcoinSignPsbtRequest, BitcoinSignPsbtResponse, BitcoinWalletStandardMethods, SignPsbtOptions, } from './types';
|
|
3
|
+
export type { BitcoinTransaction, BitcoinSignProtocol, BitcoinSignPsbtRequest, BitcoinSignPsbtResponse, BitcoinWalletStandardMethods, SignPsbtOptions, EmbeddedWalletSignPsbtRequest, } from './types';
|
|
4
4
|
export * from './utils';
|
|
5
5
|
export * from './wallet';
|
|
6
6
|
export { UnisatConnector, BitcoinSatsConnectConnector, DynamicWaasBitcoinConnector, } from './connectors';
|
|
@@ -70,21 +70,44 @@ class MempoolApiService {
|
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
72
|
/**
|
|
73
|
-
* Estimates transaction fees based on number of inputs and outputs
|
|
73
|
+
* Estimates transaction fees based on number of inputs and outputs using accurate vSize
|
|
74
74
|
* @param address - The Bitcoin address to determine the network from
|
|
75
75
|
* @param numInputs - Number of transaction inputs
|
|
76
76
|
* @param numOutputs - Number of transaction outputs
|
|
77
|
+
* @param feePriority - Fee priority level (high/medium/low)
|
|
77
78
|
* @returns Estimated fee in satoshis
|
|
78
79
|
*/
|
|
79
|
-
estimateTransactionFee(
|
|
80
|
-
return _tslib.__awaiter(this,
|
|
80
|
+
estimateTransactionFee(address_1, numInputs_1, numOutputs_1) {
|
|
81
|
+
return _tslib.__awaiter(this, arguments, void 0, function* (address, numInputs, numOutputs, feePriority = 'medium') {
|
|
81
82
|
try {
|
|
82
83
|
const feeData = yield this.getFeeRecommendations(address);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
// High (Fastest): fastestFee -> Block 1
|
|
85
|
+
// Medium (Fast): halfHourFee -> Blocks 2-3
|
|
86
|
+
// Low (Eco): economyFee (preferred) or hourFee -> Blocks 6+
|
|
87
|
+
let feePerByte;
|
|
88
|
+
switch (feePriority) {
|
|
89
|
+
case 'high':
|
|
90
|
+
feePerByte =
|
|
91
|
+
feeData.fastestFee || feeData.halfHourFee || feeData.hourFee || 1;
|
|
92
|
+
break;
|
|
93
|
+
case 'low':
|
|
94
|
+
feePerByte = feeData.economyFee || feeData.hourFee || 1;
|
|
95
|
+
break;
|
|
96
|
+
case 'medium':
|
|
97
|
+
default:
|
|
98
|
+
feePerByte =
|
|
99
|
+
feeData.halfHourFee || feeData.hourFee || feeData.economyFee || 1;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
logger.debug(`Fee estimation - Priority: ${feePriority}, fastestFee: ${feeData.fastestFee}, halfHourFee: ${feeData.halfHourFee}, hourFee: ${feeData.hourFee}, economyFee: ${feeData.economyFee}, selected: ${feePerByte} sat/vB`);
|
|
103
|
+
// Use accurate vSize calculation for Native SegWit (P2WPKH)
|
|
104
|
+
// Formula: overhead + (inputs × 68) + (outputs × 31)
|
|
105
|
+
const vSize = _const.VSIZE_OVERHEAD +
|
|
106
|
+
numInputs * _const.VSIZE_INPUT_P2WPKH +
|
|
107
|
+
numOutputs * _const.VSIZE_OUTPUT_P2WPKH;
|
|
108
|
+
const estimatedFee = Math.ceil(feePerByte * vSize) + _const.MIN_RELAY_FEE;
|
|
109
|
+
logger.debug(`Fee calculation - Priority: ${feePriority}, vSize: ${vSize}, feePerByte: ${feePerByte} sat/vB, estimatedFee: ${estimatedFee} satoshis`);
|
|
110
|
+
logger.debug(`[MempoolApiService] Fee Priority: ${feePriority}, Selected Rate: ${feePerByte} sat/vB, vSize: ${vSize}, Estimated Fee: ${estimatedFee} satoshis`);
|
|
88
111
|
return estimatedFee;
|
|
89
112
|
}
|
|
90
113
|
catch (error) {
|
|
@@ -24,13 +24,14 @@ export declare class MempoolApiService {
|
|
|
24
24
|
*/
|
|
25
25
|
getFeeRecommendations(address: string): Promise<FeeRecommendations>;
|
|
26
26
|
/**
|
|
27
|
-
* Estimates transaction fees based on number of inputs and outputs
|
|
27
|
+
* Estimates transaction fees based on number of inputs and outputs using accurate vSize
|
|
28
28
|
* @param address - The Bitcoin address to determine the network from
|
|
29
29
|
* @param numInputs - Number of transaction inputs
|
|
30
30
|
* @param numOutputs - Number of transaction outputs
|
|
31
|
+
* @param feePriority - Fee priority level (high/medium/low)
|
|
31
32
|
* @returns Estimated fee in satoshis
|
|
32
33
|
*/
|
|
33
|
-
estimateTransactionFee(address: string, numInputs: number, numOutputs: number): Promise<number>;
|
|
34
|
+
estimateTransactionFee(address: string, numInputs: number, numOutputs: number, feePriority?: 'high' | 'medium' | 'low'): Promise<number>;
|
|
34
35
|
/**
|
|
35
36
|
* Sends a raw Bitcoin transaction to the mempool
|
|
36
37
|
* @param address - The Bitcoin address to determine the network from
|
|
@@ -9,7 +9,7 @@ import '@dynamic-labs/wallet-book';
|
|
|
9
9
|
import '@dynamic-labs/sdk-api-core';
|
|
10
10
|
import '@wallet-standard/app';
|
|
11
11
|
import { getMempoolApiUrl } from '../utils/getMempoolApiUrl.js';
|
|
12
|
-
import {
|
|
12
|
+
import { VSIZE_OVERHEAD, VSIZE_INPUT_P2WPKH, VSIZE_OUTPUT_P2WPKH, MIN_RELAY_FEE, DEFAULT_FEE_ESTIMATE } from '../const.js';
|
|
13
13
|
import 'jsontokens';
|
|
14
14
|
import '../connectors/DynamicWaasBitcoinConnector/DynamicWaasBitcoinConnector.js';
|
|
15
15
|
|
|
@@ -66,21 +66,44 @@ class MempoolApiService {
|
|
|
66
66
|
});
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
|
-
* Estimates transaction fees based on number of inputs and outputs
|
|
69
|
+
* Estimates transaction fees based on number of inputs and outputs using accurate vSize
|
|
70
70
|
* @param address - The Bitcoin address to determine the network from
|
|
71
71
|
* @param numInputs - Number of transaction inputs
|
|
72
72
|
* @param numOutputs - Number of transaction outputs
|
|
73
|
+
* @param feePriority - Fee priority level (high/medium/low)
|
|
73
74
|
* @returns Estimated fee in satoshis
|
|
74
75
|
*/
|
|
75
|
-
estimateTransactionFee(
|
|
76
|
-
return __awaiter(this,
|
|
76
|
+
estimateTransactionFee(address_1, numInputs_1, numOutputs_1) {
|
|
77
|
+
return __awaiter(this, arguments, void 0, function* (address, numInputs, numOutputs, feePriority = 'medium') {
|
|
77
78
|
try {
|
|
78
79
|
const feeData = yield this.getFeeRecommendations(address);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
// High (Fastest): fastestFee -> Block 1
|
|
81
|
+
// Medium (Fast): halfHourFee -> Blocks 2-3
|
|
82
|
+
// Low (Eco): economyFee (preferred) or hourFee -> Blocks 6+
|
|
83
|
+
let feePerByte;
|
|
84
|
+
switch (feePriority) {
|
|
85
|
+
case 'high':
|
|
86
|
+
feePerByte =
|
|
87
|
+
feeData.fastestFee || feeData.halfHourFee || feeData.hourFee || 1;
|
|
88
|
+
break;
|
|
89
|
+
case 'low':
|
|
90
|
+
feePerByte = feeData.economyFee || feeData.hourFee || 1;
|
|
91
|
+
break;
|
|
92
|
+
case 'medium':
|
|
93
|
+
default:
|
|
94
|
+
feePerByte =
|
|
95
|
+
feeData.halfHourFee || feeData.hourFee || feeData.economyFee || 1;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
logger.debug(`Fee estimation - Priority: ${feePriority}, fastestFee: ${feeData.fastestFee}, halfHourFee: ${feeData.halfHourFee}, hourFee: ${feeData.hourFee}, economyFee: ${feeData.economyFee}, selected: ${feePerByte} sat/vB`);
|
|
99
|
+
// Use accurate vSize calculation for Native SegWit (P2WPKH)
|
|
100
|
+
// Formula: overhead + (inputs × 68) + (outputs × 31)
|
|
101
|
+
const vSize = VSIZE_OVERHEAD +
|
|
102
|
+
numInputs * VSIZE_INPUT_P2WPKH +
|
|
103
|
+
numOutputs * VSIZE_OUTPUT_P2WPKH;
|
|
104
|
+
const estimatedFee = Math.ceil(feePerByte * vSize) + MIN_RELAY_FEE;
|
|
105
|
+
logger.debug(`Fee calculation - Priority: ${feePriority}, vSize: ${vSize}, feePerByte: ${feePerByte} sat/vB, estimatedFee: ${estimatedFee} satoshis`);
|
|
106
|
+
logger.debug(`[MempoolApiService] Fee Priority: ${feePriority}, Selected Rate: ${feePerByte} sat/vB, vSize: ${vSize}, Estimated Fee: ${estimatedFee} satoshis`);
|
|
84
107
|
return estimatedFee;
|
|
85
108
|
}
|
|
86
109
|
catch (error) {
|
|
@@ -24,104 +24,234 @@ class PsbtBuilderService {
|
|
|
24
24
|
constructor(mempoolApiService) {
|
|
25
25
|
this.mempoolApiService = mempoolApiService;
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Filters out Taproot (P2TR) UTXOs to prevent accidental spending of Ordinals/Runes
|
|
29
|
+
* Since we only support Native SegWit (P2WPKH), we ensure the account address is not Taproot
|
|
30
|
+
* @param accountAddress - The account address to check
|
|
31
|
+
* @param utxos - Array of UTXOs to filter
|
|
32
|
+
* @returns Filtered array of UTXOs (only P2WPKH compatible)
|
|
33
|
+
*/
|
|
34
|
+
filterTaprootUTXOs(accountAddress, utxos) {
|
|
35
|
+
// Safety check: Ensure account address is not Taproot (bc1p...)
|
|
36
|
+
if (accountAddress.toLowerCase().startsWith('bc1p') ||
|
|
37
|
+
accountAddress.toLowerCase().startsWith('tb1p')) {
|
|
38
|
+
logger.warn(`Account address ${accountAddress} appears to be Taproot. Only Native SegWit (P2WPKH) is supported.`);
|
|
39
|
+
throw new utils.DynamicError('Taproot addresses are not supported. Only Native SegWit (P2WPKH) addresses are allowed.');
|
|
40
|
+
}
|
|
41
|
+
return utxos;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Selects UTXOs using Largest-First (Accumulator) strategy
|
|
45
|
+
* Sorts UTXOs by value (descending) and selects until we have enough to cover amount + fees
|
|
46
|
+
* @param utxos - Available UTXOs
|
|
47
|
+
* @param targetAmount - Target amount including fees and dust limit
|
|
48
|
+
* @returns Selected UTXOs
|
|
49
|
+
*/
|
|
50
|
+
selectUTXOsLargestFirst(utxos, targetAmount) {
|
|
51
|
+
// Sort UTXOs by value (largest first)
|
|
52
|
+
const sortedUTXOs = [...utxos].sort((a, b) => b.value - a.value);
|
|
53
|
+
// Accumulate UTXOs until we have enough
|
|
54
|
+
const selected = [];
|
|
55
|
+
let total = 0;
|
|
56
|
+
for (const utxo of sortedUTXOs) {
|
|
57
|
+
selected.push(utxo);
|
|
58
|
+
total += utxo.value;
|
|
59
|
+
// Stop when we have enough to cover the target amount
|
|
60
|
+
if (total >= targetAmount) {
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return selected;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Calculates the total value of UTXOs
|
|
68
|
+
* @param utxos - Array of UTXOs
|
|
69
|
+
* @returns Total value in satoshis
|
|
70
|
+
*/
|
|
71
|
+
calculateUTXOTotal(utxos) {
|
|
72
|
+
return utxos.reduce((total, utxo) => total + utxo.value, 0);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Validates and ensures sufficient funds for the transaction
|
|
76
|
+
* @param selectedUTXOs - Initially selected UTXOs
|
|
77
|
+
* @param allUTXOs - All available UTXOs
|
|
78
|
+
* @param selectedTotal - Total value of selected UTXOs
|
|
79
|
+
* @param amountInSatoshisNumber - Transaction amount
|
|
80
|
+
* @param feeEstimate - Estimated fee
|
|
81
|
+
* @returns Validated selected UTXOs
|
|
82
|
+
* @throws {DynamicError} If insufficient funds
|
|
83
|
+
*/
|
|
84
|
+
validateAndSelectUTXOs(selectedUTXOs, allUTXOs, selectedTotal, amountInSatoshisNumber, feeEstimate) {
|
|
85
|
+
const requiredAmount = amountInSatoshisNumber + feeEstimate;
|
|
86
|
+
if (selectedTotal >= requiredAmount) {
|
|
87
|
+
return selectedUTXOs;
|
|
88
|
+
}
|
|
89
|
+
// Try with all UTXOs if selection wasn't enough
|
|
90
|
+
if (selectedUTXOs.length < allUTXOs.length) {
|
|
91
|
+
const allTotal = this.calculateUTXOTotal(allUTXOs);
|
|
92
|
+
if (allTotal < requiredAmount) {
|
|
93
|
+
throw new utils.DynamicError(`Insufficient funds. Available: ${allTotal / _const.SATOSHIS_PER_BTC} BTC (${allTotal} satoshis), Required: ${amountInSatoshisNumber / _const.SATOSHIS_PER_BTC} BTC (${amountInSatoshisNumber} satoshis) + fees`);
|
|
94
|
+
}
|
|
95
|
+
return allUTXOs;
|
|
96
|
+
}
|
|
97
|
+
throw new utils.DynamicError(`Insufficient funds. Available: ${selectedTotal / _const.SATOSHIS_PER_BTC} BTC (${selectedTotal} satoshis), Required: ${amountInSatoshisNumber / _const.SATOSHIS_PER_BTC} BTC (${amountInSatoshisNumber} satoshis) + fees`);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Calculates fee estimate and change amount, handling dust limit
|
|
101
|
+
* @param accountAddress - Account address for fee estimation
|
|
102
|
+
* @param selectedUTXOs - Selected UTXOs
|
|
103
|
+
* @param selectedTotalValue - Total value of selected UTXOs
|
|
104
|
+
* @param amountInSatoshis - Transaction amount
|
|
105
|
+
* @param feePriority - Fee priority level
|
|
106
|
+
* @returns Object with feeEstimate, changeAmount, changeAmountNumber, and hasChangeOutput
|
|
107
|
+
*/
|
|
108
|
+
calculateFeeAndChange(accountAddress, selectedUTXOs, selectedTotalValue, amountInSatoshis, feePriority) {
|
|
109
|
+
return _tslib.__awaiter(this, void 0, void 0, function* () {
|
|
110
|
+
// Re-estimate fee with actual number of inputs
|
|
111
|
+
let feeEstimate = yield this.mempoolApiService.estimateTransactionFee(accountAddress, selectedUTXOs.length, 1, // Start with 1 output (recipient only)
|
|
112
|
+
feePriority);
|
|
113
|
+
let maxToSpend = selectedTotalValue - feeEstimate;
|
|
114
|
+
let changeAmount = BigInt(maxToSpend) - amountInSatoshis;
|
|
115
|
+
// If change will be above dust limit, re-estimate fees for 2 outputs
|
|
116
|
+
const changeAmountNumber = Number(changeAmount);
|
|
117
|
+
if (changeAmount > 0 && changeAmountNumber >= _const.DUST_LIMIT) {
|
|
118
|
+
feeEstimate = yield this.mempoolApiService.estimateTransactionFee(accountAddress, selectedUTXOs.length, 2, // recipient + change output
|
|
119
|
+
feePriority);
|
|
120
|
+
maxToSpend = selectedTotalValue - feeEstimate;
|
|
121
|
+
changeAmount = BigInt(maxToSpend) - amountInSatoshis;
|
|
122
|
+
}
|
|
123
|
+
const finalChangeAmountNumber = Number(changeAmount);
|
|
124
|
+
const hasChangeOutput = changeAmount > 0 && finalChangeAmountNumber >= _const.DUST_LIMIT;
|
|
125
|
+
// Final fee adjustment: if change < dust limit, add it to fee
|
|
126
|
+
if (changeAmount > 0 && finalChangeAmountNumber < _const.DUST_LIMIT) {
|
|
127
|
+
feeEstimate += finalChangeAmountNumber;
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
changeAmount,
|
|
131
|
+
changeAmountNumber: finalChangeAmountNumber,
|
|
132
|
+
feeEstimate,
|
|
133
|
+
hasChangeOutput,
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Adds inputs to PSBT from selected UTXOs
|
|
139
|
+
* @param psbt - PSBT to add inputs to
|
|
140
|
+
* @param selectedUTXOs - Selected UTXOs
|
|
141
|
+
* @param publicKeyPair - ECPair public key pair
|
|
142
|
+
* @param network - Bitcoin network
|
|
143
|
+
*/
|
|
144
|
+
addInputsToPsbt(psbt, selectedUTXOs, publicKeyPair, network) {
|
|
145
|
+
for (const utxo of selectedUTXOs) {
|
|
146
|
+
const outputScript = bitcoinjsLib.payments.p2wpkh({
|
|
147
|
+
network,
|
|
148
|
+
pubkey: publicKeyPair.publicKey,
|
|
149
|
+
}).output;
|
|
150
|
+
if (!outputScript) {
|
|
151
|
+
throw new utils.DynamicError('Failed to create segwit output script');
|
|
152
|
+
}
|
|
153
|
+
// Convert txid from hex string to Buffer and reverse it (Bitcoin uses little-endian)
|
|
154
|
+
// The txid from the API is in big-endian format, but bitcoinjs-lib expects little-endian
|
|
155
|
+
const txidBuffer = Buffer.from(utxo.txid, 'hex').reverse();
|
|
156
|
+
psbt.addInput({
|
|
157
|
+
hash: txidBuffer,
|
|
158
|
+
index: utxo.vout,
|
|
159
|
+
sequence: _const.RBF_SEQUENCE, // Enable RBF (Replace-By-Fee)
|
|
160
|
+
witnessUtxo: {
|
|
161
|
+
script: outputScript,
|
|
162
|
+
value: utxo.value,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Adds outputs to PSBT (recipient and optionally change)
|
|
169
|
+
* @param psbt - PSBT to add outputs to
|
|
170
|
+
* @param recipientAddress - Recipient address
|
|
171
|
+
* @param accountAddress - Account address for change
|
|
172
|
+
* @param amountInSatoshisNumber - Transaction amount
|
|
173
|
+
* @param changeAmountNumber - Change amount
|
|
174
|
+
* @param hasChangeOutput - Whether to include change output
|
|
175
|
+
* @param network - Bitcoin network
|
|
176
|
+
*/
|
|
177
|
+
addOutputsToPsbt(psbt, recipientAddress, accountAddress, amountInSatoshisNumber, changeAmountNumber, hasChangeOutput, network) {
|
|
178
|
+
if (amountInSatoshisNumber < _const.DUST_LIMIT) {
|
|
179
|
+
throw new utils.DynamicError(`Amount is below dust limit of ${_const.DUST_LIMIT} satoshis (${_const.DUST_LIMIT / _const.SATOSHIS_PER_BTC} BTC)`);
|
|
180
|
+
}
|
|
181
|
+
psbt.addOutput({
|
|
182
|
+
script: bitcoinjsLib.address.toOutputScript(recipientAddress, network),
|
|
183
|
+
value: amountInSatoshisNumber,
|
|
184
|
+
});
|
|
185
|
+
if (hasChangeOutput) {
|
|
186
|
+
psbt.addOutput({
|
|
187
|
+
script: bitcoinjsLib.address.toOutputScript(accountAddress, network),
|
|
188
|
+
value: changeAmountNumber,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
27
192
|
/**
|
|
28
193
|
* Builds a PSBT for a Bitcoin transaction with real UTXOs
|
|
194
|
+
* Uses Largest-First UTXO selection strategy with accurate vSize fee estimation
|
|
29
195
|
* @param options - Options for building the PSBT
|
|
30
196
|
* @returns A PSBT in Base64 format
|
|
31
197
|
* @throws {DynamicError} If insufficient funds, no UTXOs, or other errors
|
|
32
198
|
*/
|
|
33
199
|
buildPsbt(options) {
|
|
34
200
|
return _tslib.__awaiter(this, void 0, void 0, function* () {
|
|
35
|
-
const { accountAddress, recipientAddress, amountInSatoshis, publicKeyHex, network, } = options;
|
|
201
|
+
const { accountAddress, recipientAddress, amountInSatoshis, publicKeyHex, network, feePriority = 'medium', } = options;
|
|
202
|
+
logger.debug(`buildPsbt called with feePriority: ${feePriority}, amount: ${amountInSatoshis} satoshis`);
|
|
36
203
|
if (amountInSatoshis <= BigInt(0)) {
|
|
37
204
|
throw new utils.DynamicError('Amount must be greater than 0');
|
|
38
205
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (utxos.length === 0) {
|
|
206
|
+
const allUTXOs = yield this.mempoolApiService.getUTXOs(accountAddress);
|
|
207
|
+
if (allUTXOs.length === 0) {
|
|
42
208
|
throw new utils.DynamicError('No UTXOs found for this address');
|
|
43
209
|
}
|
|
44
|
-
|
|
210
|
+
const utxos = this.filterTaprootUTXOs(accountAddress, allUTXOs);
|
|
45
211
|
const publicKeyBuffer = Buffer.from(publicKeyHex, 'hex');
|
|
46
|
-
// Use ECPair to ensure the public key is properly formatted for bitcoinjs-lib
|
|
47
212
|
const ECPair = ecpair.ECPairFactory(ecc__default["default"]);
|
|
48
213
|
const publicKeyPair = ECPair.fromPublicKey(publicKeyBuffer, {
|
|
49
214
|
compressed: true,
|
|
50
215
|
});
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
let
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
216
|
+
const amountInSatoshisNumber = Number(amountInSatoshis);
|
|
217
|
+
// Initial fee estimate with 1 input and 1 output
|
|
218
|
+
const initialFeeEstimate = yield this.mempoolApiService.estimateTransactionFee(accountAddress, 1, 1, feePriority);
|
|
219
|
+
// Target amount: transaction amount + fee + dust limit (for change output safety)
|
|
220
|
+
const targetAmount = amountInSatoshisNumber + initialFeeEstimate + _const.DUST_LIMIT;
|
|
221
|
+
// Select UTXOs using Largest-First strategy
|
|
222
|
+
let selectedUTXOs = this.selectUTXOsLargestFirst(utxos, targetAmount);
|
|
223
|
+
const selectedTotal = this.calculateUTXOTotal(selectedUTXOs);
|
|
224
|
+
// Validate and ensure sufficient funds
|
|
225
|
+
selectedUTXOs = this.validateAndSelectUTXOs(selectedUTXOs, utxos, selectedTotal, amountInSatoshisNumber, initialFeeEstimate);
|
|
226
|
+
const selectedTotalValue = this.calculateUTXOTotal(selectedUTXOs);
|
|
227
|
+
// Calculate fee and change with proper dust limit handling
|
|
228
|
+
const { feeEstimate, changeAmountNumber, hasChangeOutput } = yield this.calculateFeeAndChange(accountAddress, selectedUTXOs, selectedTotalValue, amountInSatoshis, feePriority);
|
|
229
|
+
// Final check: ensure we have enough after final fee calculation
|
|
230
|
+
const maxToSpend = selectedTotalValue - feeEstimate;
|
|
231
|
+
if (maxToSpend < amountInSatoshisNumber) {
|
|
66
232
|
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
233
|
}
|
|
68
|
-
// Create PSBT
|
|
234
|
+
// Create PSBT and add inputs/outputs
|
|
69
235
|
const psbt = new bitcoinjsLib.Psbt({ network });
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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`);
|
|
236
|
+
this.addInputsToPsbt(psbt, selectedUTXOs, publicKeyPair, network);
|
|
237
|
+
this.addOutputsToPsbt(psbt, recipientAddress, accountAddress, amountInSatoshisNumber, changeAmountNumber, hasChangeOutput, network);
|
|
238
|
+
logger.debug(`buildPsbt created PSBT for recipientAddress: ${recipientAddress}, amount: ${amountInSatoshisNumber} satoshis (${amountInSatoshisNumber / _const.SATOSHIS_PER_BTC} BTC), change: ${changeAmountNumber} satoshis, estimated fee: ${feeEstimate} satoshis, feePriority: ${feePriority}, inputs: ${selectedUTXOs.length}`);
|
|
111
239
|
return psbt.toBase64();
|
|
112
240
|
});
|
|
113
241
|
}
|
|
114
242
|
/**
|
|
115
243
|
* Helper method to create BuildPsbtOptions from transaction and account details
|
|
116
244
|
* @param accountAddress - The account address
|
|
117
|
-
* @param transaction - The Bitcoin transaction
|
|
245
|
+
* @param transaction - The Bitcoin transaction (must have amount and recipientAddress)
|
|
118
246
|
* @param publicKeyHex - The public key in hex format
|
|
247
|
+
* @param feePriority - Optional fee priority (defaults to 'medium')
|
|
119
248
|
* @returns BuildPsbtOptions
|
|
120
249
|
*/
|
|
121
|
-
static createBuildOptions(accountAddress, transaction, publicKeyHex) {
|
|
250
|
+
static createBuildOptions(accountAddress, transaction, publicKeyHex, feePriority = 'medium') {
|
|
122
251
|
return {
|
|
123
252
|
accountAddress,
|
|
124
253
|
amountInSatoshis: transaction.amount,
|
|
254
|
+
feePriority,
|
|
125
255
|
network: getBitcoinNetwork.getBitcoinNetwork(accountAddress),
|
|
126
256
|
publicKeyHex,
|
|
127
257
|
recipientAddress: transaction.recipientAddress,
|
|
@@ -6,8 +6,71 @@ import { MempoolApiService } from './MempoolApiService';
|
|
|
6
6
|
export declare class PsbtBuilderService {
|
|
7
7
|
private mempoolApiService;
|
|
8
8
|
constructor(mempoolApiService: MempoolApiService);
|
|
9
|
+
/**
|
|
10
|
+
* Filters out Taproot (P2TR) UTXOs to prevent accidental spending of Ordinals/Runes
|
|
11
|
+
* Since we only support Native SegWit (P2WPKH), we ensure the account address is not Taproot
|
|
12
|
+
* @param accountAddress - The account address to check
|
|
13
|
+
* @param utxos - Array of UTXOs to filter
|
|
14
|
+
* @returns Filtered array of UTXOs (only P2WPKH compatible)
|
|
15
|
+
*/
|
|
16
|
+
private filterTaprootUTXOs;
|
|
17
|
+
/**
|
|
18
|
+
* Selects UTXOs using Largest-First (Accumulator) strategy
|
|
19
|
+
* Sorts UTXOs by value (descending) and selects until we have enough to cover amount + fees
|
|
20
|
+
* @param utxos - Available UTXOs
|
|
21
|
+
* @param targetAmount - Target amount including fees and dust limit
|
|
22
|
+
* @returns Selected UTXOs
|
|
23
|
+
*/
|
|
24
|
+
private selectUTXOsLargestFirst;
|
|
25
|
+
/**
|
|
26
|
+
* Calculates the total value of UTXOs
|
|
27
|
+
* @param utxos - Array of UTXOs
|
|
28
|
+
* @returns Total value in satoshis
|
|
29
|
+
*/
|
|
30
|
+
private calculateUTXOTotal;
|
|
31
|
+
/**
|
|
32
|
+
* Validates and ensures sufficient funds for the transaction
|
|
33
|
+
* @param selectedUTXOs - Initially selected UTXOs
|
|
34
|
+
* @param allUTXOs - All available UTXOs
|
|
35
|
+
* @param selectedTotal - Total value of selected UTXOs
|
|
36
|
+
* @param amountInSatoshisNumber - Transaction amount
|
|
37
|
+
* @param feeEstimate - Estimated fee
|
|
38
|
+
* @returns Validated selected UTXOs
|
|
39
|
+
* @throws {DynamicError} If insufficient funds
|
|
40
|
+
*/
|
|
41
|
+
private validateAndSelectUTXOs;
|
|
42
|
+
/**
|
|
43
|
+
* Calculates fee estimate and change amount, handling dust limit
|
|
44
|
+
* @param accountAddress - Account address for fee estimation
|
|
45
|
+
* @param selectedUTXOs - Selected UTXOs
|
|
46
|
+
* @param selectedTotalValue - Total value of selected UTXOs
|
|
47
|
+
* @param amountInSatoshis - Transaction amount
|
|
48
|
+
* @param feePriority - Fee priority level
|
|
49
|
+
* @returns Object with feeEstimate, changeAmount, changeAmountNumber, and hasChangeOutput
|
|
50
|
+
*/
|
|
51
|
+
private calculateFeeAndChange;
|
|
52
|
+
/**
|
|
53
|
+
* Adds inputs to PSBT from selected UTXOs
|
|
54
|
+
* @param psbt - PSBT to add inputs to
|
|
55
|
+
* @param selectedUTXOs - Selected UTXOs
|
|
56
|
+
* @param publicKeyPair - ECPair public key pair
|
|
57
|
+
* @param network - Bitcoin network
|
|
58
|
+
*/
|
|
59
|
+
private addInputsToPsbt;
|
|
60
|
+
/**
|
|
61
|
+
* Adds outputs to PSBT (recipient and optionally change)
|
|
62
|
+
* @param psbt - PSBT to add outputs to
|
|
63
|
+
* @param recipientAddress - Recipient address
|
|
64
|
+
* @param accountAddress - Account address for change
|
|
65
|
+
* @param amountInSatoshisNumber - Transaction amount
|
|
66
|
+
* @param changeAmountNumber - Change amount
|
|
67
|
+
* @param hasChangeOutput - Whether to include change output
|
|
68
|
+
* @param network - Bitcoin network
|
|
69
|
+
*/
|
|
70
|
+
private addOutputsToPsbt;
|
|
9
71
|
/**
|
|
10
72
|
* Builds a PSBT for a Bitcoin transaction with real UTXOs
|
|
73
|
+
* Uses Largest-First UTXO selection strategy with accurate vSize fee estimation
|
|
11
74
|
* @param options - Options for building the PSBT
|
|
12
75
|
* @returns A PSBT in Base64 format
|
|
13
76
|
* @throws {DynamicError} If insufficient funds, no UTXOs, or other errors
|
|
@@ -16,12 +79,13 @@ export declare class PsbtBuilderService {
|
|
|
16
79
|
/**
|
|
17
80
|
* Helper method to create BuildPsbtOptions from transaction and account details
|
|
18
81
|
* @param accountAddress - The account address
|
|
19
|
-
* @param transaction - The Bitcoin transaction
|
|
82
|
+
* @param transaction - The Bitcoin transaction (must have amount and recipientAddress)
|
|
20
83
|
* @param publicKeyHex - The public key in hex format
|
|
84
|
+
* @param feePriority - Optional fee priority (defaults to 'medium')
|
|
21
85
|
* @returns BuildPsbtOptions
|
|
22
86
|
*/
|
|
23
87
|
static createBuildOptions(accountAddress: string, transaction: {
|
|
24
88
|
amount: bigint;
|
|
25
89
|
recipientAddress: string;
|
|
26
|
-
}, publicKeyHex: string): BuildPsbtOptions;
|
|
90
|
+
}, publicKeyHex: string, feePriority?: 'high' | 'medium' | 'low'): BuildPsbtOptions;
|
|
27
91
|
}
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import { __awaiter } from '../../_virtual/_tslib.js';
|
|
3
3
|
import ecc from '@bitcoinerlab/secp256k1';
|
|
4
4
|
import { ECPairFactory } from 'ecpair';
|
|
5
|
-
import {
|
|
5
|
+
import { payments, address, Psbt } from 'bitcoinjs-lib';
|
|
6
6
|
import { Logger } from '@dynamic-labs/logger';
|
|
7
7
|
import { DynamicError } from '@dynamic-labs/utils';
|
|
8
|
-
import { DUST_LIMIT,
|
|
8
|
+
import { SATOSHIS_PER_BTC, DUST_LIMIT, RBF_SEQUENCE } from '../const.js';
|
|
9
9
|
import { getBitcoinNetwork } from '../utils/getBitcoinNetwork/getBitcoinNetwork.js';
|
|
10
10
|
|
|
11
11
|
const logger = new Logger('PsbtBuilderService');
|
|
@@ -16,104 +16,234 @@ class PsbtBuilderService {
|
|
|
16
16
|
constructor(mempoolApiService) {
|
|
17
17
|
this.mempoolApiService = mempoolApiService;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Filters out Taproot (P2TR) UTXOs to prevent accidental spending of Ordinals/Runes
|
|
21
|
+
* Since we only support Native SegWit (P2WPKH), we ensure the account address is not Taproot
|
|
22
|
+
* @param accountAddress - The account address to check
|
|
23
|
+
* @param utxos - Array of UTXOs to filter
|
|
24
|
+
* @returns Filtered array of UTXOs (only P2WPKH compatible)
|
|
25
|
+
*/
|
|
26
|
+
filterTaprootUTXOs(accountAddress, utxos) {
|
|
27
|
+
// Safety check: Ensure account address is not Taproot (bc1p...)
|
|
28
|
+
if (accountAddress.toLowerCase().startsWith('bc1p') ||
|
|
29
|
+
accountAddress.toLowerCase().startsWith('tb1p')) {
|
|
30
|
+
logger.warn(`Account address ${accountAddress} appears to be Taproot. Only Native SegWit (P2WPKH) is supported.`);
|
|
31
|
+
throw new DynamicError('Taproot addresses are not supported. Only Native SegWit (P2WPKH) addresses are allowed.');
|
|
32
|
+
}
|
|
33
|
+
return utxos;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Selects UTXOs using Largest-First (Accumulator) strategy
|
|
37
|
+
* Sorts UTXOs by value (descending) and selects until we have enough to cover amount + fees
|
|
38
|
+
* @param utxos - Available UTXOs
|
|
39
|
+
* @param targetAmount - Target amount including fees and dust limit
|
|
40
|
+
* @returns Selected UTXOs
|
|
41
|
+
*/
|
|
42
|
+
selectUTXOsLargestFirst(utxos, targetAmount) {
|
|
43
|
+
// Sort UTXOs by value (largest first)
|
|
44
|
+
const sortedUTXOs = [...utxos].sort((a, b) => b.value - a.value);
|
|
45
|
+
// Accumulate UTXOs until we have enough
|
|
46
|
+
const selected = [];
|
|
47
|
+
let total = 0;
|
|
48
|
+
for (const utxo of sortedUTXOs) {
|
|
49
|
+
selected.push(utxo);
|
|
50
|
+
total += utxo.value;
|
|
51
|
+
// Stop when we have enough to cover the target amount
|
|
52
|
+
if (total >= targetAmount) {
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return selected;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Calculates the total value of UTXOs
|
|
60
|
+
* @param utxos - Array of UTXOs
|
|
61
|
+
* @returns Total value in satoshis
|
|
62
|
+
*/
|
|
63
|
+
calculateUTXOTotal(utxos) {
|
|
64
|
+
return utxos.reduce((total, utxo) => total + utxo.value, 0);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Validates and ensures sufficient funds for the transaction
|
|
68
|
+
* @param selectedUTXOs - Initially selected UTXOs
|
|
69
|
+
* @param allUTXOs - All available UTXOs
|
|
70
|
+
* @param selectedTotal - Total value of selected UTXOs
|
|
71
|
+
* @param amountInSatoshisNumber - Transaction amount
|
|
72
|
+
* @param feeEstimate - Estimated fee
|
|
73
|
+
* @returns Validated selected UTXOs
|
|
74
|
+
* @throws {DynamicError} If insufficient funds
|
|
75
|
+
*/
|
|
76
|
+
validateAndSelectUTXOs(selectedUTXOs, allUTXOs, selectedTotal, amountInSatoshisNumber, feeEstimate) {
|
|
77
|
+
const requiredAmount = amountInSatoshisNumber + feeEstimate;
|
|
78
|
+
if (selectedTotal >= requiredAmount) {
|
|
79
|
+
return selectedUTXOs;
|
|
80
|
+
}
|
|
81
|
+
// Try with all UTXOs if selection wasn't enough
|
|
82
|
+
if (selectedUTXOs.length < allUTXOs.length) {
|
|
83
|
+
const allTotal = this.calculateUTXOTotal(allUTXOs);
|
|
84
|
+
if (allTotal < requiredAmount) {
|
|
85
|
+
throw new DynamicError(`Insufficient funds. Available: ${allTotal / SATOSHIS_PER_BTC} BTC (${allTotal} satoshis), Required: ${amountInSatoshisNumber / SATOSHIS_PER_BTC} BTC (${amountInSatoshisNumber} satoshis) + fees`);
|
|
86
|
+
}
|
|
87
|
+
return allUTXOs;
|
|
88
|
+
}
|
|
89
|
+
throw new DynamicError(`Insufficient funds. Available: ${selectedTotal / SATOSHIS_PER_BTC} BTC (${selectedTotal} satoshis), Required: ${amountInSatoshisNumber / SATOSHIS_PER_BTC} BTC (${amountInSatoshisNumber} satoshis) + fees`);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Calculates fee estimate and change amount, handling dust limit
|
|
93
|
+
* @param accountAddress - Account address for fee estimation
|
|
94
|
+
* @param selectedUTXOs - Selected UTXOs
|
|
95
|
+
* @param selectedTotalValue - Total value of selected UTXOs
|
|
96
|
+
* @param amountInSatoshis - Transaction amount
|
|
97
|
+
* @param feePriority - Fee priority level
|
|
98
|
+
* @returns Object with feeEstimate, changeAmount, changeAmountNumber, and hasChangeOutput
|
|
99
|
+
*/
|
|
100
|
+
calculateFeeAndChange(accountAddress, selectedUTXOs, selectedTotalValue, amountInSatoshis, feePriority) {
|
|
101
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
102
|
+
// Re-estimate fee with actual number of inputs
|
|
103
|
+
let feeEstimate = yield this.mempoolApiService.estimateTransactionFee(accountAddress, selectedUTXOs.length, 1, // Start with 1 output (recipient only)
|
|
104
|
+
feePriority);
|
|
105
|
+
let maxToSpend = selectedTotalValue - feeEstimate;
|
|
106
|
+
let changeAmount = BigInt(maxToSpend) - amountInSatoshis;
|
|
107
|
+
// If change will be above dust limit, re-estimate fees for 2 outputs
|
|
108
|
+
const changeAmountNumber = Number(changeAmount);
|
|
109
|
+
if (changeAmount > 0 && changeAmountNumber >= DUST_LIMIT) {
|
|
110
|
+
feeEstimate = yield this.mempoolApiService.estimateTransactionFee(accountAddress, selectedUTXOs.length, 2, // recipient + change output
|
|
111
|
+
feePriority);
|
|
112
|
+
maxToSpend = selectedTotalValue - feeEstimate;
|
|
113
|
+
changeAmount = BigInt(maxToSpend) - amountInSatoshis;
|
|
114
|
+
}
|
|
115
|
+
const finalChangeAmountNumber = Number(changeAmount);
|
|
116
|
+
const hasChangeOutput = changeAmount > 0 && finalChangeAmountNumber >= DUST_LIMIT;
|
|
117
|
+
// Final fee adjustment: if change < dust limit, add it to fee
|
|
118
|
+
if (changeAmount > 0 && finalChangeAmountNumber < DUST_LIMIT) {
|
|
119
|
+
feeEstimate += finalChangeAmountNumber;
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
changeAmount,
|
|
123
|
+
changeAmountNumber: finalChangeAmountNumber,
|
|
124
|
+
feeEstimate,
|
|
125
|
+
hasChangeOutput,
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Adds inputs to PSBT from selected UTXOs
|
|
131
|
+
* @param psbt - PSBT to add inputs to
|
|
132
|
+
* @param selectedUTXOs - Selected UTXOs
|
|
133
|
+
* @param publicKeyPair - ECPair public key pair
|
|
134
|
+
* @param network - Bitcoin network
|
|
135
|
+
*/
|
|
136
|
+
addInputsToPsbt(psbt, selectedUTXOs, publicKeyPair, network) {
|
|
137
|
+
for (const utxo of selectedUTXOs) {
|
|
138
|
+
const outputScript = payments.p2wpkh({
|
|
139
|
+
network,
|
|
140
|
+
pubkey: publicKeyPair.publicKey,
|
|
141
|
+
}).output;
|
|
142
|
+
if (!outputScript) {
|
|
143
|
+
throw new DynamicError('Failed to create segwit output script');
|
|
144
|
+
}
|
|
145
|
+
// Convert txid from hex string to Buffer and reverse it (Bitcoin uses little-endian)
|
|
146
|
+
// The txid from the API is in big-endian format, but bitcoinjs-lib expects little-endian
|
|
147
|
+
const txidBuffer = Buffer.from(utxo.txid, 'hex').reverse();
|
|
148
|
+
psbt.addInput({
|
|
149
|
+
hash: txidBuffer,
|
|
150
|
+
index: utxo.vout,
|
|
151
|
+
sequence: RBF_SEQUENCE, // Enable RBF (Replace-By-Fee)
|
|
152
|
+
witnessUtxo: {
|
|
153
|
+
script: outputScript,
|
|
154
|
+
value: utxo.value,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Adds outputs to PSBT (recipient and optionally change)
|
|
161
|
+
* @param psbt - PSBT to add outputs to
|
|
162
|
+
* @param recipientAddress - Recipient address
|
|
163
|
+
* @param accountAddress - Account address for change
|
|
164
|
+
* @param amountInSatoshisNumber - Transaction amount
|
|
165
|
+
* @param changeAmountNumber - Change amount
|
|
166
|
+
* @param hasChangeOutput - Whether to include change output
|
|
167
|
+
* @param network - Bitcoin network
|
|
168
|
+
*/
|
|
169
|
+
addOutputsToPsbt(psbt, recipientAddress, accountAddress, amountInSatoshisNumber, changeAmountNumber, hasChangeOutput, network) {
|
|
170
|
+
if (amountInSatoshisNumber < DUST_LIMIT) {
|
|
171
|
+
throw new DynamicError(`Amount is below dust limit of ${DUST_LIMIT} satoshis (${DUST_LIMIT / SATOSHIS_PER_BTC} BTC)`);
|
|
172
|
+
}
|
|
173
|
+
psbt.addOutput({
|
|
174
|
+
script: address.toOutputScript(recipientAddress, network),
|
|
175
|
+
value: amountInSatoshisNumber,
|
|
176
|
+
});
|
|
177
|
+
if (hasChangeOutput) {
|
|
178
|
+
psbt.addOutput({
|
|
179
|
+
script: address.toOutputScript(accountAddress, network),
|
|
180
|
+
value: changeAmountNumber,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
19
184
|
/**
|
|
20
185
|
* Builds a PSBT for a Bitcoin transaction with real UTXOs
|
|
186
|
+
* Uses Largest-First UTXO selection strategy with accurate vSize fee estimation
|
|
21
187
|
* @param options - Options for building the PSBT
|
|
22
188
|
* @returns A PSBT in Base64 format
|
|
23
189
|
* @throws {DynamicError} If insufficient funds, no UTXOs, or other errors
|
|
24
190
|
*/
|
|
25
191
|
buildPsbt(options) {
|
|
26
192
|
return __awaiter(this, void 0, void 0, function* () {
|
|
27
|
-
const { accountAddress, recipientAddress, amountInSatoshis, publicKeyHex, network, } = options;
|
|
193
|
+
const { accountAddress, recipientAddress, amountInSatoshis, publicKeyHex, network, feePriority = 'medium', } = options;
|
|
194
|
+
logger.debug(`buildPsbt called with feePriority: ${feePriority}, amount: ${amountInSatoshis} satoshis`);
|
|
28
195
|
if (amountInSatoshis <= BigInt(0)) {
|
|
29
196
|
throw new DynamicError('Amount must be greater than 0');
|
|
30
197
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (utxos.length === 0) {
|
|
198
|
+
const allUTXOs = yield this.mempoolApiService.getUTXOs(accountAddress);
|
|
199
|
+
if (allUTXOs.length === 0) {
|
|
34
200
|
throw new DynamicError('No UTXOs found for this address');
|
|
35
201
|
}
|
|
36
|
-
|
|
202
|
+
const utxos = this.filterTaprootUTXOs(accountAddress, allUTXOs);
|
|
37
203
|
const publicKeyBuffer = Buffer.from(publicKeyHex, 'hex');
|
|
38
|
-
// Use ECPair to ensure the public key is properly formatted for bitcoinjs-lib
|
|
39
204
|
const ECPair = ECPairFactory(ecc);
|
|
40
205
|
const publicKeyPair = ECPair.fromPublicKey(publicKeyBuffer, {
|
|
41
206
|
compressed: true,
|
|
42
207
|
});
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
let
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
208
|
+
const amountInSatoshisNumber = Number(amountInSatoshis);
|
|
209
|
+
// Initial fee estimate with 1 input and 1 output
|
|
210
|
+
const initialFeeEstimate = yield this.mempoolApiService.estimateTransactionFee(accountAddress, 1, 1, feePriority);
|
|
211
|
+
// Target amount: transaction amount + fee + dust limit (for change output safety)
|
|
212
|
+
const targetAmount = amountInSatoshisNumber + initialFeeEstimate + DUST_LIMIT;
|
|
213
|
+
// Select UTXOs using Largest-First strategy
|
|
214
|
+
let selectedUTXOs = this.selectUTXOsLargestFirst(utxos, targetAmount);
|
|
215
|
+
const selectedTotal = this.calculateUTXOTotal(selectedUTXOs);
|
|
216
|
+
// Validate and ensure sufficient funds
|
|
217
|
+
selectedUTXOs = this.validateAndSelectUTXOs(selectedUTXOs, utxos, selectedTotal, amountInSatoshisNumber, initialFeeEstimate);
|
|
218
|
+
const selectedTotalValue = this.calculateUTXOTotal(selectedUTXOs);
|
|
219
|
+
// Calculate fee and change with proper dust limit handling
|
|
220
|
+
const { feeEstimate, changeAmountNumber, hasChangeOutput } = yield this.calculateFeeAndChange(accountAddress, selectedUTXOs, selectedTotalValue, amountInSatoshis, feePriority);
|
|
221
|
+
// Final check: ensure we have enough after final fee calculation
|
|
222
|
+
const maxToSpend = selectedTotalValue - feeEstimate;
|
|
223
|
+
if (maxToSpend < amountInSatoshisNumber) {
|
|
58
224
|
throw new DynamicError(`Insufficient funds. Available: ${maxToSpend / SATOSHIS_PER_BTC} BTC (${maxToSpend} satoshis), Required: ${amountInSatoshisNumber / SATOSHIS_PER_BTC} BTC (${amountInSatoshisNumber} satoshis)`);
|
|
59
225
|
}
|
|
60
|
-
// Create PSBT
|
|
226
|
+
// Create PSBT and add inputs/outputs
|
|
61
227
|
const psbt = new Psbt({ network });
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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`);
|
|
228
|
+
this.addInputsToPsbt(psbt, selectedUTXOs, publicKeyPair, network);
|
|
229
|
+
this.addOutputsToPsbt(psbt, recipientAddress, accountAddress, amountInSatoshisNumber, changeAmountNumber, hasChangeOutput, network);
|
|
230
|
+
logger.debug(`buildPsbt created PSBT for recipientAddress: ${recipientAddress}, amount: ${amountInSatoshisNumber} satoshis (${amountInSatoshisNumber / SATOSHIS_PER_BTC} BTC), change: ${changeAmountNumber} satoshis, estimated fee: ${feeEstimate} satoshis, feePriority: ${feePriority}, inputs: ${selectedUTXOs.length}`);
|
|
103
231
|
return psbt.toBase64();
|
|
104
232
|
});
|
|
105
233
|
}
|
|
106
234
|
/**
|
|
107
235
|
* Helper method to create BuildPsbtOptions from transaction and account details
|
|
108
236
|
* @param accountAddress - The account address
|
|
109
|
-
* @param transaction - The Bitcoin transaction
|
|
237
|
+
* @param transaction - The Bitcoin transaction (must have amount and recipientAddress)
|
|
110
238
|
* @param publicKeyHex - The public key in hex format
|
|
239
|
+
* @param feePriority - Optional fee priority (defaults to 'medium')
|
|
111
240
|
* @returns BuildPsbtOptions
|
|
112
241
|
*/
|
|
113
|
-
static createBuildOptions(accountAddress, transaction, publicKeyHex) {
|
|
242
|
+
static createBuildOptions(accountAddress, transaction, publicKeyHex, feePriority = 'medium') {
|
|
114
243
|
return {
|
|
115
244
|
accountAddress,
|
|
116
245
|
amountInSatoshis: transaction.amount,
|
|
246
|
+
feePriority,
|
|
117
247
|
network: getBitcoinNetwork(accountAddress),
|
|
118
248
|
publicKeyHex,
|
|
119
249
|
recipientAddress: transaction.recipientAddress,
|
package/src/types.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ export type BitcoinConnectedAccount = {
|
|
|
32
32
|
export type BitcoinTransaction = {
|
|
33
33
|
amount: bigint;
|
|
34
34
|
recipientAddress: string;
|
|
35
|
+
feePriority?: FeePriority;
|
|
35
36
|
};
|
|
36
37
|
export type SignPsbtOptions = {
|
|
37
38
|
autoFinalized: boolean;
|
|
@@ -52,6 +53,21 @@ export type BitcoinSignPsbtRequestSignature = {
|
|
|
52
53
|
signingIndexes: number[] | undefined;
|
|
53
54
|
disableAddressValidation?: boolean;
|
|
54
55
|
};
|
|
56
|
+
/**
|
|
57
|
+
* PSBT signing request specifically for embedded wallets (DynamicWaasBitcoinConnector)
|
|
58
|
+
*
|
|
59
|
+
* Embedded wallets:
|
|
60
|
+
* - Only support PSBT format (Base64)
|
|
61
|
+
* - Automatically sign all inputs that belong to the wallet address
|
|
62
|
+
* - Always use SIGHASH_ALL (0x01)
|
|
63
|
+
*/
|
|
64
|
+
export type EmbeddedWalletSignPsbtRequest = {
|
|
65
|
+
/**
|
|
66
|
+
* The unsigned PSBT in Base64 format.
|
|
67
|
+
* Embedded wallets only support PSBT format, not raw transaction hex.
|
|
68
|
+
*/
|
|
69
|
+
unsignedPsbtBase64: string;
|
|
70
|
+
};
|
|
55
71
|
export type SatsConnectSignTransactionInput = {
|
|
56
72
|
message?: string;
|
|
57
73
|
psbtBase64: string;
|
|
@@ -173,11 +189,13 @@ export interface ParsedTransaction {
|
|
|
173
189
|
inputs: ParsedTransactionInput[];
|
|
174
190
|
outputs: ParsedTransactionOutput[];
|
|
175
191
|
}
|
|
192
|
+
export type FeePriority = 'high' | 'medium' | 'low';
|
|
176
193
|
export interface BuildPsbtOptions {
|
|
177
194
|
accountAddress: string;
|
|
178
195
|
recipientAddress: string;
|
|
179
196
|
amountInSatoshis: bigint;
|
|
180
197
|
publicKeyHex: string;
|
|
181
198
|
network: Network;
|
|
199
|
+
feePriority?: FeePriority;
|
|
182
200
|
}
|
|
183
201
|
export {};
|