@@ -36,6 +36,7 @@ import {
36
36
addressToLockScript ,
37
37
createSighashPreimage ,
38
38
validateRecipient ,
39
+ utxoComparator ,
39
40
} from './utils.js' ;
40
41
import { P2SH_OUTPUT_SIZE , DUST_LIMIT } from './constants.js' ;
41
42
import NetworkProvider from './network/NetworkProvider.js' ;
@@ -49,9 +50,9 @@ export class Transaction {
49
50
50
51
private sequence = 0xfffffffe ;
51
52
private locktime : number ;
52
- private hardcodedFee : number ;
53
- private feePerByte = 1.0 ;
54
- private minChange = DUST_LIMIT ;
53
+ private feePerByte : number = 1.0 ;
54
+ private hardcodedFee : bigint ;
55
+ private minChange : bigint = DUST_LIMIT ;
55
56
56
57
constructor (
57
58
private address : string ,
@@ -90,11 +91,11 @@ export class Transaction {
90
91
return this ;
91
92
}
92
93
93
- to ( to : string , amount : number ) : this;
94
+ to ( to : string , amount : bigint ) : this;
94
95
to ( outputs : Recipient [ ] ) : this;
95
96
96
- to ( toOrOutputs : string | Recipient [ ] , amount ?: number ) : this {
97
- if ( typeof toOrOutputs === 'string' && typeof amount === 'number ' ) {
97
+ to ( toOrOutputs : string | Recipient [ ] , amount ?: bigint ) : this {
98
+ if ( typeof toOrOutputs === 'string' && typeof amount === 'bigint ' ) {
98
99
return this . to ( [ { to : toOrOutputs , amount } ] ) ;
99
100
}
100
101
@@ -122,7 +123,7 @@ export class Transaction {
122
123
return this ;
123
124
}
124
125
125
- withHardcodedFee ( hardcodedFee : number ) : this {
126
+ withHardcodedFee ( hardcodedFee : bigint ) : this {
126
127
this . hardcodedFee = hardcodedFee ;
127
128
return this ;
128
129
}
@@ -132,13 +133,13 @@ export class Transaction {
132
133
return this ;
133
134
}
134
135
135
- withMinChange ( minChange : number ) : this {
136
+ withMinChange ( minChange : bigint ) : this {
136
137
this . minChange = minChange ;
137
138
return this ;
138
139
}
139
140
140
141
withoutChange ( ) : this {
141
- return this . withMinChange ( Number . MAX_VALUE ) ;
142
+ return this . withMinChange ( BigInt ( Number . MAX_VALUE ) ) ;
142
143
}
143
144
144
145
async build ( ) : Promise < string > {
@@ -160,7 +161,7 @@ export class Transaction {
160
161
? addressToLockScript ( output . to )
161
162
: output . to ;
162
163
163
- const satoshis = bigIntToBinUint64LE ( BigInt ( output . amount ) ) ;
164
+ const satoshis = bigIntToBinUint64LE ( output . amount ) ;
164
165
165
166
return { lockingBytecode, satoshis } ;
166
167
} ) ;
@@ -294,35 +295,38 @@ export class Transaction {
294
295
// Add one extra byte per input to over-estimate tx-in count
295
296
const inputSize = getInputSize ( placeholderScript ) + 1 ;
296
297
298
+ // Note that we use the addPrecision function to add "decimal points" to BigInt numbers
299
+
297
300
// Calculate amount to send and base fee (excluding additional fees per UTXO)
298
- const amount = this . outputs . reduce ( ( acc , output ) => acc + output . amount , 0 ) ;
299
- let fee = this . hardcodedFee ?? getTxSizeWithoutInputs ( this . outputs ) * this . feePerByte ;
301
+ let amount = addPrecision ( this . outputs . reduce ( ( acc , output ) => acc + output . amount , BigInt ( 0 ) ) ) ;
302
+ let fee = addPrecision ( this . hardcodedFee ?? getTxSizeWithoutInputs ( this . outputs ) * this . feePerByte ) ;
300
303
301
304
// Select and gather UTXOs and calculate fees and available funds
302
- let satsAvailable = 0 ;
305
+ let satsAvailable = BigInt ( 0 ) ;
303
306
if ( this . inputs . length > 0 ) {
304
- // If inputs are already defined, the user provided the UTXOs
305
- // and we perform no further UTXO selection
306
- if ( ! this . hardcodedFee ) fee += this . inputs . length * inputSize * this . feePerByte ;
307
- satsAvailable = this . inputs . reduce ( ( acc , input ) => acc + input . satoshis , 0 ) ;
307
+ // If inputs are already defined, the user provided the UTXOs and we perform no further UTXO selection
308
+ if ( ! this . hardcodedFee ) fee += addPrecision ( this . inputs . length * inputSize * this . feePerByte ) ;
309
+ satsAvailable = addPrecision ( this . inputs . reduce ( ( acc , input ) => acc + input . satoshis , BigInt ( 0 ) ) ) ;
308
310
} else {
309
311
// If inputs are not defined yet, we retrieve the contract's UTXOs and perform selection
310
312
const utxos = await this . provider . getUtxos ( this . address ) ;
311
313
312
314
// We sort the UTXOs mainly so there is consistent behaviour between network providers
313
315
// even if they report UTXOs in a different order
314
- utxos . sort ( ( a , b ) => b . satoshis - a . satoshis ) ;
316
+ utxos . sort ( utxoComparator ) . reverse ( ) ;
315
317
316
318
for ( const utxo of utxos ) {
317
319
this . inputs . push ( utxo ) ;
318
- satsAvailable += utxo . satoshis ;
319
- if ( ! this . hardcodedFee ) fee += inputSize * this . feePerByte ;
320
+ satsAvailable += addPrecision ( utxo . satoshis ) ;
321
+ if ( ! this . hardcodedFee ) fee += addPrecision ( inputSize * this . feePerByte ) ;
320
322
if ( satsAvailable > amount + fee ) break ;
321
323
}
322
324
}
323
325
324
- // Fee per byte can be a decimal number, but we need the total fee to be an integer
325
- fee = Math . ceil ( fee ) ;
326
+ // Remove "decimal points" from BigInt numbers (rounding up for fee, down for others)
327
+ satsAvailable = removePrecisionFloor ( satsAvailable ) ;
328
+ amount = removePrecisionFloor ( amount ) ;
329
+ fee = removePrecisionCeil ( fee ) ;
326
330
327
331
// Calculate change and check available funds
328
332
let change = satsAvailable - amount - fee ;
@@ -331,9 +335,9 @@ export class Transaction {
331
335
throw new Error ( `Insufficient funds: available (${ satsAvailable } ) < needed (${ amount + fee } ).` ) ;
332
336
}
333
337
334
- // Account for the fee of a change output
338
+ // Account for the fee of adding a change output
335
339
if ( ! this . hardcodedFee ) {
336
- change -= P2SH_OUTPUT_SIZE ;
340
+ change -= BigInt ( P2SH_OUTPUT_SIZE * this . feePerByte ) ;
337
341
}
338
342
339
343
// Add a change output if applicable
@@ -342,3 +346,25 @@ export class Transaction {
342
346
}
343
347
}
344
348
}
349
+
350
+ // Note: the below is a very simple implementation of a "decimal point" system for BigInt numbers
351
+ // It is safe to use for UTXO fee calculations due to its low numbers, but should not be used for other purposes
352
+ // Also note that multiplication and division between two "decimal" bigints is not supported
353
+
354
+ // High precision may not work with some 'number' inputs, so we set the default to 6 "decimal places"
355
+ const addPrecision = ( amount : number | bigint , precision : number = 6 ) : bigint => {
356
+ if ( typeof amount === 'number' ) {
357
+ return BigInt ( Math . ceil ( amount * 10 ** precision ) ) ;
358
+ }
359
+
360
+ return amount * BigInt ( 10 ** precision ) ;
361
+ } ;
362
+
363
+ const removePrecisionFloor = ( amount : bigint , precision : number = 6 ) : bigint => (
364
+ amount / ( BigInt ( 10 ) ** BigInt ( precision ) )
365
+ ) ;
366
+
367
+ const removePrecisionCeil = ( amount : bigint , precision : number = 6 ) : bigint => {
368
+ const multiplier = BigInt ( 10 ) ** BigInt ( precision ) ;
369
+ return ( amount + multiplier - BigInt ( 1 ) ) / multiplier ;
370
+ } ;
0 commit comments