Skip to content

Commit

Permalink
Merge pull request #21 from libsv/changetooutput
Browse files Browse the repository at this point in the history
  • Loading branch information
jadwahab authored May 6, 2021
2 parents e5e01aa + 94ffaf8 commit 80bf78a
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 16 deletions.
1 change: 0 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,6 @@ linters:
- exhaustive
- sqlclosecheck
- nolintlint
- gci
- goconst
- lll
disable:
Expand Down
4 changes: 2 additions & 2 deletions bscript/address.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func NewAddressFromPublicKeyHash(hash []byte, mainnet bool) (*Address, error) {
if !mainnet {
bb[0] = 111
}

// nolint:makezero // stop complaining
bb = append(bb, hash...)

return &Address{
Expand All @@ -99,7 +99,7 @@ func NewAddressFromPublicKey(pubKey *bsvec.PublicKey, mainnet bool) (*Address, e
if !mainnet {
bb[0] = 111
}

// nolint:makezero // stop complaining
bb = append(bb, hash...)

return &Address{
Expand Down
61 changes: 49 additions & 12 deletions tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,43 +233,81 @@ func (tx *Tx) ChangeToAddress(addr string, f []*Fee) error {
// Change calculates the amount of fees needed to cover the transaction
// and adds the left over change in a new output using the script provided.
func (tx *Tx) Change(s *bscript.Script, f []*Fee) error {
available, hasChange, err := tx.change(s, f, true)
if err != nil {
return err
}
if hasChange {
// add rest of available sats to the change output
tx.Outputs[len(tx.GetOutputs())-1].Satoshis = available
}
return nil
}

// ChangeToOutput will calculate fees and add them to an output at the index specified (0 based).
// If an invalid index is supplied and error is returned.
func (tx *Tx) ChangeToOutput(index uint, f []*Fee) error {
if int(index) > len(tx.Outputs)-1 {
return errors.New("index is greater than number of inputs in transaction")
}
available, hasChange, err := tx.change(tx.Outputs[index].LockingScript, f, false)
if err != nil {
return err
}
if hasChange {
tx.Outputs[index].Satoshis += available
}
return nil
}

// CalculateFee will return the amount of fees the current transaction will
// require.
func (tx *Tx) CalculateFee(f []*Fee) (uint64, error) {
total := tx.GetTotalInputSatoshis() - tx.GetTotalOutputSatoshis()
sats, _, err := tx.change(nil, f, false)
if err != nil {
return 0, err
}
return total - sats, nil
}

// change will return the amount of satoshis to add to an input after fees are removed.
// True will be returned if change has been added.
func (tx *Tx) change(s *bscript.Script, f []*Fee, newOutput bool) (uint64, bool, error) {
inputAmount := tx.GetTotalInputSatoshis()
outputAmount := tx.GetTotalOutputSatoshis()

if inputAmount < outputAmount {
return errors.New("satoshis inputted to the tx are less than the outputted satoshis")
return 0, false, errors.New("satoshis inputted to the tx are less than the outputted satoshis")
}

available := inputAmount - outputAmount

standardFees, err := GetStandardFee(f)
if err != nil {
return err
return 0, false, err
}

if !tx.canAddChange(available, standardFees) {
return nil
return 0, false, err
}
if newOutput {
tx.AddOutput(&Output{Satoshis: 0, LockingScript: s})
}

tx.AddOutput(&Output{Satoshis: 0, LockingScript: s})

var preSignedFeeRequired uint64
if preSignedFeeRequired, err = tx.getPreSignedFeeRequired(f); err != nil {
return err
return 0, false, err
}

var expectedUnlockingScriptFees uint64
if expectedUnlockingScriptFees, err = tx.getExpectedUnlockingScriptFees(f); err != nil {
return err
return 0, false, err
}

available -= preSignedFeeRequired + expectedUnlockingScriptFees

// add rest of available sats to the change output
tx.Outputs[len(tx.GetOutputs())-1].Satoshis = available

return nil
return available, true, nil
}

func (tx *Tx) canAddChange(available uint64, standardFees *Fee) bool {
Expand Down Expand Up @@ -305,7 +343,6 @@ func (tx *Tx) getPreSignedFeeRequired(f []*Fee) (uint64, error) {
}

fr += dataBytes * dataFee.MiningFee.Satoshis / dataFee.MiningFee.Bytes

return uint64(fr), nil
}

Expand Down
141 changes: 140 additions & 1 deletion tx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package bt_test

import (
"encoding/hex"
"errors"
"reflect"
"testing"

"github.com/bitcoinsv/bsvutil"
"github.com/stretchr/testify/assert"

"github.com/libsv/go-bt"
"github.com/libsv/go-bt/bscript"
"github.com/stretchr/testify/assert"
)

func TestNewTx(t *testing.T) {
Expand Down Expand Up @@ -779,3 +781,140 @@ func TestTx_SignAuto(t *testing.T) {
assert.Equal(t, rawTxBefore, tx.ToString())
})
}

func TestTx_ChangeToOutput(t *testing.T) {
tests := map[string]struct {
tx *bt.Tx
index uint
fees []*bt.Fee
expOutputTotal uint64
expChangeOutput uint64
err error
}{
"no change to add should return no change output": {
tx: func() *bt.Tx {
tx := bt.NewTx()
assert.NoError(t, tx.From(
"07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b",
0,
"76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac",
1000))
assert.NoError(t, tx.PayTo("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 1000))
return tx
}(),
index: 0,
fees: bt.DefaultFees(),
expOutputTotal: 1000,
expChangeOutput: 1000,
err: nil,
}, "change to add should add change to output": {
tx: func() *bt.Tx {
tx := bt.NewTx()
assert.NoError(t, tx.From(
"07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b",
0,
"76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac",
1000))
assert.NoError(t, tx.PayTo("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 500))
return tx
}(),
index: 0,
fees: bt.DefaultFees(),
expOutputTotal: 904,
expChangeOutput: 904,
err: nil,
}, "change to add should add change to specified output": {
tx: func() *bt.Tx {
tx := bt.NewTx()
assert.NoError(t, tx.From(
"07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b",
0,
"76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac",
2500))
assert.NoError(t, tx.PayTo("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 500))
assert.NoError(t, tx.PayTo("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 500))
assert.NoError(t, tx.PayTo("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 500))
assert.NoError(t, tx.PayTo("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 500))
return tx
}(),
index: 3,
fees: bt.DefaultFees(),
expOutputTotal: 2353,
expChangeOutput: 853,
err: nil,
}, "index out of range should return error": {
tx: func() *bt.Tx {
tx := bt.NewTx()
assert.NoError(t, tx.From(
"07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b",
0,
"76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac",
1000))
assert.NoError(t, tx.PayTo("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 500))
return tx
}(),
index: 1,
fees: bt.DefaultFees(),
err: errors.New("index is greater than number of inputs in transaction"),
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
err := test.tx.ChangeToOutput(test.index, test.fees)
if test.err != nil {
assert.Error(t, err)
assert.Equal(t, test.err, err)
return
}
assert.Equal(t, test.expOutputTotal, test.tx.GetTotalOutputSatoshis())
assert.Equal(t, test.expChangeOutput, test.tx.Outputs[test.index].Satoshis)
})
}
}

func TestTx_CalculateChange(t *testing.T) {
tests := map[string]struct {
tx *bt.Tx
fees []*bt.Fee
expFees uint64
err error
}{
"Transaction with one input one output should return 96": {
tx: func() *bt.Tx {
tx := bt.NewTx()
assert.NoError(t, tx.From(
"07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b",
0,
"76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac",
1000))
assert.NoError(t, tx.PayTo("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 500))
return tx
}(),
fees: bt.DefaultFees(),
expFees: 96,
}, "Transaction with one input 4 outputs should return 147": {
tx: func() *bt.Tx {
tx := bt.NewTx()
assert.NoError(t, tx.From(
"07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b",
0,
"76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac",
2500))
assert.NoError(t, tx.PayTo("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 500))
assert.NoError(t, tx.PayTo("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 500))
assert.NoError(t, tx.PayTo("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 500))
assert.NoError(t, tx.PayTo("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 500))
return tx
}(),
fees: bt.DefaultFees(),
expFees: 147,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
fee, err := test.tx.CalculateFee(test.fees)
assert.Equal(t, test.err, err)
assert.Equal(t, test.expFees, fee)
})
}
}

0 comments on commit 80bf78a

Please sign in to comment.