11import BigNumber from 'bignumber.js' ;
22import * as bitcoin from 'groestlcoinjs-lib' ;
33import bitcoinMessage from 'groestlcoinjs-message' ;
4- import coinSelect , { CoinSelectOutput , CoinSelectReturnInput , CoinSelectTarget } from 'coinselect' ;
4+ import coinSelect , { CoinSelectOptions , CoinSelectOutput , CoinSelectReturnInput , CoinSelectTarget } from 'coinselect' ;
55import coinSelectSplit from 'coinselect/split' ;
66import { ECPairAPI , ECPairFactory , Signer } from 'ecpairgrs' ;
77
@@ -373,6 +373,31 @@ export class LegacyWallet extends AbstractWallet {
373373 return broadcast . length === 64 ; // this means return string is txid (precise length), so it was broadcasted ok
374374 }
375375
376+ /**
377+ * Whether size of at least one of the targets is overestimated by coinselect lib, comparing to its real scriptPubKey
378+ */
379+ _targetsAreOverestimated ( targets : CreateTransactionTarget [ ] ) : boolean {
380+ return targets . some ( t => {
381+ if ( ! t . address ) return false ;
382+ const address = t . address . toLowerCase ( ) ; // bech32 addresses are case insensitive
383+ const assumed = t . script ?. length ?? 25 ; // lib assumes p2pkh
384+ if ( address . startsWith ( 'grssp1' ) ) return assumed > 34 ; // silent payment ends up as p2tr output
385+ try {
386+ return assumed > bitcoin . address . toOutputScript ( address . startsWith ( 'grs1' ) ? address : t . address ) . length ;
387+ } catch ( _ ) {
388+ return false ;
389+ }
390+ } ) ;
391+ }
392+
393+ /**
394+ * Length of the scriptPubKey of this wallet's change output, in bytes. Passed to coinselect lib when it is bigger than
395+ * p2pkh (25 bytes), which is what the lib assumes
396+ */
397+ getChangeScriptLength ( ) : number {
398+ return this . segwitType === 'p2tr' ? 34 : 25 ;
399+ }
400+
376401 coinselect (
377402 utxos : CreateTransactionUtxo [ ] ,
378403 targets : CreateTransactionTarget [ ] ,
@@ -406,10 +431,11 @@ export class LegacyWallet extends AbstractWallet {
406431
407432 // counting the number of vbytes for each script type:
408433 if ( this . segwitType === 'p2wpkh' ) {
409- // 72 (high R low S signature) + 1 + 33 (comp pubkey) + 1 = 107 / 4 = 26.75 rounded up.
434+ // witness: 1 (items count) + 1 + 72 (high R low S signature with sighash byte) + 1 + 33 (comp pubkey) = 108 / 4 = 27.
435+ // exact for the biggest signature, no slack here
410436 u . script = { length : 27 } ;
411437 } else if ( this . segwitType === 'p2sh(p2wpkh)' ) {
412- // ((72 (high R low S signature) + 1 + 33 (comp pubkey) + 1) / 4) + 22 ( P2WPKH output on scriptSig stack) + 1 = 49.75 rounded up
438+ // same 27 vbytes of witness as p2wpkh + 23 bytes of scriptSig (1 + 22 of P2WPKH output on scriptSig stack) = 50
413439 u . script = { length : 50 } ;
414440 } else if ( this . segwitType === 'p2tr' ) {
415441 // taproot key path spend is just a 64 or 65 byte signature on the witness stack.
@@ -423,18 +449,37 @@ export class LegacyWallet extends AbstractWallet {
423449 }
424450
425451 for ( const t of _targets ) {
426- if ( t . address ?. startsWith ( 'bc1' ) ) {
452+ // bech32 addresses are case insensitive
453+ const address = t . address ?. toLowerCase ( ) ;
454+ if ( address ?. startsWith ( 'bc1' ) ) {
427455 // in case address is non-typical and takes more bytes than coinselect library anticipates by default
428- t . script = { length : bitcoin . address . toOutputScript ( t . address ) . length + 3 } ;
456+ t . script = { length : bitcoin . address . toOutputScript ( address ) . length + 3 } ;
457+ } else if ( address ?. startsWith ( 'grssp1' ) ) {
458+ // silent payment address ends up as p2tr output (34 bytes), sizing it the same way as bc1p address above
459+ t . script = { length : 34 + 3 } ;
429460 }
430461
431462 if ( t . script ?. hex ) {
432463 // setting length for coinselect lib manually as it is not aware of our field `hex`
433- t . script . length = t . script . hex . length / 2 - 4 ;
464+ t . script . length = t . script . hex . length / 2 ;
434465 }
435466 }
436467
437- const { inputs, outputs, fee } = algo ( _utxos , _targets as CoinSelectTarget [ ] , feeRate ) ;
468+ // coinselect lib assumes change output has 25 bytes script (p2pkh). if our change script is bigger (p2tr, p2wsh) we have
469+ // to tell it, otherwise change output is not fully paid for and resulting feerate is lower than requested.
470+ // smaller change scripts are left as is, that only makes us overestimate by 2-3 bytes
471+ const changeScriptLength = this . getChangeScriptLength ( ) ;
472+ const options : CoinSelectOptions = { } ;
473+ if ( changeScriptLength > 25 ) options . changeScript = { length : changeScriptLength } ;
474+
475+ // coinselect lib is not aware of segwit marker & flag (2 bytes = 0.5 vbyte), and since vsize is rounded up the tx can end
476+ // up 1 vbyte bigger than estimated. p2tr inputs are overestimated by 0.5 vbyte each, so only p2wpkh inputs are affected.
477+ // we also dont need it when outputs are already overestimated (`+ 3` above; p2sh is 23 bytes while lib assumes 25)
478+ if ( ( this . segwitType === 'p2wpkh' || this . segwitType === 'p2sh(p2wpkh)' ) && ! this . _targetsAreOverestimated ( _targets ) ) {
479+ options . txExtraBytes = 1 ;
480+ }
481+
482+ const { inputs, outputs, fee } = algo ( _utxos , _targets as CoinSelectTarget [ ] , feeRate , options ) ;
438483
439484 // .inputs and .outputs will be undefined if no solution was found
440485 if ( ! inputs || ! outputs ) {
@@ -583,7 +628,7 @@ export class LegacyWallet extends AbstractWallet {
583628 if ( ! address ) return false ;
584629 let cleanAddress = address ;
585630
586- if ( this . segwitType === 'p2wpkh' ) {
631+ if ( this . segwitType === 'p2wpkh' || this . segwitType === 'p2tr' ) {
587632 cleanAddress = address . toLowerCase ( ) ;
588633 }
589634
0 commit comments