diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba4f05c --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +.vscode diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1734a34 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 StringSquirrel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b116706 --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +test: + go test -race -timeout 30s `go list ./... | grep -v /vendor/` diff --git a/README.md b/README.md new file mode 100644 index 0000000..4b5ce1d --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# crptwav + +Simple wallet address validator for validating Bitcoin and other altcoins addresses. + +It is a port of JavaScript library [https://github.com/ognus/wallet-address-validator](https://github.com/ognus/wallet-address-validator). + +## Example + +```go +package main + +import ( + "fmt" + "github.com/strsqr/crptwav" +) + +func main() { + fmt.Println(crptwav.IsValid("0xE37c0D48d68da5c5b14E5c1a9f1CFE802776D9FF", "ETH", "both")) +} +``` + +### Supported crypto currencies (todo) + +- [ ] Auroracoin/AUR, `'auroracoin'` or `'AUR'` +- [ ] BeaverCoin/BVC, `'beavercoin'` or `'BVC'` +- [ ] Biocoin/BIO, `'biocoin'` or `'BIO'` +- [ ] Bitcoin/BTC, `'bitcoin'` or `'BTC'` +- [ ] BitcoinCash/BCH, `'bitcoincash'` or `'BCH'` +- [ ] BitcoinGold/BTG, `'bitcoingold'` or `'BTG'` +- [ ] BitcoinPrivate/BTCP, `'bitcoinprivate'` or `'BTCP'` +- [ ] BitcoinZ/BTCZ, `'bitcoinz'` or `'BTCZ'` +- [x] Callisto/CLO, `'callisto'` or `'CLO'` +- [ ] Dash, `'dash'` or `'DASH'` +- [ ] Decred/DCR, `'decred'` or `'DCR'` +- [ ] Digibyte/DGB, `'digibyte'` or `'DGB'` +- [ ] Dogecoin/DOGE, `'dogecoin'` or `'DOGE'` +- [x] Ethereum/ETH, `'ethereum'` or `'ETH'` +- [x] EthereumClassic/ETH, `'ethereumclassic'` or `'ETC'` +- [x] EthereumZero/ETZ, `'etherzero'` or `'ETZ'` +- [ ] Freicoin/FRC, `'freicoin'` or `'FRC'` +- [ ] Garlicoin/GRLC, `'garlicoin'` or `'GRLC'` +- [ ] Hush/HUSH, `'hush'` or `'HUSH'` +- [ ] Komodo/KMD, `'komodo'` or `'KMD'` +- [ ] Litecoin/LTC, `'litecoin'` or `'LTC'` +- [ ] Megacoin/MEC, `'megacoin'` or `'MEC'` +- [ ] Namecoin/NMC, `'namecoin'` or `'NMC'` +- [ ] NEO/NEO, `'NEO'` or `'NEO'` +- [ ] NeoGas/GAS, `'neogas'` or `'GAS'` +- [ ] Peercoin/PPCoin/PPC, `'peercoin'` or `'PPC'` +- [ ] Primecoin/XPM, `'primecoin'` or `'XPM'` +- [ ] Protoshares/PTS, `'protoshares'` or `'PTS'` +- [ ] Qtum/QTUM, `'qtum'` or `'QTUM'` +- [ ] Ripple/XRP, `'ripple'` or `'XRP'` +- [ ] Snowgem/SNG, `'snowgem'` or `'SNG'` +- [ ] Vertcoin/VTC, `'vertcoin'` or `'VTC'` +- [ ] Votecoin/VTC, `'votecoin'` or `'VOT'` +- [ ] Zcash/ZEC, `'zcash'` or `'ZEC'` +- [ ] Zclassic/ZCL, `'zclassic'` or `'ZCL'` +- [ ] ZenCash/ZEN, `'zencash'` or `'ZEN'` + +## License + +[MIT](LICENSE) diff --git a/crptv.go b/crptv.go new file mode 100644 index 0000000..9cf9d5b --- /dev/null +++ b/crptv.go @@ -0,0 +1,18 @@ +package crptwav + +import "strings" + +// IsValid validates the given currency address. +// address - Wallet address to validate. +// currency - Currency name or symbol, e.g. "bitcoin", "litecoin" or "ETH". +// network - Use "prod" to enforce standard address, "testnet" to enforce +// testnet address and "both" to enforce nothing. +func IsValid(address, currency, network string) bool { + nameOrSymbol := strings.ToLower(currency) + for _, c := range currencies { + if c.name == nameOrSymbol || c.symbol == nameOrSymbol { + return c.validator(address, network) + } + } + return false +} diff --git a/crypto/crypto.go b/crypto/crypto.go new file mode 100644 index 0000000..db71227 --- /dev/null +++ b/crypto/crypto.go @@ -0,0 +1,10 @@ +package crypto + +import "golang.org/x/crypto/sha3" + +// Keccak256 calculates and returns the Keccak256 hash of the input data. +func Keccak256(b []byte) []byte { + h := sha3.NewLegacyKeccak256() + h.Write(b) + return h.Sum(nil) +} diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go new file mode 100644 index 0000000..0e097a8 --- /dev/null +++ b/crypto/crypto_test.go @@ -0,0 +1,63 @@ +package crypto + +import ( + "encoding/hex" + "strings" + "testing" +) + +func TestKeccak256(t *testing.T) { + tt := []struct { + desc string + input []byte + repeat int // input will be concatenated the input this many times. + want string + }{ + // Inputs of 8, 248, and 264 bits from http://keccak.noekeon.org/ are included below. + { + desc: "short-8b", + input: decodeHex("CC"), + repeat: 1, + want: "EEAD6DBFC7340A56CAEDC044696A168870549A6A7F6F56961E84A54BD9970B8A", + }, + { + desc: "short-248b", + input: decodeHex("84FB51B517DF6C5ACCB5D022F8F28DA09B10232D42320FFC32DBECC3835B29"), + repeat: 1, + want: "D477FB02CAAA95B3280EC8EE882C29D9E8A654B21EF178E0F97571BF9D4D3C1C", + }, + { + desc: "short-264b", + input: decodeHex("DE8F1B3FAA4B7040ED4563C3B8E598253178E87E4D0DF75E4FF2F2DEDD5A0BE046"), + repeat: 1, + want: "E78C421E6213AFF8DE1F025759A4F2C943DB62BBDE359C8737E19B3776ED2DD2", + }, + // The computed test vector is 64 MiB long and is a truncated version + // of the ExtremelyLongMsgKAT taken from http://keccak.noekeon.org/. + { + desc: "long-64MiB", + input: []byte("abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmno"), + repeat: 1024 * 1024, + want: "5015A4935F0B51E091C6550A94DCD262C08998232CCAA22E7F0756DEAC0DC0D0", + }, + } + for _, tc := range tt { + input := []byte{} + for i := 0; i < tc.repeat; i++ { + input = append(input, tc.input...) + } + got := strings.ToUpper(hex.EncodeToString(Keccak256(input))) + if got != tc.want { + t.Errorf("%s, got %q, want %q", tc.desc, got, tc.want) + } + } +} + +// decodeHex converts an hex-encoded string into a raw byte string. +func decodeHex(s string) []byte { + b, err := hex.DecodeString(s) + if err != nil { + panic(err) + } + return b +} diff --git a/currencies.go b/currencies.go new file mode 100644 index 0000000..be6636f --- /dev/null +++ b/currencies.go @@ -0,0 +1,36 @@ +package crptwav + +type currency struct { + name string + symbol string + validator func(address, network string) bool +} + +// It defines P2PKH and P2SH address types for standard (prod) +// and testnet networks. +var currencies = []currency{ + currency{ + name: "ethereum", + symbol: "eth", + validator: ethValidator, + }, + currency{ + name: "etherzero", + symbol: "etz", + validator: ethValidator, + }, + currency{ + name: "ethereumclassic", + symbol: "etc", + validator: ethValidator, + }, + currency{ + name: "callisto", + symbol: "clo", + validator: ethValidator, + }, +} + +func ethValidator(address, network string) bool { + return isValidETH(address) +} diff --git a/ethereum.go b/ethereum.go new file mode 100644 index 0000000..0a91079 --- /dev/null +++ b/ethereum.go @@ -0,0 +1,51 @@ +package crptwav + +import ( + "encoding/hex" + "regexp" + "strconv" + "strings" + + "github.com/strsqr/crptwav/crypto" +) + +func isValidETH(address string) bool { + baseRequirements, _ := regexp.Compile("^0x[0-9a-fA-F]{40}$") + if !baseRequirements.MatchString(address) { + return false + } + + allSmall, _ := regexp.Compile("^0x[0-9a-f]{40}$") + if allSmall.MatchString(address) { + return true + } + + allCaps, _ := regexp.Compile("^0x?[0-9A-F]{40}$") + if allCaps.MatchString(address) { + return true + } + + // Otherwise check each case + return isValidChecksum(address) +} + +func isValidChecksum(address string) bool { + address = strings.Replace(address, "0x", "", -1) + + hash := hex.EncodeToString(crypto.Keccak256([]byte(strings.ToLower(address)))) + + for i := 0; i < 40; i++ { + num, _ := strconv.ParseInt(string(hash[i]), 16, 8) + sign := string(address[i]) + + if num > 7 && strings.ToUpper(sign) != sign { + return false + } + + if num <= 7 && strings.ToLower(sign) != sign { + return false + } + } + + return true +} diff --git a/ethereum_test.go b/ethereum_test.go new file mode 100644 index 0000000..84f30af --- /dev/null +++ b/ethereum_test.go @@ -0,0 +1,34 @@ +package crptwav + +import "testing" + +func TestIsValid(t *testing.T) { + tt := []struct { + currency string + address string + }{ + {address: "0xE37c0D48d68da5c5b14E5c1a9f1CFE802776D9FF", currency: "ethereum"}, + {address: "0xa00354276d2fC74ee91e37D085d35748613f4748", currency: "ethereum"}, + {address: "0xAff4d6793F584a473348EbA058deb8caad77a288", currency: "ETH"}, + {address: "0xc6d9d2cd449a754c494264e1809c50e34d64562b", currency: "ETH"}, + {address: "0x52908400098527886E0F7030069857D2E4169EE7", currency: "ETH"}, + {address: "0x8617E340B3D01FA5F11F306F4090FD50E238070D", currency: "ETH"}, + {address: "0xde709f2102306220921060314715629080e2fb77", currency: "ETH"}, + {address: "0x27b1fdb04752bbc536007a920d24acb045561c26", currency: "ETH"}, + {address: "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", currency: "ETH"}, + {address: "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359", currency: "ETH"}, + {address: "0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB", currency: "ETH"}, + {address: "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb", currency: "ETH"}, + {address: "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb", currency: "ethereumclassic"}, + {address: "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb", currency: "ETC"}, + {address: "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb", currency: "etherzero"}, + {address: "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb", currency: "ETZ"}, + {address: "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb", currency: "callisto"}, + {address: "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb", currency: "CLO"}, + } + for _, tc := range tt { + if !IsValid(tc.address, tc.currency, "both") { + t.Errorf("Address %s should be valid %s address", tc.address, tc.currency) + } + } +}