diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 5665465a..90207913 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -17,7 +17,7 @@ jobs: test: strategy: matrix: - go-version: [ 1.14.x, 1.15.x ] + go-version: [ 1.15.x,1.16.x, ] os: [ ubuntu-latest ] runs-on: ${{ matrix.os }} steps: diff --git a/bscript/addressvalidation.go b/bscript/addressvalidation.go index fe3e085f..7ad28118 100644 --- a/bscript/addressvalidation.go +++ b/bscript/addressvalidation.go @@ -53,9 +53,7 @@ func (a *a25) set58(s []byte) error { // Checks both mainnet and testnet. func ValidateAddress(address string) (bool, error) { if strings.HasPrefix(address, "bitcoin-script:") { - _, _, _, _, err := DecodeBIP276(address) - - if err != nil { + if _, err := DecodeBIP276(address); err != nil { return false, fmt.Errorf("bitcoin-script invalid [%w]", err) } return true, nil diff --git a/bscript/bip276.go b/bscript/bip276.go index 9a75726b..5e773ab5 100644 --- a/bscript/bip276.go +++ b/bscript/bip276.go @@ -10,6 +10,15 @@ import ( "github.com/libsv/go-bk/crypto" ) +// BIP276 proposes a scheme for encoding typed bitcoin related data in a user friendly way +// see https://github.com/moneybutton/bips/blob/master/bip-0276.mediawiki +type BIP276 struct { + Prefix string + Version int + Network int + Data []byte +} + // PrefixScript is the prefix in the BIP276 standard which // specifies if it is a script or template. const PrefixScript = "bitcoin-script" @@ -35,52 +44,54 @@ var validBIP276 = regexp.MustCompile(`^(.+?):(\d{2})(\d{2})([0-9A-Fa-f]+)([0-9A- // EncodeBIP276 is used to encode specific (non-standard) scripts in BIP276 format. // See https://github.com/moneybutton/bips/blob/master/bip-0276.mediawiki -func EncodeBIP276(prefix string, network, version int, data []byte) string { - if version == 0 || version > 255 || network == 0 || network > 255 { +func EncodeBIP276(script BIP276) string { + if script.Version == 0 || script.Version > 255 || script.Network == 0 || script.Network > 255 { return "ERROR" } - p, c := createBIP276(prefix, network, version, data) + p, c := createBIP276(script) return p + c } -func createBIP276(prefix string, network, version int, data []byte) (string, string) { - payload := fmt.Sprintf("%s:%.2x%.2x%x", prefix, network, version, data) +func createBIP276(script BIP276) (string, string) { + payload := fmt.Sprintf("%s:%.2x%.2x%x", script.Prefix, script.Network, script.Version, script.Data) return payload, hex.EncodeToString(crypto.Sha256d([]byte(payload))[:4]) } // DecodeBIP276 is used to decode BIP276 formatted data into specific (non-standard) scripts. // See https://github.com/moneybutton/bips/blob/master/bip-0276.mediawiki -func DecodeBIP276(text string) (prefix string, version, network int, data []byte, err error) { +func DecodeBIP276(text string) (*BIP276, error) { // Determine if regex match res := validBIP276.FindStringSubmatch(text) // Check if we got a result from the regex match first if len(res) == 0 { - err = fmt.Errorf("text did not match the BIP276 format") - return + return nil, fmt.Errorf("text did not match the BIP276 format") } - - // Set the prefix - prefix = res[1] - - if version, err = strconv.Atoi(res[2]); err != nil { - return + s := BIP276{ + Prefix: res[1], } - if network, err = strconv.Atoi(res[3]); err != nil { - return + version, err := strconv.Atoi(res[2]) + if err != nil { + return nil, err } - - if data, err = hex.DecodeString(res[4]); err != nil { - return + s.Version = version + network, err := strconv.Atoi(res[3]) + if err != nil { + return nil, err } - - if _, checkSum := createBIP276(prefix, network, version, data); res[5] != checkSum { - err = errors.New("invalid checksum") + s.Network = network + data, err := hex.DecodeString(res[4]) + if err != nil { + return nil, err + } + s.Data = data + if _, checkSum := createBIP276(s); res[5] != checkSum { + return nil, errors.New("invalid checksum") } - return + return &s, nil } diff --git a/bscript/bip276_test.go b/bscript/bip276_test.go index 3cd137b4..bf7e0a5e 100644 --- a/bscript/bip276_test.go +++ b/bscript/bip276_test.go @@ -13,10 +13,12 @@ func TestEncodeBIP276(t *testing.T) { t.Run("valid encode (mainnet)", func(t *testing.T) { s := bscript.EncodeBIP276( - bscript.PrefixScript, - bscript.NetworkMainnet, - bscript.CurrentVersion, - []byte("fake script"), + bscript.BIP276{ + Prefix: bscript.PrefixScript, + Version: bscript.CurrentVersion, + Network: bscript.NetworkMainnet, + Data: []byte("fake script"), + }, ) assert.Equal(t, "bitcoin-script:010166616b65207363726970746f0cd86a", s) @@ -24,10 +26,12 @@ func TestEncodeBIP276(t *testing.T) { t.Run("valid encode (testnet)", func(t *testing.T) { s := bscript.EncodeBIP276( - bscript.PrefixScript, - bscript.NetworkTestnet, - bscript.CurrentVersion, - []byte("fake script"), + bscript.BIP276{ + Prefix: bscript.PrefixScript, + Version: bscript.CurrentVersion, + Network: bscript.NetworkTestnet, + Data: []byte("fake script"), + }, ) assert.Equal(t, "bitcoin-script:020166616b65207363726970742577a444", s) @@ -35,10 +39,12 @@ func TestEncodeBIP276(t *testing.T) { t.Run("invalid version = 0", func(t *testing.T) { s := bscript.EncodeBIP276( - bscript.PrefixScript, - bscript.NetworkMainnet, - 0, - []byte("fake script"), + bscript.BIP276{ + Prefix: bscript.PrefixScript, + Version: 0, + Network: bscript.NetworkMainnet, + Data: []byte("fake script"), + }, ) assert.Equal(t, "ERROR", s) @@ -46,10 +52,12 @@ func TestEncodeBIP276(t *testing.T) { t.Run("invalid version > 255", func(t *testing.T) { s := bscript.EncodeBIP276( - bscript.PrefixScript, - bscript.NetworkMainnet, - 256, - []byte("fake script"), + bscript.BIP276{ + Prefix: bscript.PrefixScript, + Version: 256, + Network: bscript.NetworkMainnet, + Data: []byte("fake script"), + }, ) assert.Equal(t, "ERROR", s) @@ -57,21 +65,12 @@ func TestEncodeBIP276(t *testing.T) { t.Run("invalid network = 0", func(t *testing.T) { s := bscript.EncodeBIP276( - bscript.PrefixScript, - 0, - bscript.CurrentVersion, - []byte("fake script"), - ) - - assert.Equal(t, "ERROR", s) - }) - - t.Run("invalid version > 255", func(t *testing.T) { - s := bscript.EncodeBIP276( - bscript.PrefixScript, - 256, - bscript.CurrentVersion, - []byte("fake script"), + bscript.BIP276{ + Prefix: bscript.PrefixScript, + Version: bscript.CurrentVersion, + Network: 0, + Data: []byte("fake script"), + }, ) assert.Equal(t, "ERROR", s) @@ -79,10 +78,12 @@ func TestEncodeBIP276(t *testing.T) { t.Run("different prefix", func(t *testing.T) { s := bscript.EncodeBIP276( - "different-prefix", - bscript.NetworkMainnet, - bscript.CurrentVersion, - []byte("fake script"), + bscript.BIP276{ + Prefix: "different-prefix", + Version: bscript.CurrentVersion, + Network: bscript.NetworkMainnet, + Data: []byte("fake script"), + }, ) assert.Equal(t, "different-prefix:010166616b6520736372697074effdb090", s) @@ -90,10 +91,12 @@ func TestEncodeBIP276(t *testing.T) { t.Run("template prefix", func(t *testing.T) { s := bscript.EncodeBIP276( - bscript.PrefixTemplate, - bscript.NetworkMainnet, - bscript.CurrentVersion, - []byte("fake script"), + bscript.BIP276{ + Prefix: bscript.PrefixTemplate, + Version: bscript.CurrentVersion, + Network: bscript.NetworkMainnet, + Data: []byte("fake script"), + }, ) assert.Equal(t, "bitcoin-template:010166616b65207363726970749e31aa72", s) @@ -104,25 +107,23 @@ func TestDecodeBIP276(t *testing.T) { t.Parallel() t.Run("valid decode", func(t *testing.T) { - prefix, network, version, data, err := bscript.DecodeBIP276("bitcoin-script:010166616b65207363726970746f0cd86a") + script, err := bscript.DecodeBIP276("bitcoin-script:010166616b65207363726970746f0cd86a") assert.NoError(t, err) - assert.Equal(t, `"bitcoin-script"`, fmt.Sprintf("%q", prefix)) - assert.Equal(t, 1, network) - assert.Equal(t, 1, version) - assert.Equal(t, "fake script", fmt.Sprintf("%s", data)) + assert.Equal(t, `"bitcoin-script"`, fmt.Sprintf("%q", script.Prefix)) + assert.Equal(t, 1, script.Network) + assert.Equal(t, 1, script.Version) + assert.Equal(t, "fake script", string(script.Data)) }) t.Run("invalid decode", func(t *testing.T) { - _, _, _, _, err := bscript.DecodeBIP276("bitcoin-script:01") + script, err := bscript.DecodeBIP276("bitcoin-script:01") assert.Error(t, err) + assert.Nil(t, script) }) t.Run("valid format, bad checksum", func(t *testing.T) { - prefix, network, version, data, err := bscript.DecodeBIP276("bitcoin-script:010166616b65207363726970746f0cd8") + script, err := bscript.DecodeBIP276("bitcoin-script:010166616b65207363726970746f0cd8") assert.Error(t, err) - assert.Equal(t, `"bitcoin-script"`, fmt.Sprintf("%q", prefix)) - assert.Equal(t, 1, network) - assert.Equal(t, 1, version) - assert.Equal(t, "fake scrip", fmt.Sprintf("%s", data)) + assert.Nil(t, script) }) } diff --git a/bscript/script.go b/bscript/script.go index 69e3fe6a..f8174dc5 100644 --- a/bscript/script.go +++ b/bscript/script.go @@ -47,11 +47,9 @@ func NewFromBytes(b []byte) *Script { // NewFromASM creates a new script from a BitCoin ASM formatted string. func NewFromASM(str string) (*Script, error) { - sections := strings.Split(str, " ") + s := Script{} - s := &Script{} - - for _, section := range sections { + for _, section := range strings.Split(str, " ") { if val, ok := opCodeStrings[section]; ok { s.AppendOpCode(val) } else { @@ -61,14 +59,13 @@ func NewFromASM(str string) (*Script, error) { } } - return s, nil + return &s, nil } // NewP2PKHFromPubKeyEC takes a public key hex string (in // compressed format) and creates a P2PKH script from it. func NewP2PKHFromPubKeyEC(pubKey *bec.PublicKey) (*Script, error) { - pubKeyBytes := pubKey.SerialiseCompressed() - return NewP2PKHFromPubKeyBytes(pubKeyBytes) + return NewP2PKHFromPubKeyBytes(pubKey.SerialiseCompressed()) } // NewP2PKHFromPubKeyStr takes a public key hex string (in @@ -200,8 +197,8 @@ func (s *Script) AppendOpCode(o uint8) *Script { return s } -// ToString returns hex string of script. -func (s *Script) String() string { // TODO: change to HexString? +// String implements the stringer interface and returns the hex string of script. +func (s *Script) String() string { return hex.EncodeToString(*s) } diff --git a/bscript/unlockingscript.go b/bscript/unlockingscript.go index f5c61e6a..0a3c0db4 100644 --- a/bscript/unlockingscript.go +++ b/bscript/unlockingscript.go @@ -5,11 +5,10 @@ import "github.com/libsv/go-bt/sighash" // NewP2PKHUnlockingScript creates a new unlocking script which spends // a P2PKH locking script from a public key, a signature, and // a SIGHASH flag. -func NewP2PKHUnlockingScript(pubKey []byte, sig []byte, - sigHashFlag sighash.Flag) (*Script, error) { +func NewP2PKHUnlockingScript(pubKey []byte, sig []byte, sigHashFlag sighash.Flag) (*Script, error) { // append SIGHASH to DER sig - sigBuf := []byte{} + sigBuf := make([]byte, 0) sigBuf = append(sigBuf, sig...) sigBuf = append(sigBuf, uint8(sigHashFlag)) diff --git a/examples/create_tx/create_tx.go b/examples/create_tx/create_tx.go index a7df293a..4c7303f3 100644 --- a/examples/create_tx/create_tx.go +++ b/examples/create_tx/create_tx.go @@ -17,7 +17,7 @@ func main() { "76a914eb0bd5edba389198e73f8efabddfc61666969ff788ac6a0568656c6c6f", 1500) - _ = tx.PayTo("1NRoySJ9Lvby6DuE2UQYnyT67AASwNZxGb", 1000) + _ = tx.PayToAddress("1NRoySJ9Lvby6DuE2UQYnyT67AASwNZxGb", 1000) wif, _ := wif.DecodeWIF("KznvCNc6Yf4iztSThoMH6oHWzH9EgjfodKxmeuUGPq5DEX5maspS") @@ -25,5 +25,5 @@ func main() { if err != nil && len(inputsSigned) > 0 { log.Fatal(err.Error()) } - log.Println("tx: ", tx.String()) + log.Printf("tx: %s\n", tx) } diff --git a/examples/create_tx_with_opreturn/create_tx_with_opreturn.go b/examples/create_tx_with_opreturn/create_tx_with_opreturn.go index 14f33bce..0aecb9c4 100644 --- a/examples/create_tx_with_opreturn/create_tx_with_opreturn.go +++ b/examples/create_tx_with_opreturn/create_tx_with_opreturn.go @@ -17,7 +17,7 @@ func main() { "76a9149cbe9f5e72fa286ac8a38052d1d5337aa363ea7f88ac", 1000) - _ = tx.PayTo("1C8bzHM8XFBHZ2ZZVvFy2NSoAZbwCXAicL", 900) + _ = tx.PayToAddress("1C8bzHM8XFBHZ2ZZVvFy2NSoAZbwCXAicL", 900) _ = tx.AddOpReturnOutput([]byte("You are using go-bt!")) diff --git a/input.go b/input.go index 27c06e3e..d938ff8a 100644 --- a/input.go +++ b/input.go @@ -28,7 +28,7 @@ const DefaultSequenceNumber uint32 = 0xFFFFFFFF // DO NOT CHANGE ORDER - Optimised for memory via maligned // type Input struct { - PreviousTxIDBytes []byte + previousTxID []byte PreviousTxSatoshis uint64 PreviousTxScript *bscript.Script UnlockingScript *bscript.Script @@ -61,7 +61,7 @@ func (i *Input) MarshalJSON() ([]byte, error) { return nil, err } input := &inputJSON{ - TxID: hex.EncodeToString(i.PreviousTxIDBytes), + TxID: hex.EncodeToString(i.previousTxID), Vout: i.PreviousTxOutIndex, UnlockingScript: &struct { Asm string `json:"asm"` @@ -94,17 +94,44 @@ func (i *Input) UnmarshalJSON(b []byte) error { return err } i.UnlockingScript = s - i.PreviousTxIDBytes = ptxID + i.previousTxID = ptxID i.PreviousTxOutIndex = ij.Vout i.SequenceNumber = ij.Sequence return nil } +// PreviousTxIDAdd will add the supplied txID bytes to the Input, +// if it isn't a valid transaction id an ErrInvalidTxID error will be returned. +func (i *Input) PreviousTxIDAdd(txID []byte) error { + if !IsValidTxID(txID) { + return ErrInvalidTxID + } + i.previousTxID = txID + return nil +} + +// PreviousTxIDAddStr will validate and add the supplied txID string to the Input, +// if it isn't a valid transaction id an ErrInvalidTxID error will be returned. +func (i *Input) PreviousTxIDAddStr(txID string) error { + bb, err := hex.DecodeString(txID) + if err != nil { + return err + } + return i.PreviousTxIDAdd(bb) +} + +// PreviousTxID will return the PreviousTxID if set. +func (i *Input) PreviousTxID() []byte { + return i.previousTxID +} + // PreviousTxIDStr returns the Previous TxID as a hex string. func (i *Input) PreviousTxIDStr() string { - return hex.EncodeToString(i.PreviousTxIDBytes) + return hex.EncodeToString(i.previousTxID) } +// String implements the Stringer interface and returns a string +// representation of a transaction input. func (i *Input) String() string { return fmt.Sprintf( `prevTxHash: %s @@ -113,7 +140,7 @@ scriptLen: %d script: %s sequence: %x `, - hex.EncodeToString(i.PreviousTxIDBytes), + hex.EncodeToString(i.previousTxID), i.PreviousTxOutIndex, len(*i.UnlockingScript), i.UnlockingScript, @@ -121,11 +148,11 @@ sequence: %x ) } -// ToBytes encodes the Input into a hex byte array. -func (i *Input) ToBytes(clear bool) []byte { +// Bytes encodes the Input into a hex byte array. +func (i *Input) Bytes(clear bool) []byte { h := make([]byte, 0) - h = append(h, ReverseBytes(i.PreviousTxIDBytes)...) + h = append(h, ReverseBytes(i.previousTxID)...) h = append(h, LittleEndianBytes(i.PreviousTxOutIndex, 4)...) if clear { h = append(h, 0x00) diff --git a/localsigner.go b/localsigner.go index 4e737bd5..65f314d0 100644 --- a/localsigner.go +++ b/localsigner.go @@ -7,8 +7,8 @@ import ( "github.com/libsv/go-bt/sighash" ) -// LocalSigner implements the Signer interface. It is used to sign a Tx locally -// using a bec PrivateKey. +// LocalSigner implements the Signer interface. It is used to sign Tx Inputs locally +// using a bkec PrivateKey. type LocalSigner struct { PrivateKey *bec.PrivateKey } @@ -32,8 +32,7 @@ func (is *LocalSigner) Sign(ctx context.Context, unsignedTx *Tx, index uint32, // SignHash a transaction at a given a hash digest using the PrivateKey passed in through the // InternalSigner struct. -func (is *LocalSigner) SignHash(ctx context.Context, hash []byte) (publicKey []byte, signature []byte, err error) { - +func (is *LocalSigner) SignHash(ctx context.Context, hash []byte) (publicKey, signature []byte, err error) { sig, err := is.PrivateKey.Sign(hash) if err != nil { return diff --git a/localsigner_test.go b/localsigner_test.go index 9a055b69..f677fa9b 100644 --- a/localsigner_test.go +++ b/localsigner_test.go @@ -5,9 +5,9 @@ import ( "encoding/hex" "testing" + . "github.com/libsv/go-bk/wif" "github.com/libsv/go-bt" "github.com/libsv/go-bt/bscript" - . "github.com/libsv/go-bk/wif" "github.com/stretchr/testify/assert" ) @@ -20,8 +20,8 @@ func TestInternalSigner_SignAuto(t *testing.T) { assert.NotNil(t, tx) // Add the UTXO amount and script. - tx.Inputs[0].PreviousTxSatoshis = 100000000 - tx.Inputs[0].PreviousTxScript, err = bscript.NewFromHexString("76a914c0a3c167a28cabb9fbb495affa0761e6e74ac60d88ac") + tx.InputIdx(0).PreviousTxSatoshis = 100000000 + tx.InputIdx(0).PreviousTxScript, err = bscript.NewFromHexString("76a914c0a3c167a28cabb9fbb495affa0761e6e74ac60d88ac") assert.NoError(t, err) // Our private key @@ -30,11 +30,11 @@ func TestInternalSigner_SignAuto(t *testing.T) { assert.NoError(t, err) signer := bt.LocalSigner{PrivateKey: wif.PrivKey} - _, err = tx.SignAuto(context.Background(),&signer) + _, err = tx.SignAuto(context.Background(), &signer) assert.NoError(t, err) expectedSignedTx := "010000000193a35408b6068499e0d5abd799d3e827d9bfe70c9b75ebe209c91d2507232651000000006b483045022100c1d77036dc6cd1f3fa1214b0688391ab7f7a16cd31ea4e5a1f7a415ef167df820220751aced6d24649fa235132f1e6969e163b9400f80043a72879237dab4a1190ad412103b8b40a84123121d260f5c109bc5a46ec819c2e4002e5ba08638783bfb4e01435ffffffff02404b4c00000000001976a91404ff367be719efa79d76e4416ffb072cd53b208888acde94a905000000001976a91404d03f746652cfcb6cb55119ab473a045137d26588ac00000000" - assert.Equal(t, expectedSignedTx, hex.EncodeToString(tx.ToBytes())) + assert.Equal(t, expectedSignedTx, hex.EncodeToString(tx.Bytes())) // TODO: what is this for? // if unsignedTx == expectedSignedTx { diff --git a/output.go b/output.go index 2c4d6262..487d5013 100644 --- a/output.go +++ b/output.go @@ -113,8 +113,8 @@ script: %s `, o.Satoshis, len(*o.LockingScript), o.LockingScript) } -// ToBytes encodes the Output into a byte array. -func (o *Output) ToBytes() []byte { +// Bytes encodes the Output into a byte array. +func (o *Output) Bytes() []byte { b := make([]byte, 8) binary.LittleEndian.PutUint64(b, o.Satoshis) diff --git a/signaturehash.go b/signaturehash.go index 687f08b1..52ef8d44 100644 --- a/signaturehash.go +++ b/signaturehash.go @@ -28,12 +28,12 @@ func (tx *Tx) CalcInputSignatureHash(inputNumber uint32, sigHashFlag sighash.Fla // see https://github.com/bitcoin-sv/bitcoin-sv/blob/master/doc/abc/replay-protected-sighash.md#digest-algorithm func (tx *Tx) CalcInputPreimage(inputNumber uint32, sigHashFlag sighash.Flag) ([]byte, error) { - if tx.Inputs[inputNumber] == nil { + if tx.InputIdx(int(inputNumber)) == nil { return nil, errors.New("specified input does not exist") } - in := tx.Inputs[inputNumber] + in := tx.InputIdx(int(inputNumber)) - if len(in.PreviousTxIDBytes) == 0 { + if len(in.PreviousTxID()) == 0 { return nil, errors.New("'PreviousTxID' not supplied") } if in.PreviousTxScript == nil { @@ -59,7 +59,7 @@ func (tx *Tx) CalcInputPreimage(inputNumber uint32, sigHashFlag sighash.Flag) ([ if (sigHashFlag&31) != sighash.Single && (sigHashFlag&31) != sighash.None { // This will be executed in the usual BSV case (where sigHashType = SighashAllForkID) hashOutputs = tx.getOutputsHash(-1) - } else if (sigHashFlag&31) == sighash.Single && inputNumber < uint32(len(tx.Outputs)) { + } else if (sigHashFlag&31) == sighash.Single && inputNumber < uint32(tx.OutputCount()) { // This will *not* be executed in the usual BSV case (where sigHashType = SighashAllForkID) hashOutputs = tx.getOutputsHash(int32(inputNumber)) } @@ -76,7 +76,7 @@ func (tx *Tx) CalcInputPreimage(inputNumber uint32, sigHashFlag sighash.Flag) ([ buf = append(buf, hashSequence...) // outpoint (32-byte hash + 4-byte little endian) - buf = append(buf, ReverseBytes(in.PreviousTxIDBytes)...) + buf = append(buf, ReverseBytes(in.PreviousTxID())...) oi := make([]byte, 4) binary.LittleEndian.PutUint32(oi, in.PreviousTxOutIndex) buf = append(buf, oi...) @@ -116,7 +116,7 @@ func (tx *Tx) getPreviousOutHash() []byte { buf := make([]byte, 0) for _, in := range tx.Inputs { - buf = append(buf, ReverseBytes(in.PreviousTxIDBytes)...) + buf = append(buf, ReverseBytes(in.PreviousTxID())...) oi := make([]byte, 4) binary.LittleEndian.PutUint32(oi, in.PreviousTxOutIndex) buf = append(buf, oi...) diff --git a/signaturehash_test.go b/signaturehash_test.go index aceec0e8..f3d1bbdf 100644 --- a/signaturehash_test.go +++ b/signaturehash_test.go @@ -16,7 +16,7 @@ func TestTx_CalcInputPreimage(t *testing.T) { var testVector = []struct { name string unsignedTx string - index uint32 + index int previousTxSatoshis uint64 previousTxScript string sigHashType sighash.Flag @@ -61,12 +61,12 @@ func TestTx_CalcInputPreimage(t *testing.T) { assert.NotNil(t, tx) // Add the UTXO amount and script (PreviousTxScript already in unsiged tx) - tx.Inputs[test.index].PreviousTxSatoshis = test.previousTxSatoshis - tx.Inputs[test.index].PreviousTxScript, err = bscript.NewFromHexString(test.previousTxScript) + tx.InputIdx(test.index).PreviousTxSatoshis = test.previousTxSatoshis + tx.InputIdx(test.index).PreviousTxScript, err = bscript.NewFromHexString(test.previousTxScript) assert.NoError(t, err) var actualSigHash []byte - actualSigHash, err = tx.CalcInputPreimage(test.index, sighash.All|sighash.ForkID) + actualSigHash, err = tx.CalcInputPreimage(uint32(test.index), sighash.All|sighash.ForkID) assert.NoError(t, err) assert.Equal(t, test.expectedPreimage, hex.EncodeToString(actualSigHash)) }) diff --git a/signer.go b/signer.go index 6b433bd5..d8942070 100644 --- a/signer.go +++ b/signer.go @@ -25,8 +25,8 @@ type Signer interface { // canonical in accordance with RFC6979 and BIP0062. // // To automatically sign, the PublicKey() method must also be implemented in order to -// use the public key to check which inputs can be signed for before signing. +// use the public key to check which Inputs can be signed for before signing. type AutoSigner interface { Signer - PublicKey(ctx context.Context ) (publicKey []byte) + PublicKey(ctx context.Context) (publicKey []byte) } diff --git a/tx.go b/tx.go index 70e86d88..50a39aae 100644 --- a/tx.go +++ b/tx.go @@ -20,12 +20,12 @@ Version no currently 1 In-counter positive integer VI = VarInt 1 - 9 bytes -list of inputs the first input of the first transaction is also called "coinbase" -many inputs +list of Inputs the first input of the first transaction is also called "coinbase" -many Inputs (its content was ignored in earlier versions) Out-counter positive integer VI = VarInt 1 - 9 bytes -list of outputs the outputs of the first transaction spend the mined -many outputs +list of Outputs the Outputs of the first transaction spend the mined -many Outputs bitcoins for the block lock_time if non-zero and sequence numbers are < 0xFFFFFFFF: block height or 4 bytes @@ -33,15 +33,20 @@ lock_time if non-zero and sequence numbers are < 0xFFFFFFFF: block height -------------------------------------------------------- */ +// Sentinel errors for transactions. +var ( + ErrInvalidTxID = errors.New("invalid TxID") +) + // Tx wraps a bitcoin transaction // // DO NOT CHANGE ORDER - Optimised memory via malign // type Tx struct { - Inputs []*Input `json:"vin"` - Outputs []*Output `json:"vout"` - Version uint32 `json:"version"` - LockTime uint32 `json:"locktime"` + Inputs []*Input + Outputs []*Output + Version uint32 + LockTime uint32 } type txJSON struct { @@ -70,7 +75,7 @@ func (tx *Tx) MarshalJSON() ([]byte, error) { Outputs: tx.Outputs, TxID: tx.TxID(), Hash: tx.TxID(), - Size: len(tx.ToBytes()), + Size: len(tx.Bytes()), Hex: tx.String(), } return json.Marshal(txj) @@ -106,12 +111,12 @@ func NewTx() *Tx { // NewTxFromString takes a toBytesHelper string representation of a bitcoin transaction // and returns a Tx object. func NewTxFromString(str string) (*Tx, error) { - b, err := hex.DecodeString(str) + bb, err := hex.DecodeString(str) if err != nil { return nil, err } - return NewTxFromBytes(b) + return NewTxFromBytes(bb) } // NewTxFromBytes takes an array of bytes, constructs a Tx and returns it. @@ -129,7 +134,7 @@ func NewTxFromBytes(b []byte) (*Tx, error) { return tx, nil } -// NewTxFromStream takes an array of bytes and contructs a Tx from it, returning the Tx and the bytes used. +// NewTxFromStream takes an array of bytes and constructs a Tx from it, returning the Tx and the bytes used. // Despite the name, this is not actually reading a stream in the true sense: it is a byte slice that contains // many transactions one after another. func NewTxFromStream(b []byte) (*Tx, int, error) { @@ -147,7 +152,7 @@ func NewTxFromStream(b []byte) (*Tx, int, error) { inputCount, size := DecodeVarInt(b[offset:]) offset += size - // create inputs + // create Inputs var i uint64 var err error var input *Input @@ -157,11 +162,10 @@ func NewTxFromStream(b []byte) (*Tx, int, error) { return nil, 0, err } offset += size - - t.Inputs = append(t.Inputs, input) + t.addInput(input) } - // create outputs + // create Outputs var outputCount uint64 var output *Output outputCount, size = DecodeVarInt(b[offset:]) @@ -173,7 +177,7 @@ func NewTxFromStream(b []byte) (*Tx, int, error) { } output.index = int(i) offset += size - t.Outputs = append(t.Outputs, output) + t.AddOutput(output) } t.LockTime = binary.LittleEndian.Uint32(b[offset:]) @@ -193,6 +197,28 @@ func (tx *Tx) HasDataOutputs() bool { return false } +// InputIdx will return the input at the specified index. +// +// This will consume an overflow error and simply return nil if the input +// isn't found at the index. +func (tx *Tx) InputIdx(i int) *Input { + if i > tx.InputCount()-1 { + return nil + } + return tx.Inputs[i] +} + +// OutputIdx will return the output at the specified index. +// +// This will consume an overflow error and simply return nil if the output +// isn't found at the index. +func (tx *Tx) OutputIdx(i int) *Output { + if i > tx.OutputCount()-1 { + return nil + } + return tx.Outputs[i] +} + // IsCoinbase determines if this transaction is a coinbase by // checking if the tx input is a standard coinbase input. func (tx *Tx) IsCoinbase() bool { @@ -202,7 +228,7 @@ func (tx *Tx) IsCoinbase() bool { cbi := make([]byte, 32) - if !bytes.Equal(tx.Inputs[0].PreviousTxIDBytes, cbi) { + if !bytes.Equal(tx.Inputs[0].PreviousTxID(), cbi) { return false } @@ -213,32 +239,39 @@ func (tx *Tx) IsCoinbase() bool { return false } -// TxIDAsBytes returns the transaction ID of the transaction as bytes +// TxIDBytes returns the transaction ID of the transaction as bytes // (which is also the transaction hash). -func (tx *Tx) TxIDAsBytes() []byte { - return ReverseBytes(crypto.Sha256d(tx.ToBytes())) +func (tx *Tx) TxIDBytes() []byte { + return ReverseBytes(crypto.Sha256d(tx.Bytes())) } // TxID returns the transaction ID of the transaction // (which is also the transaction hash). func (tx *Tx) TxID() string { - return hex.EncodeToString(ReverseBytes(crypto.Sha256d(tx.ToBytes()))) + return hex.EncodeToString(ReverseBytes(crypto.Sha256d(tx.Bytes()))) } // String encodes the transaction into a hex string. func (tx *Tx) String() string { - return hex.EncodeToString(tx.ToBytes()) + return hex.EncodeToString(tx.Bytes()) +} + +// IsValidTxID will check that the txid bytes are valid. +// +// A txid should be of 32 bytes length. +func IsValidTxID(txid []byte) bool { + return len(txid) == 32 } -// ToBytes encodes the transaction into a byte array. +// Bytes encodes the transaction into a byte array. // See https://chainquery.com/bitcoin-cli/decoderawtransaction -func (tx *Tx) ToBytes() []byte { +func (tx *Tx) Bytes() []byte { return tx.toBytesHelper(0, nil) } -// ToBytesWithClearedInputs encodes the transaction into a byte array but clears its inputs first. +// BytesWithClearedInputs encodes the transaction into a byte array but clears its Inputs first. // This is used when signing transactions. -func (tx *Tx) ToBytesWithClearedInputs(index int, lockingScript []byte) []byte { +func (tx *Tx) BytesWithClearedInputs(index int, lockingScript []byte) []byte { return tx.toBytesHelper(index, lockingScript) } @@ -250,7 +283,7 @@ func (tx *Tx) toBytesHelper(index int, lockingScript []byte) []byte { h = append(h, VarInt(uint64(len(tx.Inputs)))...) for i, in := range tx.Inputs { - s := in.ToBytes(lockingScript != nil) + s := in.Bytes(lockingScript != nil) if i == index && lockingScript != nil { h = append(h, VarInt(uint64(len(lockingScript)))...) h = append(h, lockingScript...) @@ -261,7 +294,7 @@ func (tx *Tx) toBytesHelper(index int, lockingScript []byte) []byte { h = append(h, VarInt(uint64(len(tx.Outputs)))...) for _, out := range tx.Outputs { - h = append(h, out.ToBytes()...) + h = append(h, out.Bytes()...) } lt := make([]byte, 4) diff --git a/tx_test.go b/tx_test.go index b1216dc1..948946e9 100644 --- a/tx_test.go +++ b/tx_test.go @@ -34,7 +34,7 @@ func TestNewTx(t *testing.T) { func TestNewTxFromString(t *testing.T) { t.Parallel() - t.Run("valid tx no inputs", func(t *testing.T) { + t.Run("valid tx no Inputs", func(t *testing.T) { tx, err := bt.NewTxFromString("01000000000100000000000000001a006a07707265666978310c6578616d706c65206461746102133700000000") assert.NoError(t, err) assert.NotNil(t, tx) @@ -58,24 +58,24 @@ func TestNewTxFromString(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, tx) - // Check version, locktime, inputs + // Check version, locktime, Inputs assert.Equal(t, uint32(2), tx.Version) assert.Equal(t, uint32(0), tx.LockTime) assert.Equal(t, 1, len(tx.Inputs)) // Create a new unlocking script - ptid, _ := hex.DecodeString("9c5b1428aaad5e9b0196c89be8628b366f33c7b22933da0489b921d487a7cb1c") - i := bt.Input{ - PreviousTxIDBytes: ptid, + //ptid, _ := hex.DecodeString("9c5b1428aaad5e9b0196c89be8628b366f33c7b22933da0489b921d487a7cb1c") + i := &bt.Input{ PreviousTxOutIndex: 0, SequenceNumber: bt.DefaultSequenceNumber, } + assert.NoError(t, i.PreviousTxIDAdd(tx.InputIdx(0).PreviousTxID())) i.UnlockingScript, err = bscript.NewFromHexString("47304402205cc711985ce2a6d61eece4f9b6edd6337bad3b7eca3aa3ce59bc15620d8de2a80220410c92c48a226ba7d5a9a01105524097f673f31320d46c3b61d2378e6f05320041") assert.NoError(t, err) assert.NotNil(t, i.UnlockingScript) // Check input type - assert.Equal(t, true, reflect.DeepEqual(*tx.Inputs[0], i)) + assert.Equal(t, tx.InputIdx(0), i) // Check output assert.Equal(t, 1, len(tx.Outputs)) @@ -148,7 +148,7 @@ func TestVersion(t *testing.T) { func TestTx_IsCoinbase(t *testing.T) { t.Parallel() - t.Run("invalid number of inputs", func(t *testing.T) { + t.Run("invalid number of Inputs", func(t *testing.T) { tx := bt.NewTx() assert.NotNil(t, tx) assert.Equal(t, false, tx.IsCoinbase()) @@ -202,7 +202,7 @@ func TestTx_CreateTx(t *testing.T) { 2000000) assert.NoError(t, err) - err = tx.PayTo("n2wmGVP89x3DsLNqk3NvctfQy9m9pvt7mk", 1999942) + err = tx.PayToAddress("n2wmGVP89x3DsLNqk3NvctfQy9m9pvt7mk", 1999942) assert.NoError(t, err) var wif *WIF @@ -217,7 +217,7 @@ func TestTx_CreateTx(t *testing.T) { func TestTx_HasDataOutputs(t *testing.T) { t.Parallel() - t.Run("has data outputs", func(t *testing.T) { + t.Run("has data Outputs", func(t *testing.T) { tx := bt.NewTx() assert.NotNil(t, tx) @@ -228,7 +228,7 @@ func TestTx_HasDataOutputs(t *testing.T) { 2000000) assert.NoError(t, err) - err = tx.PayTo("n2wmGVP89x3DsLNqk3NvctfQy9m9pvt7mk", 1999942) + err = tx.PayToAddress("n2wmGVP89x3DsLNqk3NvctfQy9m9pvt7mk", 1999942) assert.NoError(t, err) // Add op return data @@ -249,7 +249,7 @@ func TestTx_HasDataOutputs(t *testing.T) { assert.Equal(t, true, tx.HasDataOutputs()) }) - t.Run("no data outputs", func(t *testing.T) { + t.Run("no data Outputs", func(t *testing.T) { tx := bt.NewTx() assert.NotNil(t, tx) @@ -260,7 +260,7 @@ func TestTx_HasDataOutputs(t *testing.T) { 2000000) assert.NoError(t, err) - err = tx.PayTo("n2wmGVP89x3DsLNqk3NvctfQy9m9pvt7mk", 1999942) + err = tx.PayToAddress("n2wmGVP89x3DsLNqk3NvctfQy9m9pvt7mk", 1999942) assert.NoError(t, err) var wif *WIF @@ -296,7 +296,7 @@ func TestTx_JSON(t *testing.T) { 0, "76a914eb0bd5edba389198e73f8efabddfc61666969ff788ac", 2000000)) - assert.NoError(t, tx.PayTo("n2wmGVP89x3DsLNqk3NvctfQy9m9pvt7mk", 1000)) + assert.NoError(t, tx.PayToAddress("n2wmGVP89x3DsLNqk3NvctfQy9m9pvt7mk", 1000)) var wif *WIF wif, err := DecodeWIF("KznvCNc6Yf4iztSThoMH6oHWzH9EgjfodKxmeuUGPq5DEX5maspS") assert.NoError(t, err) @@ -314,7 +314,7 @@ func TestTx_JSON(t *testing.T) { 0, "76a914eb0bd5edba389198e73f8efabddfc61666969ff788ac", 2000000)) - assert.NoError(t, tx.PayTo("n2wmGVP89x3DsLNqk3NvctfQy9m9pvt7mk", 1000)) + assert.NoError(t, tx.PayToAddress("n2wmGVP89x3DsLNqk3NvctfQy9m9pvt7mk", 1000)) var wif *WIF wif, err := DecodeWIF("KznvCNc6Yf4iztSThoMH6oHWzH9EgjfodKxmeuUGPq5DEX5maspS") assert.NoError(t, err) @@ -397,7 +397,7 @@ func TestTx_MarshallJSON(t *testing.T) { } ] }`, - }, "transaction with multiple inputs": { + }, "transaction with multiple Inputs": { tx: func() *bt.Tx { tx := bt.NewTx() assert.NoError(t, tx.From( @@ -415,7 +415,7 @@ func TestTx_MarshallJSON(t *testing.T) { 114, "76a914eb0bd5edba389198e73f8efabddfc61666969ff788ac", 10000)) - assert.NoError(t, tx.PayTo("n2wmGVP89x3DsLNqk3NvctfQy9m9pvt7mk", 1000)) + assert.NoError(t, tx.PayToAddress("n2wmGVP89x3DsLNqk3NvctfQy9m9pvt7mk", 1000)) var w *wif.WIF w, err := wif.DecodeWIF("KznvCNc6Yf4iztSThoMH6oHWzH9EgjfodKxmeuUGPq5DEX5maspS") assert.NoError(t, err) @@ -647,3 +647,211 @@ func TestTx_UnmarshalJSON(t *testing.T) { }) } } + +func TestTx_OutputIdx(t *testing.T) { + t.Parallel() + tests := map[string]struct { + tx *bt.Tx + idx int + expOutput *bt.Output + }{ + "tx with 3 Outputs and output idx 0 requested should return output": { + tx: func() *bt.Tx { + tx := bt.NewTx() + assert.NoError(t, tx.PayToAddress("myUmQeCYxQECGHXbupe539n41u6BTBz1Eh", 1000)) + assert.NoError(t, tx.PayToAddress("n2wmGVP89x3DsLNqk3NvctfQy9m9pvt7mz", 1000)) + assert.NoError(t, tx.PayToAddress("n2wmGVP89x3DsLNqk3NvctfQy9m9pvt7mz", 1000)) + return tx + }(), + idx: 0, + expOutput: &bt.Output{ + Satoshis: 1000, + LockingScript: func() *bscript.Script { + s, err := bscript.NewP2PKHFromAddress("myUmQeCYxQECGHXbupe539n41u6BTBz1Eh") + assert.NoError(t, err) + return s + }(), + }, + }, "tx with 3 Outputs and output idx 2 requested should return output": { + tx: func() *bt.Tx { + tx := bt.NewTx() + assert.NoError(t, tx.PayToAddress("myUmQeCYxQECGHXbupe539n41u6BTBz1Eh", 1000)) + assert.NoError(t, tx.PayToAddress("n2wmGVP89x3DsLNqk3NvctfQy9m9pvt7mz", 1000)) + assert.NoError(t, tx.PayToAddress("mywmGVP89x3DsLNqk3NvctfQy9m9pvt7mz", 1000)) + return tx + }(), + idx: 2, + expOutput: &bt.Output{ + Satoshis: 1000, + LockingScript: func() *bscript.Script { + s, err := bscript.NewP2PKHFromAddress("mywmGVP89x3DsLNqk3NvctfQy9m9pvt7mz") + assert.NoError(t, err) + return s + }(), + }, + }, "tx with 3 Outputs and output idx 5 requested should return nil": { + tx: func() *bt.Tx { + tx := bt.NewTx() + assert.NoError(t, tx.PayToAddress("myUmQeCYxQECGHXbupe539n41u6BTBz1Eh", 1000)) + assert.NoError(t, tx.PayToAddress("n2wmGVP89x3DsLNqk3NvctfQy9m9pvt7mz", 1000)) + assert.NoError(t, tx.PayToAddress("mywmGVP89x3DsLNqk3NvctfQy9m9pvt7mz", 1000)) + return tx + }(), + idx: 5, + expOutput: nil, + }, "tx with 0 Outputs and output idx 5 requested should return nil": { + tx: func() *bt.Tx { + return bt.NewTx() + }(), + idx: 5, + expOutput: nil, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + o := test.tx.OutputIdx(test.idx) + assert.Equal(t, test.expOutput, o) + }) + } +} + +func TestTx_InputIdx(t *testing.T) { + t.Parallel() + tests := map[string]struct { + tx *bt.Tx + idx int + expInput *bt.Input + }{ + "tx with 3 Inputs and input idx 0 requested should return correct input": { + tx: func() *bt.Tx { + tx := bt.NewTx() + assert.NoError(t, tx.From( + "3c8edde27cb9a9132c22038dac4391496be9db16fd21351565cc1006966fdad5", + 0, + "76a914eb0bd5edba389198e73f8efabddfc61666969ff788ac", + 1000)) + assert.NoError(t, tx.From( + "3c8edde27cb9a9132c22038dac4391496be9db16fd21351565cc1006966fdad5", + 0, + "76a914eb0bd5edba389198e73f8efabddfc61666969ff788ac", + 2000000)) + assert.NoError(t, tx.From( + "3c8edde27cb9a9132c22038dac4391496be9db16fd21351565cc1006966fdad5", + 0, + "76a914eb0bd5edba389198e73f8efabddfc61666969ff788ac", + 2000000)) + return tx + }(), + idx: 0, + expInput: func() *bt.Input { + in := &bt.Input{ + PreviousTxSatoshis: 1000, + PreviousTxScript: func() *bscript.Script { + b, err := bscript.NewFromHexString("76a914eb0bd5edba389198e73f8efabddfc61666969ff788ac") + assert.NoError(t, err) + return b + }(), + PreviousTxOutIndex: 0, + SequenceNumber: bt.DefaultSequenceNumber, + } + _ = in.PreviousTxIDAddStr("3c8edde27cb9a9132c22038dac4391496be9db16fd21351565cc1006966fdad5") + return in + }(), + }, "tx with 3 Outputs and output idx 2 requested should return output": { + tx: func() *bt.Tx { + tx := bt.NewTx() + assert.NoError(t, tx.From( + "3c8edde27cb9a9132c22038dac4391496be9db16fd21351565cc1006966fdad5", + 0, + "76a914eb0bd5edba389198e73f8efabddfc61666969ff788ac", + 1000)) + assert.NoError(t, tx.From( + "3c8edde27cb9a9132c22038dac4391496be9db16fd21351565cc1006966fdad5", + 0, + "76a914eb0bd5edba389198e73f8efabddfc61666969ff788ac", + 2000000)) + assert.NoError(t, tx.From( + "3c8edde27cb9a9132c22038dac4391496be9db16fd21351565cc1006966fdac4", + 0, + "76a914eb0bd5edba389198e73f8efabddfc61666969ff788ac", + 999)) + return tx + }(), + idx: 2, + expInput: func() *bt.Input { + in := &bt.Input{ + PreviousTxSatoshis: 999, + PreviousTxScript: func() *bscript.Script { + b, err := bscript.NewFromHexString("76a914eb0bd5edba389198e73f8efabddfc61666969ff788ac") + assert.NoError(t, err) + return b + }(), + PreviousTxOutIndex: 0, + SequenceNumber: bt.DefaultSequenceNumber, + } + _ = in.PreviousTxIDAddStr("3c8edde27cb9a9132c22038dac4391496be9db16fd21351565cc1006966fdac4") + return in + }(), + }, "tx with 3 Outputs and output idx 5 requested should return nil": { + tx: func() *bt.Tx { + tx := bt.NewTx() + assert.NoError(t, tx.From( + "3c8edde27cb9a9132c22038dac4391496be9db16fd21351565cc1006966fdad5", + 0, + "76a914eb0bd5edba389198e73f8efabddfc61666969ff788ac", + 1000)) + assert.NoError(t, tx.From( + "3c8edde27cb9a9132c22038dac4391496be9db16fd21351565cc1006966fdad5", + 0, + "76a914eb0bd5edba389198e73f8efabddfc61666969ff788ac", + 2000000)) + assert.NoError(t, tx.From( + "3c8edde27cb9a9132c22038dac4391496be9db16fd21351565cc1006966fdad5", + 0, + "76a914eb0bd5edba389198e73f8efabddfc61666969ff788ac", + 999)) + return tx + }(), + idx: 5, + expInput: nil, + }, "tx with 0 Outputs and output idx 5 requested should return nil": { + tx: func() *bt.Tx { + return bt.NewTx() + }(), + idx: 5, + expInput: nil, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + o := test.tx.InputIdx(test.idx) + assert.Equal(t, test.expInput, o) + }) + } +} + +func Test_IsValidTxID(t *testing.T) { + t.Parallel() + tests := map[string]struct { + txid string + exp bool + }{ + "valid txID should return true": { + txid: "a2a55ecc61f418e300888b1f82eaf84024496b34e3e538f3d32d342fd753adab", + exp: true, + }, + "invalid txID should return false": { + txid: "a2a55ecc61f418e300888b1f82eaf84024496b34e3e538f3d32d342fd753adZZ", + exp: false, + }, "empty txID should return false": { + txid: "", + exp: false, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + bb, _ := hex.DecodeString(test.txid) + assert.Equal(t, test.exp, bt.IsValidTxID(bb)) + }) + } +} diff --git a/txchange.go b/txchange.go index 2ad5e605..5f89bf7b 100644 --- a/txchange.go +++ b/txchange.go @@ -26,7 +26,7 @@ func (tx *Tx) Change(s *bscript.Script, f []*Fee) error { } if hasChange { // add rest of available sats to the change output - tx.Outputs[len(tx.Outputs)-1].Satoshis = available + tx.Outputs[tx.OutputCount()-1].Satoshis = available } return nil } @@ -34,8 +34,8 @@ func (tx *Tx) Change(s *bscript.Script, f []*Fee) error { // ChangeToExistingOutput 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) ChangeToExistingOutput(index uint, f []*Fee) error { - if int(index) > len(tx.Outputs)-1 { - return errors.New("index is greater than number of inputs in transaction") + if int(index) > tx.OutputCount()-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 { @@ -101,7 +101,7 @@ func (tx *Tx) canAddChange(available uint64, standardFees *Fee) bool { varIntUpper := VarIntUpperLimitInc(uint64(tx.OutputCount())) if varIntUpper == -1 { - return false // upper limit of outputs in one tx reached + return false // upper limit of Outputs in one tx reached } changeOutputFee := uint64(varIntUpper) @@ -155,13 +155,13 @@ func (tx *Tx) getExpectedUnlockingScriptFees(f []*Fee) (uint64, error) { } func (tx *Tx) getStandardAndDataBytes() (standardBytes, dataBytes int) { - // Subtract the value of each output as well as keeping track of data outputs + // Subtract the value of each output as well as keeping track of data Outputs for _, out := range tx.Outputs { if out.LockingScript.IsData() && len(*out.LockingScript) > 0 { dataBytes += len(*out.LockingScript) } } - standardBytes = len(tx.ToBytes()) - dataBytes + standardBytes = len(tx.Bytes()) - dataBytes return } diff --git a/txchange_test.go b/txchange_test.go index 79c4eb6b..d24b9a05 100644 --- a/txchange_test.go +++ b/txchange_test.go @@ -118,7 +118,7 @@ func TestTx_Change(t *testing.T) { // Correct script hex string assert.Equal(t, "76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac", - tx.Outputs[0].LockingScriptHexString(), + tx.OutputIdx(0).LockingScriptHexString(), ) }) @@ -136,7 +136,7 @@ func TestTx_Change(t *testing.T) { assert.NoError(t, err) // pay to - err = tx.PayTo("1C8bzHM8XFBHZ2ZZVvFy2NSoAZbwCXAicL", 500) + err = tx.PayToAddress("1C8bzHM8XFBHZ2ZZVvFy2NSoAZbwCXAicL", 500) assert.NoError(t, err) // add some op return @@ -162,7 +162,7 @@ func TestTx_Change(t *testing.T) { feePaid := tx.TotalInputSatoshis() - tx.TotalOutputSatoshis() assert.Equal(t, uint64(122), feePaid) - txSize := len(tx.ToBytes()) + txSize := len(tx.Bytes()) assert.Equal(t, 251, txSize) feeRate := float64(feePaid) / float64(txSize) @@ -207,10 +207,10 @@ func TestTx_Change(t *testing.T) { 4000000) assert.NoError(t, err) - err = tx.PayTo("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 1000000) + err = tx.PayToAddress("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 1000000) assert.NoError(t, err) - err = tx.PayTo("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 3000000) + err = tx.PayToAddress("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 3000000) assert.NoError(t, err) err = tx.ChangeToAddress("mwV3YgnowbJJB3LcyCuqiKpdivvNNFiK7M", bt.DefaultFees()) @@ -241,10 +241,10 @@ func TestTx_Change(t *testing.T) { 4000000) assert.NoError(t, err) - err = tx.PayTo("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 999995) + err = tx.PayToAddress("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 999995) assert.NoError(t, err) - err = tx.PayTo("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 3000000) + err = tx.PayToAddress("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 3000000) assert.NoError(t, err) err = tx.ChangeToAddress("mwV3YgnowbJJB3LcyCuqiKpdivvNNFiK7M", bt.DefaultFees()) @@ -260,13 +260,13 @@ func TestTx_Change(t *testing.T) { assert.Equal(t, "01000000010b94a1ef0fb352aa2adc54207ce47ba55d5a1c1609afda58fe9520e472299107000000006b483045022100fd07316603e9abf393e695192e8ce1e7f808d2735cc57039109a2210ad32d9a7022000e301e2a988b23ab3872b041df8b6eb0315238e0918944cbaf8b6abdde75cac412102c8803fdd437d902f08e3c2344cb33065c99d7c99982018ff9f7219c3dd352ff0ffffffff023b420f00000000001976a914b6aa34534d2b11e66b438c7525f819aee01e397c88acc0c62d00000000001976a914b6aa34534d2b11e66b438c7525f819aee01e397c88ac00000000", tx.String()) - // todo: expected the pay-to inputs to change based on the fee :P + // todo: expected the pay-to Inputs to change based on the fee :P assert.Equal(t, uint64(999995), tx.Outputs[0].Satoshis) assert.Equal(t, uint64(3000000), tx.Outputs[1].Satoshis) }) - t.Run("multiple inputs, spend all", func(t *testing.T) { + t.Run("multiple Inputs, spend all", func(t *testing.T) { tx := bt.NewTx() assert.NotNil(t, tx) @@ -319,7 +319,7 @@ func TestTx_ChangeToOutput(t *testing.T) { 0, "76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac", 1000)) - assert.NoError(t, tx.PayTo("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 1000)) + assert.NoError(t, tx.PayToAddress("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 1000)) return tx }(), index: 0, @@ -335,7 +335,7 @@ func TestTx_ChangeToOutput(t *testing.T) { 0, "76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac", 1000)) - assert.NoError(t, tx.PayTo("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 500)) + assert.NoError(t, tx.PayToAddress("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 500)) return tx }(), index: 0, @@ -351,10 +351,10 @@ func TestTx_ChangeToOutput(t *testing.T) { 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)) + assert.NoError(t, tx.PayToAddress("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 500)) + assert.NoError(t, tx.PayToAddress("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 500)) + assert.NoError(t, tx.PayToAddress("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 500)) + assert.NoError(t, tx.PayToAddress("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 500)) return tx }(), index: 3, @@ -370,12 +370,12 @@ func TestTx_ChangeToOutput(t *testing.T) { 0, "76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac", 1000)) - assert.NoError(t, tx.PayTo("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 500)) + assert.NoError(t, tx.PayToAddress("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 500)) return tx }(), index: 1, fees: bt.DefaultFees(), - err: errors.New("index is greater than number of inputs in transaction"), + err: errors.New("index is greater than number of Inputs in transaction"), }, } for name, test := range tests { @@ -407,12 +407,12 @@ func TestTx_CalculateChange(t *testing.T) { 0, "76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac", 1000)) - assert.NoError(t, tx.PayTo("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 500)) + assert.NoError(t, tx.PayToAddress("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 500)) return tx }(), fees: bt.DefaultFees(), expFees: 96, - }, "Transaction with one input 4 outputs should return 147": { + }, "Transaction with one input 4 Outputs should return 147": { tx: func() *bt.Tx { tx := bt.NewTx() assert.NoError(t, tx.From( @@ -420,10 +420,10 @@ func TestTx_CalculateChange(t *testing.T) { 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)) + assert.NoError(t, tx.PayToAddress("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 500)) + assert.NoError(t, tx.PayToAddress("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 500)) + assert.NoError(t, tx.PayToAddress("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 500)) + assert.NoError(t, tx.PayToAddress("mxAoAyZFXX6LZBWhoam3vjm6xt9NxPQ15f", 500)) return tx }(), fees: bt.DefaultFees(), diff --git a/txinput.go b/txinput.go index 35904cc8..db3e89f0 100644 --- a/txinput.go +++ b/txinput.go @@ -3,7 +3,6 @@ package bt import ( "bytes" "encoding/binary" - "encoding/hex" "fmt" "github.com/libsv/go-bk/crypto" @@ -28,7 +27,7 @@ func NewInputFromBytes(bytes []byte) (*Input, int, error) { } return &Input{ - PreviousTxIDBytes: ReverseBytes(bytes[0:32]), + previousTxID: ReverseBytes(bytes[0:32]), PreviousTxOutIndex: binary.LittleEndian.Uint32(bytes[32:36]), SequenceNumber: binary.LittleEndian.Uint32(bytes[offset+int(l):]), UnlockingScript: bscript.NewFromBytes(bytes[offset : offset+int(l)]), @@ -47,9 +46,9 @@ func (tx *Tx) addInput(input *Input) { tx.Inputs = append(tx.Inputs, input) } -// AddInputFromTx will add all outputs of given previous transaction +// AddP2PKHInputsFromTx will add all Outputs of given previous transaction // that match a specific public key to your transaction. -func (tx *Tx) AddInputFromTx(pvsTx *Tx, matchPK []byte) error { +func (tx *Tx) AddP2PKHInputsFromTx(pvsTx *Tx, matchPK []byte) error { for i, utxo := range pvsTx.Outputs { utxoPkHASH160, err := utxo.LockingScript.PublicKeyHash() if err != nil { @@ -75,23 +74,21 @@ func (tx *Tx) From(prevTxID string, vout uint32, prevTxLockingScript string, sat return err } - ptxid, err := hex.DecodeString(prevTxID) - if err != nil { - return err - } - - tx.addInput(&Input{ - PreviousTxIDBytes: ptxid, + i := &Input{ PreviousTxOutIndex: vout, PreviousTxSatoshis: satoshis, PreviousTxScript: pts, SequenceNumber: DefaultSequenceNumber, // use default finalised sequence number - }) + } + if err := i.PreviousTxIDAddStr(prevTxID); err != nil { + return err + } + tx.addInput(i) return nil } -// InputCount returns the number of transaction inputs. +// InputCount returns the number of transaction Inputs. func (tx *Tx) InputCount() int { return len(tx.Inputs) } diff --git a/txinput_test.go b/txinput_test.go index 416d1781..ee3f64fd 100644 --- a/txinput_test.go +++ b/txinput_test.go @@ -21,7 +21,7 @@ func TestAddInputFromTx(t *testing.T) { assert.NoError(t, err) newTx := bt.NewTx() - err = newTx.AddInputFromTx(prvTx, pubkey1) + err = newTx.AddP2PKHInputsFromTx(prvTx, pubkey1) assert.NoError(t, err) assert.Equal(t, newTx.InputCount(), 2) // only 2 utxos added assert.Equal(t, newTx.TotalInputSatoshis(), uint64(200000)) @@ -72,7 +72,7 @@ func TestTx_From(t *testing.T) { inputs := tx.Inputs assert.Equal(t, 1, len(inputs)) - assert.Equal(t, "07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b", hex.EncodeToString(inputs[0].PreviousTxIDBytes)) + assert.Equal(t, "07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b", hex.EncodeToString(inputs[0].PreviousTxID())) assert.Equal(t, uint32(0), inputs[0].PreviousTxOutIndex) assert.Equal(t, uint64(4000000), inputs[0].PreviousTxSatoshis) assert.Equal(t, bt.DefaultSequenceNumber, inputs[0].SequenceNumber) diff --git a/txoutput.go b/txoutput.go index 32092e11..479952d0 100644 --- a/txoutput.go +++ b/txoutput.go @@ -3,10 +3,11 @@ package bt import ( "encoding/binary" "encoding/hex" + "errors" "fmt" - "github.com/libsv/go-bt/bscript" "github.com/libsv/go-bk/crypto" + "github.com/libsv/go-bt/bscript" ) // NewOutputFromBytes returns a transaction Output from the bytes provided @@ -97,6 +98,18 @@ func (tx *Tx) AddP2PKHOutputFromAddress(addr string, satoshis uint64) error { return nil } +// AddP2PKHOutputFromScript makes an output to a P2PKH script paid to the provided locking script with a value. +func (tx *Tx) AddP2PKHOutputFromScript(script *bscript.Script, satoshis uint64) error { + if !script.IsP2PKH() { + return errors.New("script is not a valid P2PKH script") + } + tx.AddOutput(&Output{ + Satoshis: satoshis, + LockingScript: script, + }) + return nil +} + // AddHashPuzzleOutput makes an output to a hash puzzle + PKH with a value. func (tx *Tx) AddHashPuzzleOutput(secret, publicKeyHash string, satoshis uint64) error { publicKeyHashBytes, err := hex.DecodeString(publicKeyHash) @@ -165,7 +178,7 @@ func createOpReturnOutput(data [][]byte) (*Output, error) { return &Output{LockingScript: s}, nil } -// OutputCount returns the number of transaction inputs. +// OutputCount returns the number of transaction Inputs. func (tx *Tx) OutputCount() int { return len(tx.Outputs) } @@ -177,6 +190,12 @@ func (tx *Tx) AddOutput(output *Output) { // PayTo creates a new P2PKH output from a BitCoin address (base58) // and the satoshis amount and adds that to the transaction. -func (tx *Tx) PayTo(addr string, satoshis uint64) error { +func (tx *Tx) PayTo(script *bscript.Script, satoshis uint64) error { + return tx.AddP2PKHOutputFromScript(script, satoshis) +} + +// PayToAddress creates a new P2PKH output from a BitCoin address (base58) +// and the satoshis amount and adds that to the transaction. +func (tx *Tx) PayToAddress(addr string, satoshis uint64) error { return tx.AddP2PKHOutputFromAddress(addr, satoshis) } diff --git a/txoutput_test.go b/txoutput_test.go index d0e9ca76..fd406168 100644 --- a/txoutput_test.go +++ b/txoutput_test.go @@ -2,6 +2,7 @@ package bt_test import ( "encoding/hex" + "errors" "fmt" "testing" @@ -127,14 +128,14 @@ func TestTx_TotalOutputSatoshis(t *testing.T) { assert.Equal(t, uint64((29.89999582+20.00)*1e8), tx.TotalOutputSatoshis()) }) - t.Run("zero outputs", func(t *testing.T) { + t.Run("zero Outputs", func(t *testing.T) { tx := bt.NewTx() assert.NotNil(t, tx) assert.Equal(t, uint64(0), tx.TotalOutputSatoshis()) }) } -func TestTx_PayTo(t *testing.T) { +func TestTx_PayToAddress(t *testing.T) { t.Run("missing pay to address", func(t *testing.T) { tx := bt.NewTx() assert.NotNil(t, tx) @@ -145,7 +146,7 @@ func TestTx_PayTo(t *testing.T) { 4000000) assert.NoError(t, err) - err = tx.PayTo("", 100) + err = tx.PayToAddress("", 100) assert.Error(t, err) }) @@ -159,7 +160,7 @@ func TestTx_PayTo(t *testing.T) { 4000000) assert.NoError(t, err) - err = tx.PayTo("1234567", 100) + err = tx.PayToAddress("1234567", 100) assert.Error(t, err) }) @@ -173,8 +174,51 @@ func TestTx_PayTo(t *testing.T) { 4000000) assert.NoError(t, err) - err = tx.PayTo("1GHMW7ABrFma2NSwiVe9b9bZxkMB7tuPZi", 100) + err = tx.PayToAddress("1GHMW7ABrFma2NSwiVe9b9bZxkMB7tuPZi", 100) assert.NoError(t, err) assert.Equal(t, 1, tx.OutputCount()) }) } + +func TestTx_PayTo(t *testing.T) { + t.Parallel() + tests := map[string]struct { + script *bscript.Script + err error + }{ + "valid p2pkh script should create valid output": { + script: func() *bscript.Script { + s, err := bscript.NewP2PKHFromAddress("1GHMW7ABrFma2NSwiVe9b9bZxkMB7tuPZi") + assert.NoError(t, err) + return s + }(), + err: nil, + }, "empty p2pkh script should return error": { + script: &bscript.Script{}, + err: errors.New("script is not a valid P2PKH script"), + }, "non p2pkh script should return error": { + script: bscript.NewFromBytes([]byte("test")), + err: errors.New("script is not a valid P2PKH script"), + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + tx := bt.NewTx() + assert.NotNil(t, tx) + err := tx.From( + "07912972e42095fe58daaf09161c5a5da57be47c2054dc2aaa52b30fefa1940b", + 0, + "76a914af2590a45ae401651fdbdf59a76ad43d1862534088ac", + 4000000) + assert.NoError(t, err) + err = tx.PayTo(test.script, 100) + if test.err == nil { + assert.NoError(t, err) + assert.Equal(t, 1, tx.OutputCount()) + return + } + assert.EqualError(t, err, test.err.Error()) + assert.Equal(t, 0, tx.OutputCount()) + }) + } +} diff --git a/txsign.go b/txsign.go index cd7e8507..34afcda1 100644 --- a/txsign.go +++ b/txsign.go @@ -5,8 +5,8 @@ import ( "encoding/hex" "fmt" - "github.com/libsv/go-bt/bscript" "github.com/libsv/go-bk/crypto" + "github.com/libsv/go-bt/bscript" "github.com/libsv/go-bt/sighash" ) @@ -74,7 +74,7 @@ func (tx *Tx) ApplyUnlockingScript(index uint32, s *bscript.Script) error { return fmt.Errorf("no input at index %d", index) } -// SignAuto is used to automatically check which P2PKH inputs are +// SignAuto is used to automatically check which P2PKH Inputs are // able to be signed (match the public key) and then sign them. // It takes a Signed interface as a parameter so that different // signing implementations can be used to sign the transaction -