diff --git a/README.md b/README.md index 9aaa10d..90596e1 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,8 @@ View the generated [documentation](https://pkg.go.dev/github.com/bitcoinschema/g - [Generate HD Keys](hd_key.go) - [Get PrivateKey by Path](hd_key.go) - [Get HD Key by Path](hd_key.go) +- [Create Tx](transaction.go) +- [Tx from Hex](transaction.go) ### ToDo - Support `testnet` addresses & keys @@ -63,6 +65,7 @@ View the generated [documentation](https://pkg.go.dev/github.com/bitcoinschema/g - [bitcoinsv/bsvd](https://github.com/bitcoinsv/bsvd) - [bitcoinsv/bsvutil](https://github.com/bitcoinsv/bsvutil) - [itchyny/base58-go](https://github.com/itchyny/base58-go) +- [libsv/libsv](https://github.com/libsv/libsv) - [piotrnar/gocoin](https://github.com/piotrnar/gocoin) diff --git a/examples/create_tx/create_tx.go b/examples/create_tx/create_tx.go new file mode 100644 index 0000000..0a54a85 --- /dev/null +++ b/examples/create_tx/create_tx.go @@ -0,0 +1,38 @@ +package main + +import ( + "log" + + "github.com/bitcoinschema/go-bitcoin" +) + +func main() { + + // Use a new UTXO + utxo := &bitcoin.Utxo{ + TxID: "b7b0650a7c3a1bd4716369783876348b59f5404784970192cec1996e86950576", + Vout: 0, + ScriptSig: "76a9149cbe9f5e72fa286ac8a38052d1d5337aa363ea7f88ac", + Satoshis: 1000, + } + + // Add a pay-to address + payTo := &bitcoin.PayToAddress{ + Address: "1C8bzHM8XFBHZ2ZZVvFy2NSoAZbwCXAicL", + Satoshis: 500, + } + + // Add some op return data + opReturn1 := bitcoin.OpReturnData{[]byte("prefix1"), []byte("example data"), []byte{0x13, 0x37}} + opReturn2 := bitcoin.OpReturnData{[]byte("prefix2"), []byte("more example data")} + + // Generate the TX + rawTx, err := bitcoin.CreateTx([]*bitcoin.Utxo{utxo}, []*bitcoin.PayToAddress{payTo}, []bitcoin.OpReturnData{opReturn1, opReturn2}, "L3VJH2hcRGYYG6YrbWGmsxQC1zyYixA82YjgEyrEUWDs4ALgk8Vu") + if err != nil { + log.Printf("error occurred: %s", err.Error()) + return + } + + // Success! + log.Printf("rawTx: %s", rawTx.ToString()) +} diff --git a/examples/tx_from_hex/tx_from_hex.go b/examples/tx_from_hex/tx_from_hex.go new file mode 100644 index 0000000..9cb3714 --- /dev/null +++ b/examples/tx_from_hex/tx_from_hex.go @@ -0,0 +1,21 @@ +package main + +import ( + "log" + + "github.com/bitcoinschema/go-bitcoin" +) + +func main() { + + // Example raw tx + exampleTx := "0100000001760595866e99c1ce920197844740f5598b34763878696371d41b3a7c0a65b0b7000000006b483045022100eea3d606bd1627be6459a9de4860919225db74843d2fc7f4e7caa5e01f42c2d0022017978d9c6a0e934955a70e7dda71d68cb614f7dd89eb7b9d560aea761834ddd4412102ea87d1fd77d169bd56a71e700628113d0f8dfe57faa0ba0e55a36f9ce8e10be3ffffffff03f4010000000000001976a9147a1980655efbfec416b2b0c663a7b3ac0b6a25d288ac00000000000000001a006a07707265666978310c6578616d706c65206461746102133700000000000000001c006a0770726566697832116d6f7265206578616d706c65206461746100000000" + + rawTx, err := bitcoin.TxFromHex(exampleTx) + if err != nil { + log.Printf("error occurred: %s", err.Error()) + return + } + + log.Printf("tx id: %s", rawTx.GetTxID()) +} diff --git a/transaction.go b/transaction.go index 95ec35b..d112045 100644 --- a/transaction.go +++ b/transaction.go @@ -31,12 +31,15 @@ func TxFromHex(rawHex string) (*transaction.Transaction, error) { return transaction.NewFromString(rawHex) } -// CreateTx will create a basic transaction -func CreateTx(utxos []*Utxo, addresses []*PayToAddress, opReturns []OpReturnData, wif string) (string, error) { +// CreateTx will create a basic transaction and return the raw transaction (*transaction.Transaction) +// +// Get the raw hex version: tx.ToString() +// Get the tx id: tx.GetTxID() +func CreateTx(utxos []*Utxo, addresses []*PayToAddress, opReturns []OpReturnData, wif string) (*transaction.Transaction, error) { // Missing utxos if len(utxos) == 0 { - return "", errors.New("utxos are required to create a tx") + return nil, errors.New("utxos are required to create a tx") } // Start creating a new transaction @@ -46,14 +49,14 @@ func CreateTx(utxos []*Utxo, addresses []*PayToAddress, opReturns []OpReturnData var err error for _, utxo := range utxos { if err = tx.From(utxo.TxID, utxo.Vout, utxo.ScriptSig, utxo.Satoshis); err != nil { - return "", err + return nil, err } } // Loop any pay addresses for _, address := range addresses { if err = tx.PayTo(address.Address, address.Satoshis); err != nil { - return "", err + return nil, err } } @@ -61,7 +64,7 @@ func CreateTx(utxos []*Utxo, addresses []*PayToAddress, opReturns []OpReturnData var outPut *output.Output for _, op := range opReturns { if outPut, err = output.NewOpReturnParts(op); err != nil { - return "", err + return nil, err } tx.AddOutput(outPut) } @@ -69,15 +72,15 @@ func CreateTx(utxos []*Utxo, addresses []*PayToAddress, opReturns []OpReturnData // Decode the WIF var decodedWif *bsvutil.WIF if decodedWif, err = bsvutil.DecodeWIF(wif); err != nil { - return "", err + return nil, err } // Sign the transaction signer := signature.InternalSigner{PrivateKey: decodedWif.PrivKey, SigHashFlag: 0} if err = tx.SignAuto(&signer); err != nil { - return "", err + return nil, err } // Return the transaction as a raw string - return tx.ToString(), nil + return tx, nil } diff --git a/transaction_test.go b/transaction_test.go index 38c44b5..f9570e6 100644 --- a/transaction_test.go +++ b/transaction_test.go @@ -87,5 +87,186 @@ func TestCreateTx(t *testing.T) { } // Show the results - t.Logf("created tx: %s", rawTx) + t.Logf("created tx: %s", rawTx.ToString()) +} + +// ExampleCreateTx example using CreateTx() +func ExampleCreateTx() { + + // Use a new UTXO + utxo := &Utxo{ + TxID: "b7b0650a7c3a1bd4716369783876348b59f5404784970192cec1996e86950576", + Vout: 0, + ScriptSig: "76a9149cbe9f5e72fa286ac8a38052d1d5337aa363ea7f88ac", + Satoshis: 1000, + } + + // Add a pay-to address + payTo := &PayToAddress{ + Address: "1C8bzHM8XFBHZ2ZZVvFy2NSoAZbwCXAicL", + Satoshis: 500, + } + + // Add some op return data + opReturn1 := OpReturnData{[]byte("prefix1"), []byte("example data"), []byte{0x13, 0x37}} + opReturn2 := OpReturnData{[]byte("prefix2"), []byte("more example data")} + + // Generate the TX + rawTx, err := CreateTx([]*Utxo{utxo}, []*PayToAddress{payTo}, []OpReturnData{opReturn1, opReturn2}, "L3VJH2hcRGYYG6YrbWGmsxQC1zyYixA82YjgEyrEUWDs4ALgk8Vu") + if err != nil { + fmt.Printf("error occurred: %s", err.Error()) + return + } + + fmt.Printf("rawTx: %s", rawTx.ToString()) + // Output:rawTx: 0100000001760595866e99c1ce920197844740f5598b34763878696371d41b3a7c0a65b0b7000000006b483045022100eea3d606bd1627be6459a9de4860919225db74843d2fc7f4e7caa5e01f42c2d0022017978d9c6a0e934955a70e7dda71d68cb614f7dd89eb7b9d560aea761834ddd4412102ea87d1fd77d169bd56a71e700628113d0f8dfe57faa0ba0e55a36f9ce8e10be3ffffffff03f4010000000000001976a9147a1980655efbfec416b2b0c663a7b3ac0b6a25d288ac00000000000000001a006a07707265666978310c6578616d706c65206461746102133700000000000000001c006a0770726566697832116d6f7265206578616d706c65206461746100000000 +} + +// BenchmarkCreateTx benchmarks the method CreateTx() +func BenchmarkCreateTx(b *testing.B) { + // Use a new UTXO + utxo := &Utxo{TxID: "b7b0650a7c3a1bd4716369783876348b59f5404784970192cec1996e86950576", Vout: 0, ScriptSig: "76a9149cbe9f5e72fa286ac8a38052d1d5337aa363ea7f88ac", Satoshis: 1000} + + // Add a pay-to address + payTo := &PayToAddress{Address: "1C8bzHM8XFBHZ2ZZVvFy2NSoAZbwCXAicL", Satoshis: 500} + + // Add some op return data + opReturn1 := OpReturnData{[]byte("prefix1"), []byte("example data"), []byte{0x13, 0x37}} + opReturn2 := OpReturnData{[]byte("prefix2"), []byte("more example data")} + + for i := 0; i < b.N; i++ { + _, _ = CreateTx([]*Utxo{utxo}, []*PayToAddress{payTo}, []OpReturnData{opReturn1, opReturn2}, "L3VJH2hcRGYYG6YrbWGmsxQC1zyYixA82YjgEyrEUWDs4ALgk8Vu") + } +} + +// TestCreateTxErrors will test the method CreateTx() +func TestCreateTxErrors(t *testing.T) { + + t.Parallel() + + // Create the list of tests + var tests = []struct { + inputUtxos []*Utxo + inputAddresses []*PayToAddress + inputOpReturns []OpReturnData + inputWif string + expectedRawTx string + expectedNil bool + expectedError bool + }{ + {[]*Utxo{{ + TxID: "b7b0650a7c3a1bd4716369783876348b59f5404784970192cec1996e86950576", + Vout: 0, + ScriptSig: "76a9149cbe9f5e72fa286ac8a38052d1d5337aa363ea7f88ac", + Satoshis: 1000, + }}, + []*PayToAddress{{ + Address: "1C8bzHM8XFBHZ2ZZVvFy2NSoAZbwCXAicL", + Satoshis: 500, + }}, + []OpReturnData{{[]byte("prefix1"), []byte("example data"), []byte{0x13, 0x37}}}, + "L3VJH2hcRGYYG6YrbWGmsxQC1zyYixA82YjgEyrEUWDs4ALgk8Vu", + "0100000001760595866e99c1ce920197844740f5598b34763878696371d41b3a7c0a65b0b7000000006b483045022100bd31b3d9fbe18468086c0470e99f096e370f0c6ff41b6bb71f1a1d5c1b068ce302204f0c83d792a40337909b8b1bcea192722161f48dc475c653b7c352baa38eea6c412102ea87d1fd77d169bd56a71e700628113d0f8dfe57faa0ba0e55a36f9ce8e10be3ffffffff02f4010000000000001976a9147a1980655efbfec416b2b0c663a7b3ac0b6a25d288ac00000000000000001a006a07707265666978310c6578616d706c65206461746102133700000000", + false, + false, + }, + {nil, + []*PayToAddress{{ + Address: "1C8bzHM8XFBHZ2ZZVvFy2NSoAZbwCXAicL", + Satoshis: 500, + }}, + []OpReturnData{{[]byte("prefix1"), []byte("example data"), []byte{0x13, 0x37}}}, + "L3VJH2hcRGYYG6YrbWGmsxQC1zyYixA82YjgEyrEUWDs4ALgk8Vu", + "", + true, + true, + }, + {[]*Utxo{{ + TxID: "b7b0650a7c3a1bd4716369783876348b59f5404784970192cec1996e86950576", + Vout: 0, + ScriptSig: "76a9149cbe9f5e72fa286ac8a38052d1d5337aa363ea7f88ac", + Satoshis: 1000, + }}, nil, + []OpReturnData{{[]byte("prefix1"), []byte("example data"), []byte{0x13, 0x37}}}, + "L3VJH2hcRGYYG6YrbWGmsxQC1zyYixA82YjgEyrEUWDs4ALgk8Vu", + "0100000001760595866e99c1ce920197844740f5598b34763878696371d41b3a7c0a65b0b7000000006a47304402205ba1a246371bf8db3fb6dfa75e1edaa18b6b86dc1775dc3f2aa3c38f22803ccc022057850f794ebf78e542228d301420d4ec896c30a2bc009b7e55c66120f6c5a57a412102ea87d1fd77d169bd56a71e700628113d0f8dfe57faa0ba0e55a36f9ce8e10be3ffffffff0100000000000000001a006a07707265666978310c6578616d706c65206461746102133700000000", + false, + false, + }, + {[]*Utxo{{ + TxID: "b7b0650a7c3a1bd4716369783876348b59f5404784970192cec1996e86950576", + Vout: 0, + ScriptSig: "76a9149cbe9f5e72fa286ac8a38052d1d5337aa363ea7f88ac", + Satoshis: 1000, + }}, nil, + nil, + "L3VJH2hcRGYYG6YrbWGmsxQC1zyYixA82YjgEyrEUWDs4ALgk8Vu", + "0100000001760595866e99c1ce920197844740f5598b34763878696371d41b3a7c0a65b0b7000000006a47304402200083bb297d53210cf9379b3f47de2eff38e6906e5982fbfeef9bf59778750f3e022046da020811e9a2d1e6db8da103d17598abc194125612be6b108d49cb60cbca95412102ea87d1fd77d169bd56a71e700628113d0f8dfe57faa0ba0e55a36f9ce8e10be3ffffffff0000000000", + false, + false, + }, + {[]*Utxo{{ + TxID: "b7b0650a7c3a1bd4716369783876348b59f5404784970192cec1996e86950576", + Vout: 0, + ScriptSig: "76a9149cbe9f5e72fa286ac8a38052d1d5337aa363ea7f88ac", + Satoshis: 1000, + }}, + []*PayToAddress{{ + Address: "1C8bzHM8XFBHZ2ZZVvFy2NSoAZbwCXAicL", + Satoshis: 500, + }}, + []OpReturnData{{[]byte("prefix1"), []byte("example data"), []byte{0x13, 0x37}}}, + "", + "", + true, + true, + }, + {[]*Utxo{{ + TxID: "b7b0650a7c3a1bd4716369783876348b59f5404784970192cec1996e86950576", + Vout: 0, + ScriptSig: "invalid-script", + Satoshis: 1000, + }}, + []*PayToAddress{{ + Address: "1C8bzHM8XFBHZ2ZZVvFy2NSoAZbwCXAicL", + Satoshis: 500, + }}, + []OpReturnData{{[]byte("prefix1"), []byte("example data"), []byte{0x13, 0x37}}}, + "L3VJH2hcRGYYG6YrbWGmsxQC1zyYixA82YjgEyrEUWDs4ALgk8Vu", + "", + true, + true, + }, + {[]*Utxo{{ + TxID: "b7b0650a7c3a1bd4716369783876348b59f5404784970192cec1996e86950576", + Vout: 0, + ScriptSig: "76a9149cbe9f5e72fa286ac8a38052d1d5337aa363ea7f88ac", + Satoshis: 1000, + }}, + []*PayToAddress{{ + Address: "invalid-address", + Satoshis: 500, + }}, + []OpReturnData{{[]byte("prefix1"), []byte("example data"), []byte{0x13, 0x37}}}, + "L3VJH2hcRGYYG6YrbWGmsxQC1zyYixA82YjgEyrEUWDs4ALgk8Vu", + "", + true, + true, + }, + } + + // Run tests + for _, test := range tests { + if rawTx, err := CreateTx(test.inputUtxos, test.inputAddresses, test.inputOpReturns, test.inputWif); err != nil && !test.expectedError { + t.Errorf("%s Failed: [%v] [%v] [%v] [%s] inputted and error not expected but got: %s", t.Name(), test.inputUtxos, test.inputAddresses, test.inputOpReturns, test.inputWif, err.Error()) + } else if err == nil && test.expectedError { + t.Errorf("%s Failed: [%v] [%v] [%v] [%s] inputted and error was expected", t.Name(), test.inputUtxos, test.inputAddresses, test.inputOpReturns, test.inputWif) + } else if rawTx == nil && !test.expectedNil { + t.Errorf("%s Failed: [%v] [%v] [%v] [%s] inputted and nil was not expected", t.Name(), test.inputUtxos, test.inputAddresses, test.inputOpReturns, test.inputWif) + } else if rawTx != nil && test.expectedNil { + t.Errorf("%s Failed: [%v] [%v] [%v] [%s] inputted and nil was expected", t.Name(), test.inputUtxos, test.inputAddresses, test.inputOpReturns, test.inputWif) + } else if rawTx != nil && rawTx.ToString() != test.expectedRawTx { + t.Errorf("%s Failed: [%v] [%v] [%v] [%s] inputted [%s] expected but failed comparison of scripts, got: %s", t.Name(), test.inputUtxos, test.inputAddresses, test.inputOpReturns, test.inputWif, test.expectedRawTx, rawTx.ToString()) + } + } }