Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancement: EstimateIsFeePaidEnough #44

Merged
merged 1 commit into from
Sep 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,30 @@ func (tx *Tx) IsFeePaidEnough(fees *FeeQuote) (bool, error) {
return actualFeePaid >= expFeesPaid.TotalFeePaid, nil
}

// EstimateIsFeePaidEnough will calculate the fees that this transaction is paying
// including the individual fee types (std/data/etc.), and will add 107 bytes to the unlocking
// script of any unsigned inputs (only P2PKH for now) found to give a final size
// estimate of the tx size for fee calculation.
func (tx *Tx) EstimateIsFeePaidEnough(fees *FeeQuote) (bool, error) {
tempTx, err := tx.estimatedFinalTx()
if err != nil {
return false, err
}
expFeesPaid, err := tempTx.feesPaid(tempTx.SizeWithTypes(), fees)
if err != nil {
return false, err
}
totalInputSatoshis := tempTx.TotalInputSatoshis()
totalOutputSatoshis := tempTx.TotalOutputSatoshis()

if totalInputSatoshis < totalOutputSatoshis {
return false, nil
}

actualFeePaid := totalInputSatoshis - totalOutputSatoshis
return actualFeePaid >= expFeesPaid.TotalFeePaid, nil
}

// EstimateFeesPaid will estimate how big the tx will be when finalised
// by estimating input unlocking scripts that have not yet been filled
// including the individual fee types (std/data/etc.).
Expand Down
193 changes: 193 additions & 0 deletions tx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -901,6 +901,186 @@ func TestTx_Clone(t *testing.T) {
}
})
}
func Test_EstimateIsFeePaidEnough(t *testing.T) {
tests := map[string]struct {
tx *bt.Tx
dataLength uint64
expSize *bt.TxSize
isEnough bool
}{
"unsigned transaction (1 input 1 P2PKHOutput + no change) paying less by 1 satoshi": {
tx: func() *bt.Tx {
tx := bt.NewTx()
assert.NoError(t, tx.From("a4c76f8a7c05a91dcf5699b95b54e856298e50c1ceca9a8a5569c8532c500c11",
0, "76a91455b61be43392125d127f1780fb038437cd67ef9c88ac", 1000))

assert.NoError(t, tx.AddP2PKHOutputFromAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", 905))
return tx
}(),
expSize: &bt.TxSize{
TotalBytes: 85,
TotalStdBytes: 85,
},
isEnough: false,
}, "unsigned transaction (1 input 1 P2PKHOutput + change) should pay exact amount": {
tx: func() *bt.Tx {
tx := bt.NewTx()
assert.NoError(t, tx.From("a4c76f8a7c05a91dcf5699b95b54e856298e50c1ceca9a8a5569c8532c500c11",
0, "76a91455b61be43392125d127f1780fb038437cd67ef9c88ac", 834709))

assert.NoError(t, tx.AddP2PKHOutputFromAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", 256559))
assert.NoError(t, tx.ChangeToAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", bt.NewFeeQuote()))
return tx
}(),
expSize: &bt.TxSize{
TotalBytes: 119,
TotalStdBytes: 119,
TotalDataBytes: 0,
},
isEnough: true,
}, "unsigned transaction (0 input 1 P2PKHOutput) should not pay": {
tx: func() *bt.Tx {
tx := bt.NewTx()

assert.NoError(t, tx.AddP2PKHOutputFromAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", 256559))
return tx
}(),
expSize: &bt.TxSize{
TotalBytes: 44,
TotalStdBytes: 44,
TotalDataBytes: 0,
},
isEnough: false,
}, "unsigned transaction (1 input 2 P2PKHOutputs) should pay exact amount": {
tx: func() *bt.Tx {
tx := bt.NewTx()
assert.NoError(t, tx.From("a4c76f8a7c05a91dcf5699b95b54e856298e50c1ceca9a8a5569c8532c500c11",
0, "76a91455b61be43392125d127f1780fb038437cd67ef9c88ac", 834763))

assert.NoError(t, tx.AddP2PKHOutputFromAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", 256559))
assert.NoError(t, tx.AddP2PKHOutputFromAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", 578091))
return tx
}(),
expSize: &bt.TxSize{
TotalBytes: 119,
TotalStdBytes: 119,
TotalDataBytes: 0,
},
isEnough: true,
}, "unsigned transaction (1 input 2 P2PKHOutputs) should fail paying less by 1 sat": {
tx: func() *bt.Tx {
tx := bt.NewTx()
assert.NoError(t, tx.From("a4c76f8a7c05a91dcf5699b95b54e856298e50c1ceca9a8a5569c8532c500c11",
0, "76a91455b61be43392125d127f1780fb038437cd67ef9c88ac", 834763))

assert.NoError(t, tx.AddP2PKHOutputFromAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", 256560))
assert.NoError(t, tx.AddP2PKHOutputFromAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", 578091))
return tx
}(),
expSize: &bt.TxSize{
TotalBytes: 119,
TotalStdBytes: 119,
TotalDataBytes: 0,
},
isEnough: false,
}, "226B signed transaction (1 input 1 P2PKHOutput + change) no data should return 113 sats fee": {
tx: func() *bt.Tx {
tx := bt.NewTx()
w, err := wif.DecodeWIF("cRhdUmZx4MbsjxVxGH4bM4geNLzQEPxspnhGtDCvMmfCLcED8Q6G")
if err != nil {
log.Fatal(err)
}
assert.NoError(t, tx.From("a4c76f8a7c05a91dcf5699b95b54e856298e50c1ceca9a8a5569c8532c500c11",
0, "76a914ff8c9344d4e76c0580420142f697e5fc2ce5c98e88ac", 834709))

assert.NoError(t, tx.AddP2PKHOutputFromAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", 256559))
assert.NoError(t, tx.ChangeToAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", bt.NewFeeQuote()))
tx.SignAuto(context.Background(), &bt.LocalSigner{PrivateKey: w.PrivKey})
return tx
}(),
expSize: &bt.TxSize{
TotalBytes: 226,
TotalStdBytes: 226,
},
isEnough: true,
}, "192B signed transaction (1 input 1 P2PKHOutput + no change) should pay exact amount": {
tx: func() *bt.Tx {
tx := bt.NewTx()
w, err := wif.DecodeWIF("cRhdUmZx4MbsjxVxGH4bM4geNLzQEPxspnhGtDCvMmfCLcED8Q6G")
if err != nil {
log.Fatal(err)
}
assert.NoError(t, tx.From("a4c76f8a7c05a91dcf5699b95b54e856298e50c1ceca9a8a5569c8532c500c11",
0, "76a914ff8c9344d4e76c0580420142f697e5fc2ce5c98e88ac", 1000))

assert.NoError(t, tx.AddP2PKHOutputFromAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", 904))
tx.SignAuto(context.Background(), &bt.LocalSigner{PrivateKey: w.PrivKey})
return tx
}(),
expSize: &bt.TxSize{
TotalBytes: 192,
TotalStdBytes: 192,
},
isEnough: true,
}, "214B signed transaction (1 input, 1 change output, 1 opreturn) should pay exact amount": {
tx: func() *bt.Tx {
w, err := wif.DecodeWIF("cRhdUmZx4MbsjxVxGH4bM4geNLzQEPxspnhGtDCvMmfCLcED8Q6G")
if err != nil {
log.Fatal(err)
}
tx := bt.NewTx()
assert.NoError(t, tx.From("160f06232540dcb0e9b6db9b36a27f01da1e7e473989df67859742cf098d498f",
0, "76a914ff8c9344d4e76c0580420142f697e5fc2ce5c98e88ac", 1000))
assert.NoError(t, tx.AddOpReturnOutput([]byte("hellohello")))
assert.NoError(t, tx.AddP2PKHOutputFromAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", 894))
is, err := tx.SignAuto(context.Background(), &bt.LocalSigner{PrivateKey: w.PrivKey})
assert.Nil(t, err)
assert.Equal(t, 1, len(is))
return tx
}(),
expSize: &bt.TxSize{
TotalBytes: 214,
TotalStdBytes: 201,
TotalDataBytes: 13,
},
isEnough: true,
}, "214B signed transaction (1 input, 1 change output, 1 opreturn) should fail paying less by 1 sat": {
tx: func() *bt.Tx {
w, err := wif.DecodeWIF("cRhdUmZx4MbsjxVxGH4bM4geNLzQEPxspnhGtDCvMmfCLcED8Q6G")
if err != nil {
log.Fatal(err)
}
tx := bt.NewTx()
assert.NoError(t, tx.From("160f06232540dcb0e9b6db9b36a27f01da1e7e473989df67859742cf098d498f",
0, "76a914ff8c9344d4e76c0580420142f697e5fc2ce5c98e88ac", 1000))
assert.NoError(t, tx.AddOpReturnOutput([]byte("hellohello")))
assert.NoError(t, tx.AddP2PKHOutputFromAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", 895))
is, err := tx.SignAuto(context.Background(), &bt.LocalSigner{PrivateKey: w.PrivKey})
assert.Nil(t, err)
assert.Equal(t, 1, len(is))
return tx
}(),
expSize: &bt.TxSize{
TotalBytes: 213,
TotalStdBytes: 200,
TotalDataBytes: 13,
},
isEnough: false,
},
// TODO: add tests for different fee type values
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
fee := bt.NewFeeQuote()
isEnough, err := test.tx.EstimateIsFeePaidEnough(fee)
assert.NoError(t, err)
assert.Equal(t, test.isEnough, isEnough)

swt := test.tx.SizeWithTypes()
assert.Equal(t, test.expSize, swt)
})
}
}

func Test_IsFeePaidEnough(t *testing.T) {
tests := map[string]struct {
Expand Down Expand Up @@ -939,6 +1119,19 @@ func Test_IsFeePaidEnough(t *testing.T) {
TotalDataBytes: 0,
},
isEnough: true,
}, "unsigned transaction (0 input 1 P2PKHOutput) should not pay": {
tx: func() *bt.Tx {
tx := bt.NewTx()

assert.NoError(t, tx.AddP2PKHOutputFromAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", 256559))
return tx
}(),
expSize: &bt.TxSize{
TotalBytes: 44,
TotalStdBytes: 44,
TotalDataBytes: 0,
},
isEnough: false,
}, "unsigned transaction (1 input 2 P2PKHOutputs) should pay exact amount": {
tx: func() *bt.Tx {
tx := bt.NewTx()
Expand Down