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

Feature: FromUTXOs #45

Merged
merged 24 commits into from
Sep 22, 2021
Merged
Show file tree
Hide file tree
Changes from 14 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
84 changes: 75 additions & 9 deletions txinput.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,25 @@ package bt

import (
"bytes"
"context"
"encoding/binary"
"errors"
"fmt"

"github.com/libsv/go-bk/crypto"

"github.com/libsv/go-bt/v2/bscript"
)

// ErrNoInput signals the InputGetterFunc has reached the end of its input.
var ErrNoInput = errors.New("no remaining inputs")

// InputGetterFunc is used for FromInputs. It expects *bt.Input to be returned containing
// relevant input information, and an err informing any retrieval errors.
//
// It is expected that bt.ErrNoInput will be returned once the input source is depleted.
type InputGetterFunc func(ctx context.Context) (*Input, error)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i see this went thought a few iterations haha but i still think it may be able to go for 1 more iteration to make it little clearer - let's discuss on monday 👍

Copy link
Member

@jadwahab jadwahab Sep 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i really like where this is going actually, but i have a much better idea of how i would like this to work now and it follows the reasoning that we were discussing about potentially making inputs and outputs private that are used in a tx - since (unless someone can convince me otherwise) we will never have an input or output that isn't part of a tx and it just creates confusion, as we've seen previously, when separating them.

1st: we should probably return an array here instead of just one (input) since many times you will have an array of utxos to choose from and then we can add functionality to choose different utxos and stuff. plus of the utxogetter involved i/o then we'd want to minimise that and create a bulk call or something like that which we've done many times before.

2nd: it doesn't really feel accurate to say input getter since we're not getting the inputs, we're getting the utxos and creating the input... i think it would be much better to change this to UTXOGetterFunc and have it return an array of utxos and then we internally create the input from the utxo(s) and add them to the tx.

i'm still unsure whether we should still make the inputs/outputs private (ideally we could just leave them public to retain flexibility for users that know what they're doing but to remove all functions that imply separation between inputs/outputs and txs - for example the NewInput/Ouput funcs)...

thoughts?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • add satoshis as a param and return array instead of only 1 element
  • create new data type: UTXO with the following 4 fields: prevTxID string, vout uint32, prevTxLockingScript string, satoshis uint64
  • refactor tx.From() to take UTXO instead of 4 params
  • rename InputGetterFunc to UTXOGetterFunc and return array of UTXOs


// NewInputFromBytes returns a transaction input from the bytes provided.
func NewInputFromBytes(bytes []byte) (*Input, int, error) {
if len(bytes) < 36 {
Expand All @@ -34,6 +45,28 @@ func NewInputFromBytes(bytes []byte) (*Input, int, error) {
}, totalLength, nil
}

// NewInputFrom builds and returns a new input from the specified UTXO fields, using the default
// finalised sequence number (0xFFFFFFFF). If you want a different nSeq, change it manually
// afterwards.
func NewInputFrom(prevTxID string, vout uint32, prevTxLockingScript string, satoshis uint64) (*Input, error) {
Copy link
Member

@jadwahab jadwahab Sep 18, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't these functions go into the inputs.go file instead of here? i think what we were going for was to try an minimise the number of ways users could do things. so in v1 the user had to create the input himself and then add that input to the tx, whereas with v2 the point is to use the .From() method to create the input as part of the tx straight away since having an input alone isn't much use is it? maybe we can keep this func but just put it in inputs.go and make in private? what do you guys think? @theflyingcodr @Tigh-Gherr ? that was the reasoning behind making tx.addInput private as well especially since it confused a couple devs into using it the wrong way in the past...

pts, err := bscript.NewFromHexString(prevTxLockingScript)
if err != nil {
return nil, err
}

i := &Input{
PreviousTxOutIndex: vout,
PreviousTxSatoshis: satoshis,
PreviousTxScript: pts,
SequenceNumber: DefaultSequenceNumber, // use default finalised sequence number
}
if err := i.PreviousTxIDAddStr(prevTxID); err != nil {
return nil, err
}

return i, nil
}

// TotalInputSatoshis returns the total Satoshis inputted to the transaction.
func (tx *Tx) TotalInputSatoshis() (total uint64) {
for _, in := range tx.Inputs {
Expand Down Expand Up @@ -69,21 +102,54 @@ func (tx *Tx) AddP2PKHInputsFromTx(pvsTx *Tx, matchPK []byte) error {
// finalised sequence number (0xFFFFFFFF). If you want a different nSeq, change it manually
// afterwards.
func (tx *Tx) From(prevTxID string, vout uint32, prevTxLockingScript string, satoshis uint64) error {
pts, err := bscript.NewFromHexString(prevTxLockingScript)
i, err := NewInputFrom(prevTxID, vout, prevTxLockingScript, satoshis)
if err != nil {
return err
}

i := &Input{
PreviousTxOutIndex: vout,
PreviousTxSatoshis: satoshis,
PreviousTxScript: pts,
SequenceNumber: DefaultSequenceNumber, // use default finalised sequence number
tx.addInput(i)
return nil
}

// FromInputs continuously calls the provided bt.InputGetterFunc, adding each returned input
// as an input via tx.From(...), until it is estimated that inputs cover the outputs + fees.
//
// After completion, the receiver is ready for `Change(...)` to be called, and then be signed.
// Note, this function works under the assumption that receiver *bt.Tx alread has all the outputs
// which need covered.
//
// Example usage, for when working with a list:
// tx.FromInputs(ctx, bt.NewFeeQuote(), func() bt.InputGetterFunc {
// i := 0
// return func(ctx context.Context) (*bt.Input, error) {
// if i >= len(utxos) {
// return nil, bt.ErrNoInput
// }
// defer func() { i++ }()
// return bt.NewInputFrom(utxos[i].TxID, utxo[i].Vout, utxos[i].Script, utxos[i].Satoshis), true
// }
// }())
func (tx *Tx) FromInputs(ctx context.Context, fq *FeeQuote, next InputGetterFunc) (err error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ooh this is good shit, i like it 👍

var feesPaid bool
for !feesPaid {
input, err := next(ctx)
if err != nil {
if errors.Is(err, ErrNoInput) {
break
}

return err
}
tx.addInput(input)

feesPaid, err = tx.EstimateIsFeePaidEnough(fq)
if err != nil {
return err
}
}
if err := i.PreviousTxIDAddStr(prevTxID); err != nil {
return err
if !feesPaid {
return errors.New("insufficient inputs provided")
}
tx.addInput(i)

return nil
}
Expand Down
150 changes: 150 additions & 0 deletions txinput_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package bt_test

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

"github.com/libsv/go-bt/v2"
Expand Down Expand Up @@ -79,3 +81,151 @@ func TestTx_From(t *testing.T) {
assert.Equal(t, "76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac", inputs[0].PreviousTxScript.String())
})
}

func TestTx_AddInputs(t *testing.T) {
tests := map[string]struct {
tx *bt.Tx
inputs []*bt.Input
inputGetterFunc bt.InputGetterFunc
expTotalInputs int
expErr error
}{
"tx with exact inputs and surplus inputs is covered": {
tx: func() *bt.Tx {
tx := bt.NewTx()
assert.NoError(t, tx.AddP2PKHOutputFromAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", 1500))
return tx
}(),
inputs: func() []*bt.Input {
tx := bt.NewTx()
assert.NoError(t, tx.From("07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b", 0, "76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac", 1000))
assert.NoError(t, tx.From("07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b", 0, "76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac", 1000))

return tx.Inputs
}(),
expTotalInputs: 2,
},
"tx with extra inputs and surplus inputs is covered with minimum needed inputs": {
tx: func() *bt.Tx {
tx := bt.NewTx()
assert.NoError(t, tx.AddP2PKHOutputFromAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", 1500))
return tx
}(),
inputs: func() []*bt.Input {
tx := bt.NewTx()
assert.NoError(t, tx.From("07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b", 0, "76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac", 1000))
assert.NoError(t, tx.From("07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b", 0, "76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac", 1000))
assert.NoError(t, tx.From("07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b", 0, "76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac", 1000))

return tx.Inputs
}(),
expTotalInputs: 2,
},
"tx with exact input satshis is covered": {
tx: func() *bt.Tx {
tx := bt.NewTx()
assert.NoError(t, tx.AddP2PKHOutputFromAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", 1500))
return tx
}(),
inputs: func() []*bt.Input {
tx := bt.NewTx()
assert.NoError(t, tx.From("07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b", 0, "76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac", 1000))
assert.NoError(t, tx.From("07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b", 0, "76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac", 670))

return tx.Inputs
}(),
expTotalInputs: 2,
},
"tx with large amount of satoshis is covered": {
tx: func() *bt.Tx {
tx := bt.NewTx()
assert.NoError(t, tx.AddP2PKHOutputFromAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", 5000))
return tx
}(),
inputs: func() []*bt.Input {
tx := bt.NewTx()
assert.NoError(t, tx.From("07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b", 0, "76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac", 500))
assert.NoError(t, tx.From("07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b", 0, "76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac", 670))
assert.NoError(t, tx.From("07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b", 0, "76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac", 700))
assert.NoError(t, tx.From("07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b", 0, "76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac", 1000))
assert.NoError(t, tx.From("07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b", 0, "76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac", 1000))
assert.NoError(t, tx.From("07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b", 0, "76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac", 1000))
assert.NoError(t, tx.From("07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b", 0, "76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac", 1000))
assert.NoError(t, tx.From("07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b", 0, "76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac", 650))

return tx.Inputs
}(),
expTotalInputs: 7,
},
"getter with no inputs error": {
tx: func() *bt.Tx {
tx := bt.NewTx()
assert.NoError(t, tx.AddP2PKHOutputFromAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", 1500))
return tx
}(),
inputs: []*bt.Input{},
expErr: errors.New("insufficient inputs provided"),
},
"getter with insufficient inputs errors": {
tx: func() *bt.Tx {
tx := bt.NewTx()
assert.NoError(t, tx.AddP2PKHOutputFromAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", 25400))
return tx
}(),
inputs: func() []*bt.Input {
tx := bt.NewTx()
assert.NoError(t, tx.From("07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b", 0, "76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac", 500))
assert.NoError(t, tx.From("07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b", 0, "76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac", 670))
assert.NoError(t, tx.From("07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b", 0, "76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac", 700))
assert.NoError(t, tx.From("07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b", 0, "76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac", 1000))
assert.NoError(t, tx.From("07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b", 0, "76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac", 1000))
assert.NoError(t, tx.From("07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b", 0, "76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac", 1000))
assert.NoError(t, tx.From("07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b", 0, "76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac", 1000))
assert.NoError(t, tx.From("07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b", 0, "76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac", 650))

return tx.Inputs
}(),
expErr: errors.New("insufficient inputs provided"),
},
"error is returned to the user": {
tx: func() *bt.Tx {
tx := bt.NewTx()
assert.NoError(t, tx.AddP2PKHOutputFromAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", 100))
return tx
}(),
inputGetterFunc: func(context.Context) (*bt.Input, error) {
return nil, errors.New("custom error")
},
inputs: []*bt.Input{},
expErr: errors.New("custom error"),
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
iptFn := func() bt.InputGetterFunc {
idx := 0
return func(ctx context.Context) (*bt.Input, error) {
if idx == len(test.inputs) {
return nil, bt.ErrNoInput
}
defer func() { idx++ }()
return test.inputs[idx], nil
}
}()
if test.inputGetterFunc != nil {
iptFn = test.inputGetterFunc
}

err := test.tx.FromInputs(context.Background(), bt.NewFeeQuote(), iptFn)
if test.expErr != nil {
assert.Error(t, err)
assert.EqualError(t, err, test.expErr.Error())
return
}

assert.NoError(t, err)
assert.Equal(t, test.expTotalInputs, test.tx.InputCount())
})
}
}