diff --git a/README.md b/README.md index cf17cf5..fa26587 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,11 @@ View the generated [documentation](https://pkg.go.dev/github.com/bitcoinschema/g ### Features - [Sign](sign.go) / [Verify](verify.go) Bitcoin Message -- [Private key (string) to Address (string)](address.go) -- [Address from Private key (bsvec.PrivateKey)](address.go) -- [Create Private Key](private_key.go) -- [Create PubKey From Private Key](pubkey.go) +- [PrivateKey (string) to Address (string)](address.go) +- [Address from PrivateKey (bsvec.PrivateKey)](address.go) +- [Address from Script](address.go) +- [Create PrivateKey](private_key.go) +- [Create PubKey from PrivateKey](pubkey.go)
diff --git a/address.go b/address.go index b3bd0cd..ad3b855 100644 --- a/address.go +++ b/address.go @@ -2,6 +2,7 @@ package bitcoin import ( "bytes" + "encoding/hex" "errors" "fmt" @@ -9,6 +10,7 @@ import ( "github.com/bitcoinsv/bsvd/bsvec" "github.com/bitcoinsv/bsvd/chaincfg" + "github.com/bitcoinsv/bsvd/txscript" "github.com/bitcoinsv/bsvutil" ) @@ -72,6 +74,22 @@ func (a *A25) ComputeChecksum() (c [4]byte) { return } +// ValidA58 validates a base58 encoded bitcoin address. An address is valid +// if it can be decoded into a 25 byte address, the version number is 0, +// and the checksum validates. Return value ok will be true for valid +// addresses. If ok is false, the address is invalid and the error value +// may indicate why. +func ValidA58(a58 []byte) (bool, error) { + var a A25 + if err := a.Set58(a58); err != nil { + return false, err + } + if a.Version() != 0 { + return false, errors.New("not version 0") + } + return a.EmbeddedChecksum() == a.ComputeChecksum(), nil +} + // AddressFromPrivateKey takes a private key string and returns a Bitcoin address func AddressFromPrivateKey(privateKey string) (string, error) { rawKey, err := PrivateKeyFromString(privateKey) @@ -95,18 +113,40 @@ func AddressFromPubKey(publicKey *bsvec.PublicKey) (*bsvutil.LegacyAddressPubKey return bsvutil.NewLegacyAddressPubKeyHash(bsvutil.Hash160(publicKey.SerializeCompressed()), &chaincfg.MainNetParams) } -// ValidA58 validates a base58 encoded bitcoin address. An address is valid -// if it can be decoded into a 25 byte address, the version number is 0, -// and the checksum validates. Return value ok will be true for valid -// addresses. If ok is false, the address is invalid and the error value -// may indicate why. -func ValidA58(a58 []byte) (bool, error) { - var a A25 - if err := a.Set58(a58); err != nil { - return false, err +// AddressFromScript will take an output script and extract a standard bitcoin address +func AddressFromScript(script string) (string, error) { + + // No script? + if len(script) == 0 { + return "", errors.New("missing script") } - if a.Version() != 0 { - return false, errors.New("not version 0") + + // Decode the hex string into bytes + scriptBytes, err := hex.DecodeString(script) + if err != nil { + return "", err } - return a.EmbeddedChecksum() == a.ComputeChecksum(), nil + + // Extract the components from the script + var addresses []bsvutil.Address + _, addresses, _, err = txscript.ExtractPkScriptAddrs(scriptBytes, &chaincfg.MainNetParams) + if err != nil { + return "", err + } + + // Missing an address? + if len(addresses) == 0 { + // This error case should not occur since the error above will occur when no address is found, + // however we ensure that we have an address for the NewLegacyAddressPubKeyHash() below + return "", fmt.Errorf("invalid output script, missing an address") + } + + // Extract the address from the pubkey hash + var address *bsvutil.LegacyAddressPubKeyHash + if address, err = bsvutil.NewLegacyAddressPubKeyHash(addresses[0].ScriptAddress(), &chaincfg.MainNetParams); err != nil { + return "", err + } + + // Use the encoded version of the address + return address.EncodeAddress(), nil } diff --git a/address_test.go b/address_test.go index da6351b..2f1ec36 100644 --- a/address_test.go +++ b/address_test.go @@ -177,3 +177,55 @@ func BenchmarkAddressFromPubKey(b *testing.B) { _, _ = AddressFromPubKey(pubKey) } } + +// TestAddressFromScript will test the method AddressFromScript() +func TestAddressFromScript(t *testing.T) { + t.Parallel() + + // Create the list of tests + var tests = []struct { + inputScript string + expectedAddress string + expectedError bool + }{ + {"", "", true}, + {"0", "", true}, + {"76a914b424110292f4ea2ac92beb9e83cf5e6f0fa2996388ac", "1HRVqUGDzpZSMVuNSZxJVaB9xjneEShfA7", false}, + {"76a914b424110292f4ea2ac92beb9e83cf5e6f0fa2", "", true}, + {"76a914b424110292f4ea2ac92beb9e83", "", true}, + {"76a914b424110292f", "", true}, + {"1HRVqUGDzpZSMVuNSZxJVaB9xjneEShfA7", "", true}, + {"514104cc71eb30d653c0c3163990c47b976f3fb3f37cccdcbedb169a1dfef58bbfbfaff7d8a473e7e2e6d317b87bafe8bde97e3cf8f065dec022b51d11fcdd0d348ac4410461cbdcc5409fb4b4d42b51d33381354d80e550078cb532a34bfa2fcfdeb7d76519aecc62770f5b0e4ef8551946d8a540911abe3e7854a26f39f58b25c15342af52ae", "", true}, + {"410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3", "", true}, + {"47304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901", "", true}, + } + + // Run tests + for _, test := range tests { + if address, err := AddressFromScript(test.inputScript); err != nil && !test.expectedError { + t.Errorf("%s Failed: [%v] inputted and error not expected but got: %s", t.Name(), test.inputScript, err.Error()) + } else if err == nil && test.expectedError { + t.Errorf("%s Failed: [%v] inputted and error was expected", t.Name(), test.inputScript) + } else if address != test.expectedAddress { + t.Errorf("%s Failed: [%v] inputted [%s] expected but failed comparison of addresses, got: %s", t.Name(), test.inputScript, test.expectedAddress, address) + } + } +} + +// ExampleAddressFromScript example using AddressFromScript() +func ExampleAddressFromScript() { + address, err := AddressFromScript("76a914b424110292f4ea2ac92beb9e83cf5e6f0fa2996388ac") + if err != nil { + fmt.Printf("error occurred: %s", err.Error()) + return + } + fmt.Printf("address found: %s", address) + // Output:address found: 1HRVqUGDzpZSMVuNSZxJVaB9xjneEShfA7 +} + +// BenchmarkAddressFromScript benchmarks the method AddressFromScript() +func BenchmarkAddressFromScript(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = AddressFromScript("76a914b424110292f4ea2ac92beb9e83cf5e6f0fa2996388ac") + } +}