diff --git a/README.md b/README.md index c6012b586..628c5124f 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,8 @@ validate := validator.New(validator.WithRequiredStructEnabled()) | bcp47_language_tag | Language tag (BCP 47) | | btc_addr | Bitcoin Address | | btc_addr_bech32 | Bitcoin Bech32 Address (segwit) | +| trx_addr | Tron Address | +| eth_addr | Ethereum Address | | credit_card | Credit Card Number | | mongodb | MongoDB ObjectID | | mongodb_connection_string | MongoDB Connection String | @@ -180,7 +182,6 @@ validate := validator.New(validator.WithRequiredStructEnabled()) | e164 | e164 formatted phone number | | ein | U.S. Employeer Identification Number | | email | E-mail String -| eth_addr | Ethereum Address | | hexadecimal | Hexadecimal String | | hexcolor | Hexcolor String | | hsl | HSL String | diff --git a/baked_in.go b/baked_in.go index 7fcc6737a..f71bffcc4 100644 --- a/baked_in.go +++ b/baked_in.go @@ -166,6 +166,7 @@ var ( "eth_addr_checksum": isEthereumAddressChecksum, "btc_addr": isBitcoinAddress, "btc_addr_bech32": isBitcoinBech32Address, + "trx_addr": isTronAddress, "uuid": isUUID, "uuid3": isUUID3, "uuid4": isUUID4, @@ -874,6 +875,47 @@ func isBitcoinBech32Address(fl FieldLevel) bool { return true } +// isTronAddress is the validation function for validating if the field's value is a valid Tron address. +func isTronAddress(fl FieldLevel) bool { + address := fl.Field().String() + + if !trxAddressRegex().MatchString(address) { + return false + } + + alphabet := []byte("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz") + + decode := [25]byte{} + + for _, n := range []byte(address) { + d := bytes.IndexByte(alphabet, n) + + for i := 24; i >= 0; i-- { + d += 58 * int(decode[i]) + decode[i] = byte(d % 256) + d /= 256 + } + } + + if decode[0] != 0x41 { + return false + } + + h := sha256.New() + _, _ = h.Write(decode[:21]) + d := h.Sum(nil) + h = sha256.New() + _, _ = h.Write(d) + + validchecksum := [4]byte{} + computedchecksum := [4]byte{} + + copy(computedchecksum[:], h.Sum(d[:0])) + copy(validchecksum[:], decode[21:]) + + return validchecksum == computedchecksum +} + // excludesRune is the validation function for validating that the field's value does not contain the rune specified within the param. func excludesRune(fl FieldLevel) bool { return !containsRune(fl) diff --git a/doc.go b/doc.go index 7b9e77c65..713c38d43 100644 --- a/doc.go +++ b/doc.go @@ -1020,6 +1020,14 @@ The format of the string is checked to ensure it matches the standard Ethereum a Usage: eth_addr +# Tron Address + +This validates that a string value contains a valid TRON address. +The address is decoded using Base58Check encoding and validated by verifying +the version byte and checksum. + + Usage: trx_addr + # Contains This validates that a string value contains the substring value. diff --git a/regexes.go b/regexes.go index 5cf063534..3ce7d269b 100644 --- a/regexes.go +++ b/regexes.go @@ -64,6 +64,7 @@ const ( ethAddressRegexString = `^0x[0-9a-fA-F]{40}$` ethAddressUpperRegexString = `^0x[0-9A-F]{40}$` ethAddressLowerRegexString = `^0x[0-9a-f]{40}$` + trxAddressRegexString = `^T[1-9A-HJ-NP-Za-km-z]{33}$` uRLEncodedRegexString = `^(?:[^%]|%[0-9A-Fa-f]{2})*$` hTMLEncodedRegexString = `&#[x]?([0-9a-fA-F]{2})|(>)|(<)|(")|(&)+[;]?` hTMLRegexString = `<[/]?([a-zA-Z]+).*?>` @@ -151,6 +152,7 @@ var ( btcUpperAddressRegexBech32 = lazyRegexCompile(btcAddressUpperRegexStringBech32) btcLowerAddressRegexBech32 = lazyRegexCompile(btcAddressLowerRegexStringBech32) ethAddressRegex = lazyRegexCompile(ethAddressRegexString) + trxAddressRegex = lazyRegexCompile(trxAddressRegexString) uRLEncodedRegex = lazyRegexCompile(uRLEncodedRegexString) hTMLEncodedRegex = lazyRegexCompile(hTMLEncodedRegexString) hTMLRegex = lazyRegexCompile(hTMLRegexString) diff --git a/validator_test.go b/validator_test.go index b46186e0f..186eab808 100644 --- a/validator_test.go +++ b/validator_test.go @@ -6470,6 +6470,63 @@ func TestBitcoinBech32AddressValidation(t *testing.T) { } } +func TestTronAddressValidation(t *testing.T) { + validate := New() + + tests := []struct { + param string + expected bool + }{ + {"", false}, + {" ", false}, + {"\n", false}, + {"\t", false}, + {"TRjE1H8dx", false}, + {"T123", false}, + {"TRjE1H8dxfmem1NZRdsmek9wo7huR4bdNzFkeodme5", false}, + {"TRjE1H8dxfmem1NZRdsmek9wo7", false}, + {"TRjE1H8dxfmem1NZRdsmek9wo7huR4bdN", false}, + {"asjE1H8dxfmem1NZRdsmek9wo7huR4bdNz", false}, + {"tRjE1H8dxfmem1NZRdsmek9wo7huR4bdNz", false}, + {"1RjE1H8dxfmem1NZRdsmek9wo7huR4bdNz", false}, + {"TRjE1H8dxfmem1NZRdsmek9wo7huR4bdN0", false}, + {"TRjE1H8dxfmem1NZRdsmek9wo7huR4bdNO", false}, + {"TRjE1H8dxfmem1NZRdsmek9wo7huR4bdNI", false}, + {"TRjE1H8dxfmem1NZRdsmek9wo7huR4bdNl", false}, + {" TQRyXh7Rzec4udkfmPc9izomNkrZBrKeFh", false}, + {"TQRyXh7Rzec4udkfmPc9izomNkrZBrKeFh ", false}, + {"\nTQRyXh7Rzec4udkfmPc9izomNkrZBrKeFh", false}, + {"TQRyXh7Rzec4udkfmPc9izomNkrZBrKeFâ˜ș", false}, + {"3ALJH9Y951VCGcVZYAdpA3KchoP9McEj1G", false}, + {"12KYrjTdVGjFMtaxERSk3gphreJ5US8aUP", false}, + {"12QeMLzSrB8XH8FvEzPMVoRxVAzTr5XM2y", false}, + {"TQRyXh7Rzec4udkaVPc9izomNkrZBrKeFh", true}, + {"TRjE1H8dxypKM1NZRdysbs9wo7huR4bdNz", true}, + {"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", true}, + {"TDiikeBPMsHwcq5NSDvHgPiUVDwsEDxmDN", true}, + {"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", true}, + } + + for i, test := range tests { + errs := validate.Var(test.param, "trx_addr") + + if test.expected { + if !IsEqual(errs, nil) { + t.Fatalf("Index: %d trx_addr failed with Error: %s", i, errs) + } + } else { + if IsEqual(errs, nil) { + t.Fatalf("Index: %d trx_addr failed with Error: %s", i, errs) + } else { + val := getError(errs, "", "") + if val.Tag() != "trx_addr" { + t.Fatalf("Index: %d Latitude failed with Error: %s", i, errs) + } + } + } + } +} + func TestNoStructLevelValidation(t *testing.T) { type Inner struct { Test string `validate:"len=5"`