From a1f81e60a82911bce485af3b06a4ba407c607ce2 Mon Sep 17 00:00:00 2001 From: Andrey Drozd Date: Thu, 9 Nov 2017 19:37:03 +0300 Subject: [PATCH 01/65] prepare for laravel --- README.md | 2 +- composer.json | 4 +- .../CryptocurrencyValidatorNotFound.php | 12 + src/Utils/Sha3.php | 391 ++++++++++++++++++ src/Validation.php | 106 +++-- src/Validation/BTC.php | 4 +- src/Validation/DASH.php | 4 +- src/Validation/ETH.php | 53 +++ src/Validation/LTC.php | 14 +- tests/BTCTest.php | 2 +- tests/DASHTest.php | 2 +- tests/LTCTest.php | 2 +- 12 files changed, 541 insertions(+), 55 deletions(-) create mode 100644 src/Exception/CryptocurrencyValidatorNotFound.php create mode 100644 src/Utils/Sha3.php create mode 100644 src/Validation/ETH.php diff --git a/README.md b/README.md index 13d9f13..bf4f333 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ One day I will add other crypto currencies. Or how about you? :) ```php -use Murich\PhpCryptocurrencyAddressValidation\Validation\BTC as BTCValidator; +use Merkeleon\PhpCryptocurrencyAddressValidation\Validation\BTC as BTCValidator; $validator = new BTCValidator('1QLbGuc3WGKKKpLs4pBp9H6jiQ2MgPkXRp'); var_dump($validator->validate()); diff --git a/composer.json b/composer.json index 9e9ed7d..28c5b97 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "murich/php-cryptocurrency-address-validation", + "name": "Merkeleon/php-cryptocurrency-address-validation", "description": "Cryptocurrency address validation. Currently supports litecoin and bitcoin.", "authors": [ { @@ -13,7 +13,7 @@ }, "autoload": { "psr-4": { - "Murich\\PhpCryptocurrencyAddressValidation\\": "src" + "Merkeleon\\PhpCryptocurrencyAddressValidation\\": "src" } } } diff --git a/src/Exception/CryptocurrencyValidatorNotFound.php b/src/Exception/CryptocurrencyValidatorNotFound.php new file mode 100644 index 0000000..fcfaff4 --- /dev/null +++ b/src/Exception/CryptocurrencyValidatorNotFound.php @@ -0,0 +1,12 @@ +> 31)) & (0xFFFFFFFF), + $bc[($i + 4) % 5][1] ^ (($bc[($i + 1) % 5][1] << 1) | ($bc[($i + 1) % 5][0] >> 31)) & (0xFFFFFFFF) + ]; + for ($j = 0; $j < 25; $j += 5) + { + $st[$j + $i] = [ + $st[$j + $i][0] ^ $t[0], + $st[$j + $i][1] ^ $t[1] + ]; + } + } + // Rho Pi + $t = $st[1]; + for ($i = 0; $i < 24; $i++) + { + $j = self::$keccakf_piln[$i]; + $bc[0] = $st[$j]; + $n = self::$keccakf_rotc[$i]; + $hi = $t[0]; + $lo = $t[1]; + if ($n >= 32) + { + $n -= 32; + $hi = $t[1]; + $lo = $t[0]; + } + $st[$j] = [ + (($hi << $n) | ($lo >> (32 - $n))) & (0xFFFFFFFF), + (($lo << $n) | ($hi >> (32 - $n))) & (0xFFFFFFFF) + ]; + $t = $bc[0]; + } + // Chi + for ($j = 0; $j < 25; $j += 5) + { + for ($i = 0; $i < 5; $i++) + { + $bc[$i] = $st[$j + $i]; + } + for ($i = 0; $i < 5; $i++) + { + $st[$j + $i] = [ + $st[$j + $i][0] ^ ~$bc[($i + 1) % 5][0] & $bc[($i + 2) % 5][0], + $st[$j + $i][1] ^ ~$bc[($i + 1) % 5][1] & $bc[($i + 2) % 5][1] + ]; + } + } + // Iota + $st[0] = [ + $st[0][0] ^ $keccakf_rndc[$round][0], + $st[0][1] ^ $keccakf_rndc[$round][1] + ]; + } + } + + private static function keccak64($in_raw, $capacity, $outputlength, $suffix, $raw_output) + { + $capacity /= 8; + $inlen = self::ourStrlen($in_raw); + $rsiz = 200 - 2 * $capacity; + $rsizw = $rsiz / 8; + $st = []; + for ($i = 0; $i < 25; $i++) + { + $st[] = [0, 0]; + } + for ($in_t = 0; $inlen >= $rsiz; $inlen -= $rsiz, $in_t += $rsiz) + { + for ($i = 0; $i < $rsizw; $i++) + { + $t = unpack('V*', self::ourSubstr($in_raw, $i * 8 + $in_t, 8)); + $st[$i] = [ + $st[$i][0] ^ $t[2], + $st[$i][1] ^ $t[1] + ]; + } + self::keccakf64($st, self::KECCAK_ROUNDS); + } + $temp = self::ourSubstr($in_raw, $in_t, $inlen); + $temp = str_pad($temp, $rsiz, "\x0", STR_PAD_RIGHT); + $temp[$inlen] = chr($suffix); + $temp[$rsiz - 1] = chr($temp[$rsiz - 1] | 0x80); + for ($i = 0; $i < $rsizw; $i++) + { + $t = unpack('V*', self::ourSubstr($temp, $i * 8, 8)); + $st[$i] = [ + $st[$i][0] ^ $t[2], + $st[$i][1] ^ $t[1] + ]; + } + self::keccakf64($st, self::KECCAK_ROUNDS); + $out = ''; + for ($i = 0; $i < 25; $i++) + { + $out .= $t = pack('V*', $st[$i][1], $st[$i][0]); + } + $r = self::ourSubstr($out, 0, $outputlength / 8); + + return $raw_output ? $r : bin2hex($r); + } + + private static function keccakf32(&$st, $rounds) + { + $keccakf_rndc = [ + [0x0000, 0x0000, 0x0000, 0x0001], [0x0000, 0x0000, 0x0000, 0x8082], [0x8000, 0x0000, 0x0000, 0x0808a], [0x8000, 0x0000, 0x8000, 0x8000], + [0x0000, 0x0000, 0x0000, 0x808b], [0x0000, 0x0000, 0x8000, 0x0001], [0x8000, 0x0000, 0x8000, 0x08081], [0x8000, 0x0000, 0x0000, 0x8009], + [0x0000, 0x0000, 0x0000, 0x008a], [0x0000, 0x0000, 0x0000, 0x0088], [0x0000, 0x0000, 0x8000, 0x08009], [0x0000, 0x0000, 0x8000, 0x000a], + [0x0000, 0x0000, 0x8000, 0x808b], [0x8000, 0x0000, 0x0000, 0x008b], [0x8000, 0x0000, 0x0000, 0x08089], [0x8000, 0x0000, 0x0000, 0x8003], + [0x8000, 0x0000, 0x0000, 0x8002], [0x8000, 0x0000, 0x0000, 0x0080], [0x0000, 0x0000, 0x0000, 0x0800a], [0x8000, 0x0000, 0x8000, 0x000a], + [0x8000, 0x0000, 0x8000, 0x8081], [0x8000, 0x0000, 0x0000, 0x8080], [0x0000, 0x0000, 0x8000, 0x00001], [0x8000, 0x0000, 0x8000, 0x8008] + ]; + $bc = []; + for ($round = 0; $round < $rounds; $round++) + { + // Theta + for ($i = 0; $i < 5; $i++) + { + $bc[$i] = [ + $st[$i][0] ^ $st[$i + 5][0] ^ $st[$i + 10][0] ^ $st[$i + 15][0] ^ $st[$i + 20][0], + $st[$i][1] ^ $st[$i + 5][1] ^ $st[$i + 10][1] ^ $st[$i + 15][1] ^ $st[$i + 20][1], + $st[$i][2] ^ $st[$i + 5][2] ^ $st[$i + 10][2] ^ $st[$i + 15][2] ^ $st[$i + 20][2], + $st[$i][3] ^ $st[$i + 5][3] ^ $st[$i + 10][3] ^ $st[$i + 15][3] ^ $st[$i + 20][3] + ]; + } + for ($i = 0; $i < 5; $i++) + { + $t = [ + $bc[($i + 4) % 5][0] ^ ((($bc[($i + 1) % 5][0] << 1) | ($bc[($i + 1) % 5][1] >> 15)) & (0xFFFF)), + $bc[($i + 4) % 5][1] ^ ((($bc[($i + 1) % 5][1] << 1) | ($bc[($i + 1) % 5][2] >> 15)) & (0xFFFF)), + $bc[($i + 4) % 5][2] ^ ((($bc[($i + 1) % 5][2] << 1) | ($bc[($i + 1) % 5][3] >> 15)) & (0xFFFF)), + $bc[($i + 4) % 5][3] ^ ((($bc[($i + 1) % 5][3] << 1) | ($bc[($i + 1) % 5][0] >> 15)) & (0xFFFF)) + ]; + for ($j = 0; $j < 25; $j += 5) + { + $st[$j + $i] = [ + $st[$j + $i][0] ^ $t[0], + $st[$j + $i][1] ^ $t[1], + $st[$j + $i][2] ^ $t[2], + $st[$j + $i][3] ^ $t[3] + ]; + } + } + // Rho Pi + $t = $st[1]; + for ($i = 0; $i < 24; $i++) + { + $j = self::$keccakf_piln[$i]; + $bc[0] = $st[$j]; + $n = self::$keccakf_rotc[$i] >> 4; + $m = self::$keccakf_rotc[$i] % 16; + $st[$j] = [ + ((($t[(0 + $n) % 4] << $m) | ($t[(1 + $n) % 4] >> (16 - $m))) & (0xFFFF)), + ((($t[(1 + $n) % 4] << $m) | ($t[(2 + $n) % 4] >> (16 - $m))) & (0xFFFF)), + ((($t[(2 + $n) % 4] << $m) | ($t[(3 + $n) % 4] >> (16 - $m))) & (0xFFFF)), + ((($t[(3 + $n) % 4] << $m) | ($t[(0 + $n) % 4] >> (16 - $m))) & (0xFFFF)) + ]; + $t = $bc[0]; + } + // Chi + for ($j = 0; $j < 25; $j += 5) + { + for ($i = 0; $i < 5; $i++) + { + $bc[$i] = $st[$j + $i]; + } + for ($i = 0; $i < 5; $i++) + { + $st[$j + $i] = [ + $st[$j + $i][0] ^ ~$bc[($i + 1) % 5][0] & $bc[($i + 2) % 5][0], + $st[$j + $i][1] ^ ~$bc[($i + 1) % 5][1] & $bc[($i + 2) % 5][1], + $st[$j + $i][2] ^ ~$bc[($i + 1) % 5][2] & $bc[($i + 2) % 5][2], + $st[$j + $i][3] ^ ~$bc[($i + 1) % 5][3] & $bc[($i + 2) % 5][3] + ]; + } + } + // Iota + $st[0] = [ + $st[0][0] ^ $keccakf_rndc[$round][0], + $st[0][1] ^ $keccakf_rndc[$round][1], + $st[0][2] ^ $keccakf_rndc[$round][2], + $st[0][3] ^ $keccakf_rndc[$round][3] + ]; + } + } + + private static function keccak32($in_raw, $capacity, $outputlength, $suffix, $raw_output) + { + $capacity /= 8; + $inlen = self::ourStrlen($in_raw); + $rsiz = 200 - 2 * $capacity; + $rsizw = $rsiz / 8; + $st = []; + for ($i = 0; $i < 25; $i++) + { + $st[] = [0, 0, 0, 0]; + } + for ($in_t = 0; $inlen >= $rsiz; $inlen -= $rsiz, $in_t += $rsiz) + { + for ($i = 0; $i < $rsizw; $i++) + { + $t = unpack('v*', self::ourSubstr($in_raw, $i * 8 + $in_t, 8)); + $st[$i] = [ + $st[$i][0] ^ $t[4], + $st[$i][1] ^ $t[3], + $st[$i][2] ^ $t[2], + $st[$i][3] ^ $t[1] + ]; + } + self::keccakf32($st, self::KECCAK_ROUNDS); + } + $temp = self::ourSubstr($in_raw, $in_t, $inlen); + $temp = str_pad($temp, $rsiz, "\x0", STR_PAD_RIGHT); + $temp[$inlen] = chr($suffix); + $temp[$rsiz - 1] = chr($temp[$rsiz - 1] | 0x80); + for ($i = 0; $i < $rsizw; $i++) + { + $t = unpack('v*', self::ourSubstr($temp, $i * 8, 8)); + $st[$i] = [ + $st[$i][0] ^ $t[4], + $st[$i][1] ^ $t[3], + $st[$i][2] ^ $t[2], + $st[$i][3] ^ $t[1] + ]; + } + self::keccakf32($st, self::KECCAK_ROUNDS); + $out = ''; + for ($i = 0; $i < 25; $i++) + { + $out .= $t = pack('v*', $st[$i][3], $st[$i][2], $st[$i][1], $st[$i][0]); + } + $r = self::ourSubstr($out, 0, $outputlength / 8); + + return $raw_output ? $r : bin2hex($r); + } + + // 0 = not run, 1 = 64 bit passed, 2 = 32 bit passed, 3 = failed + private static $test_state = 0; + + private static function selfTest() + { + if (self::$test_state === 1 || self::$test_state === 2) + { + return; + } + if (self::$test_state === 3) + { + throw new \Exception('Sha3 previous self test failed!'); + } + $in = ''; + $md = '6b4e03423667dbb73b6e15454f0eb1abd4597f9a1b078e3f5b5a6bc7'; + if (self::keccak64($in, 224, 224, 0x06, false) === $md) + { + self::$test_state = 1; + + return; + } + if (self::keccak32($in, 224, 224, 0x06, false) === $md) + { + self::$test_state = 2; + + return; + } + self::$test_state = 3; + throw new \Exception('Sha3 self test failed!'); + } + + private static function keccak($in_raw, $capacity, $outputlength, $suffix, $raw_output) + { + self::selfTest(); + if (self::$test_state === 1) + { + return self::keccak64($in_raw, $capacity, $outputlength, $suffix, $raw_output); + } + + return self::keccak32($in_raw, $capacity, $outputlength, $suffix, $raw_output); + } + + public static function hash($in, $mdlen, $raw_output = false) + { + if (!in_array($mdlen, [224, 256, 384, 512], true)) + { + throw new \Exception('Unsupported Sha3 Hash output size.'); + } + + return self::keccak($in, $mdlen, $mdlen, 0x06, $raw_output); + } + + public static function shake($in, $security_level, $outlen, $raw_output = false) + { + if (!in_array($security_level, [128, 256], true)) + { + throw new \Exception('Unsupported Sha3 Shake security level.'); + } + + return self::keccak($in, $security_level, $outlen, 0x1f, $raw_output); + } + /** + * Multi-byte-safe string functions borrowed from https://github.com/sarciszewski/php-future + */ + /** + * Multi-byte-safe string length calculation + * + * @param string $str + * @return int + */ + private static function ourStrlen($str) + { + // Premature optimization: cache the function_exists() result + static $exists = null; + if ($exists === null) + { + $exists = \function_exists('\\mb_strlen'); + } + // If it exists, we need to make sure we're using 8bit mode + if ($exists) + { + $length = \mb_strlen($str, '8bit'); + if ($length === false) + { + throw new \Exception('mb_strlen() failed.'); + } + + return $length; + } + + return \strlen($str); + } + + /** + * Multi-byte-safe substring calculation + * + * @param string $str + * @param int $start + * @param int $length (optional) + * @return string + */ + private static function ourSubstr($str, $start = 0, $length = null) + { + // Premature optimization: cache the function_exists() result + static $exists = null; + if ($exists === null) + { + $exists = \function_exists('\\mb_substr'); + } + // If it exists, we need to make sure we're using 8bit mode + if ($exists) + { + return \mb_substr($str, $start, $length, '8bit'); + } + elseif ($length !== null) + { + return \substr($str, $start, $length); + } + + return \substr($str, $start); + } +} \ No newline at end of file diff --git a/src/Validation.php b/src/Validation.php index 8ffbbb8..e60ca55 100644 --- a/src/Validation.php +++ b/src/Validation.php @@ -1,6 +1,8 @@ address = $address; - $this->determineVersion(); } protected static function decodeHex($hex) { - $hex = strtoupper($hex); - $chars = "0123456789ABCDEF"; + $hex = strtoupper($hex); + $chars = "0123456789ABCDEF"; $return = "0"; - for ($i = 0; $i < strlen($hex); $i++) { + for ($i = 0; $i < strlen($hex); $i++) + { $current = (string)strpos($chars, $hex[$i]); - $return = (string)bcmul($return, "16", 0); - $return = (string)bcadd($return, $current, 0); + $return = (string)bcmul($return, "16", 0); + $return = (string)bcadd($return, $current, 0); } + return $return; } protected static function encodeHex($dec) { - $chars = "0123456789ABCDEF"; + $chars = "0123456789ABCDEF"; $return = ""; - while (bccomp($dec, 0) == 1) { - $dv = (string)bcdiv($dec, "16", 0); - $rem = (integer)bcmod($dec, "16"); - $dec = $dv; + while (bccomp($dec, 0) == 1) + { + $dv = (string)bcdiv($dec, "16", 0); + $rem = (integer)bcmod($dec, "16"); + $dec = $dv; $return = $return . $chars[$rem]; } + return strrev($return); } @@ -44,22 +48,25 @@ protected static function base58ToHex($base58) { $origbase58 = $base58; - $chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + $chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; $return = "0"; - for ($i = 0; $i < strlen($base58); $i++) { + for ($i = 0; $i < strlen($base58); $i++) + { $current = (string)strpos($chars, $base58[$i]); - $return = (string)bcmul($return, "58", 0); - $return = (string)bcadd($return, $current, 0); + $return = (string)bcmul($return, "58", 0); + $return = (string)bcadd($return, $current, 0); } $return = self::encodeHex($return); //leading zeros - for ($i = 0; $i < strlen($origbase58) && $origbase58[$i] == "1"; $i++) { + for ($i = 0; $i < strlen($origbase58) && $origbase58[$i] == "1"; $i++) + { $return = "00" . $return; } - if (strlen($return) % 2 != 0) { + if (strlen($return) % 2 != 0) + { $return = "0" . $return; } @@ -70,19 +77,21 @@ protected static function encodeBase58($hex) { $orighex = $hex; - $chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; - $hex = self::decodeHex($hex); + $chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + $hex = self::decodeHex($hex); $return = ""; - while (bccomp($hex, 0) == 1) { - $dv = (string)bcdiv($hex, "58", 0); - $rem = (integer)bcmod($hex, "58"); - $hex = $dv; + while (bccomp($hex, 0) == 1) + { + $dv = (string)bcdiv($hex, "58", 0); + $rem = (integer)bcmod($hex, "58"); + $hex = $dv; $return = $return . $chars[$rem]; } $return = strrev($return); //leading zeros - for ($i = 0; $i < strlen($orighex) && substr($orighex, $i, 2) == "00"; $i += 2) { + for ($i = 0; $i < strlen($orighex) && substr($orighex, $i, 2) == "00"; $i += 2) + { $return = "1" . $return; } @@ -92,12 +101,13 @@ protected static function encodeBase58($hex) protected function hash160ToAddress($hash160) { $hash160 = $this->addressVersion . $hash160; - $check = pack("H*", $hash160); - $check = hash("sha256", hash("sha256", $check, true)); - $check = substr($check, 0, 8); + $check = pack("H*", $hash160); + $check = hash("sha256", hash("sha256", $check, true)); + $check = substr($check, 0, 8); $hash160 = strtoupper($hash160 . $check); - if (strlen($hash160) % 2 != 0) { + if (strlen($hash160) % 2 != 0) + { $this->addressVersion = null; } @@ -108,12 +118,14 @@ protected static function addressToHash160($addr) { $addr = self::base58ToHex($addr); $addr = substr($addr, 2, strlen($addr) - 10); + return $addr; } protected static function hash160($data) { - $data=pack("H*" , $data); + $data = pack("H*", $data); + return strtoupper(hash("ripemd160", hash("sha256", $data, true))); } @@ -129,24 +141,41 @@ protected function validateVersion($version) protected function determineVersion() { - if (isset($this->base58PrefixToHexVersion[$this->address[0]])) { - $this->addressVersion = $this->base58PrefixToHexVersion[$this->address[0]]; + if (isset($this->base58PrefixToHexVersion[$this->address[0]])) + { + $this->addressVersion = $this->base58PrefixToHexVersion[$this->address[0]]; } } - function validate() + public static function make($iso) { - if (is_null($this->addressVersion)) { + $class = 'Merkeleon\PhpCryptocurrencyAddressValidation\Validation\\' . strtoupper($iso); + if (class_exists($class)) + { + return new $class(); + } + throw new CryptocurrencyValidatorNotFound($iso); + } + + public function validate($address) + { + $this->address = $address; + $this->determineVersion(); + + if (is_null($this->addressVersion)) + { return false; } $hexAddress = self::base58ToHex($this->address); - if (strlen($hexAddress) != 50) { + if (strlen($hexAddress) != 50) + { return false; } $version = substr($hexAddress, 0, 2); - if (!$this->validateVersion($version)) { + if (!$this->validateVersion($version)) + { return false; } @@ -154,6 +183,7 @@ function validate() $check = pack("H*", $check); $check = strtoupper(hash("sha256", hash("sha256", $check, true))); $check = substr($check, 0, 8); + return $check == substr($hexAddress, strlen($hexAddress) - 8); } } \ No newline at end of file diff --git a/src/Validation/BTC.php b/src/Validation/BTC.php index 3a85b35..7b402e0 100644 --- a/src/Validation/BTC.php +++ b/src/Validation/BTC.php @@ -1,8 +1,8 @@ isChecksumAddress($address); + } + } + + public function isChecksumAddress($address) + { + // Check each case + $address = str_replace('0x', '', $address); + $addressHash = Sha3::hash(strtolower($address), 224); + $addressArray = str_split($address); + $addressHashArray = str_split($addressHash); + + for ($i = 0; $i < 40; $i++) + { + // the nth letter should be uppercase if the nth digit of casemap is 1 + if ((intval($addressHashArray[$i], 16) > 7 && strtoupper($addressArray[$i]) !== $addressArray[$i]) || (intval($addressHashArray[$i], 16) <= 7 && strtolower($addressArray[$i]) !== $addressArray[$i])) + { + return false; + } + } + + return true; + } + + public function validate($address) + { + return $this->isAddress($address); + } +} \ No newline at end of file diff --git a/src/Validation/LTC.php b/src/Validation/LTC.php index 566e8ea..9611cfe 100644 --- a/src/Validation/LTC.php +++ b/src/Validation/LTC.php @@ -1,24 +1,24 @@ '30', - 'M' => '31', - '3' => self::DEPRECATED_ADDRESS_VERSION // deprecated for litecoin, should not be allowed for new user's inputs + 'L' => 30, + 'M' => 31, + '3' => 5 ]; protected function validateVersion($version) { - if ($this->addressVersion == self::DEPRECATED_ADDRESS_VERSION && !$this->deprecatedAllowed) { + if (!$this->deprecatedAllowed && !in_array($this->addressVersion, self::DEPRECATED_ADDRESS_VERSIONS)) { return false; } return hexdec($version) == hexdec($this->addressVersion); diff --git a/tests/BTCTest.php b/tests/BTCTest.php index 1cc2363..e24d8cd 100644 --- a/tests/BTCTest.php +++ b/tests/BTCTest.php @@ -1,6 +1,6 @@ Date: Thu, 9 Nov 2017 19:38:16 +0300 Subject: [PATCH 02/65] update readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index bf4f333..4b191bd 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,9 @@ One day I will add other crypto currencies. Or how about you? :) ```php -use Merkeleon\PhpCryptocurrencyAddressValidation\Validation\BTC as BTCValidator; +use Merkeleon\PhpCryptocurrencyAddressValidation\Validation; -$validator = new BTCValidator('1QLbGuc3WGKKKpLs4pBp9H6jiQ2MgPkXRp'); -var_dump($validator->validate()); +$validator = Validation::make('BTC'); +var_dump($validator->validate('1QLbGuc3WGKKKpLs4pBp9H6jiQ2MgPkXRp')); ``` From bab19abd729d4e0606e05da339ac5e015ca0a214 Mon Sep 17 00:00:00 2001 From: Andrey Drozd Date: Thu, 9 Nov 2017 20:01:09 +0300 Subject: [PATCH 03/65] fix name --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 28c5b97..49c48d7 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "Merkeleon/php-cryptocurrency-address-validation", + "name": "merkeleon/php-cryptocurrency-address-validation", "description": "Cryptocurrency address validation. Currently supports litecoin and bitcoin.", "authors": [ { From 747bc157e1ba47c192a029a70a9bd47688dae06e Mon Sep 17 00:00:00 2001 From: Andrey Drozd Date: Thu, 9 Nov 2017 20:15:36 +0300 Subject: [PATCH 04/65] DOGE --- src/Validation/DOGE.php | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/Validation/DOGE.php diff --git a/src/Validation/DOGE.php b/src/Validation/DOGE.php new file mode 100644 index 0000000..e8fd5ed --- /dev/null +++ b/src/Validation/DOGE.php @@ -0,0 +1,29 @@ + Date: Thu, 9 Nov 2017 20:26:37 +0300 Subject: [PATCH 05/65] update DOGE --- src/Validation/DOGE.php | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/src/Validation/DOGE.php b/src/Validation/DOGE.php index e8fd5ed..3987c78 100644 --- a/src/Validation/DOGE.php +++ b/src/Validation/DOGE.php @@ -7,23 +7,7 @@ class DOGE extends Validation { - - public function validate($address) - { - $address = hex2bin(self::base58ToHex($address)); - if (strlen($address) !== 25) - { - return false; - } - $checksum = substr($address, 21, 4); - $rawAddress = substr($address, 0, 21); - if (substr(hex2bin(hash('sha256', hex2bin(hash('sha256', $rawAddress)))), 0, 4) === $checksum) - { - return true; - } - else - { - return false; - } - } + protected $base58PrefixToHexVersion = [ + 'D' => '1E', + ]; } \ No newline at end of file From 0f8755871608e7aef62cc6434d23970a4da424cc Mon Sep 17 00:00:00 2001 From: Andrey Drozd Date: Thu, 9 Nov 2017 20:26:43 +0300 Subject: [PATCH 06/65] fix LTC --- src/Validation/LTC.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Validation/LTC.php b/src/Validation/LTC.php index 9611cfe..5445f2b 100644 --- a/src/Validation/LTC.php +++ b/src/Validation/LTC.php @@ -6,14 +6,14 @@ class LTC extends Validation { - const DEPRECATED_ADDRESS_VERSIONS = [31]; + const DEPRECATED_ADDRESS_VERSIONS = ['31']; protected $deprecatedAllowed = false; protected $base58PrefixToHexVersion = [ - 'L' => 30, - 'M' => 31, - '3' => 5 + 'L' => '30', + 'M' => '31', + '3' => '05' ]; protected function validateVersion($version) From a95e8cfee810cc7dfc6a08b4af0b00de04871623 Mon Sep 17 00:00:00 2001 From: codeator Date: Thu, 9 Nov 2017 20:29:09 +0300 Subject: [PATCH 07/65] Update LTC.php --- src/Validation/LTC.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Validation/LTC.php b/src/Validation/LTC.php index 5445f2b..f304df2 100644 --- a/src/Validation/LTC.php +++ b/src/Validation/LTC.php @@ -6,7 +6,7 @@ class LTC extends Validation { - const DEPRECATED_ADDRESS_VERSIONS = ['31']; + const DEPRECATED_ADDRESS_VERSIONS = ['31', '03']; protected $deprecatedAllowed = false; @@ -40,4 +40,4 @@ public function setDeprecatedAllowed($deprecatedAllowed) $this->deprecatedAllowed = $deprecatedAllowed; } -} \ No newline at end of file +} From 912a4cda4a7693bcdb89766d5f26a5679a0fff45 Mon Sep 17 00:00:00 2001 From: Andrey Drozd Date: Thu, 9 Nov 2017 21:02:28 +0300 Subject: [PATCH 08/65] fix --- src/Validation/LTC.php | 42 ------------------------------------------ 1 file changed, 42 deletions(-) diff --git a/src/Validation/LTC.php b/src/Validation/LTC.php index 5445f2b..8b13789 100644 --- a/src/Validation/LTC.php +++ b/src/Validation/LTC.php @@ -1,43 +1 @@ - '30', - 'M' => '31', - '3' => '05' - ]; - - protected function validateVersion($version) - { - if (!$this->deprecatedAllowed && !in_array($this->addressVersion, self::DEPRECATED_ADDRESS_VERSIONS)) { - return false; - } - return hexdec($version) == hexdec($this->addressVersion); - } - - /** - * @return boolean - */ - public function isDeprecatedAllowed() - { - return $this->deprecatedAllowed; - } - - /** - * @param boolean $deprecatedAllowed - */ - public function setDeprecatedAllowed($deprecatedAllowed) - { - $this->deprecatedAllowed = $deprecatedAllowed; - } - -} \ No newline at end of file From ff73c5b2cfc6e894600800cda7fafa13e20dd27b Mon Sep 17 00:00:00 2001 From: Andrey Drozd Date: Thu, 9 Nov 2017 21:02:42 +0300 Subject: [PATCH 09/65] fix --- src/Validation/LTC.php | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/Validation/LTC.php b/src/Validation/LTC.php index 8b13789..e546c3c 100644 --- a/src/Validation/LTC.php +++ b/src/Validation/LTC.php @@ -1 +1,43 @@ + '30', + 'M' => '31', + '3' => '05' + ]; + + protected function validateVersion($version) + { + if (!$this->deprecatedAllowed && in_array($this->addressVersion, self::DEPRECATED_ADDRESS_VERSIONS)) { + return false; + } + return hexdec($version) == hexdec($this->addressVersion); + } + + /** + * @return boolean + */ + public function isDeprecatedAllowed() + { + return $this->deprecatedAllowed; + } + + /** + * @param boolean $deprecatedAllowed + */ + public function setDeprecatedAllowed($deprecatedAllowed) + { + $this->deprecatedAllowed = $deprecatedAllowed; + } + +} From dba52ac4d7614ac2fc3aac12254e9233c2e6f8a5 Mon Sep 17 00:00:00 2001 From: Andrey Drozd Date: Fri, 10 Nov 2017 17:47:41 +0300 Subject: [PATCH 10/65] temporary hack with ETH validation --- src/Validation/ETH.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Validation/ETH.php b/src/Validation/ETH.php index 062b6a7..8244fbe 100644 --- a/src/Validation/ETH.php +++ b/src/Validation/ETH.php @@ -2,13 +2,19 @@ namespace Merkeleon\PhpCryptocurrencyAddressValidation\Validation; -use Merkeleon\PhpCryptocurrencyAddressValidation\Validation; use Merkeleon\PhpCryptocurrencyAddressValidation\Utils\Sha3; +use Merkeleon\PhpCryptocurrencyAddressValidation\Validation; class ETH extends Validation { public function isAddress($address) { + if (preg_match('/^(0x)[0-9a-f]{40}$/i', $address)) + { + //TODO: fix sha3, and remove this hack + return true; + } + if (!preg_match('/^(0x)[0-9a-f]{40}$/i', $address)) { // check if it has the basic requirements of an address @@ -30,7 +36,7 @@ public function isChecksumAddress($address) { // Check each case $address = str_replace('0x', '', $address); - $addressHash = Sha3::hash(strtolower($address), 224); + $addressHash = Sha3::hash(strtolower($address), 256); $addressArray = str_split($address); $addressHashArray = str_split($addressHash); From 0686ec8bdb216def777fb64b6edb9ae74176dc4e Mon Sep 17 00:00:00 2001 From: Andrey Drozd Date: Mon, 4 Dec 2017 15:22:18 +0300 Subject: [PATCH 11/65] allow LTC address started with 3 --- src/Validation/LTC.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Validation/LTC.php b/src/Validation/LTC.php index 972dd57..0f82715 100644 --- a/src/Validation/LTC.php +++ b/src/Validation/LTC.php @@ -7,7 +7,7 @@ class LTC extends Validation { - const DEPRECATED_ADDRESS_VERSIONS = ['31', '05']; + const DEPRECATED_ADDRESS_VERSIONS = ['31']; protected $deprecatedAllowed = false; From a8061b2639b16d25c9197ac6318798336f16da80 Mon Sep 17 00:00:00 2001 From: Andrey Drozd Date: Tue, 6 Mar 2018 17:56:03 +0300 Subject: [PATCH 12/65] fix M addresses, add tests --- composer.json | 5 +++++ phpunit.xml | 4 ++-- src/Validation/LTC.php | 2 +- tests/BTCTest.php | 8 ++++++-- tests/DASHTest.php | 8 ++++++-- tests/LTCTest.php | 22 ++++++++++------------ 6 files changed, 30 insertions(+), 19 deletions(-) diff --git a/composer.json b/composer.json index 49c48d7..f19db53 100644 --- a/composer.json +++ b/composer.json @@ -15,5 +15,10 @@ "psr-4": { "Merkeleon\\PhpCryptocurrencyAddressValidation\\": "src" } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } } } diff --git a/phpunit.xml b/phpunit.xml index 0e0687b..4e88f4d 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -10,8 +10,8 @@ stopOnFailure="false" syntaxCheck="false"> - - ./tests/ + + ./tests/ \ No newline at end of file diff --git a/src/Validation/LTC.php b/src/Validation/LTC.php index 0f82715..01ee791 100644 --- a/src/Validation/LTC.php +++ b/src/Validation/LTC.php @@ -13,7 +13,7 @@ class LTC extends Validation protected $base58PrefixToHexVersion = [ 'L' => '30', - 'M' => '31', + 'M' => '32', '3' => '05' ]; diff --git a/tests/BTCTest.php b/tests/BTCTest.php index e24d8cd..388003f 100644 --- a/tests/BTCTest.php +++ b/tests/BTCTest.php @@ -1,6 +1,10 @@ assertEquals($row[1], $validator->validate()); + $validator = Validation::make('BTC'); + $this->assertEquals($row[1], $validator->validate($row[0])); } } diff --git a/tests/DASHTest.php b/tests/DASHTest.php index 1165298..6e33046 100644 --- a/tests/DASHTest.php +++ b/tests/DASHTest.php @@ -1,6 +1,10 @@ assertEquals($row[1], $validator->validate()); + $validator = Validation::make('DASH'); + $this->assertEquals($row[1], $validator->validate($row[0])); } } diff --git a/tests/LTCTest.php b/tests/LTCTest.php index 8587784..a54a4f3 100644 --- a/tests/LTCTest.php +++ b/tests/LTCTest.php @@ -1,35 +1,33 @@ assertEquals($row[1], $validator->validate()); + $validator = Validation::make('LTC'); + $this->assertEquals($row[1], $validator->validate($row[0])); } } public function testLitecoinDeprecatedMultisigAddress() { - $validator = new LTC('3CDJNfdWX8m2NwuGUV3nhXHXEeLygMXoAj'); + $validator = Validation::make('LTC'); $validator->setDeprecatedAllowed(true); - $this->assertEquals(true, $validator->validate()); + $this->assertEquals(true, $validator->validate('3CDJNfdWX8m2NwuGUV3nhXHXEeLygMXoAj')); } } \ No newline at end of file From d7bc82c16db473301275ab9df24b810e086fc7f4 Mon Sep 17 00:00:00 2001 From: Andrey Drozd Date: Tue, 20 Mar 2018 11:43:37 +0300 Subject: [PATCH 13/65] add ZEC-t validation ZEC-z - is disabled --- src/Validation.php | 10 +++++++++- src/Validation/ZEC.php | 19 +++++++++++++++++++ tests/ZECTest.php | 26 ++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 src/Validation/ZEC.php create mode 100644 tests/ZECTest.php diff --git a/src/Validation.php b/src/Validation.php index e60ca55..9c27ea0 100644 --- a/src/Validation.php +++ b/src/Validation.php @@ -9,6 +9,8 @@ abstract class Validation protected $address; protected $addressVersion; protected $base58PrefixToHexVersion; + protected $length = 50; + protected $lengths = []; protected function __construct() { @@ -168,7 +170,13 @@ public function validate($address) } $hexAddress = self::base58ToHex($this->address); - if (strlen($hexAddress) != 50) + $length = $this->length; + if (!empty($this->lengths[$this->address[0]])) + { + $length = $this->lengths[$this->address[0]]; + } + + if (strlen($hexAddress) != $length) { return false; } diff --git a/src/Validation/ZEC.php b/src/Validation/ZEC.php new file mode 100644 index 0000000..35a79a2 --- /dev/null +++ b/src/Validation/ZEC.php @@ -0,0 +1,19 @@ + '1C', +// 'z' => '16', + ]; + + protected $lengths = [ + 't' => 52, + 'z' => 140 + ]; +} diff --git a/tests/ZECTest.php b/tests/ZECTest.php new file mode 100644 index 0000000..1f34533 --- /dev/null +++ b/tests/ZECTest.php @@ -0,0 +1,26 @@ +assertEquals($row[1], $validator->validate($row[0])); + } + + } +} \ No newline at end of file From 57cc6971354dca0c3dc3110f1feb2940e974a00b Mon Sep 17 00:00:00 2001 From: Andrey Drozd Date: Tue, 15 May 2018 16:43:09 +0300 Subject: [PATCH 14/65] BCH has same format as BTC --- src/Validation/BCH.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/Validation/BCH.php diff --git a/src/Validation/BCH.php b/src/Validation/BCH.php new file mode 100644 index 0000000..93eb8ff --- /dev/null +++ b/src/Validation/BCH.php @@ -0,0 +1,14 @@ + '00', + '3' => '05' + ]; +} From 0d9ff4052a3a70f7e3ae998b7d0b8d7ac99eaad4 Mon Sep 17 00:00:00 2001 From: Andrey Drozd Date: Tue, 15 May 2018 17:02:31 +0300 Subject: [PATCH 15/65] merge with old bch validator --- src/Utils/CashAddress.php | 505 +++++++++++++++++++++++++++++ src/Utils/CashAddressException.php | 15 + src/Validation/BCH.php | 14 + tests/BCHTest.php | 28 ++ 4 files changed, 562 insertions(+) create mode 100644 src/Utils/CashAddress.php create mode 100644 src/Utils/CashAddressException.php create mode 100644 tests/BCHTest.php diff --git a/src/Utils/CashAddress.php b/src/Utils/CashAddress.php new file mode 100644 index 0000000..6e28115 --- /dev/null +++ b/src/Utils/CashAddress.php @@ -0,0 +1,505 @@ + 0, "2" => 1, "3" => 2, "4" => 3, "5" => 4, "6" => 5, "7" => 6, + "8" => 7, "9" => 8, "A" => 9, "B" => 10, "C" => 11, "D" => 12, "E" => 13, "F" => 14, "G" => 15, + "H" => 16, "J" => 17, "K" => 18, "L" => 19, "M" => 20, "N" => 21, "P" => 22, "Q" => 23, "R" => 24, + "S" => 25, "T" => 26, "U" => 27, "V" => 28, "W" => 29, "X" => 30, "Y" => 31, "Z" => 32, "a" => 33, + "b" => 34, "c" => 35, "d" => 36, "e" => 37, "f" => 38, "g" => 39, "h" => 40, "i" => 41, "j" => 42, + "k" => 43, "m" => 44, "n" => 45, "o" => 46, "p" => 47, "q" => 48, "r" => 49, "s" => 50, "t" => 51, + "u" => 52, "v" => 53, "w" => 54, "x" => 55, "y" => 56, "z" => 57]; + const BECH_ALPHABET = ["q" => 0, "p" => 1, + "z" => 2, "r" => 3, "y" => 4, "9" => 5, "x" => 6, "8" => 7, + "g" => 8, "f" => 9, "2" => 10, "t" => 11, "v" => 12, "d" => 13, + "w" => 14, "0" => 15, "s" => 16, "3" => 17, "j" => 18, "n" => 19, + "5" => 20, "4" => 21, "k" => 22, "h" => 23, "c" => 24, "e" => 25, + "6" => 26, "m" => 27, "u" => 28, "a" => 29, "7" => 30, "l" => 31]; + const EXPAND_PREFIX = [2, 9, 20, 3, 15, 9, 14, 3, 1, 19, 8, 0]; + const EXPAND_PREFIX_TESTNET = [2, 3, 8, 20, 5, 19, 20, 0]; + const BASE16 = ["0" => 0, "1" => 1, "2" => 2, "3" => 3, + "4" => 4, "5" => 5, "6" => 6, "7" => 7, + "8" => 8, "9" => 9, "a" => 10, "b" => 11, + "c" => 12, "d" => 13, "e" => 14, "f" => 15]; + + public function __construct() + { + if (PHP_INT_SIZE < 5) + { + // Requires x64 system and PHP! + throw new CashAddressException('Run it on a x64 system (+ 64 bit PHP)'); + } + } + + /** + * convertBits is the internal function to convert 256-based bytes + * to base-32 grouped bit arrays and vice versa. + * @param array $data Data whose bits to be re-grouped + * @param integer $fromBits Bits per input group of the $data + * @param integer $toBits Bits to be put to each output group + * @param boolean $pad Whether to add extra zeroes + * @return array $ret + * @throws CashAddressException + */ + static private function convertBits(array $data, $fromBits, $toBits, $pad = true) + { + $acc = 0; + $bits = 0; + $ret = []; + $maxv = (1 << $toBits) - 1; + $maxacc = (1 << ($fromBits + $toBits - 1)) - 1; + for ($i = 0; $i < sizeof($data); $i++) + { + $value = $data[$i]; + if ($value < 0 || $value >> $fromBits != 0) + { + throw new CashAddressException("Error!"); + } + $acc = (($acc << $fromBits) | $value) & $maxacc; + $bits += $fromBits; + while ($bits >= $toBits) + { + $bits -= $toBits; + $ret[] = (($acc >> $bits) & $maxv); + } + } + if ($pad) + { + if ($bits) + { + $ret[] = ($acc << $toBits - $bits) & $maxv; + } + } + else if ($bits >= $fromBits || ((($acc << ($toBits - $bits))) & $maxv)) + { + throw new CashAddressException("Error!"); + } + + return $ret; + } + + /** + * polyMod is the internal function create BCH codes. + * @param array $var 5-bit grouped data array whose polyMod to be calculated. + * @return integer $polymodValue polymod result + */ + static private function polyMod($var) + { + $c = 1; + for ($i = 0; $i < sizeof($var); $i++) + { + $c0 = $c >> 35; + $c = (($c & 0x07ffffffff) << 5) ^ $var[$i]; + if ($c0 & 1) + { + $c ^= 0x98f2bc8e61; + } + if ($c0 & 2) + { + $c ^= 0x79b76d99e2; + } + if ($c0 & 4) + { + $c ^= 0xf33e5fb3c4; + } + if ($c0 & 8) + { + $c ^= 0xae2eabe2a8; + } + if ($c0 & 16) + { + $c ^= 0x1e4f43e470; + } + } + + return $c ^ 1; + } + + /** + * rebuildAddress is the internal function to recreate error + * corrected addresses. + * @param array $addressBytes + * @return string $correctedAddress + */ + static private function rebuildAddress($addressBytes) + { + $ret = ""; + $i = 0; + while ($addressBytes[$i] != 0) + { + // 96 = ord('a') & 0xe0 + $ret .= chr(96 + $addressBytes[$i]); + $i++; + } + $ret .= ':'; + for ($i++; $i < sizeof($addressBytes); $i++) + { + $ret .= self::CHARSET[$addressBytes[$i]]; + } + + return $ret; + } + + /** + * old2new converts an address in old format to the new Cash Address format. + * @param string $oldAddress (either Mainnet or Testnet) + * @return string $newAddress Cash Address result + * @throws CashAddressException + */ + static public function old2new($oldAddress) + { + $bytes = [0]; + for ($x = 0; $x < strlen($oldAddress); $x++) + { + if (!array_key_exists($oldAddress[$x], self::ALPHABET_MAP)) + { + throw new CashAddressException('Unexpected character in address!'); + } + $value = self::ALPHABET_MAP[$oldAddress[$x]]; + $carry = $value; + for ($j = 0; $j < sizeof($bytes); $j++) + { + $carry += $bytes[$j] * 58; + $bytes[$j] = $carry & 0xff; + $carry >>= 8; + } + while ($carry > 0) + { + array_push($bytes, $carry & 0xff); + $carry >>= 8; + } + } + for ($numZeros = 0; $numZeros < strlen($oldAddress) && $oldAddress[$numZeros] === "1"; $numZeros++) + { + array_push($bytes, 0); + } + // reverse array + $answer = []; + for ($i = sizeof($bytes) - 1; $i >= 0; $i--) + { + array_push($answer, $bytes[$i]); + } + $version = $answer[0]; + $payload = array_slice($answer, 1, sizeof($answer) - 5); + if (sizeof($payload) % 4 != 0) + { + throw new CashAddressException('Unexpected address length!'); + } + // Assume the checksum of the old address is right + // Here, the Cash Address conversion starts + if ($version == 0x00) + { + // P2PKH + $addressType = 0; + $realNet = true; + } + else if ($version == 0x05) + { + // P2SH + $addressType = 1; + $realNet = true; + } + else if ($version == 0x6f) + { + // Testnet P2PKH + $addressType = 0; + $realNet = false; + } + else if ($version == 0xc4) + { + // Testnet P2SH + $addressType = 1; + $realNet = false; + } + else if ($version == 0x1c) + { + // BitPay P2PKH + $addressType = 0; + $realNet = true; + } + else if ($version == 0x28) + { + // BitPay P2SH + $addressType = 1; + $realNet = true; + } + else + { + throw new CashAddressException('Unknown address type!'); + } + $encodedSize = (sizeof($payload) - 20) / 4; + $versionByte = ($addressType << 3) | $encodedSize; + $data = array_merge([$versionByte], $payload); + $payloadConverted = self::convertBits($data, 8, 5, true); + $ret = ''; + if ($realNet) + { + $arr = array_merge(self::EXPAND_PREFIX, $payloadConverted, [0, 0, 0, 0, 0, 0, 0, 0]); +// $ret = "bitcoincash:"; + } + else + { + $arr = array_merge(self::EXPAND_PREFIX_TESTNET, $payloadConverted, [0, 0, 0, 0, 0, 0, 0, 0]); +// $ret = "bchtest:"; + } + $mod = self::polymod($arr); + $checksum = [0, 0, 0, 0, 0, 0, 0, 0]; + for ($i = 0; $i < 8; $i++) + { + // Convert the 5-bit groups in mod to checksum values. + // $checksum[$i] = ($mod >> 5*(7-$i)) & 0x1f; + $checksum[$i] = ($mod >> (5 * (7 - $i))) & 0x1f; + } + $combined = array_merge($payloadConverted, $checksum); + for ($i = 0; $i < sizeof($combined); $i++) + { + $ret .= self::CHARSET[$combined[$i]]; + } + + return $ret; + } + + /** + * Decodes Cash Address. + * @param string $inputNew New address to be decoded. + * @param boolean $shouldFixErrors Whether to fix typing errors. + * @param boolean &$isTestnetAddressResult Is pointer, set to whether it's + * a testnet address. + * @return array $decoded Returns decoded byte array if it can be decoded. + * @return string $correctedAddress Returns the corrected address if there's + * a typing error. + * @throws CashAddressException + */ + static public function decodeNewAddr($inputNew, $shouldFixErrors, &$isTestnetAddressResult) + { + $inputNew = strtolower($inputNew); + if (strpos($inputNew, ":") === false) + { + $afterPrefix = 0; + $data = self::EXPAND_PREFIX; + $isTestnetAddressResult = false; + } + else if (substr($inputNew, 0, 12) === "bitcoincash:") + { + $afterPrefix = 12; + $data = self::EXPAND_PREFIX; + $isTestnetAddressResult = false; + } + else if (substr($inputNew, 0, 8) === "bchtest:") + { + $afterPrefix = 8; + $data = self::EXPAND_PREFIX_TESTNET; + $isTestnetAddressResult = true; + } + else + { + throw new CashAddressException('Unknown address type'); + } + for ($values = []; $afterPrefix < strlen($inputNew); $afterPrefix++) + { + if (!array_key_exists($inputNew[$afterPrefix], self::BECH_ALPHABET)) + { + throw new CashAddressException('Unexpected character in address!'); + } + array_push($values, self::BECH_ALPHABET[$inputNew[$afterPrefix]]); + } + $data = array_merge($data, $values); + $checksum = self::polyMod($data); + if ($checksum != 0) + { + // Checksum is wrong! + // Try to fix up to two errors + if ($shouldFixErrors) + { + $syndromes = []; + for ($p = 0; $p < sizeof($data); $p++) + { + for ($e = 1; $e < 32; $e++) + { + $data[$p] ^= $e; + $c = self::polyMod($data); + if ($c == 0) + { + return self::rebuildAddress($data); + } + $syndromes[$c ^ $checksum] = $p * 32 + $e; + $data[$p] ^= $e; + } + } + foreach ($syndromes as $s0 => $pe) + { + if (array_key_exists($s0 ^ $checksum, $syndromes)) + { + $data[$pe >> 5] ^= $pe % 32; + $data[$syndromes[$s0 ^ $checksum] >> 5] ^= $syndromes[$s0 ^ $checksum] % 32; + + return self::rebuildAddress($data); + } + } + // Can't correct errors! + throw new CashAddressException('Can\'t correct typing errors!'); + } + } + + return $values; + } + + /** + * Corrects Cash Address typing errors. + * @param string $inputNew Cash Address to be corrected. + * @return string $correctedAddress Error corrected address, or the input itself + * if there are no errors. + * @throws CashAddressException + */ + static public function fixCashAddrErrors($inputNew) + { + try + { + $corrected = self::decodeNewAddr($inputNew, true, $isTestnet); + if (gettype($corrected) === "array") + { + return $inputNew; + } + else + { + return $corrected; + } + } + catch (CashAddressException $e) + { + throw $e; + } + } + + /** + * new2old converts an address in the Cash Address format to the old format. + * @param string $inputNew Cash Address (either mainnet or testnet) + * @param boolean $shouldFixErrors Whether to fix typing errors. + * @return string $oldAddress Old style 1... or 3... address + * @throws CashAddressException + */ + static public function new2old($inputNew, $shouldFixErrors = false) + { + try + { + $corrected = self::decodeNewAddr($inputNew, $shouldFixErrors, $isTestnet); + if (gettype($corrected) === "array") + { + $values = $corrected; + } + else + { + $values = self::decodeNewAddr($corrected, false, $isTestnet); + } + } + catch (\Exception $e) + { + throw new CashAddressException('Unexpected character in address!'); + } + + $values = self::convertBits(array_slice($values, 0, sizeof($values) - 8), 5, 8, false); + $addressType = $values[0] >> 3; + $addressHash = array_slice($values, 1, 21); + // Encode Address + if ($isTestnet) + { + if ($addressType) + { + $bytes = [0xc4]; + } + else + { + $bytes = [0x6f]; + } + } + else + { + if ($addressType) + { + $bytes = [0x05]; + } + else + { + $bytes = [0x00]; + } + } + $bytes = array_merge($bytes, $addressHash); + $merged = array_merge($bytes, self::doubleSha256ByteArray($bytes)); + $digits = [0]; + for ($i = 0; $i < sizeof($merged); $i++) + { + $carry = $merged[$i]; + for ($j = 0; $j < sizeof($digits); $j++) + { + $carry += $digits[$j] << 8; + $digits[$j] = $carry % 58; + $carry = intdiv($carry, 58); + } + while ($carry > 0) + { + array_push($digits, $carry % 58); + $carry = intdiv($carry, 58); + } + } + // leading zero bytes + for ($i = 0; $i < sizeof($merged) && $merged[$i] === 0; $i++) + { + array_push($digits, 0); + } + // reverse + $converted = ""; + for ($i = sizeof($digits) - 1; $i >= 0; $i--) + { + if ($digits[$i] > strlen(self::ALPHABET)) + { + throw new CashAddressException('Error!'); + } + $converted .= self::ALPHABET[$digits[$i]]; + } + + return $converted; + } + + /** + * internal function to calculate sha256 + * @param array $byteArray Byte array of data to be hashed + * @return array $hashResult First four bytes of sha256 result + */ + private static function doubleSha256ByteArray($byteArray) + { + $stringToBeHashed = ""; + for ($i = 0; $i < sizeof($byteArray); $i++) + { + $stringToBeHashed .= chr($byteArray[$i]); + } + $hash = hash("sha256", $stringToBeHashed); + $hashArray = []; + for ($i = 0; $i < 32; $i++) + { + array_push($hashArray, self::BASE16[$hash[2 * $i]] * 16 + self::BASE16[$hash[2 * $i + 1]]); + } + $stringToBeHashed = ""; + for ($i = 0; $i < sizeof($hashArray); $i++) + { + $stringToBeHashed .= chr($hashArray[$i]); + } + $hashArray = []; + $hash = hash("sha256", $stringToBeHashed); + for ($i = 0; $i < 4; $i++) + { + array_push($hashArray, self::BASE16[$hash[2 * $i]] * 16 + self::BASE16[$hash[2 * $i + 1]]); + } + + return $hashArray; + } +} \ No newline at end of file diff --git a/src/Utils/CashAddressException.php b/src/Utils/CashAddressException.php new file mode 100644 index 0000000..253d8d9 --- /dev/null +++ b/src/Utils/CashAddressException.php @@ -0,0 +1,15 @@ + '00', '3' => '05' ]; + + public function validate($address) + { + try + { + $legacy = CashAddress::new2old($address); + } + catch (\Exception $ex) + { + $legacy = $address; + } + return parent::validate($legacy); + } } diff --git a/tests/BCHTest.php b/tests/BCHTest.php new file mode 100644 index 0000000..4867665 --- /dev/null +++ b/tests/BCHTest.php @@ -0,0 +1,28 @@ +assertEquals($row[1], $validator->validate($row[0])); + } + + } +} \ No newline at end of file From cb812a91aa1d5812ee7d49c789c6b65e9e584380 Mon Sep 17 00:00:00 2001 From: Alex Winter Date: Wed, 13 Nov 2019 12:18:58 +0700 Subject: [PATCH 16/65] Implement parts of bitwasp/bech32 for BTC & LTC bech32 address validation --- src/Utils/Bech32Decoder.php | 129 ++++++++++++++++++++++++++++++++++ src/Utils/Bech32Exception.php | 8 +++ src/Validation/BTC.php | 16 +++++ src/Validation/LTC.php | 16 +++++ tests/BTCTest.php | 4 +- tests/LTCTest.php | 2 + 6 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 src/Utils/Bech32Decoder.php create mode 100644 src/Utils/Bech32Exception.php diff --git a/src/Utils/Bech32Decoder.php b/src/Utils/Bech32Decoder.php new file mode 100644 index 0000000..75526c9 --- /dev/null +++ b/src/Utils/Bech32Decoder.php @@ -0,0 +1,129 @@ + 126) { + throw new Bech32Exception('Out of range character in bech32 string'); + } + if ($x >= 0x61 && $x <= 0x7a) { + $haveLower = true; + } + if ($x >= 0x41 && $x <= 0x5a) { + $haveUpper = true; + $x = $chars[$i] = $x + 0x20; + } + // find location of last '1' character + if ($x === 0x31) { + $positionOne = $i; + } + } + if ($haveUpper && $haveLower) { + throw new Bech32Exception('Data contains mixture of higher/lower case characters'); + } + if ($positionOne === -1) { + throw new Bech32Exception("Missing separator character"); + } + if ($positionOne < 1) { + throw new Bech32Exception("Empty HRP"); + } + if (($positionOne + 7) > $length) { + throw new Bech32Exception('Too short checksum'); + } + $hrp = \pack("C*", ...\array_slice($chars, 0, $positionOne)); + $data = []; + for ($i = $positionOne + 1; $i < $length; $i++) { + $data[] = ($chars[$i] & 0x80) ? -1 : self::CHARKEY_KEY[$chars[$i]]; + } + if (!self::verifyChecksum($hrp, $data)) { + throw new Bech32Exception('Invalid bech32 checksum'); + } + + return [$hrp, array_slice($data, 0, -6)]; + } + + /** + * Verifies the checksum given $hrp and $convertedDataChars. + * + * @param string $hrp + * @param int[] $convertedDataChars + * @return bool + */ + private static function verifyChecksum($hrp, array $convertedDataChars) + { + $expandHrp = self::hrpExpand($hrp, \strlen($hrp)); + $r = \array_merge($expandHrp, $convertedDataChars); + $poly = self::polyMod($r, \count($r)); + return $poly === 1; + } + + /** + * Expands the human readable part into a character array for checksumming. + * @param string $hrp + * @param int $hrpLen + * @return int[] + */ + private static function hrpExpand($hrp, $hrpLen) + { + $expand1 = []; + $expand2 = []; + for ($i = 0; $i < $hrpLen; $i++) { + $o = \ord($hrp[$i]); + $expand1[] = $o >> 5; + $expand2[] = $o & 31; + } + return \array_merge($expand1, [0], $expand2); + } + + /** + * @param int[] $values + * @param int $numValues + * @return int + */ + private static function polyMod(array $values, $numValues) + { + $chk = 1; + for ($i = 0; $i < $numValues; $i++) { + $top = $chk >> 25; + $chk = ($chk & 0x1ffffff) << 5 ^ $values[$i]; + for ($j = 0; $j < 5; $j++) { + $value = (($top >> $j) & 1) ? self::GENERATOR[$j] : 0; + $chk ^= $value; + } + } + return $chk; + } +} \ No newline at end of file diff --git a/src/Utils/Bech32Exception.php b/src/Utils/Bech32Exception.php new file mode 100644 index 0000000..cd7eea4 --- /dev/null +++ b/src/Utils/Bech32Exception.php @@ -0,0 +1,8 @@ + '00', '3' => '05' ]; + + public function validate($address) + { + $valid = parent::validate($address); + + if (!$valid) { + // maybe it's a bech32 address + try { + $valid = is_array($decoded = Bech32Decoder::decodeRaw($address)) && 'bc' === $decoded[0]; + } catch (Bech32Exception $exception) {} + } + + return $valid; + } } \ No newline at end of file diff --git a/src/Validation/LTC.php b/src/Validation/LTC.php index 01ee791..b254158 100644 --- a/src/Validation/LTC.php +++ b/src/Validation/LTC.php @@ -2,6 +2,8 @@ namespace Merkeleon\PhpCryptocurrencyAddressValidation\Validation; +use Merkeleon\PhpCryptocurrencyAddressValidation\Utils\Bech32Decoder; +use Merkeleon\PhpCryptocurrencyAddressValidation\Utils\Bech32Exception; use Merkeleon\PhpCryptocurrencyAddressValidation\Validation; class LTC extends Validation @@ -17,6 +19,20 @@ class LTC extends Validation '3' => '05' ]; + public function validate($address) + { + $valid = parent::validate($address); + + if (!$valid) { + // maybe it's a bech32 address + try { + $valid = is_array($decoded = Bech32Decoder::decodeRaw($address)) && 'ltc' === $decoded[0]; + } catch (Bech32Exception $exception) {} + } + + return $valid; + } + protected function validateVersion($version) { if (!$this->deprecatedAllowed && in_array($this->addressVersion, self::DEPRECATED_ADDRESS_VERSIONS)) { diff --git a/tests/BTCTest.php b/tests/BTCTest.php index 388003f..d0c91e4 100644 --- a/tests/BTCTest.php +++ b/tests/BTCTest.php @@ -14,7 +14,9 @@ public function testValidator() $testData = [ ['1QLbGuc3WGKKKpLs4pBp9H6jiQ2MgPkXRp', true], ['3QJmV3qfvL9SuYo34YihAf3sRCW3qSinyC', true], - ['LbTjMGN7gELw4KbeyQf6cTCq859hD18guE', false] + ['LbTjMGN7gELw4KbeyQf6cTCq859hD18guE', false], + ['bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq', true], + ['ltc1qy4rwhdkujk35ga26774gqmng67kgggtqnsx9vp0xgzp3wz3yjkhqashszw', false] ]; foreach ($testData as $row) { diff --git a/tests/LTCTest.php b/tests/LTCTest.php index a54a4f3..08776ee 100644 --- a/tests/LTCTest.php +++ b/tests/LTCTest.php @@ -15,6 +15,8 @@ public function testValidator() ['3CDJNfdWX8m2NwuGUV3nhXHXEeLygMXoAj', true], ['LbTjMGN7gELw4KbeyQf6cTCq859hD18guE', true], ['MJRSgZ3UUFcTBTBAaN38XAXvZLwRe8WVw7', true], + ['ltc1qy4rwhdkujk35ga26774gqmng67kgggtqnsx9vp0xgzp3wz3yjkhqashszw', true], + ['bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq', false] ]; foreach ($testData as $row) { From 053723dc1a6ef24feb6763a9b1d235f4d8f6da8b Mon Sep 17 00:00:00 2001 From: "andrei.drozd" Date: Fri, 27 Mar 2020 12:26:34 +0300 Subject: [PATCH 17/65] add and edit multiple validators --- src/Base58Validation.php | 178 +++++++++++++++++++++++++++++++++++++++ src/Validation.php | 169 +------------------------------------ src/Validation/BCH.php | 4 +- src/Validation/BNB.php | 20 +++++ src/Validation/BSV.php | 9 ++ src/Validation/BTC.php | 4 +- src/Validation/DASH.php | 4 +- src/Validation/DOGE.php | 6 +- src/Validation/ETC.php | 8 ++ src/Validation/LTC.php | 4 +- src/Validation/NEO.php | 14 +++ src/Validation/TBTC.php | 31 +++++++ src/Validation/ZEC.php | 4 +- 13 files changed, 275 insertions(+), 180 deletions(-) create mode 100644 src/Base58Validation.php create mode 100644 src/Validation/BNB.php create mode 100644 src/Validation/BSV.php create mode 100644 src/Validation/ETC.php create mode 100644 src/Validation/NEO.php create mode 100644 src/Validation/TBTC.php diff --git a/src/Base58Validation.php b/src/Base58Validation.php new file mode 100644 index 0000000..4c4b066 --- /dev/null +++ b/src/Base58Validation.php @@ -0,0 +1,178 @@ +addressVersion . $hash160; + $check = pack("H*", $hash160); + $check = hash("sha256", hash("sha256", $check, true)); + $check = substr($check, 0, 8); + $hash160 = strtoupper($hash160 . $check); + + if (strlen($hash160) % 2 != 0) + { + $this->addressVersion = null; + } + + return self::encodeBase58($hash160); + } + + protected static function addressToHash160($addr) + { + $addr = self::base58ToHex($addr); + $addr = substr($addr, 2, strlen($addr) - 10); + + return $addr; + } + + protected static function hash160($data) + { + $data = pack("H*", $data); + + return strtoupper(hash("ripemd160", hash("sha256", $data, true))); + } + + protected function pubKeyToAddress($pubkey) + { + return $this->hash160ToAddress(self::hash160($pubkey)); + } + + protected function validateVersion($version) + { + return hexdec($version) == hexdec($this->addressVersion); + } + + protected function determineVersion() + { + if (isset($this->base58PrefixToHexVersion[$this->address[0]])) + { + $this->addressVersion = $this->base58PrefixToHexVersion[$this->address[0]]; + } + } + + public function validate($address) + { + $this->address = $address; + $this->determineVersion(); + + if (is_null($this->addressVersion)) + { + return false; + } + + $hexAddress = self::base58ToHex($this->address); + $length = $this->length; + if (!empty($this->lengths[$this->address[0]])) + { + $length = $this->lengths[$this->address[0]]; + } + + if (strlen($hexAddress) != $length) + { + return false; + } + $version = substr($hexAddress, 0, 2); + + if (!$this->validateVersion($version)) + { + return false; + } + + $check = substr($hexAddress, 0, strlen($hexAddress) - 8); + $check = pack("H*", $check); + $check = strtoupper(hash("sha256", hash("sha256", $check, true))); + $check = substr($check, 0, 8); + + return $check == substr($hexAddress, strlen($hexAddress) - 8); + } +} \ No newline at end of file diff --git a/src/Validation.php b/src/Validation.php index 9c27ea0..77d5e3b 100644 --- a/src/Validation.php +++ b/src/Validation.php @@ -16,139 +16,6 @@ protected function __construct() { } - protected static function decodeHex($hex) - { - $hex = strtoupper($hex); - $chars = "0123456789ABCDEF"; - $return = "0"; - for ($i = 0; $i < strlen($hex); $i++) - { - $current = (string)strpos($chars, $hex[$i]); - $return = (string)bcmul($return, "16", 0); - $return = (string)bcadd($return, $current, 0); - } - - return $return; - } - - protected static function encodeHex($dec) - { - $chars = "0123456789ABCDEF"; - $return = ""; - while (bccomp($dec, 0) == 1) - { - $dv = (string)bcdiv($dec, "16", 0); - $rem = (integer)bcmod($dec, "16"); - $dec = $dv; - $return = $return . $chars[$rem]; - } - - return strrev($return); - } - - protected static function base58ToHex($base58) - { - $origbase58 = $base58; - - $chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; - $return = "0"; - for ($i = 0; $i < strlen($base58); $i++) - { - $current = (string)strpos($chars, $base58[$i]); - $return = (string)bcmul($return, "58", 0); - $return = (string)bcadd($return, $current, 0); - } - - $return = self::encodeHex($return); - - //leading zeros - for ($i = 0; $i < strlen($origbase58) && $origbase58[$i] == "1"; $i++) - { - $return = "00" . $return; - } - - if (strlen($return) % 2 != 0) - { - $return = "0" . $return; - } - - return $return; - } - - protected static function encodeBase58($hex) - { - $orighex = $hex; - - $chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; - $hex = self::decodeHex($hex); - $return = ""; - while (bccomp($hex, 0) == 1) - { - $dv = (string)bcdiv($hex, "58", 0); - $rem = (integer)bcmod($hex, "58"); - $hex = $dv; - $return = $return . $chars[$rem]; - } - $return = strrev($return); - - //leading zeros - for ($i = 0; $i < strlen($orighex) && substr($orighex, $i, 2) == "00"; $i += 2) - { - $return = "1" . $return; - } - - return $return; - } - - protected function hash160ToAddress($hash160) - { - $hash160 = $this->addressVersion . $hash160; - $check = pack("H*", $hash160); - $check = hash("sha256", hash("sha256", $check, true)); - $check = substr($check, 0, 8); - $hash160 = strtoupper($hash160 . $check); - - if (strlen($hash160) % 2 != 0) - { - $this->addressVersion = null; - } - - return self::encodeBase58($hash160); - } - - protected static function addressToHash160($addr) - { - $addr = self::base58ToHex($addr); - $addr = substr($addr, 2, strlen($addr) - 10); - - return $addr; - } - - protected static function hash160($data) - { - $data = pack("H*", $data); - - return strtoupper(hash("ripemd160", hash("sha256", $data, true))); - } - - protected function pubKeyToAddress($pubkey) - { - return $this->hash160ToAddress(self::hash160($pubkey)); - } - - protected function validateVersion($version) - { - return hexdec($version) == hexdec($this->addressVersion); - } - - protected function determineVersion() - { - if (isset($this->base58PrefixToHexVersion[$this->address[0]])) - { - $this->addressVersion = $this->base58PrefixToHexVersion[$this->address[0]]; - } - } - public static function make($iso) { $class = 'Merkeleon\PhpCryptocurrencyAddressValidation\Validation\\' . strtoupper($iso); @@ -159,39 +26,5 @@ public static function make($iso) throw new CryptocurrencyValidatorNotFound($iso); } - public function validate($address) - { - $this->address = $address; - $this->determineVersion(); - - if (is_null($this->addressVersion)) - { - return false; - } - - $hexAddress = self::base58ToHex($this->address); - $length = $this->length; - if (!empty($this->lengths[$this->address[0]])) - { - $length = $this->lengths[$this->address[0]]; - } - - if (strlen($hexAddress) != $length) - { - return false; - } - $version = substr($hexAddress, 0, 2); - - if (!$this->validateVersion($version)) - { - return false; - } - - $check = substr($hexAddress, 0, strlen($hexAddress) - 8); - $check = pack("H*", $check); - $check = strtoupper(hash("sha256", hash("sha256", $check, true))); - $check = substr($check, 0, 8); - - return $check == substr($hexAddress, strlen($hexAddress) - 8); - } + abstract public function validate($address); } \ No newline at end of file diff --git a/src/Validation/BCH.php b/src/Validation/BCH.php index f6cc4c7..ca4b79d 100644 --- a/src/Validation/BCH.php +++ b/src/Validation/BCH.php @@ -2,10 +2,10 @@ namespace Merkeleon\PhpCryptocurrencyAddressValidation\Validation; -use Merkeleon\PhpCryptocurrencyAddressValidation\Validation; +use Merkeleon\PhpCryptocurrencyAddressValidation\Base58Validation; use Merkeleon\PhpCryptocurrencyAddressValidation\Utils\CashAddress; -class BCH extends Validation +class BCH extends Base58Validation { // more info at https://en.bitcoin.it/wiki/List_of_address_prefixes protected $base58PrefixToHexVersion = [ diff --git a/src/Validation/BNB.php b/src/Validation/BNB.php new file mode 100644 index 0000000..d763ba9 --- /dev/null +++ b/src/Validation/BNB.php @@ -0,0 +1,20 @@ + '1E', + '9' => '16', + 'A' => '16', ]; } \ No newline at end of file diff --git a/src/Validation/ETC.php b/src/Validation/ETC.php new file mode 100644 index 0000000..8f83002 --- /dev/null +++ b/src/Validation/ETC.php @@ -0,0 +1,8 @@ + '17' + ]; + +} diff --git a/src/Validation/TBTC.php b/src/Validation/TBTC.php new file mode 100644 index 0000000..27131a8 --- /dev/null +++ b/src/Validation/TBTC.php @@ -0,0 +1,31 @@ + '6F', + 'n' => '6F', + '2' => 'C4' + ]; + + public function validate($address) + { + $valid = parent::validate($address); + + if (!$valid) { + // maybe it's a bech32 address + try { + $valid = is_array($decoded = Bech32Decoder::decodeRaw($address)) && 'tb' === $decoded[0]; + } catch (Bech32Exception $exception) {} + } + + return $valid; + } +} \ No newline at end of file diff --git a/src/Validation/ZEC.php b/src/Validation/ZEC.php index 35a79a2..8515634 100644 --- a/src/Validation/ZEC.php +++ b/src/Validation/ZEC.php @@ -2,9 +2,9 @@ namespace Merkeleon\PhpCryptocurrencyAddressValidation\Validation; -use Merkeleon\PhpCryptocurrencyAddressValidation\Validation; +use Merkeleon\PhpCryptocurrencyAddressValidation\Base58Validation; -class ZEC extends Validation +class ZEC extends Base58Validation { // more info at https://en.bitcoin.it/wiki/List_of_address_prefixes protected $base58PrefixToHexVersion = [ From 1429b983d47b0439238b6ace215dd5935748369c Mon Sep 17 00:00:00 2001 From: "andrei.drozd" Date: Fri, 27 Mar 2020 20:16:39 +0300 Subject: [PATCH 18/65] XRP address validation --- src/Base58Validation.php | 8 +++----- src/Validation/XRP.php | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 src/Validation/XRP.php diff --git a/src/Base58Validation.php b/src/Base58Validation.php index 4c4b066..7b96d05 100644 --- a/src/Base58Validation.php +++ b/src/Base58Validation.php @@ -41,11 +41,10 @@ protected static function base58ToHex($base58) { $origbase58 = $base58; - $chars = static::$base58Dictionary; $return = "0"; for ($i = 0; $i < strlen($base58); $i++) { - $current = (string)strpos($chars, $base58[$i]); + $current = (string)strpos(static::$base58Dictionary, $base58[$i]); $return = (string)bcmul($return, "58", 0); $return = (string)bcadd($return, $current, 0); } @@ -53,7 +52,7 @@ protected static function base58ToHex($base58) $return = self::encodeHex($return); //leading zeros - for ($i = 0; $i < strlen($origbase58) && $origbase58[$i] == "1"; $i++) + for ($i = 0; $i < strlen($origbase58) && $origbase58[$i] == static::$base58Dictionary[0]; $i++) { $return = "00" . $return; } @@ -70,7 +69,6 @@ protected static function encodeBase58($hex) { $orighex = $hex; - $chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; $hex = self::decodeHex($hex); $return = ""; while (bccomp($hex, 0) == 1) @@ -78,7 +76,7 @@ protected static function encodeBase58($hex) $dv = (string)bcdiv($hex, "58", 0); $rem = (integer)bcmod($hex, "58"); $hex = $dv; - $return = $return . $chars[$rem]; + $return = $return . static::$base58Dictionary[$rem]; } $return = strrev($return); diff --git a/src/Validation/XRP.php b/src/Validation/XRP.php new file mode 100644 index 0000000..13aab4d --- /dev/null +++ b/src/Validation/XRP.php @@ -0,0 +1,21 @@ + Date: Sat, 28 Mar 2020 08:54:22 +0300 Subject: [PATCH 19/65] add more tests --- composer.json | 2 +- phpunit.xml | 3 +- src/Base58Validation.php | 1 + src/Validation/BCH.php | 1 + src/Validation/BTC.php | 1 + src/Validation/LTC.php | 1 + src/Validation/TBTC.php | 1 + src/Validation/XRP.php | 1 + tests/BTCTest.php | 28 ---------------- tests/MakeTest.php | 35 +++++++++++++++++++ tests/TestCase.php | 11 ++++++ tests/{ => Validation}/BCHTest.php | 23 ++++++------- tests/Validation/BNBTest.php | 25 ++++++++++++++ tests/Validation/BSVTest.php | 27 +++++++++++++++ tests/Validation/BTCTest.php | 30 +++++++++++++++++ tests/Validation/BaseValidationTestCase.php | 21 ++++++++++++ tests/{ => Validation}/DASHTest.php | 27 +++++++-------- tests/Validation/DOGETest.php | 26 +++++++++++++++ tests/Validation/ETCTest.php | 22 ++++++++++++ tests/Validation/ETHTest.php | 22 ++++++++++++ tests/{ => Validation}/LTCTest.php | 37 ++++++++++----------- tests/Validation/NEOTest.php | 24 +++++++++++++ tests/Validation/TBTCTest.php | 30 +++++++++++++++++ tests/Validation/XRPTest.php | 29 ++++++++++++++++ tests/{ => Validation}/ZECTest.php | 20 +++++------ 25 files changed, 359 insertions(+), 89 deletions(-) delete mode 100644 tests/BTCTest.php create mode 100644 tests/MakeTest.php create mode 100644 tests/TestCase.php rename tests/{ => Validation}/BCHTest.php (51%) create mode 100644 tests/Validation/BNBTest.php create mode 100644 tests/Validation/BSVTest.php create mode 100644 tests/Validation/BTCTest.php create mode 100644 tests/Validation/BaseValidationTestCase.php rename tests/{ => Validation}/DASHTest.php (61%) create mode 100644 tests/Validation/DOGETest.php create mode 100644 tests/Validation/ETCTest.php create mode 100644 tests/Validation/ETHTest.php rename tests/{ => Validation}/LTCTest.php (65%) create mode 100644 tests/Validation/NEOTest.php create mode 100644 tests/Validation/TBTCTest.php create mode 100644 tests/Validation/XRPTest.php rename tests/{ => Validation}/ZECTest.php (54%) diff --git a/composer.json b/composer.json index f19db53..9bf1424 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,7 @@ ], "require": {}, "require-dev": { - "phpunit/phpunit": "4.0.*" + "phpunit/phpunit": "~8.0" }, "autoload": { "psr-4": { diff --git a/phpunit.xml b/phpunit.xml index 4e88f4d..f5abe30 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,8 +7,7 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" - stopOnFailure="false" - syntaxCheck="false"> + stopOnFailure="false"> ./tests/ diff --git a/src/Base58Validation.php b/src/Base58Validation.php index 7b96d05..9198f49 100644 --- a/src/Base58Validation.php +++ b/src/Base58Validation.php @@ -140,6 +140,7 @@ protected function determineVersion() public function validate($address) { + $address = (string)$address; $this->address = $address; $this->determineVersion(); diff --git a/src/Validation/BCH.php b/src/Validation/BCH.php index ca4b79d..a39a5ec 100644 --- a/src/Validation/BCH.php +++ b/src/Validation/BCH.php @@ -15,6 +15,7 @@ class BCH extends Base58Validation public function validate($address) { + $address = (string)$address; try { $legacy = CashAddress::new2old($address); diff --git a/src/Validation/BTC.php b/src/Validation/BTC.php index f129c60..e6e8e6b 100644 --- a/src/Validation/BTC.php +++ b/src/Validation/BTC.php @@ -16,6 +16,7 @@ class BTC extends Base58Validation public function validate($address) { + $address = (string)$address; $valid = parent::validate($address); if (!$valid) { diff --git a/src/Validation/LTC.php b/src/Validation/LTC.php index ff88fb3..22d7fe0 100644 --- a/src/Validation/LTC.php +++ b/src/Validation/LTC.php @@ -21,6 +21,7 @@ class LTC extends Base58Validation public function validate($address) { + $address = (string)$address; $valid = parent::validate($address); if (!$valid) { diff --git a/src/Validation/TBTC.php b/src/Validation/TBTC.php index 27131a8..475fece 100644 --- a/src/Validation/TBTC.php +++ b/src/Validation/TBTC.php @@ -17,6 +17,7 @@ class TBTC extends Base58Validation public function validate($address) { + $address = (string)$address; $valid = parent::validate($address); if (!$valid) { diff --git a/src/Validation/XRP.php b/src/Validation/XRP.php index 13aab4d..32c4e60 100644 --- a/src/Validation/XRP.php +++ b/src/Validation/XRP.php @@ -10,6 +10,7 @@ class XRP extends Base58Validation public function validate($address) { + $address = (string)$address; $hexAddress = static::base58ToHex($address); $check = substr($hexAddress, 0, strlen($hexAddress) - 8); $check = pack("H*", $check); diff --git a/tests/BTCTest.php b/tests/BTCTest.php deleted file mode 100644 index d0c91e4..0000000 --- a/tests/BTCTest.php +++ /dev/null @@ -1,28 +0,0 @@ -assertEquals($row[1], $validator->validate($row[0])); - } - - } -} \ No newline at end of file diff --git a/tests/MakeTest.php b/tests/MakeTest.php new file mode 100644 index 0000000..f8bc73c --- /dev/null +++ b/tests/MakeTest.php @@ -0,0 +1,35 @@ +assertInstanceOf($class, $instance); + } + + public function makeProvider() { + return [ + ['BCH', 'Merkeleon\PhpCryptocurrencyAddressValidation\Validation\BCH'], + ['BNB', 'Merkeleon\PhpCryptocurrencyAddressValidation\Validation\BNB'], + ['BSV', 'Merkeleon\PhpCryptocurrencyAddressValidation\Validation\BSV'], + ['BTC', 'Merkeleon\PhpCryptocurrencyAddressValidation\Validation\BTC'], + ['DASH', 'Merkeleon\PhpCryptocurrencyAddressValidation\Validation\DASH'], + ['DOGE', 'Merkeleon\PhpCryptocurrencyAddressValidation\Validation\DOGE'], + ['ETC', 'Merkeleon\PhpCryptocurrencyAddressValidation\Validation\ETC'], + ['ETH', 'Merkeleon\PhpCryptocurrencyAddressValidation\Validation\ETH'], + ['LTC', 'Merkeleon\PhpCryptocurrencyAddressValidation\Validation\LTC'], + ['NEO', 'Merkeleon\PhpCryptocurrencyAddressValidation\Validation\NEO'], + ['TBTC', 'Merkeleon\PhpCryptocurrencyAddressValidation\Validation\TBTC'], + ['XRP', 'Merkeleon\PhpCryptocurrencyAddressValidation\Validation\XRP'], + ['ZEC', 'Merkeleon\PhpCryptocurrencyAddressValidation\Validation\ZEC'], + ]; + } +} \ No newline at end of file diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..05347fe --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,11 @@ +assertEquals($row[1], $validator->validate($row[0])); - } - } } \ No newline at end of file diff --git a/tests/Validation/BNBTest.php b/tests/Validation/BNBTest.php new file mode 100644 index 0000000..38213d9 --- /dev/null +++ b/tests/Validation/BNBTest.php @@ -0,0 +1,25 @@ +getValidationInstance(); + $this->assertEquals($isValid, $validator->validate($address)); + } +} \ No newline at end of file diff --git a/tests/DASHTest.php b/tests/Validation/DASHTest.php similarity index 61% rename from tests/DASHTest.php rename to tests/Validation/DASHTest.php index 6e33046..fe2d843 100644 --- a/tests/DASHTest.php +++ b/tests/Validation/DASHTest.php @@ -1,32 +1,29 @@ assertEquals($row[1], $validator->validate($row[0])); - } - } } \ No newline at end of file diff --git a/tests/Validation/DOGETest.php b/tests/Validation/DOGETest.php new file mode 100644 index 0000000..7f6ebc2 --- /dev/null +++ b/tests/Validation/DOGETest.php @@ -0,0 +1,26 @@ +setDeprecatedAllowed(true); + $this->assertEquals(true, $validator->validate('3CDJNfdWX8m2NwuGUV3nhXHXEeLygMXoAj')); + } + + public function getValidationInstance(): Validation + { + return Validation::make('LTC'); + } + + public function addressProvider() { - $testData = [ + return [ ['1QLbGuc3WGKKKpLs4pBp9H6jiQ2MgPkXRp', false], ['3CDJNfdWX8m2NwuGUV3nhXHXEeLygMXoAj', true], ['LbTjMGN7gELw4KbeyQf6cTCq859hD18guE', true], ['MJRSgZ3UUFcTBTBAaN38XAXvZLwRe8WVw7', true], ['ltc1qy4rwhdkujk35ga26774gqmng67kgggtqnsx9vp0xgzp3wz3yjkhqashszw', true], - ['bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq', false] + ['bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq', false], ]; - - foreach ($testData as $row) { - $validator = Validation::make('LTC'); - $this->assertEquals($row[1], $validator->validate($row[0])); - } - - } - - public function testLitecoinDeprecatedMultisigAddress() - { - $validator = Validation::make('LTC'); - $validator->setDeprecatedAllowed(true); - $this->assertEquals(true, $validator->validate('3CDJNfdWX8m2NwuGUV3nhXHXEeLygMXoAj')); } } \ No newline at end of file diff --git a/tests/Validation/NEOTest.php b/tests/Validation/NEOTest.php new file mode 100644 index 0000000..8fc1682 --- /dev/null +++ b/tests/Validation/NEOTest.php @@ -0,0 +1,24 @@ +assertEquals($row[1], $validator->validate($row[0])); - } - } } \ No newline at end of file From 31a05857638ae822f912cc7eb02ff4971b16e563 Mon Sep 17 00:00:00 2001 From: Andrey Drozd Date: Fri, 16 Apr 2021 11:08:20 +0300 Subject: [PATCH 20/65] TRX validation --- src/Validation/TRX.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/Validation/TRX.php diff --git a/src/Validation/TRX.php b/src/Validation/TRX.php new file mode 100644 index 0000000..eccb0ad --- /dev/null +++ b/src/Validation/TRX.php @@ -0,0 +1,12 @@ + '41', + ]; +} From 80bf1024beb5662869373a8ec0ec30cf42c7aaf7 Mon Sep 17 00:00:00 2001 From: "andrei.drozd" Date: Fri, 16 Apr 2021 15:29:46 +0300 Subject: [PATCH 21/65] cardano address validator --- composer.json | 5 +++- src/Validation/ADA.php | 60 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 src/Validation/ADA.php diff --git a/composer.json b/composer.json index 9bf1424..d33e379 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,9 @@ "email": "andrey@phpteam.pro" } ], - "require": {}, + "require": { + "spomky-labs/cbor-php": "^2.0" + }, "require-dev": { "phpunit/phpunit": "~8.0" }, @@ -22,3 +24,4 @@ } } } + diff --git a/src/Validation/ADA.php b/src/Validation/ADA.php new file mode 100644 index 0000000..f8fa93c --- /dev/null +++ b/src/Validation/ADA.php @@ -0,0 +1,60 @@ +add(OtherObject\SimpleObject::class); + + $tagManager = new Tag\TagObjectManager(); + $tagManager->add(Tag\PositiveBigIntegerTag::class); + + $decoder = new Decoder($tagManager, $otherObjectManager); + $data = hex2bin($addressHex); + $stream = new StringStream($data); + $object = $decoder->decode($stream); + + $normalizedData = $object->getNormalizedData(); + if ($object->getMajorType() != 4) { + return false; + } + if (count($normalizedData) != 2) { + return false; + } + if (!is_numeric($normalizedData[1])) { + return false; + } + $crcCalculated = crc32($normalizedData[0]->getValue()); + $validCrc = $normalizedData[1]; + + return $crcCalculated == (int)$validCrc; + } catch (\Exception $e) { + return false; + } + } + public function validate($address) { + $valid = $this->isValidV1($address); + if (!$valid) { + // maybe it's a bech32 address + try { + $valid = is_array($decoded = Bech32Decoder::decodeRaw($address)) && 'addr' === $decoded[0]; + } catch (Bech32Exception $exception) {} + } + + return $valid; + } +} From 35a00bc5b72616c7674d3d2572792d585d694ad5 Mon Sep 17 00:00:00 2001 From: "andrei.drozd" Date: Fri, 16 Apr 2021 15:35:31 +0300 Subject: [PATCH 22/65] add tests --- tests/Validation/ADATest.php | 27 +++++++++++++++++++++++++++ tests/Validation/TRXTest.php | 27 +++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 tests/Validation/ADATest.php create mode 100644 tests/Validation/TRXTest.php diff --git a/tests/Validation/ADATest.php b/tests/Validation/ADATest.php new file mode 100644 index 0000000..9b1f047 --- /dev/null +++ b/tests/Validation/ADATest.php @@ -0,0 +1,27 @@ + Date: Wed, 18 Aug 2021 17:28:34 +0300 Subject: [PATCH 23/65] Type base58 addresses validation fix. --- .gitignore | 3 ++- src/Base58Validation.php | 16 ++++++++++++---- tests/Validation/BTCTest.php | 1 + 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index bf12543..c1285db 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ composer.lock vendor -.idea \ No newline at end of file +.idea +.phpunit.result.cache \ No newline at end of file diff --git a/src/Base58Validation.php b/src/Base58Validation.php index 9198f49..7e0517d 100644 --- a/src/Base58Validation.php +++ b/src/Base58Validation.php @@ -144,19 +144,27 @@ public function validate($address) $this->address = $address; $this->determineVersion(); + $addressLength = strlen($address); + for ($i = 0; $i < $addressLength; $i++) { + if (strpos(static::$base58Dictionary, $address[$i]) === false) { + return false; + } + } + if (is_null($this->addressVersion)) { return false; } $hexAddress = self::base58ToHex($this->address); - $length = $this->length; + $length = $this->length; if (!empty($this->lengths[$this->address[0]])) { $length = $this->lengths[$this->address[0]]; } - if (strlen($hexAddress) != $length) + $hexAddressLength = strlen($hexAddress); + if ($hexAddressLength != $length) { return false; } @@ -167,11 +175,11 @@ public function validate($address) return false; } - $check = substr($hexAddress, 0, strlen($hexAddress) - 8); + $check = substr($hexAddress, 0, $hexAddressLength - 8); $check = pack("H*", $check); $check = strtoupper(hash("sha256", hash("sha256", $check, true))); $check = substr($check, 0, 8); - return $check == substr($hexAddress, strlen($hexAddress) - 8); + return $check == substr($hexAddress, $hexAddressLength - 8); } } \ No newline at end of file diff --git a/tests/Validation/BTCTest.php b/tests/Validation/BTCTest.php index 3dfd140..4a1cdb6 100644 --- a/tests/Validation/BTCTest.php +++ b/tests/Validation/BTCTest.php @@ -25,6 +25,7 @@ public function addressProvider() ['mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn', false], ['2MzQwSSnBHWHqSAqtTVQ6v47XtaisrJa1Vc', false], ['tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx', false], + ['3PpQtDyDza5Sgyo6duza8c4VjnlwA8cVoC', false], ]; } } \ No newline at end of file From 9d4b6f0deb99efd4bf1374dc31503dbe27d01ed8 Mon Sep 17 00:00:00 2001 From: Svetlana Temnikova Date: Mon, 22 Aug 2022 18:38:05 +0200 Subject: [PATCH 24/65] add license --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a797ec1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Merkeleon + +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. \ No newline at end of file From ec9b98aea6f4338682d6ba5049a32c8f4cf3c042 Mon Sep 17 00:00:00 2001 From: "andrei.drozd" Date: Mon, 5 Sep 2022 14:51:00 +0300 Subject: [PATCH 25/65] tests --- src/Validation/TBCH.php | 30 ++++++++++++++++++++++++++++++ tests/Validation/TBCHTest.php | 31 +++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 src/Validation/TBCH.php create mode 100644 tests/Validation/TBCHTest.php diff --git a/src/Validation/TBCH.php b/src/Validation/TBCH.php new file mode 100644 index 0000000..491d0f3 --- /dev/null +++ b/src/Validation/TBCH.php @@ -0,0 +1,30 @@ + '6F', + 'n' => '6F', + '2' => 'C4' + ]; + + public function validate($address) + { + $address = (string)$address; + try + { + $legacy = CashAddress::new2old($address); + } + catch (\Exception $ex) + { + $legacy = $address; + } + return parent::validate($legacy); + } +} diff --git a/tests/Validation/TBCHTest.php b/tests/Validation/TBCHTest.php new file mode 100644 index 0000000..61aa67b --- /dev/null +++ b/tests/Validation/TBCHTest.php @@ -0,0 +1,31 @@ + Date: Wed, 21 Sep 2022 10:18:08 +0300 Subject: [PATCH 26/65] Add ADA Testnet validation, fix ADA Mainnet validation --- src/Validation/ADA.php | 20 +++++++++++++++++--- src/Validation/TADA.php | 20 ++++++++++++++++++++ tests/Validation/ADATest.php | 4 ++++ tests/Validation/TADATest.php | 31 +++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 src/Validation/TADA.php create mode 100644 tests/Validation/TADATest.php diff --git a/src/Validation/ADA.php b/src/Validation/ADA.php index f8fa93c..c4953b0 100644 --- a/src/Validation/ADA.php +++ b/src/Validation/ADA.php @@ -2,10 +2,10 @@ namespace Merkeleon\PhpCryptocurrencyAddressValidation\Validation; +use CBOR\ByteStringObject; use Merkeleon\PhpCryptocurrencyAddressValidation\Base58Validation; use Merkeleon\PhpCryptocurrencyAddressValidation\Utils\Bech32Decoder; use Merkeleon\PhpCryptocurrencyAddressValidation\Utils\Bech32Exception; -use Merkeleon\PhpCryptocurrencyAddressValidation\Validation; use CBOR\Decoder; use CBOR\OtherObject; use CBOR\Tag; @@ -13,6 +13,12 @@ class ADA extends Base58Validation { + protected $validLengths = [ + 33, // A + 66, // D + ]; + protected $validBechPrefix = 'addr'; + public function isValidV1($address) { try { $addressHex = self::base58ToHex($address); @@ -27,7 +33,6 @@ public function isValidV1($address) { $data = hex2bin($addressHex); $stream = new StringStream($data); $object = $decoder->decode($stream); - $normalizedData = $object->getNormalizedData(); if ($object->getMajorType() != 4) { return false; @@ -38,6 +43,14 @@ public function isValidV1($address) { if (!is_numeric($normalizedData[1])) { return false; } + if (!$normalizedData[0] instanceof ByteStringObject) { + return false; + } + /** @var ByteStringObject $bs */ + $bs = $normalizedData[0]; + if (!in_array($bs->getLength(), $this->validLengths)) { + return false; + } $crcCalculated = crc32($normalizedData[0]->getValue()); $validCrc = $normalizedData[1]; @@ -46,12 +59,13 @@ public function isValidV1($address) { return false; } } + public function validate($address) { $valid = $this->isValidV1($address); if (!$valid) { // maybe it's a bech32 address try { - $valid = is_array($decoded = Bech32Decoder::decodeRaw($address)) && 'addr' === $decoded[0]; + $valid = is_array($decoded = Bech32Decoder::decodeRaw($address)) && $this->validBechPrefix === $decoded[0]; } catch (Bech32Exception $exception) {} } diff --git a/src/Validation/TADA.php b/src/Validation/TADA.php new file mode 100644 index 0000000..b89148c --- /dev/null +++ b/src/Validation/TADA.php @@ -0,0 +1,20 @@ + Date: Thu, 23 Mar 2023 01:08:56 -0700 Subject: [PATCH 27/65] Update README.md --- README.md | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4b191bd..b408ee7 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,15 @@ -# php-cryptocurrency-address-validation +# Cryptocurrency Address Validation with PHP -Easy to use PHP Bitcoin and Litecoin address validator. +Easy to use Cryptocurrency Address Validation. One day I will add other crypto currencies. Or how about you? :) -## Usage +## Installation +``` +composer require merkeleon/php-cryptocurrency-address-validation +``` + +## Usage ```php use Merkeleon\PhpCryptocurrencyAddressValidation\Validation; @@ -13,3 +18,25 @@ $validator = Validation::make('BTC'); var_dump($validator->validate('1QLbGuc3WGKKKpLs4pBp9H6jiQ2MgPkXRp')); ``` + +## List Of Support + +1. ADA +2. BCH +3. BNB +4. BSV +5. BTC +6. DASH +7. DOGE +8. ETC +9. ETH +10. LTC +11. NEO +12. TADA +13. TBCH +14. TBTC +15. TRX +16. XRP +17. ZEC + + From be678ecbc7980a6db2864895c98bdda42d9880c6 Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Fri, 21 Apr 2023 17:01:58 +0400 Subject: [PATCH 28/65] contracts --- src/Contracts/Driver.php | 11 +++++++++++ src/Contracts/Validator.php | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 src/Contracts/Driver.php create mode 100644 src/Contracts/Validator.php diff --git a/src/Contracts/Driver.php b/src/Contracts/Driver.php new file mode 100644 index 0000000..8c5361e --- /dev/null +++ b/src/Contracts/Driver.php @@ -0,0 +1,11 @@ + Date: Fri, 21 Apr 2023 17:02:04 +0400 Subject: [PATCH 29/65] enum --- src/Enums/CurrencyEnum.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/Enums/CurrencyEnum.php diff --git a/src/Enums/CurrencyEnum.php b/src/Enums/CurrencyEnum.php new file mode 100644 index 0000000..8ff18e7 --- /dev/null +++ b/src/Enums/CurrencyEnum.php @@ -0,0 +1,24 @@ + Date: Fri, 21 Apr 2023 17:03:32 +0400 Subject: [PATCH 30/65] utils --- src/Utils/Base32Decoder.php | 353 +++++++++++++++ src/Utils/Base58Decoder.php | 70 +++ src/Utils/Bech32Decoder.php | 165 ++++++- src/Utils/Bech32Exception.php | 8 - src/Utils/CashAddress.php | 505 ---------------------- src/Utils/CashAddressException.php | 15 - src/Utils/HexDecoder.php | 47 ++ src/Utils/{Sha3.php => KeccakDecoder.php} | 2 +- 8 files changed, 618 insertions(+), 547 deletions(-) create mode 100644 src/Utils/Base32Decoder.php create mode 100644 src/Utils/Base58Decoder.php delete mode 100644 src/Utils/Bech32Exception.php delete mode 100644 src/Utils/CashAddress.php delete mode 100644 src/Utils/CashAddressException.php create mode 100644 src/Utils/HexDecoder.php rename src/Utils/{Sha3.php => KeccakDecoder.php} (99%) diff --git a/src/Utils/Base32Decoder.php b/src/Utils/Base32Decoder.php new file mode 100644 index 0000000..6722d55 --- /dev/null +++ b/src/Utils/Base32Decoder.php @@ -0,0 +1,353 @@ + 0) { + $value = self::$generator[$j]; + } + + $v = self::bitwiseXor($v, gmp_init((string) $value, 10)); + } + + return $v; + } + + /** + * @param string $prefix + * + * @return resource + */ + protected static function prefixChk(string $prefix) + { + $chk = gmp_init(1); + $length = strlen($prefix); + for ($i = 0; $i < $length; $i++) { + $char = ord($prefix[$i]) & 0x1f; + $chk = self::bitwiseXor(self::polyModStep($chk), gmp_init($char, 10)); + } + + $chk = self::polyModStep($chk); + + return $chk; + } + + /** + * @param string $prefix - string prefix + * @param array $words - 5bit words (array) + * + * @return string + * @throws Base32Exception + */ + public static function encode(string $prefix, array $words): string + { + if ((strlen($prefix) + 7 + count($words)) > 90) { + throw new Base32Exception(); + } + + $prefix = strtolower($prefix); + + $chk = self::prefixChk($prefix); + $result = $prefix . self::SEPARATOR; + + foreach ($words as $iValue) { + $x = $iValue; + if ($x >> 5 !== 0) { + throw new \RuntimeException("Non 5-bit word"); + } + $chk = self::bitwiseXor(self::polyModStep($chk), gmp_init($x)); + $result .= self::$charset[$x]; + } + + for ($i = 0; $i < self::$checksumLen; ++$i) { + $chk = self::polyModStep($chk); + } + $chk = self::bitwiseXor($chk, gmp_init(1)); + + for ($i = 0; $i < self::$checksumLen; ++$i) { + $pos = 5 * (self::$checksumLen - 1 - $i); + $v2 = self::bitwiseAnd(self::rightShift($chk, $pos), gmp_init('1f', 16)); + $result .= self::$charset[(int) gmp_strval($v2, 10)]; + } + + return $result; + } + + /** + * @param string $string - base32 string + * + * @return array|string> - array> + * @throws Base32Exception + * @throws InvalidChecksumException + */ + public static function decode(string $string): array + { + $stringLen = strlen($string); + if ($stringLen < 8) { + throw new Base32Exception("Address too short"); + } + + if ($stringLen > 90) { + throw new Base32Exception("Address too long"); + } + + $chars = array_values(unpack("C*", $string)); + + $haveUpper = $haveLower = false; + $idxSeparator = -1; + $separatorChar = ord(self::SEPARATOR); + + for ($i = 0; $i < $stringLen; $i++) { + $x = $chars[$i]; + if ($x < 33 || $x > 126) { + throw new Base32Exception("Out of range character in base32 string"); + } + + if ($x >= 0x61 && $x <= 0x7a) { + $haveLower = true; + } + + if ($x >= 0x41 && $x <= 0x5a) { + $haveUpper = true; + $x = $chars[$i] = $x + 0x20; + } + + if ($x === $separatorChar) { + $idxSeparator = $i; + } + } + + if ($haveUpper && $haveLower) { + throw new Base32Exception("Data contains mixture of higher/lower case characters"); + } + + if ($idxSeparator === -1) { + throw new Base32Exception("Missing separator character"); + } else { + if ($idxSeparator === 0) { + throw new Base32Exception("Missing prefix"); + } else { + if (($idxSeparator + 7) > $stringLen) { + throw new Base32Exception("Invalid location for separator character"); + } + } + } + + $prefix = ""; + + foreach (array_slice($chars, 0, $idxSeparator) as $byte) { + $prefix .= pack("C*", $byte); + } + + + $chk = self::prefixChk($prefix); + + $words = []; + for ($i = $idxSeparator + 1; $i < $stringLen; $i++) { + $char = $chars[$i]; + if (!array_key_exists($char, self::$charsetKey)) { + throw new Base32Exception("Unknown character in address"); + } + $word = self::$charsetKey[$char]; + $chk = self::bitwiseXor(self::polyModStep($chk), gmp_init($word)); + $words[] = $word; + } + + if (gmp_cmp($chk, gmp_init(1)) !== 0) { + throw new InvalidChecksumException(); + } + + return [ + $prefix, + array_slice($words, 0, -self::$checksumLen) + ]; + } + + /** + * Convert $bytes, an array of 8 bit numbers, to + * words, an array of 5 bit numbers. + * + * @param int $numBytes + * @param int[] $bytes + * @return int[] + * @throws Base32Exception + */ + public static function toWords($numBytes, array $bytes): array + { + return self::convertBits($bytes, $numBytes, 8, 5, true); + } + + /** + * Convert $words, an array of 5 bit numbres, to + * bytes, an arrayof 8 bit numbers. + * + * @param int $numWords + * @param int[] $words + * @return int[] + * @throws Base32Exception + */ + public static function fromWords($numWords, array $words): array + { + return self::convertBits($words, $numWords, 5, 8, false); + } + + /** + * Converts words of $fromBits bits to $toBits bits in size. + * + * @param int[] $data - character array of data to convert + * @param int $inLen - number of elements in array + * @param int $fromBits - word (bit count) size of provided data + * @param int $toBits - requested word size (bit count) + * @param bool $pad - whether to pad (only when encoding) + * @return int[] + * @throws Base32Exception + */ + protected static function convertBits(array $data, $inLen, $fromBits, $toBits, $pad = true): array + { + $acc = 0; + $bits = 0; + $ret = []; + $maxv = (1 << $toBits) - 1; + $maxacc = (1 << ($fromBits + $toBits - 1)) - 1; + + for ($i = 0; $i < $inLen; $i++) { + $value = $data[$i]; + if ($value < 0 || $value >> $fromBits) { + throw new Base32Exception('Invalid value for convert bits'); + } + + $acc = (($acc << $fromBits) | $value) & $maxacc; + $bits += $fromBits; + + while ($bits >= $toBits) { + $bits -= $toBits; + $ret[] = (($acc >> $bits) & $maxv); + } + } + + if ($pad) { + if ($bits) { + $ret[] = ($acc << $toBits - $bits) & $maxv; + } + } else { + if ($bits >= $fromBits || ((($acc << ($toBits - $bits))) & $maxv)) { + throw new Base32Exception('Invalid data'); + } + } + + return $ret; + } +} \ No newline at end of file diff --git a/src/Utils/Base58Decoder.php b/src/Utils/Base58Decoder.php new file mode 100644 index 0000000..ff72f79 --- /dev/null +++ b/src/Utils/Base58Decoder.php @@ -0,0 +1,70 @@ + 90) { + throw new Bech32Exception('Bech32 string cannot exceed 90 characters in length'); + } + + return $this->decodeRaw($sBech); + } + + /** + * @param string $sBech The bech32 encoded string + * + * @return array Returns [$hrp, $dataChars] + * @throws Bech32Exception + * @throws Bech32Exception + */ + public function decodeRaw(string $sBech): array + { + $length = strlen($sBech); + if ($length < 8) { throw new Bech32Exception("Bech32 string is too short"); } + $chars = array_values(unpack('C*', $sBech)); + $haveUpper = false; $haveLower = false; $positionOne = -1; + for ($i = 0; $i < $length; $i++) { $x = $chars[$i]; + if ($x < 33 || $x > 126) { throw new Bech32Exception('Out of range character in bech32 string'); } + if ($x >= 0x61 && $x <= 0x7a) { $haveLower = true; } + if ($x >= 0x41 && $x <= 0x5a) { $haveUpper = true; $x = $chars[$i] = $x + 0x20; } + // find location of last '1' character if ($x === 0x31) { $positionOne = $i; } } + if ($haveUpper && $haveLower) { throw new Bech32Exception('Data contains mixture of higher/lower case characters'); } + if ($positionOne === -1) { throw new Bech32Exception("Missing separator character"); } + if ($positionOne < 1) { throw new Bech32Exception("Empty HRP"); } + if (($positionOne + 7) > $length) { throw new Bech32Exception('Too short checksum'); } - $hrp = \pack("C*", ...\array_slice($chars, 0, $positionOne)); + + $hrp = pack("C*", ...array_slice($chars, 0, $positionOne)); + $data = []; + for ($i = $positionOne + 1; $i < $length; $i++) { $data[] = ($chars[$i] & 0x80) ? -1 : self::CHARKEY_KEY[$chars[$i]]; } - if (!self::verifyChecksum($hrp, $data)) { + + if (!$this->verifyChecksum($hrp, $data)) { throw new Bech32Exception('Invalid bech32 checksum'); } @@ -79,51 +129,130 @@ static public function decodeRaw($sBech) * Verifies the checksum given $hrp and $convertedDataChars. * * @param string $hrp - * @param int[] $convertedDataChars + * @param int[] $convertedDataChars + * * @return bool */ - private static function verifyChecksum($hrp, array $convertedDataChars) + private function verifyChecksum(string $hrp, array $convertedDataChars): bool { - $expandHrp = self::hrpExpand($hrp, \strlen($hrp)); - $r = \array_merge($expandHrp, $convertedDataChars); - $poly = self::polyMod($r, \count($r)); + $expandHrp = $this->hrpExpand($hrp, strlen($hrp)); + $r = array_merge($expandHrp, $convertedDataChars); + $poly = $this->polyMod($r, count($r)); + return $poly === 1; } + /** - * Expands the human readable part into a character array for checksumming. + * Expands the human-readable part into a character array for checksumming. + * * @param string $hrp * @param int $hrpLen * @return int[] */ - private static function hrpExpand($hrp, $hrpLen) + private function hrpExpand(string $hrp, int $hrpLen): array { $expand1 = []; $expand2 = []; + for ($i = 0; $i < $hrpLen; $i++) { - $o = \ord($hrp[$i]); + $o = ord($hrp[$i]); $expand1[] = $o >> 5; $expand2[] = $o & 31; } - return \array_merge($expand1, [0], $expand2); + + return array_merge($expand1, [0], $expand2); } /** * @param int[] $values * @param int $numValues + * * @return int */ - private static function polyMod(array $values, $numValues) + private function polyMod(array $values, int $numValues): int { $chk = 1; for ($i = 0; $i < $numValues; $i++) { $top = $chk >> 25; $chk = ($chk & 0x1ffffff) << 5 ^ $values[$i]; + for ($j = 0; $j < 5; $j++) { $value = (($top >> $j) & 1) ? self::GENERATOR[$j] : 0; $chk ^= $value; } } + return $chk; } + + /** + * Converts words of $fromBits bits to $toBits bits in size. + * + * @param int[] $data Character array of data to convert + * @param int $inLen Number of elements in array + * @param int $fromBits Word (bit count) size of provided data + * @param int $toBits Requested word size (bit count) + * @param bool $pad Whether to pad (only when encoding) + * + * @return int[] + * + * @throws Bech32Exception + */ + private function convertBits(array $data, int $inLen, int $fromBits, int $toBits, bool $pad = true): array + { + $acc = 0; + $bits = 0; + $ret = []; + $maxv = (1 << $toBits) - 1; + $maxacc = (1 << ($fromBits + $toBits - 1)) - 1; + + for ($i = 0; $i < $inLen; $i++) { + $value = $data[$i]; + + if ($value < 0 || $value >> $fromBits) { + throw new Bech32Exception('Invalid value for convert bits'); + } + + $acc = (($acc << $fromBits) | $value) & $maxacc; + $bits += $fromBits; + + while ($bits >= $toBits) { + $bits -= $toBits; + $ret[] = (($acc >> $bits) & $maxv); + } + } + + if ($pad && $bits) { + $ret[] = ($acc << $toBits - $bits) & $maxv; + } elseif ($bits >= $fromBits || ((($acc << ($toBits - $bits))) & $maxv)) { + throw new Bech32Exception('Invalid data'); + } + + return $ret; + } + + + /** + * @param int $version + * + * @param string $program + * + * @throws RuntimeException + */ + private function validateWitnessProgram(int $version, string $program): void + { + if ($version < 0 || $version > 16) { + throw new RuntimeException("Invalid witness version"); + } + + $sizeProgram = strlen($program); + if (($version === 0) && $sizeProgram !== 20 && $sizeProgram !== 32) { + throw new RuntimeException("Invalid size for V0 witness program"); + } + + if ($sizeProgram < 2 || $sizeProgram > 40) { + throw new RuntimeException("Witness program size was out of valid range"); + } + } } \ No newline at end of file diff --git a/src/Utils/Bech32Exception.php b/src/Utils/Bech32Exception.php deleted file mode 100644 index cd7eea4..0000000 --- a/src/Utils/Bech32Exception.php +++ /dev/null @@ -1,8 +0,0 @@ - 0, "2" => 1, "3" => 2, "4" => 3, "5" => 4, "6" => 5, "7" => 6, - "8" => 7, "9" => 8, "A" => 9, "B" => 10, "C" => 11, "D" => 12, "E" => 13, "F" => 14, "G" => 15, - "H" => 16, "J" => 17, "K" => 18, "L" => 19, "M" => 20, "N" => 21, "P" => 22, "Q" => 23, "R" => 24, - "S" => 25, "T" => 26, "U" => 27, "V" => 28, "W" => 29, "X" => 30, "Y" => 31, "Z" => 32, "a" => 33, - "b" => 34, "c" => 35, "d" => 36, "e" => 37, "f" => 38, "g" => 39, "h" => 40, "i" => 41, "j" => 42, - "k" => 43, "m" => 44, "n" => 45, "o" => 46, "p" => 47, "q" => 48, "r" => 49, "s" => 50, "t" => 51, - "u" => 52, "v" => 53, "w" => 54, "x" => 55, "y" => 56, "z" => 57]; - const BECH_ALPHABET = ["q" => 0, "p" => 1, - "z" => 2, "r" => 3, "y" => 4, "9" => 5, "x" => 6, "8" => 7, - "g" => 8, "f" => 9, "2" => 10, "t" => 11, "v" => 12, "d" => 13, - "w" => 14, "0" => 15, "s" => 16, "3" => 17, "j" => 18, "n" => 19, - "5" => 20, "4" => 21, "k" => 22, "h" => 23, "c" => 24, "e" => 25, - "6" => 26, "m" => 27, "u" => 28, "a" => 29, "7" => 30, "l" => 31]; - const EXPAND_PREFIX = [2, 9, 20, 3, 15, 9, 14, 3, 1, 19, 8, 0]; - const EXPAND_PREFIX_TESTNET = [2, 3, 8, 20, 5, 19, 20, 0]; - const BASE16 = ["0" => 0, "1" => 1, "2" => 2, "3" => 3, - "4" => 4, "5" => 5, "6" => 6, "7" => 7, - "8" => 8, "9" => 9, "a" => 10, "b" => 11, - "c" => 12, "d" => 13, "e" => 14, "f" => 15]; - - public function __construct() - { - if (PHP_INT_SIZE < 5) - { - // Requires x64 system and PHP! - throw new CashAddressException('Run it on a x64 system (+ 64 bit PHP)'); - } - } - - /** - * convertBits is the internal function to convert 256-based bytes - * to base-32 grouped bit arrays and vice versa. - * @param array $data Data whose bits to be re-grouped - * @param integer $fromBits Bits per input group of the $data - * @param integer $toBits Bits to be put to each output group - * @param boolean $pad Whether to add extra zeroes - * @return array $ret - * @throws CashAddressException - */ - static private function convertBits(array $data, $fromBits, $toBits, $pad = true) - { - $acc = 0; - $bits = 0; - $ret = []; - $maxv = (1 << $toBits) - 1; - $maxacc = (1 << ($fromBits + $toBits - 1)) - 1; - for ($i = 0; $i < sizeof($data); $i++) - { - $value = $data[$i]; - if ($value < 0 || $value >> $fromBits != 0) - { - throw new CashAddressException("Error!"); - } - $acc = (($acc << $fromBits) | $value) & $maxacc; - $bits += $fromBits; - while ($bits >= $toBits) - { - $bits -= $toBits; - $ret[] = (($acc >> $bits) & $maxv); - } - } - if ($pad) - { - if ($bits) - { - $ret[] = ($acc << $toBits - $bits) & $maxv; - } - } - else if ($bits >= $fromBits || ((($acc << ($toBits - $bits))) & $maxv)) - { - throw new CashAddressException("Error!"); - } - - return $ret; - } - - /** - * polyMod is the internal function create BCH codes. - * @param array $var 5-bit grouped data array whose polyMod to be calculated. - * @return integer $polymodValue polymod result - */ - static private function polyMod($var) - { - $c = 1; - for ($i = 0; $i < sizeof($var); $i++) - { - $c0 = $c >> 35; - $c = (($c & 0x07ffffffff) << 5) ^ $var[$i]; - if ($c0 & 1) - { - $c ^= 0x98f2bc8e61; - } - if ($c0 & 2) - { - $c ^= 0x79b76d99e2; - } - if ($c0 & 4) - { - $c ^= 0xf33e5fb3c4; - } - if ($c0 & 8) - { - $c ^= 0xae2eabe2a8; - } - if ($c0 & 16) - { - $c ^= 0x1e4f43e470; - } - } - - return $c ^ 1; - } - - /** - * rebuildAddress is the internal function to recreate error - * corrected addresses. - * @param array $addressBytes - * @return string $correctedAddress - */ - static private function rebuildAddress($addressBytes) - { - $ret = ""; - $i = 0; - while ($addressBytes[$i] != 0) - { - // 96 = ord('a') & 0xe0 - $ret .= chr(96 + $addressBytes[$i]); - $i++; - } - $ret .= ':'; - for ($i++; $i < sizeof($addressBytes); $i++) - { - $ret .= self::CHARSET[$addressBytes[$i]]; - } - - return $ret; - } - - /** - * old2new converts an address in old format to the new Cash Address format. - * @param string $oldAddress (either Mainnet or Testnet) - * @return string $newAddress Cash Address result - * @throws CashAddressException - */ - static public function old2new($oldAddress) - { - $bytes = [0]; - for ($x = 0; $x < strlen($oldAddress); $x++) - { - if (!array_key_exists($oldAddress[$x], self::ALPHABET_MAP)) - { - throw new CashAddressException('Unexpected character in address!'); - } - $value = self::ALPHABET_MAP[$oldAddress[$x]]; - $carry = $value; - for ($j = 0; $j < sizeof($bytes); $j++) - { - $carry += $bytes[$j] * 58; - $bytes[$j] = $carry & 0xff; - $carry >>= 8; - } - while ($carry > 0) - { - array_push($bytes, $carry & 0xff); - $carry >>= 8; - } - } - for ($numZeros = 0; $numZeros < strlen($oldAddress) && $oldAddress[$numZeros] === "1"; $numZeros++) - { - array_push($bytes, 0); - } - // reverse array - $answer = []; - for ($i = sizeof($bytes) - 1; $i >= 0; $i--) - { - array_push($answer, $bytes[$i]); - } - $version = $answer[0]; - $payload = array_slice($answer, 1, sizeof($answer) - 5); - if (sizeof($payload) % 4 != 0) - { - throw new CashAddressException('Unexpected address length!'); - } - // Assume the checksum of the old address is right - // Here, the Cash Address conversion starts - if ($version == 0x00) - { - // P2PKH - $addressType = 0; - $realNet = true; - } - else if ($version == 0x05) - { - // P2SH - $addressType = 1; - $realNet = true; - } - else if ($version == 0x6f) - { - // Testnet P2PKH - $addressType = 0; - $realNet = false; - } - else if ($version == 0xc4) - { - // Testnet P2SH - $addressType = 1; - $realNet = false; - } - else if ($version == 0x1c) - { - // BitPay P2PKH - $addressType = 0; - $realNet = true; - } - else if ($version == 0x28) - { - // BitPay P2SH - $addressType = 1; - $realNet = true; - } - else - { - throw new CashAddressException('Unknown address type!'); - } - $encodedSize = (sizeof($payload) - 20) / 4; - $versionByte = ($addressType << 3) | $encodedSize; - $data = array_merge([$versionByte], $payload); - $payloadConverted = self::convertBits($data, 8, 5, true); - $ret = ''; - if ($realNet) - { - $arr = array_merge(self::EXPAND_PREFIX, $payloadConverted, [0, 0, 0, 0, 0, 0, 0, 0]); -// $ret = "bitcoincash:"; - } - else - { - $arr = array_merge(self::EXPAND_PREFIX_TESTNET, $payloadConverted, [0, 0, 0, 0, 0, 0, 0, 0]); -// $ret = "bchtest:"; - } - $mod = self::polymod($arr); - $checksum = [0, 0, 0, 0, 0, 0, 0, 0]; - for ($i = 0; $i < 8; $i++) - { - // Convert the 5-bit groups in mod to checksum values. - // $checksum[$i] = ($mod >> 5*(7-$i)) & 0x1f; - $checksum[$i] = ($mod >> (5 * (7 - $i))) & 0x1f; - } - $combined = array_merge($payloadConverted, $checksum); - for ($i = 0; $i < sizeof($combined); $i++) - { - $ret .= self::CHARSET[$combined[$i]]; - } - - return $ret; - } - - /** - * Decodes Cash Address. - * @param string $inputNew New address to be decoded. - * @param boolean $shouldFixErrors Whether to fix typing errors. - * @param boolean &$isTestnetAddressResult Is pointer, set to whether it's - * a testnet address. - * @return array $decoded Returns decoded byte array if it can be decoded. - * @return string $correctedAddress Returns the corrected address if there's - * a typing error. - * @throws CashAddressException - */ - static public function decodeNewAddr($inputNew, $shouldFixErrors, &$isTestnetAddressResult) - { - $inputNew = strtolower($inputNew); - if (strpos($inputNew, ":") === false) - { - $afterPrefix = 0; - $data = self::EXPAND_PREFIX; - $isTestnetAddressResult = false; - } - else if (substr($inputNew, 0, 12) === "bitcoincash:") - { - $afterPrefix = 12; - $data = self::EXPAND_PREFIX; - $isTestnetAddressResult = false; - } - else if (substr($inputNew, 0, 8) === "bchtest:") - { - $afterPrefix = 8; - $data = self::EXPAND_PREFIX_TESTNET; - $isTestnetAddressResult = true; - } - else - { - throw new CashAddressException('Unknown address type'); - } - for ($values = []; $afterPrefix < strlen($inputNew); $afterPrefix++) - { - if (!array_key_exists($inputNew[$afterPrefix], self::BECH_ALPHABET)) - { - throw new CashAddressException('Unexpected character in address!'); - } - array_push($values, self::BECH_ALPHABET[$inputNew[$afterPrefix]]); - } - $data = array_merge($data, $values); - $checksum = self::polyMod($data); - if ($checksum != 0) - { - // Checksum is wrong! - // Try to fix up to two errors - if ($shouldFixErrors) - { - $syndromes = []; - for ($p = 0; $p < sizeof($data); $p++) - { - for ($e = 1; $e < 32; $e++) - { - $data[$p] ^= $e; - $c = self::polyMod($data); - if ($c == 0) - { - return self::rebuildAddress($data); - } - $syndromes[$c ^ $checksum] = $p * 32 + $e; - $data[$p] ^= $e; - } - } - foreach ($syndromes as $s0 => $pe) - { - if (array_key_exists($s0 ^ $checksum, $syndromes)) - { - $data[$pe >> 5] ^= $pe % 32; - $data[$syndromes[$s0 ^ $checksum] >> 5] ^= $syndromes[$s0 ^ $checksum] % 32; - - return self::rebuildAddress($data); - } - } - // Can't correct errors! - throw new CashAddressException('Can\'t correct typing errors!'); - } - } - - return $values; - } - - /** - * Corrects Cash Address typing errors. - * @param string $inputNew Cash Address to be corrected. - * @return string $correctedAddress Error corrected address, or the input itself - * if there are no errors. - * @throws CashAddressException - */ - static public function fixCashAddrErrors($inputNew) - { - try - { - $corrected = self::decodeNewAddr($inputNew, true, $isTestnet); - if (gettype($corrected) === "array") - { - return $inputNew; - } - else - { - return $corrected; - } - } - catch (CashAddressException $e) - { - throw $e; - } - } - - /** - * new2old converts an address in the Cash Address format to the old format. - * @param string $inputNew Cash Address (either mainnet or testnet) - * @param boolean $shouldFixErrors Whether to fix typing errors. - * @return string $oldAddress Old style 1... or 3... address - * @throws CashAddressException - */ - static public function new2old($inputNew, $shouldFixErrors = false) - { - try - { - $corrected = self::decodeNewAddr($inputNew, $shouldFixErrors, $isTestnet); - if (gettype($corrected) === "array") - { - $values = $corrected; - } - else - { - $values = self::decodeNewAddr($corrected, false, $isTestnet); - } - } - catch (\Exception $e) - { - throw new CashAddressException('Unexpected character in address!'); - } - - $values = self::convertBits(array_slice($values, 0, sizeof($values) - 8), 5, 8, false); - $addressType = $values[0] >> 3; - $addressHash = array_slice($values, 1, 21); - // Encode Address - if ($isTestnet) - { - if ($addressType) - { - $bytes = [0xc4]; - } - else - { - $bytes = [0x6f]; - } - } - else - { - if ($addressType) - { - $bytes = [0x05]; - } - else - { - $bytes = [0x00]; - } - } - $bytes = array_merge($bytes, $addressHash); - $merged = array_merge($bytes, self::doubleSha256ByteArray($bytes)); - $digits = [0]; - for ($i = 0; $i < sizeof($merged); $i++) - { - $carry = $merged[$i]; - for ($j = 0; $j < sizeof($digits); $j++) - { - $carry += $digits[$j] << 8; - $digits[$j] = $carry % 58; - $carry = intdiv($carry, 58); - } - while ($carry > 0) - { - array_push($digits, $carry % 58); - $carry = intdiv($carry, 58); - } - } - // leading zero bytes - for ($i = 0; $i < sizeof($merged) && $merged[$i] === 0; $i++) - { - array_push($digits, 0); - } - // reverse - $converted = ""; - for ($i = sizeof($digits) - 1; $i >= 0; $i--) - { - if ($digits[$i] > strlen(self::ALPHABET)) - { - throw new CashAddressException('Error!'); - } - $converted .= self::ALPHABET[$digits[$i]]; - } - - return $converted; - } - - /** - * internal function to calculate sha256 - * @param array $byteArray Byte array of data to be hashed - * @return array $hashResult First four bytes of sha256 result - */ - private static function doubleSha256ByteArray($byteArray) - { - $stringToBeHashed = ""; - for ($i = 0; $i < sizeof($byteArray); $i++) - { - $stringToBeHashed .= chr($byteArray[$i]); - } - $hash = hash("sha256", $stringToBeHashed); - $hashArray = []; - for ($i = 0; $i < 32; $i++) - { - array_push($hashArray, self::BASE16[$hash[2 * $i]] * 16 + self::BASE16[$hash[2 * $i + 1]]); - } - $stringToBeHashed = ""; - for ($i = 0; $i < sizeof($hashArray); $i++) - { - $stringToBeHashed .= chr($hashArray[$i]); - } - $hashArray = []; - $hash = hash("sha256", $stringToBeHashed); - for ($i = 0; $i < 4; $i++) - { - array_push($hashArray, self::BASE16[$hash[2 * $i]] * 16 + self::BASE16[$hash[2 * $i + 1]]); - } - - return $hashArray; - } -} \ No newline at end of file diff --git a/src/Utils/CashAddressException.php b/src/Utils/CashAddressException.php deleted file mode 100644 index 253d8d9..0000000 --- a/src/Utils/CashAddressException.php +++ /dev/null @@ -1,15 +0,0 @@ - 0); + + return $hex; + } +} \ No newline at end of file diff --git a/src/Utils/Sha3.php b/src/Utils/KeccakDecoder.php similarity index 99% rename from src/Utils/Sha3.php rename to src/Utils/KeccakDecoder.php index e41fd95..3b30453 100644 --- a/src/Utils/Sha3.php +++ b/src/Utils/KeccakDecoder.php @@ -2,7 +2,7 @@ namespace Merkeleon\PhpCryptocurrencyAddressValidation\Utils; -final class Sha3 +final class KeccakDecoder { const KECCAK_ROUNDS = 24; private static $keccakf_rotc = [1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, 27, 41, 56, 8, 25, 43, 62, 18, 39, 61, 20, 44]; From ef557dac04b56aeb58e80869a12d12a83f6392b2 Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Fri, 21 Apr 2023 17:03:40 +0400 Subject: [PATCH 31/65] exceptions --- src/Exception/AddressValidationException.php | 15 +++++++++++++++ src/Exception/Base32Exception.php | 11 +++++++++++ src/Exception/Bech32Exception.php | 8 ++++++++ src/Exception/CryptocurrencyValidatorNotFound.php | 12 ------------ src/Exception/InvalidChecksumException.php | 11 +++++++++++ 5 files changed, 45 insertions(+), 12 deletions(-) create mode 100644 src/Exception/AddressValidationException.php create mode 100644 src/Exception/Base32Exception.php create mode 100644 src/Exception/Bech32Exception.php delete mode 100644 src/Exception/CryptocurrencyValidatorNotFound.php create mode 100644 src/Exception/InvalidChecksumException.php diff --git a/src/Exception/AddressValidationException.php b/src/Exception/AddressValidationException.php new file mode 100644 index 0000000..3c6c671 --- /dev/null +++ b/src/Exception/AddressValidationException.php @@ -0,0 +1,15 @@ + Date: Fri, 21 Apr 2023 17:04:10 +0400 Subject: [PATCH 32/65] config --- config/address_validation.php | 108 ++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 config/address_validation.php diff --git a/config/address_validation.php b/config/address_validation.php new file mode 100644 index 0000000..01675d7 --- /dev/null +++ b/config/address_validation.php @@ -0,0 +1,108 @@ +value => [ + new DriverConfig( + Drivers\Bech32Driver::class, + ['bnb' => null], + ['tbnb' => null] + ), + ], + CurrencyEnum::BINANCE->value => [ + new DriverConfig(KeccakDecoder::class), + ], + CurrencyEnum::BITCOIN_CASH->value => [ + new DriverConfig( + Drivers\Base32Driver::class, + ['bitcoincash:' => null], + ['bchtest:' => null, 'bchreg:' => null,] + ), + new DriverConfig( + Drivers\DefaultBase58Driver::class, + ['1' => '00', '3' => '05'], + ['2' => 'C4', 'm' => '6F'] + ), + ], + CurrencyEnum::BITCOIN->value => [ + new DriverConfig( + Drivers\DefaultBase58Driver::class, + ['1' => '00', '3' => '05'], + ['2' => 'C4', 'm' => '6F'] + ), + new DriverConfig( + Drivers\DefaultBase58Driver::class, + ['1' => '00', '3' => '05'], + ['2' => 'C4', 'm' => '6F'] + ), + ], + CurrencyEnum::CARDANO->value => [ + new DriverConfig( + Drivers\CardanoDriver::class, + ['addr1' => null], + ['addr_test1' => null], + ), + ], + CurrencyEnum::DASHCOIN->value => [ + new DriverConfig( + Drivers\DefaultBase58Driver::class, + ['X' => '4C', '7' => '10'], + ['y' => '8c', '8' => '13'] + ), + ], + CurrencyEnum::DOGECOIN->value => [ + new DriverConfig( + Drivers\DefaultBase58Driver::class, + ['D' => '1E', '9' => '16', 'A' => '16'], + ['n' => '6f', 'm' => '6f', '2' => 'C4',], + ), + ], + CurrencyEnum::EOS->value => [ + new DriverConfig(Drivers\EosDriver::class), + ], + CurrencyEnum::ETHEREUM_CLASSIC->value => [ + new DriverConfig(KeccakDecoder::class), + ], + CurrencyEnum::ETHEREUM->value => [ + new DriverConfig(KeccakDecoder::class), + ], + CurrencyEnum::LITECOIN->value => [ + new DriverConfig( + Drivers\DefaultBase58Driver::class, + ['L' => '30', 'M' => '32', '3' => '05'], + ['m' => '6f', '2' => 'c4', 'Q' => '3a'] + ), + ], + CurrencyEnum::RIPPLE->value => [ + new DriverConfig( + Drivers\XrpBase58Driver::class, + ['r' => '00'] + ), + new DriverConfig( + Drivers\XrpXAddressDriver::class, + ['X' => null], + ['T' => null], + ), + ], + CurrencyEnum::STELLAR->value => [ + // new DriverConfig(), + ], + CurrencyEnum::TRON->value => [ + new DriverConfig( + Drivers\DefaultBase58Driver::class, + ['T' => '41'], + ), + ], + CurrencyEnum::ZCASH->value => [ + new DriverConfig( + Drivers\DefaultBase58Driver::class, + ['t' => '1C'], + ), + ], +]; \ No newline at end of file From 0e7f468584e3d339a2d6522191b542279a4db31a Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Fri, 21 Apr 2023 17:04:28 +0400 Subject: [PATCH 33/65] clean up --- src/Base58Validation.php | 185 --------------------------------------- src/Validation.php | 30 ------- src/Validation/ADA.php | 74 ---------------- src/Validation/BCH.php | 29 ------ src/Validation/BNB.php | 20 ----- src/Validation/BSV.php | 9 -- src/Validation/BTC.php | 31 ------- src/Validation/DASH.php | 17 ---- src/Validation/DOGE.php | 15 ---- src/Validation/ETC.php | 8 -- src/Validation/ETH.php | 59 ------------- src/Validation/LTC.php | 61 ------------- src/Validation/NEO.php | 14 --- src/Validation/TADA.php | 20 ----- src/Validation/TBCH.php | 30 ------- src/Validation/TBTC.php | 32 ------- src/Validation/TRX.php | 12 --- src/Validation/XRP.php | 22 ----- src/Validation/ZEC.php | 19 ---- 19 files changed, 687 deletions(-) delete mode 100644 src/Base58Validation.php delete mode 100644 src/Validation.php delete mode 100644 src/Validation/ADA.php delete mode 100644 src/Validation/BCH.php delete mode 100644 src/Validation/BNB.php delete mode 100644 src/Validation/BSV.php delete mode 100644 src/Validation/BTC.php delete mode 100644 src/Validation/DASH.php delete mode 100644 src/Validation/DOGE.php delete mode 100644 src/Validation/ETC.php delete mode 100644 src/Validation/ETH.php delete mode 100644 src/Validation/LTC.php delete mode 100644 src/Validation/NEO.php delete mode 100644 src/Validation/TADA.php delete mode 100644 src/Validation/TBCH.php delete mode 100644 src/Validation/TBTC.php delete mode 100644 src/Validation/TRX.php delete mode 100644 src/Validation/XRP.php delete mode 100644 src/Validation/ZEC.php diff --git a/src/Base58Validation.php b/src/Base58Validation.php deleted file mode 100644 index 7e0517d..0000000 --- a/src/Base58Validation.php +++ /dev/null @@ -1,185 +0,0 @@ -addressVersion . $hash160; - $check = pack("H*", $hash160); - $check = hash("sha256", hash("sha256", $check, true)); - $check = substr($check, 0, 8); - $hash160 = strtoupper($hash160 . $check); - - if (strlen($hash160) % 2 != 0) - { - $this->addressVersion = null; - } - - return self::encodeBase58($hash160); - } - - protected static function addressToHash160($addr) - { - $addr = self::base58ToHex($addr); - $addr = substr($addr, 2, strlen($addr) - 10); - - return $addr; - } - - protected static function hash160($data) - { - $data = pack("H*", $data); - - return strtoupper(hash("ripemd160", hash("sha256", $data, true))); - } - - protected function pubKeyToAddress($pubkey) - { - return $this->hash160ToAddress(self::hash160($pubkey)); - } - - protected function validateVersion($version) - { - return hexdec($version) == hexdec($this->addressVersion); - } - - protected function determineVersion() - { - if (isset($this->base58PrefixToHexVersion[$this->address[0]])) - { - $this->addressVersion = $this->base58PrefixToHexVersion[$this->address[0]]; - } - } - - public function validate($address) - { - $address = (string)$address; - $this->address = $address; - $this->determineVersion(); - - $addressLength = strlen($address); - for ($i = 0; $i < $addressLength; $i++) { - if (strpos(static::$base58Dictionary, $address[$i]) === false) { - return false; - } - } - - if (is_null($this->addressVersion)) - { - return false; - } - - $hexAddress = self::base58ToHex($this->address); - $length = $this->length; - if (!empty($this->lengths[$this->address[0]])) - { - $length = $this->lengths[$this->address[0]]; - } - - $hexAddressLength = strlen($hexAddress); - if ($hexAddressLength != $length) - { - return false; - } - $version = substr($hexAddress, 0, 2); - - if (!$this->validateVersion($version)) - { - return false; - } - - $check = substr($hexAddress, 0, $hexAddressLength - 8); - $check = pack("H*", $check); - $check = strtoupper(hash("sha256", hash("sha256", $check, true))); - $check = substr($check, 0, 8); - - return $check == substr($hexAddress, $hexAddressLength - 8); - } -} \ No newline at end of file diff --git a/src/Validation.php b/src/Validation.php deleted file mode 100644 index 77d5e3b..0000000 --- a/src/Validation.php +++ /dev/null @@ -1,30 +0,0 @@ -add(OtherObject\SimpleObject::class); - - $tagManager = new Tag\TagObjectManager(); - $tagManager->add(Tag\PositiveBigIntegerTag::class); - - $decoder = new Decoder($tagManager, $otherObjectManager); - $data = hex2bin($addressHex); - $stream = new StringStream($data); - $object = $decoder->decode($stream); - $normalizedData = $object->getNormalizedData(); - if ($object->getMajorType() != 4) { - return false; - } - if (count($normalizedData) != 2) { - return false; - } - if (!is_numeric($normalizedData[1])) { - return false; - } - if (!$normalizedData[0] instanceof ByteStringObject) { - return false; - } - /** @var ByteStringObject $bs */ - $bs = $normalizedData[0]; - if (!in_array($bs->getLength(), $this->validLengths)) { - return false; - } - $crcCalculated = crc32($normalizedData[0]->getValue()); - $validCrc = $normalizedData[1]; - - return $crcCalculated == (int)$validCrc; - } catch (\Exception $e) { - return false; - } - } - - public function validate($address) { - $valid = $this->isValidV1($address); - if (!$valid) { - // maybe it's a bech32 address - try { - $valid = is_array($decoded = Bech32Decoder::decodeRaw($address)) && $this->validBechPrefix === $decoded[0]; - } catch (Bech32Exception $exception) {} - } - - return $valid; - } -} diff --git a/src/Validation/BCH.php b/src/Validation/BCH.php deleted file mode 100644 index a39a5ec..0000000 --- a/src/Validation/BCH.php +++ /dev/null @@ -1,29 +0,0 @@ - '00', - '3' => '05' - ]; - - public function validate($address) - { - $address = (string)$address; - try - { - $legacy = CashAddress::new2old($address); - } - catch (\Exception $ex) - { - $legacy = $address; - } - return parent::validate($legacy); - } -} diff --git a/src/Validation/BNB.php b/src/Validation/BNB.php deleted file mode 100644 index d763ba9..0000000 --- a/src/Validation/BNB.php +++ /dev/null @@ -1,20 +0,0 @@ - '00', - '3' => '05' - ]; - - public function validate($address) - { - $address = (string)$address; - $valid = parent::validate($address); - - if (!$valid) { - // maybe it's a bech32 address - try { - $valid = is_array($decoded = Bech32Decoder::decodeRaw($address)) && 'bc' === $decoded[0]; - } catch (Bech32Exception $exception) {} - } - - return $valid; - } -} \ No newline at end of file diff --git a/src/Validation/DASH.php b/src/Validation/DASH.php deleted file mode 100644 index f2b3cf4..0000000 --- a/src/Validation/DASH.php +++ /dev/null @@ -1,17 +0,0 @@ - self::VERSION_P2PKH, - '7' => self::VERSION_P2SH - ]; - -} \ No newline at end of file diff --git a/src/Validation/DOGE.php b/src/Validation/DOGE.php deleted file mode 100644 index 20ea02c..0000000 --- a/src/Validation/DOGE.php +++ /dev/null @@ -1,15 +0,0 @@ - '1E', - '9' => '16', - 'A' => '16', - ]; -} \ No newline at end of file diff --git a/src/Validation/ETC.php b/src/Validation/ETC.php deleted file mode 100644 index 8f83002..0000000 --- a/src/Validation/ETC.php +++ /dev/null @@ -1,8 +0,0 @@ -isChecksumAddress($address); - } - } - - public function isChecksumAddress($address) - { - // Check each case - $address = str_replace('0x', '', $address); - $addressHash = Sha3::hash(strtolower($address), 256); - $addressArray = str_split($address); - $addressHashArray = str_split($addressHash); - - for ($i = 0; $i < 40; $i++) - { - // the nth letter should be uppercase if the nth digit of casemap is 1 - if ((intval($addressHashArray[$i], 16) > 7 && strtoupper($addressArray[$i]) !== $addressArray[$i]) || (intval($addressHashArray[$i], 16) <= 7 && strtolower($addressArray[$i]) !== $addressArray[$i])) - { - return false; - } - } - - return true; - } - - public function validate($address) - { - return $this->isAddress($address); - } -} \ No newline at end of file diff --git a/src/Validation/LTC.php b/src/Validation/LTC.php deleted file mode 100644 index 22d7fe0..0000000 --- a/src/Validation/LTC.php +++ /dev/null @@ -1,61 +0,0 @@ - '30', - 'M' => '32', - '3' => '05' - ]; - - public function validate($address) - { - $address = (string)$address; - $valid = parent::validate($address); - - if (!$valid) { - // maybe it's a bech32 address - try { - $valid = is_array($decoded = Bech32Decoder::decodeRaw($address)) && 'ltc' === $decoded[0]; - } catch (Bech32Exception $exception) {} - } - - return $valid; - } - - protected function validateVersion($version) - { - if (!$this->deprecatedAllowed && in_array($this->addressVersion, self::DEPRECATED_ADDRESS_VERSIONS)) { - return false; - } - return hexdec($version) == hexdec($this->addressVersion); - } - - /** - * @return boolean - */ - public function isDeprecatedAllowed() - { - return $this->deprecatedAllowed; - } - - /** - * @param boolean $deprecatedAllowed - */ - public function setDeprecatedAllowed($deprecatedAllowed) - { - $this->deprecatedAllowed = $deprecatedAllowed; - } - -} diff --git a/src/Validation/NEO.php b/src/Validation/NEO.php deleted file mode 100644 index 86667fb..0000000 --- a/src/Validation/NEO.php +++ /dev/null @@ -1,14 +0,0 @@ - '17' - ]; - -} diff --git a/src/Validation/TADA.php b/src/Validation/TADA.php deleted file mode 100644 index b89148c..0000000 --- a/src/Validation/TADA.php +++ /dev/null @@ -1,20 +0,0 @@ - '6F', - 'n' => '6F', - '2' => 'C4' - ]; - - public function validate($address) - { - $address = (string)$address; - try - { - $legacy = CashAddress::new2old($address); - } - catch (\Exception $ex) - { - $legacy = $address; - } - return parent::validate($legacy); - } -} diff --git a/src/Validation/TBTC.php b/src/Validation/TBTC.php deleted file mode 100644 index 475fece..0000000 --- a/src/Validation/TBTC.php +++ /dev/null @@ -1,32 +0,0 @@ - '6F', - 'n' => '6F', - '2' => 'C4' - ]; - - public function validate($address) - { - $address = (string)$address; - $valid = parent::validate($address); - - if (!$valid) { - // maybe it's a bech32 address - try { - $valid = is_array($decoded = Bech32Decoder::decodeRaw($address)) && 'tb' === $decoded[0]; - } catch (Bech32Exception $exception) {} - } - - return $valid; - } -} \ No newline at end of file diff --git a/src/Validation/TRX.php b/src/Validation/TRX.php deleted file mode 100644 index eccb0ad..0000000 --- a/src/Validation/TRX.php +++ /dev/null @@ -1,12 +0,0 @@ - '41', - ]; -} diff --git a/src/Validation/XRP.php b/src/Validation/XRP.php deleted file mode 100644 index 32c4e60..0000000 --- a/src/Validation/XRP.php +++ /dev/null @@ -1,22 +0,0 @@ - '1C', -// 'z' => '16', - ]; - - protected $lengths = [ - 't' => 52, - 'z' => 140 - ]; -} From 136309e16897fc0e071991d2beae2d75ac32cd19 Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Fri, 21 Apr 2023 17:04:35 +0400 Subject: [PATCH 34/65] clean up --- tests/MakeTest.php | 35 ----------------------------------- 1 file changed, 35 deletions(-) delete mode 100644 tests/MakeTest.php diff --git a/tests/MakeTest.php b/tests/MakeTest.php deleted file mode 100644 index f8bc73c..0000000 --- a/tests/MakeTest.php +++ /dev/null @@ -1,35 +0,0 @@ -assertInstanceOf($class, $instance); - } - - public function makeProvider() { - return [ - ['BCH', 'Merkeleon\PhpCryptocurrencyAddressValidation\Validation\BCH'], - ['BNB', 'Merkeleon\PhpCryptocurrencyAddressValidation\Validation\BNB'], - ['BSV', 'Merkeleon\PhpCryptocurrencyAddressValidation\Validation\BSV'], - ['BTC', 'Merkeleon\PhpCryptocurrencyAddressValidation\Validation\BTC'], - ['DASH', 'Merkeleon\PhpCryptocurrencyAddressValidation\Validation\DASH'], - ['DOGE', 'Merkeleon\PhpCryptocurrencyAddressValidation\Validation\DOGE'], - ['ETC', 'Merkeleon\PhpCryptocurrencyAddressValidation\Validation\ETC'], - ['ETH', 'Merkeleon\PhpCryptocurrencyAddressValidation\Validation\ETH'], - ['LTC', 'Merkeleon\PhpCryptocurrencyAddressValidation\Validation\LTC'], - ['NEO', 'Merkeleon\PhpCryptocurrencyAddressValidation\Validation\NEO'], - ['TBTC', 'Merkeleon\PhpCryptocurrencyAddressValidation\Validation\TBTC'], - ['XRP', 'Merkeleon\PhpCryptocurrencyAddressValidation\Validation\XRP'], - ['ZEC', 'Merkeleon\PhpCryptocurrencyAddressValidation\Validation\ZEC'], - ]; - } -} \ No newline at end of file From fd3e2c9e1f8f5f925adaa1f143da0aa9ad2c22e7 Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Fri, 21 Apr 2023 17:04:55 +0400 Subject: [PATCH 35/65] validation drivers --- src/Drivers/AbstractDriver.php | 14 ++++ src/Drivers/Base32Driver.php | 110 ++++++++++++++++++++++++++++ src/Drivers/Base58Driver.php | 47 ++++++++++++ src/Drivers/Bech32Driver.php | 50 +++++++++++++ src/Drivers/CardanoDriver.php | 35 +++++++++ src/Drivers/DefaultBase58Driver.php | 27 +++++++ src/Drivers/EosDriver.php | 27 +++++++ src/Drivers/KeccakDriver.php | 41 +++++++++++ src/Drivers/XrpBase58Driver.php | 15 ++++ src/Drivers/XrpXAddressDriver.php | 26 +++++++ 10 files changed, 392 insertions(+) create mode 100644 src/Drivers/AbstractDriver.php create mode 100644 src/Drivers/Base32Driver.php create mode 100644 src/Drivers/Base58Driver.php create mode 100644 src/Drivers/Bech32Driver.php create mode 100644 src/Drivers/CardanoDriver.php create mode 100644 src/Drivers/DefaultBase58Driver.php create mode 100644 src/Drivers/EosDriver.php create mode 100644 src/Drivers/KeccakDriver.php create mode 100644 src/Drivers/XrpBase58Driver.php create mode 100644 src/Drivers/XrpXAddressDriver.php diff --git a/src/Drivers/AbstractDriver.php b/src/Drivers/AbstractDriver.php new file mode 100644 index 0000000..f787707 --- /dev/null +++ b/src/Drivers/AbstractDriver.php @@ -0,0 +1,14 @@ + 0, + 192 => 1, + 224 => 2, + 256 => 3, + 320 => 4, + 384 => 5, + 448 => 6, + 512 => 7, + ]; + + public function match(string $address): bool + { + $address = strtolower($address); + + $prefix = implode('|', array_keys($this->options)); + $pattern = sprintf('/^((%s)?([qp])[a-z0-9]{41})/', $prefix); + + return preg_match($pattern, $address) === 1; + } + + public function check(string $address, array $networks = []): bool + { + try { + $address = strtolower($address); + + [,$words] = Base32Decoder::decode($address); + + $numWords = count($words); + $bytes = Base32Decoder::fromWords($numWords, $words); + $numBytes = count($bytes); + + $this->extractPayload($numBytes, $bytes); + + return true; + } catch (Throwable) { + return false; + } + } + + /** + * @return string[] - script type and hash + */ + protected function extractPayload(int $numBytes, array $payloadBytes): array + { + if ($numBytes < 1) { + throw new RuntimeException("Empty base32 string"); + } + + [$scriptType, $hashLengthBits] = $this->decodeVersion($payloadBytes[0]); + + if (($hashLengthBits / 8) !== $numBytes - 1) { + throw new RuntimeException("Hash length does not match version"); + } + + $hash = ""; + + foreach (array_slice($payloadBytes, 1) as $byte) { + $hash .= pack("C*", $byte); + } + + return [$scriptType, $hash]; + } + + + protected function decodeVersion(int $version): array + { + if (($version >> 7) & 1) { + throw new RuntimeException("Invalid version - MSB is reserved"); + } + + $scriptMarkerBits = ($version >> 3) & 0x1f; + $hashMarkerBits = ($version & 0x07); + + $hashBitsMap = array_flip(self::$hashBits); + if (!array_key_exists($hashMarkerBits, $hashBitsMap)) { + throw new RuntimeException("Invalid version or hash length"); + } + $hashLength = $hashBitsMap[$hashMarkerBits]; + + $scriptType = match ($scriptMarkerBits) { + 0 => "pubkeyhash", + 1 => "scripthash", + default => throw new RuntimeException('Invalid version or script type'), + }; + + return [$scriptType, $hashLength]; + } +} \ No newline at end of file diff --git a/src/Drivers/Base58Driver.php b/src/Drivers/Base58Driver.php new file mode 100644 index 0000000..e6ad37f --- /dev/null +++ b/src/Drivers/Base58Driver.php @@ -0,0 +1,47 @@ +options)); + $expr = sprintf('/^(%s)[a-km-zA-HJ-NP-Z1-9]{25,34}$/', $prefix); + + return preg_match($expr, $address) === 1; + } + + protected function getVersion($address): ?string + { + $hexString = Base58Decoder::decode($address, static::$base58Alphabet); + if ($hexString) { + return null; + } + + $version = substr($hexString, 0, 2); + + $check = substr($hexString, 0, -8); + $check = pack("H*", $check); + $check = hash("sha256", $check, true); + $check = hash("sha256", $check); + $check = strtoupper($check); + $check = substr($check, 0, 8); + + $isValid = str_ends_with($hexString, $check); + + return $isValid ? $version : null; + } +} \ No newline at end of file diff --git a/src/Drivers/Bech32Driver.php b/src/Drivers/Bech32Driver.php new file mode 100644 index 0000000..71fae04 --- /dev/null +++ b/src/Drivers/Bech32Driver.php @@ -0,0 +1,50 @@ +getPattern(); + return preg_match($expr, $address) === 1; + } + + public function check(string $address): bool + { + try { + $expr = $this->getPattern(); + preg_match($expr, $address, $match); + + [$hrpGot, $data] = (new Bech32Decoder())->decode($address); + if ($hrpGot !== $match[2]) { + return false; + } + + $dataLen = count($data); + + return !($dataLen === 0 || $dataLen > 65); + } catch (Throwable) { + return false; + } + } + + private function getPattern(): string + { + $prefix = implode('|', array_keys($this->options)); + return sprintf( + '/^((%s)(0([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59})|1[ac-hj-np-z02-9]{8,87}))$/', + $prefix + ); + } +} \ No newline at end of file diff --git a/src/Drivers/CardanoDriver.php b/src/Drivers/CardanoDriver.php new file mode 100644 index 0000000..6c517bb --- /dev/null +++ b/src/Drivers/CardanoDriver.php @@ -0,0 +1,35 @@ +options)); + $expr = sprintf('/^(%s)[0-9a-z]{38,}$/', $prefix); + + return preg_match($expr, $address) === 1; + } + + public function check(string $address): bool + { + try { + $decoded = (new Bech32Decoder())->decode($address); + + return in_array(array_keys($this->options), $decoded[0], true); + } catch (Bech32Exception) { + return false; + } + } +} \ No newline at end of file diff --git a/src/Drivers/DefaultBase58Driver.php b/src/Drivers/DefaultBase58Driver.php new file mode 100644 index 0000000..ca2bf7b --- /dev/null +++ b/src/Drivers/DefaultBase58Driver.php @@ -0,0 +1,27 @@ +options[$address[0]] ?? null; + if (null === $addressVersion) { + return false; + } + + $calculatedAddressVersion = $this->getVersion($address); + if (null === $calculatedAddressVersion) { + return false; + } + + return hexdec($addressVersion) === hexdec($calculatedAddressVersion); + } +} diff --git a/src/Drivers/EosDriver.php b/src/Drivers/EosDriver.php new file mode 100644 index 0000000..8b8719d --- /dev/null +++ b/src/Drivers/EosDriver.php @@ -0,0 +1,27 @@ + 7 && strtoupper($addressArray[$i]) !== $addressArray[$i]) || + (intval($addressHashArray[$i], 16) <= 7 && strtolower($addressArray[$i]) !== $addressArray[$i]) + ) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/src/Drivers/XrpBase58Driver.php b/src/Drivers/XrpBase58Driver.php new file mode 100644 index 0000000..aeaa8b6 --- /dev/null +++ b/src/Drivers/XrpBase58Driver.php @@ -0,0 +1,15 @@ +getVersion($address) !== null; + } +} diff --git a/src/Drivers/XrpXAddressDriver.php b/src/Drivers/XrpXAddressDriver.php new file mode 100644 index 0000000..af8b207 --- /dev/null +++ b/src/Drivers/XrpXAddressDriver.php @@ -0,0 +1,26 @@ +options)); + $expr = sprintf('/^(%s)[a-km-zA-HJ-NP-Z1-9]{33,55}$/', $prefix); + + return preg_match($expr, $address) === 1; + } + + public function check(string $address): bool + { + return true; + } +} From d14f22cac4b516ffd739e2d58962f0809a763079 Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Fri, 21 Apr 2023 17:05:09 +0400 Subject: [PATCH 36/65] driver config --- src/DriverConfig.php | 48 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/DriverConfig.php diff --git a/src/DriverConfig.php b/src/DriverConfig.php new file mode 100644 index 0000000..f6bc2a5 --- /dev/null +++ b/src/DriverConfig.php @@ -0,0 +1,48 @@ + $driver + * @param array $mainnet + * @param array $testnet + */ + public function __construct( + private string $driver, + private array $mainnet = [], + private array $testnet = [] + ) + { + } + + public function makeDriver(bool $isMainNet): ?AbstractDriver + { + if (!class_exists($this->driver)) { + return null; + } + + return new $this->driver($this->getDriverOptions($isMainNet)); + + } + + private function getDriverOptions(bool $isMainNet): array + { + if ($isMainNet) { + return $this->mainnet; + } + + return $this->testnet + ?: $this->mainnet + ?: []; + } +} \ No newline at end of file From 53fb5b92c99dd79cd4c4abd02ecdb98c1a41b1d7 Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Fri, 21 Apr 2023 17:05:29 +0400 Subject: [PATCH 37/65] validator --- src/Validator.php | 73 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/Validator.php diff --git a/src/Validator.php b/src/Validator.php new file mode 100644 index 0000000..bfb88a2 --- /dev/null +++ b/src/Validator.php @@ -0,0 +1,73 @@ +value, config("address_validation.{$currency->value}"), app()->isProduction()); + } + + public function isValid(?string $address): bool + { + if (!$address) { + return false; + } + + $drivers = $this->getDrivers(); + // if there is no drivers we force address to be valid + if (null === $drivers || !$drivers->valid()) { + return true; + } + + /** @var Driver $driver */ + foreach ($drivers as $driver) { + if ($driver->match($address)) { + return $driver->check($address); + } + } + + return false; + } + + public function validate(?string $address): void + { + if (!$this->isValid($address)) { + throw new AddressValidationException($this->chain, $address); + } + } + + /** + * @return Generator|null + */ + protected function getDrivers(): ?Generator + { + /** @var DriverConfig $driverConfig */ + foreach ($this->options as $driverConfig) { + if ($driver = $driverConfig->makeDriver($this->isMainnet)) { + yield $driver; + } + } + + return null; + } + +} \ No newline at end of file From 4d092282e4e30b2ee44e9162ad7587937d6f337d Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Fri, 21 Apr 2023 17:05:35 +0400 Subject: [PATCH 38/65] provider --- src/AddressValidationServiceProvider.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/AddressValidationServiceProvider.php diff --git a/src/AddressValidationServiceProvider.php b/src/AddressValidationServiceProvider.php new file mode 100644 index 0000000..cb0cdcd --- /dev/null +++ b/src/AddressValidationServiceProvider.php @@ -0,0 +1,17 @@ +publishes([ + __DIR__.'/../config/address_validation.php' => config_path('address_validation.php'), + ]); + } +} \ No newline at end of file From c1391f91eb1a68f31d32ab0241071b6e90b4b47e Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Mon, 24 Apr 2023 14:36:52 +0400 Subject: [PATCH 39/65] extend requirements --- composer.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d33e379..51458de 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,10 @@ } ], "require": { - "spomky-labs/cbor-php": "^2.0" + "php": "^8.2", + "ext-gmp": "*", + "ext-bcmath": "*", + "laravel/framework": ">=v7.0.0" }, "require-dev": { "phpunit/phpunit": "~8.0" From 453207d36ca723a4c409bd70a9ae3602ceff759a Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Mon, 24 Apr 2023 14:37:04 +0400 Subject: [PATCH 40/65] adjust config --- config/address_validation.php | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/config/address_validation.php b/config/address_validation.php index 01675d7..d90cceb 100644 --- a/config/address_validation.php +++ b/config/address_validation.php @@ -5,7 +5,6 @@ use Merkeleon\PhpCryptocurrencyAddressValidation\DriverConfig; use Merkeleon\PhpCryptocurrencyAddressValidation\Drivers; use Merkeleon\PhpCryptocurrencyAddressValidation\Enums\CurrencyEnum; -use Merkeleon\PhpCryptocurrencyAddressValidation\Utils\KeccakDecoder; return [ CurrencyEnum::BEACON->value => [ @@ -16,7 +15,7 @@ ), ], CurrencyEnum::BINANCE->value => [ - new DriverConfig(KeccakDecoder::class), + new DriverConfig(Drivers\KeccakDriver::class), ], CurrencyEnum::BITCOIN_CASH->value => [ new DriverConfig( @@ -37,47 +36,51 @@ ['2' => 'C4', 'm' => '6F'] ), new DriverConfig( - Drivers\DefaultBase58Driver::class, - ['1' => '00', '3' => '05'], - ['2' => 'C4', 'm' => '6F'] + Drivers\Bech32Driver::class, + ['bc' => null], + ['tb' => null] ), ], CurrencyEnum::CARDANO->value => [ new DriverConfig( Drivers\CardanoDriver::class, - ['addr1' => null], - ['addr_test1' => null], + ['addr' => null], + ['addr_test' => null], ), ], CurrencyEnum::DASHCOIN->value => [ new DriverConfig( Drivers\DefaultBase58Driver::class, ['X' => '4C', '7' => '10'], - ['y' => '8c', '8' => '13'] + ['y' => '8C', '8' => '13'] ), ], CurrencyEnum::DOGECOIN->value => [ new DriverConfig( Drivers\DefaultBase58Driver::class, ['D' => '1E', '9' => '16', 'A' => '16'], - ['n' => '6f', 'm' => '6f', '2' => 'C4',], + ['n' => '71', 'm' => '6F', '2' => 'C4',], ), ], CurrencyEnum::EOS->value => [ new DriverConfig(Drivers\EosDriver::class), ], CurrencyEnum::ETHEREUM_CLASSIC->value => [ - new DriverConfig(KeccakDecoder::class), + new DriverConfig(Drivers\KeccakDriver::class), ], CurrencyEnum::ETHEREUM->value => [ - new DriverConfig(KeccakDecoder::class), + new DriverConfig(Drivers\KeccakDriver::class), ], CurrencyEnum::LITECOIN->value => [ new DriverConfig( Drivers\DefaultBase58Driver::class, ['L' => '30', 'M' => '32', '3' => '05'], - ['m' => '6f', '2' => 'c4', 'Q' => '3a'] + ['m' => '6F', '2' => 'C4', 'Q' => '3A'] ), + new DriverConfig( + Drivers\Bech32Driver::class, + ['ltc' => null], + ) ], CurrencyEnum::RIPPLE->value => [ new DriverConfig( @@ -90,9 +93,6 @@ ['T' => null], ), ], - CurrencyEnum::STELLAR->value => [ - // new DriverConfig(), - ], CurrencyEnum::TRON->value => [ new DriverConfig( Drivers\DefaultBase58Driver::class, From 03dab5002fba52563e88af00635e5de44fd330b5 Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Mon, 24 Apr 2023 14:37:15 +0400 Subject: [PATCH 41/65] provide tests --- tests/Validation/ADATest.php | 31 ------ tests/Validation/BCHTest.php | 27 ----- tests/Validation/BNBTest.php | 25 ----- tests/Validation/BSVTest.php | 27 ----- tests/Validation/BTCTest.php | 31 ------ tests/Validation/BaseValidationTestCase.php | 21 ---- tests/Validation/DASHTest.php | 29 ----- tests/Validation/DOGETest.php | 26 ----- tests/Validation/ETCTest.php | 22 ---- tests/Validation/ETHTest.php | 22 ---- tests/Validation/LTCTest.php | 32 ------ tests/Validation/NEOTest.php | 24 ----- tests/Validation/TADATest.php | 31 ------ tests/Validation/TBCHTest.php | 31 ------ tests/Validation/TBTCTest.php | 30 ------ tests/Validation/TRXTest.php | 27 ----- tests/Validation/XRPTest.php | 29 ----- tests/Validation/ZECTest.php | 24 ----- tests/ValidatorTest.php | 114 ++++++++++++++++++++ 19 files changed, 114 insertions(+), 489 deletions(-) delete mode 100644 tests/Validation/ADATest.php delete mode 100644 tests/Validation/BCHTest.php delete mode 100644 tests/Validation/BNBTest.php delete mode 100644 tests/Validation/BSVTest.php delete mode 100644 tests/Validation/BTCTest.php delete mode 100644 tests/Validation/BaseValidationTestCase.php delete mode 100644 tests/Validation/DASHTest.php delete mode 100644 tests/Validation/DOGETest.php delete mode 100644 tests/Validation/ETCTest.php delete mode 100644 tests/Validation/ETHTest.php delete mode 100644 tests/Validation/LTCTest.php delete mode 100644 tests/Validation/NEOTest.php delete mode 100644 tests/Validation/TADATest.php delete mode 100644 tests/Validation/TBCHTest.php delete mode 100644 tests/Validation/TBTCTest.php delete mode 100644 tests/Validation/TRXTest.php delete mode 100644 tests/Validation/XRPTest.php delete mode 100644 tests/Validation/ZECTest.php create mode 100644 tests/ValidatorTest.php diff --git a/tests/Validation/ADATest.php b/tests/Validation/ADATest.php deleted file mode 100644 index 4bf707b..0000000 --- a/tests/Validation/ADATest.php +++ /dev/null @@ -1,31 +0,0 @@ -getValidationInstance(); - $this->assertEquals($isValid, $validator->validate($address)); - } -} \ No newline at end of file diff --git a/tests/Validation/DASHTest.php b/tests/Validation/DASHTest.php deleted file mode 100644 index fe2d843..0000000 --- a/tests/Validation/DASHTest.php +++ /dev/null @@ -1,29 +0,0 @@ -setDeprecatedAllowed(true); - $this->assertEquals(true, $validator->validate('3CDJNfdWX8m2NwuGUV3nhXHXEeLygMXoAj')); - } - - public function getValidationInstance(): Validation - { - return Validation::make('LTC'); - } - - public function addressProvider() - { - return [ - ['1QLbGuc3WGKKKpLs4pBp9H6jiQ2MgPkXRp', false], - ['3CDJNfdWX8m2NwuGUV3nhXHXEeLygMXoAj', true], - ['LbTjMGN7gELw4KbeyQf6cTCq859hD18guE', true], - ['MJRSgZ3UUFcTBTBAaN38XAXvZLwRe8WVw7', true], - ['ltc1qy4rwhdkujk35ga26774gqmng67kgggtqnsx9vp0xgzp3wz3yjkhqashszw', true], - ['bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq', false], - ]; - } -} \ No newline at end of file diff --git a/tests/Validation/NEOTest.php b/tests/Validation/NEOTest.php deleted file mode 100644 index 8fc1682..0000000 --- a/tests/Validation/NEOTest.php +++ /dev/null @@ -1,24 +0,0 @@ -value]; + + $validator = new Validator($currency->value, $options, $net === 'mainnet'); + + $this->assertEquals( + $expected, + $validator->isValid($address), + "[{$currency->value}] address [{$address}] is invalid" + ); + } + + public function currencyAddressProvider(): array + { + return [ + 'Beacon #1' => [CurrencyEnum::BEACON, 'mainnet', true, 'bnb1fnd0k5l4p3ck2j9x9dp36chk059w977pszdgdz'], + 'Beacon #2' => [CurrencyEnum::BEACON, 'mainnet', true, 'bnb1xd8cn4w7q4hm4fc9a68xtpx22kqenju7ea8d3v'], + 'Beacon #3' => [CurrencyEnum::BEACON, 'testnet', true, 'tbnb1nuxna8asq69jf05cldcxpx9ee0m7drd9qz3aru'], + 'Beacon #4' => [CurrencyEnum::BEACON, 'mainnet', false, 'bnb1nuxna8asq69jf05cldcxpx9ee0m7drd9qz3aru'], + 'Beacon #5' => [CurrencyEnum::BEACON, 'testnet', false, 'bnb1nuxna8asq69jf05cldcxpx9ee0m7drd9qz3aru'], + // + 'Binance #1' => [CurrencyEnum::BINANCE, 'mainnet', true, '0x3f5CE5FBFe3E9af3971dD833D26bA9b5C936f0bE'], + 'Binance #2' => [CurrencyEnum::BINANCE, 'testnet', true, '0x3f5CE5FBFe3E9af3971dD833D26bA9b5C936f0bE'], + // + 'BitcoinCash #1' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, 'bitcoincash:qp009ldhprp75mgn4kgaw8jvrpadnvg8qst37j42kx'], + 'BitcoinCash #2' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, 'bitcoincash:qz7032ylhvxmndkx438pd8kjd7k7zcqxzsf26q0lvr'], + 'BitcoinCash #3' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, '32uLhn19ZasD5bsVhLdDthhM37JhJHiEE2'], + 'BitcoinCash #4' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', false, 'qz52zsruu43sq7ed0srym3g0ktpyjkdkxcm949pl2z'], + 'BitcoinCash #5' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', false, 'qpf8eq7ygvhqjwydk9n29f6nyc8rcjhlwcuwngn6xk'], + 'BitcoinCash #6' => [CurrencyEnum::BITCOIN_CASH, 'testnet', true, 'bchtest:qp2vjh349lcd22hu0hv6hv9d0pwlk43f6u04d5jk36'], + 'BitcoinCash #7' => [CurrencyEnum::BITCOIN_CASH, 'testnet', false, 'qp2vjh349lcd22hu0hv6hv9d0pwlk43f6u04d5jk36'], + 'BitcoinCash #8' => [CurrencyEnum::BITCOIN_CASH, 'testnet', false, '1KADKOasjxpNKzbfcKjnigLYWjEFPcMXqf'], + // + 'Bitcoin #1' => [CurrencyEnum::BITCOIN, 'mainnet', true, '1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2'], + 'Bitcoin #2' => [CurrencyEnum::BITCOIN, 'mainnet', true, 'bc1q6v096h88xmpl662af0nc7wd3vta56zv6pyccl8'], + 'Bitcoin #3' => [CurrencyEnum::BITCOIN, 'testnet', true, 'tb1q27dglj7x4l34mj7j2x7e6fqsexk6vf8kew6qm0'], + 'Bitcoin #4' => [CurrencyEnum::BITCOIN, 'testnet', false, 'tb1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq'], + 'Bitcoin #5' => [CurrencyEnum::BITCOIN, 'mainnet', false, 'tb1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq'], + 'Bitcoin #6' => [CurrencyEnum::BITCOIN, 'testnet', false, '1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2'], + 'Bitcoin #7' => [CurrencyEnum::BITCOIN, 'testnet', false, 'bc1q6v096h88xmpl662af0nc7wd3vta56zv6pyccl8'], + // + 'Cardano #1' => [CurrencyEnum::CARDANO, 'mainnet', true, 'addr1v9ywm0h3r8cnxrs04gfy7c3s2j44utjyvn5ldjdca0c2ltccgqdes'], + 'Cardano #2' => [CurrencyEnum::CARDANO, 'mainnet', false, 'stake1u9f9v0z5zzlldgx58n8tklphu8mf7h4jvp2j2gddluemnssjfnkzz'], + 'Cardano #3' => [CurrencyEnum::CARDANO, 'mainnet', true, 'addr1qxy3w62dupy9pzmpdfzxz4k240w5vawyagl5m9djqquyymrtm3grn7gpnjh7rwh2dy62hk8639lt6kzn32yxq960usnq9pexvt'], + // + 'Dashcoin #1' => [CurrencyEnum::DASHCOIN, 'mainnet', true, 'XpESxaUmonkq8RaLLp46Brx2K39ggQe226'], + 'Dashcoin #2' => [CurrencyEnum::DASHCOIN, 'mainnet', true, 'XmZQkfLtk3xLtbBMenTdaZMxsUBYAsRz1o'], + 'Dashcoin #3' => [CurrencyEnum::DASHCOIN, 'testnet', true, 'yNpxAuCGxLkDmVRY12m4qEWx1ttgTczSMJ'], + 'Dashcoin #4' => [CurrencyEnum::DASHCOIN, 'testnet', true, 'yi7GRZLiUGrJfX2aNDQ3v7pGSCTrnLa87o'], + // + 'Dogecoin #1' => [CurrencyEnum::DOGECOIN, 'mainnet', true, 'DFrGqzk4ZnTcK1gYtxZ9QDJsDiVM8v8gwV'], + 'Dogecoin #2' => [CurrencyEnum::DOGECOIN, 'mainnet', true, 'DMzanBYjj3yYHtCcnEucn7H8LHNY9fARB8'], + 'Dogecoin #3' => [CurrencyEnum::DOGECOIN, 'testnet', true, 'mketxxXxaBeH7AhCBMatdH5ATVad2XHQdj'], + 'Dogecoin #4' => [CurrencyEnum::DOGECOIN, 'testnet', false, 'n3TZFrdPvwGqfPC7vBb8PGgbFwc1Cnxq9h'], + 'Dogecoin #5' => [CurrencyEnum::DOGECOIN, 'testnet', true, 'nd5N1KW1waCicK1vqfwtTcBSbQCHBLv2Um'], + 'Dogecoin #6' => [CurrencyEnum::DOGECOIN, 'testnet', false, 'DFundMr7W8PjB6ZmVwGv1L1WtZ2X3m3KgQ'], + 'Dogecoin #7' => [CurrencyEnum::DOGECOIN, 'mainnet', false, 'n3TZFrdPvwGqfPC7vBb8PGgbFwc1Cnxq9h'], + // + 'EthereumClassic #1' => [CurrencyEnum::ETHEREUM_CLASSIC, 'mainnet', true, '0xe80b351948D0b87EE6A53e057A91467d54468D91'], + 'EthereumClassic #2' => [CurrencyEnum::ETHEREUM_CLASSIC, 'testnet', true, '0x799aD3Ff7Ef43DfD1473F9b8a8C4237c22D8113F'], + // + 'Ethereum #1' => [CurrencyEnum::ETHEREUM, 'mainnet', true, '0xe80b351948D0b87EE6A53e057A91467d54468D91'], + 'Ethereum #2' => [CurrencyEnum::ETHEREUM, 'testnet', true, '0x799aD3Ff7Ef43DfD1473F9b8a8C4237c22D8113F'], + // + 'Litecoin #1' => [CurrencyEnum::LITECOIN, 'mainnet', true, 'MF5yqnMuNoiCiCXbZft7iFgLK5BPG5QKbE'], + 'Litecoin #2' => [CurrencyEnum::LITECOIN, 'mainnet', false, '1QLbGuc3WGKKKpLs4pBp9H6jiQ2MgPkXRp'], + 'Litecoin #3' => [CurrencyEnum::LITECOIN, 'mainnet', true, '3CDJNfdWX8m2NwuGUV3nhXHXEeLygMXoAj'], + 'Litecoin #4' => [CurrencyEnum::LITECOIN, 'mainnet', true, 'LbTjMGN7gELw4KbeyQf6cTCq859hD18guE'], + 'Litecoin #5' => [CurrencyEnum::LITECOIN, 'mainnet', true, 'MK9xC9sbktt6DHMF6XwA3eZPJ2Vx32AXFT'], + 'Litecoin #6' => [CurrencyEnum::LITECOIN, 'testnet', true, 'mpQA36uSXDGxySjknqHFVMdsLPgPnbm7ku'], + 'Litecoin #7' => [CurrencyEnum::LITECOIN, 'mainnet', true, 'ltc1qf6wcq8kc0unt3wuaszlkms3zkuerxlfaz07zmj'], + // + 'Ripple #1' => [CurrencyEnum::RIPPLE, 'mainnet', true, 'r4dgY6Mzob3NVq8CFYdEiPnXKboRScsXRu'], + // + 'Tron #1' => [CurrencyEnum::TRON, 'mainnet', true, 'TC9fKEGcBTfmvXKXLHq5MJDC8P7dhZQM92'], + 'Tron #2' => [CurrencyEnum::TRON, 'testnet', true, 'TRALQkt1v9MjUVn3gT7csfpodJDmnC6q8s'], + // + 'Zcash #1' => [CurrencyEnum::ZCASH, 'mainnet', true, 't1YQV51DKzKP63xJcynXuRfryMjfmgTJ7Jc'], + 'Zcash #2' => [CurrencyEnum::ZCASH, 'mainnet', true, 't1VJhyyvbi63Cu6nEVVgNHSCokDRa3repZB'], + 'Zcash #3' => [CurrencyEnum::ZCASH, 'testnet', true, 't1VJhyyvbi63Cu6nEVVgNHSCokDRa3repZB'], + // + 'EOS #1' => [CurrencyEnum::EOS, 'mainnet', true, 'atticlabeosb'], + 'EOS #2' => [CurrencyEnum::EOS, 'mainnet', true, 'bitfinexeos1'], + ]; + } +} \ No newline at end of file From 0b59cc8d2d6b9c9670583bdec1d867af50ca2222 Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Mon, 24 Apr 2023 14:37:36 +0400 Subject: [PATCH 42/65] fix after testing --- src/Drivers/Base58Driver.php | 5 +- src/Drivers/Bech32Driver.php | 1 - src/Drivers/CardanoDriver.php | 6 +- src/Drivers/EosDriver.php | 13 +- src/Exception/AddressValidationException.php | 8 +- src/Utils/KeccakDecoder.php | 367 ++++++++----------- src/Validator.php | 41 ++- 7 files changed, 190 insertions(+), 251 deletions(-) diff --git a/src/Drivers/Base58Driver.php b/src/Drivers/Base58Driver.php index e6ad37f..c443e64 100644 --- a/src/Drivers/Base58Driver.php +++ b/src/Drivers/Base58Driver.php @@ -11,6 +11,7 @@ use function pack; use function preg_match; use function sprintf; +use function strtolower; use function strtoupper; use function substr; @@ -27,7 +28,7 @@ public function match(string $address): bool protected function getVersion($address): ?string { $hexString = Base58Decoder::decode($address, static::$base58Alphabet); - if ($hexString) { + if (!$hexString) { return null; } @@ -40,7 +41,7 @@ protected function getVersion($address): ?string $check = strtoupper($check); $check = substr($check, 0, 8); - $isValid = str_ends_with($hexString, $check); + $isValid = str_ends_with($hexString, strtolower($check)); return $isValid ? $version : null; } diff --git a/src/Drivers/Bech32Driver.php b/src/Drivers/Bech32Driver.php index 71fae04..b25700e 100644 --- a/src/Drivers/Bech32Driver.php +++ b/src/Drivers/Bech32Driver.php @@ -4,7 +4,6 @@ namespace Merkeleon\PhpCryptocurrencyAddressValidation\Drivers; -use Core\Crypto\Bech32; use Merkeleon\PhpCryptocurrencyAddressValidation\Utils\Bech32Decoder; use Throwable; use function array_keys; diff --git a/src/Drivers/CardanoDriver.php b/src/Drivers/CardanoDriver.php index 6c517bb..5db0bd0 100644 --- a/src/Drivers/CardanoDriver.php +++ b/src/Drivers/CardanoDriver.php @@ -17,7 +17,7 @@ class CardanoDriver extends AbstractDriver public function match(string $address): bool { $prefix = implode('|', array_keys($this->options)); - $expr = sprintf('/^(%s)[0-9a-z]{38,}$/', $prefix); + $expr = sprintf('/^((%s)(0([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59})|1[ac-hj-np-z02-9]{8,}))$/', $prefix); return preg_match($expr, $address) === 1; } @@ -25,9 +25,9 @@ public function match(string $address): bool public function check(string $address): bool { try { - $decoded = (new Bech32Decoder())->decode($address); + $decoded = (new Bech32Decoder())->decodeRaw($address); - return in_array(array_keys($this->options), $decoded[0], true); + return array_key_exists($decoded[0], $this->options); } catch (Bech32Exception) { return false; } diff --git a/src/Drivers/EosDriver.php b/src/Drivers/EosDriver.php index 8b8719d..ddd6a96 100644 --- a/src/Drivers/EosDriver.php +++ b/src/Drivers/EosDriver.php @@ -8,20 +8,11 @@ class EosDriver extends AbstractDriver { public function match(string $address): bool { - return preg_match('/^EOS[1-5a-z]{1}[1-5a-z.]{10,}$/i', $address); + return preg_match('/(^[a-z1-5.]{1,11}[a-z1-5]$)|(^[a-z1-5.]{12}[a-j1-5]$)/', $address) === 1; } public function check(string $address): bool { - // Remove the "EOS" prefix from the address - $prefixRemoved = substr($address, 3); - // Convert the address to lowercase - $lowercase = strtolower($prefixRemoved); - // Take the SHA256 hash of the lowercase address - $hash = hash('sha256', $lowercase, true); - // Take the first four bytes of the hash and convert them to hexadecimal - $hex = bin2hex(substr($hash, 0, 4)); - - return substr($address, -4) === $hex; + return true; } } diff --git a/src/Exception/AddressValidationException.php b/src/Exception/AddressValidationException.php index 3c6c671..b8d283a 100644 --- a/src/Exception/AddressValidationException.php +++ b/src/Exception/AddressValidationException.php @@ -4,12 +4,12 @@ namespace Merkeleon\PhpCryptocurrencyAddressValidation\Exception; -use Merkeleon\PhpCryptocurrencyAddressValidation\Contracts\Options; - class AddressValidationException extends \RuntimeException { - public function __construct(string $chain, string $notValidAddress) + public function __construct(string $chain, string $notValidAddress, bool $matchedPattern) { - parent::__construct("Incorrect {$chain} address [{$notValidAddress}]"); + $text = "Incorrect {$chain} address [{$notValidAddress}]"; + $text .= $matchedPattern ? ": address have wrong encoding" : ": address does not matched pattern"; + parent::__construct($text); } } \ No newline at end of file diff --git a/src/Utils/KeccakDecoder.php b/src/Utils/KeccakDecoder.php index 3b30453..8ab739e 100644 --- a/src/Utils/KeccakDecoder.php +++ b/src/Utils/KeccakDecoder.php @@ -2,14 +2,27 @@ namespace Merkeleon\PhpCryptocurrencyAddressValidation\Utils; +use Exception; +use function bin2hex; +use function chr; +use function in_array; +use function ord; +use function pack; +use function str_pad; +use function unpack; +use const PHP_INT_SIZE; +use const STR_PAD_RIGHT; + final class KeccakDecoder { - const KECCAK_ROUNDS = 24; + private const KECCAK_ROUNDS = 24; + private const LFSR = 0x01; + private const ENCODING = '8bit'; private static $keccakf_rotc = [1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, 27, 41, 56, 8, 25, 43, 62, 18, 39, 61, 20, 44]; - private static $keccakf_piln = [10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, 15, 23, 19, 13, 12, 2, 20, 14, 22, 9, 6, 1]; + private static $keccakf_piln = [10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, 15, 23, 19, 13, 12,2, 20, 14, 22, 9, 6, 1]; + private static $x64 = (PHP_INT_SIZE === 8); - private static function keccakf64(&$st, $rounds) - { + private static function keccakf64(&$st, $rounds): void { $keccakf_rndc = [ [0x00000000, 0x00000001], [0x00000000, 0x00008082], [0x80000000, 0x0000808a], [0x80000000, 0x80008000], [0x00000000, 0x0000808b], [0x00000000, 0x80000001], [0x80000000, 0x80008081], [0x80000000, 0x00008009], @@ -18,67 +31,69 @@ private static function keccakf64(&$st, $rounds) [0x80000000, 0x00008002], [0x80000000, 0x00000080], [0x00000000, 0x0000800a], [0x80000000, 0x8000000a], [0x80000000, 0x80008081], [0x80000000, 0x00008080], [0x00000000, 0x80000001], [0x80000000, 0x80008008] ]; - $bc = []; - for ($round = 0; $round < $rounds; $round++) - { + + $bc = []; + for ($round = 0; $round < $rounds; $round++) { + // Theta - for ($i = 0; $i < 5; $i++) - { + for ($i = 0; $i < 5; $i++) { $bc[$i] = [ $st[$i][0] ^ $st[$i + 5][0] ^ $st[$i + 10][0] ^ $st[$i + 15][0] ^ $st[$i + 20][0], $st[$i][1] ^ $st[$i + 5][1] ^ $st[$i + 10][1] ^ $st[$i + 15][1] ^ $st[$i + 20][1] ]; } - for ($i = 0; $i < 5; $i++) - { + + for ($i = 0; $i < 5; $i++) { $t = [ $bc[($i + 4) % 5][0] ^ (($bc[($i + 1) % 5][0] << 1) | ($bc[($i + 1) % 5][1] >> 31)) & (0xFFFFFFFF), $bc[($i + 4) % 5][1] ^ (($bc[($i + 1) % 5][1] << 1) | ($bc[($i + 1) % 5][0] >> 31)) & (0xFFFFFFFF) ]; - for ($j = 0; $j < 25; $j += 5) - { + + for ($j = 0; $j < 25; $j += 5) { $st[$j + $i] = [ $st[$j + $i][0] ^ $t[0], $st[$j + $i][1] ^ $t[1] ]; } } + // Rho Pi $t = $st[1]; - for ($i = 0; $i < 24; $i++) - { - $j = self::$keccakf_piln[$i]; + for ($i = 0; $i < 24; $i++) { + $j = self::$keccakf_piln[$i]; + $bc[0] = $st[$j]; - $n = self::$keccakf_rotc[$i]; - $hi = $t[0]; - $lo = $t[1]; - if ($n >= 32) - { - $n -= 32; + + $n = self::$keccakf_rotc[$i]; + $hi = $t[0]; + $lo = $t[1]; + if ($n >= 32) { + $n -= 32; $hi = $t[1]; $lo = $t[0]; } - $st[$j] = [ + + $st[$j] =[ (($hi << $n) | ($lo >> (32 - $n))) & (0xFFFFFFFF), (($lo << $n) | ($hi >> (32 - $n))) & (0xFFFFFFFF) ]; - $t = $bc[0]; + + $t = $bc[0]; } + // Chi - for ($j = 0; $j < 25; $j += 5) - { - for ($i = 0; $i < 5; $i++) - { + for ($j = 0; $j < 25; $j += 5) { + for ($i = 0; $i < 5; $i++) { $bc[$i] = $st[$j + $i]; } - for ($i = 0; $i < 5; $i++) - { + for ($i = 0; $i < 5; $i++) { $st[$j + $i] = [ $st[$j + $i][0] ^ ~$bc[($i + 1) % 5][0] & $bc[($i + 2) % 5][0], $st[$j + $i][1] ^ ~$bc[($i + 1) % 5][1] & $bc[($i + 2) % 5][1] ]; } } + // Iota $st[0] = [ $st[0][0] ^ $keccakf_rndc[$round][0], @@ -87,54 +102,59 @@ private static function keccakf64(&$st, $rounds) } } - private static function keccak64($in_raw, $capacity, $outputlength, $suffix, $raw_output) - { + private static function keccak64($in_raw, int $capacity, int $outputlength, $suffix, bool $raw_output): string { $capacity /= 8; - $inlen = self::ourStrlen($in_raw); - $rsiz = 200 - 2 * $capacity; - $rsizw = $rsiz / 8; - $st = []; - for ($i = 0; $i < 25; $i++) - { + + $inlen = mb_strlen($in_raw, self::ENCODING); + + $rsiz = 200 - 2 * $capacity; + $rsizw = $rsiz / 8; + + $st = []; + for ($i = 0; $i < 25; $i++) { $st[] = [0, 0]; } - for ($in_t = 0; $inlen >= $rsiz; $inlen -= $rsiz, $in_t += $rsiz) - { - for ($i = 0; $i < $rsizw; $i++) - { - $t = unpack('V*', self::ourSubstr($in_raw, $i * 8 + $in_t, 8)); + + for ($in_t = 0; $inlen >= $rsiz; $inlen -= $rsiz, $in_t += $rsiz) { + for ($i = 0; $i < $rsizw; $i++) { + $t = unpack('V*', mb_substr($in_raw, $i * 8 + $in_t, 8, self::ENCODING)); + $st[$i] = [ $st[$i][0] ^ $t[2], $st[$i][1] ^ $t[1] ]; } + self::keccakf64($st, self::KECCAK_ROUNDS); } - $temp = self::ourSubstr($in_raw, $in_t, $inlen); - $temp = str_pad($temp, $rsiz, "\x0", STR_PAD_RIGHT); - $temp[$inlen] = chr($suffix); - $temp[$rsiz - 1] = chr($temp[$rsiz - 1] | 0x80); - for ($i = 0; $i < $rsizw; $i++) - { - $t = unpack('V*', self::ourSubstr($temp, $i * 8, 8)); + + $temp = mb_substr($in_raw, $in_t, $inlen, self::ENCODING); + $temp = str_pad($temp, $rsiz, "\x0", STR_PAD_RIGHT); + + $temp[$inlen] = chr($suffix); + $temp[$rsiz - 1] = chr(ord($temp[$rsiz - 1]) | 0x80); + + for ($i = 0; $i < $rsizw; $i++) { + $t = unpack('V*', mb_substr($temp, $i * 8, 8, self::ENCODING)); + $st[$i] = [ $st[$i][0] ^ $t[2], $st[$i][1] ^ $t[1] ]; } + self::keccakf64($st, self::KECCAK_ROUNDS); + $out = ''; - for ($i = 0; $i < 25; $i++) - { + for ($i = 0; $i < 25; $i++) { $out .= $t = pack('V*', $st[$i][1], $st[$i][0]); } - $r = self::ourSubstr($out, 0, $outputlength / 8); + $r = mb_substr($out, 0, $outputlength / 8, self::ENCODING); return $raw_output ? $r : bin2hex($r); } - private static function keccakf32(&$st, $rounds) - { + private static function keccakf32(&$st, $rounds): void { $keccakf_rndc = [ [0x0000, 0x0000, 0x0000, 0x0001], [0x0000, 0x0000, 0x0000, 0x8082], [0x8000, 0x0000, 0x0000, 0x0808a], [0x8000, 0x0000, 0x8000, 0x8000], [0x0000, 0x0000, 0x0000, 0x808b], [0x0000, 0x0000, 0x8000, 0x0001], [0x8000, 0x0000, 0x8000, 0x08081], [0x8000, 0x0000, 0x0000, 0x8009], @@ -143,12 +163,12 @@ private static function keccakf32(&$st, $rounds) [0x8000, 0x0000, 0x0000, 0x8002], [0x8000, 0x0000, 0x0000, 0x0080], [0x0000, 0x0000, 0x0000, 0x0800a], [0x8000, 0x0000, 0x8000, 0x000a], [0x8000, 0x0000, 0x8000, 0x8081], [0x8000, 0x0000, 0x0000, 0x8080], [0x0000, 0x0000, 0x8000, 0x00001], [0x8000, 0x0000, 0x8000, 0x8008] ]; - $bc = []; - for ($round = 0; $round < $rounds; $round++) - { + + $bc = []; + for ($round = 0; $round < $rounds; $round++) { + // Theta - for ($i = 0; $i < 5; $i++) - { + for ($i = 0; $i < 5; $i++) { $bc[$i] = [ $st[$i][0] ^ $st[$i + 5][0] ^ $st[$i + 10][0] ^ $st[$i + 15][0] ^ $st[$i + 20][0], $st[$i][1] ^ $st[$i + 5][1] ^ $st[$i + 10][1] ^ $st[$i + 15][1] ^ $st[$i + 20][1], @@ -156,16 +176,16 @@ private static function keccakf32(&$st, $rounds) $st[$i][3] ^ $st[$i + 5][3] ^ $st[$i + 10][3] ^ $st[$i + 15][3] ^ $st[$i + 20][3] ]; } - for ($i = 0; $i < 5; $i++) - { + + for ($i = 0; $i < 5; $i++) { $t = [ $bc[($i + 4) % 5][0] ^ ((($bc[($i + 1) % 5][0] << 1) | ($bc[($i + 1) % 5][1] >> 15)) & (0xFFFF)), $bc[($i + 4) % 5][1] ^ ((($bc[($i + 1) % 5][1] << 1) | ($bc[($i + 1) % 5][2] >> 15)) & (0xFFFF)), $bc[($i + 4) % 5][2] ^ ((($bc[($i + 1) % 5][2] << 1) | ($bc[($i + 1) % 5][3] >> 15)) & (0xFFFF)), $bc[($i + 4) % 5][3] ^ ((($bc[($i + 1) % 5][3] << 1) | ($bc[($i + 1) % 5][0] >> 15)) & (0xFFFF)) ]; - for ($j = 0; $j < 25; $j += 5) - { + + for ($j = 0; $j < 25; $j += 5) { $st[$j + $i] = [ $st[$j + $i][0] ^ $t[0], $st[$j + $i][1] ^ $t[1], @@ -174,31 +194,33 @@ private static function keccakf32(&$st, $rounds) ]; } } + // Rho Pi $t = $st[1]; - for ($i = 0; $i < 24; $i++) - { - $j = self::$keccakf_piln[$i]; - $bc[0] = $st[$j]; - $n = self::$keccakf_rotc[$i] >> 4; - $m = self::$keccakf_rotc[$i] % 16; - $st[$j] = [ - ((($t[(0 + $n) % 4] << $m) | ($t[(1 + $n) % 4] >> (16 - $m))) & (0xFFFF)), - ((($t[(1 + $n) % 4] << $m) | ($t[(2 + $n) % 4] >> (16 - $m))) & (0xFFFF)), - ((($t[(2 + $n) % 4] << $m) | ($t[(3 + $n) % 4] >> (16 - $m))) & (0xFFFF)), - ((($t[(3 + $n) % 4] << $m) | ($t[(0 + $n) % 4] >> (16 - $m))) & (0xFFFF)) + for ($i = 0; $i < 24; $i++) { + $j = self::$keccakf_piln[$i]; + $bc[0] = $st[$j]; + + + $n = self::$keccakf_rotc[$i] >> 4; + $m = self::$keccakf_rotc[$i] % 16; + + $st[$j] = [ + ((($t[(0+$n) %4] << $m) | ($t[(1+$n) %4] >> (16-$m))) & (0xFFFF)), + ((($t[(1+$n) %4] << $m) | ($t[(2+$n) %4] >> (16-$m))) & (0xFFFF)), + ((($t[(2+$n) %4] << $m) | ($t[(3+$n) %4] >> (16-$m))) & (0xFFFF)), + ((($t[(3+$n) %4] << $m) | ($t[(0+$n) %4] >> (16-$m))) & (0xFFFF)) ]; - $t = $bc[0]; + + $t = $bc[0]; } + // Chi - for ($j = 0; $j < 25; $j += 5) - { - for ($i = 0; $i < 5; $i++) - { + for ($j = 0; $j < 25; $j += 5) { + for ($i = 0; $i < 5; $i++) { $bc[$i] = $st[$j + $i]; } - for ($i = 0; $i < 5; $i++) - { + for ($i = 0; $i < 5; $i++) { $st[$j + $i] = [ $st[$j + $i][0] ^ ~$bc[($i + 1) % 5][0] & $bc[($i + 2) % 5][0], $st[$j + $i][1] ^ ~$bc[($i + 1) % 5][1] & $bc[($i + 2) % 5][1], @@ -207,6 +229,7 @@ private static function keccakf32(&$st, $rounds) ]; } } + // Iota $st[0] = [ $st[0][0] ^ $keccakf_rndc[$round][0], @@ -217,22 +240,23 @@ private static function keccakf32(&$st, $rounds) } } - private static function keccak32($in_raw, $capacity, $outputlength, $suffix, $raw_output) - { + private static function keccak32($in_raw, int $capacity, int $outputlength, $suffix, bool $raw_output): string { $capacity /= 8; - $inlen = self::ourStrlen($in_raw); - $rsiz = 200 - 2 * $capacity; - $rsizw = $rsiz / 8; - $st = []; - for ($i = 0; $i < 25; $i++) - { + + $inlen = mb_strlen($in_raw, self::ENCODING); + + $rsiz = 200 - 2 * $capacity; + $rsizw = $rsiz / 8; + + $st = []; + for ($i = 0; $i < 25; $i++) { $st[] = [0, 0, 0, 0]; } - for ($in_t = 0; $inlen >= $rsiz; $inlen -= $rsiz, $in_t += $rsiz) - { - for ($i = 0; $i < $rsizw; $i++) - { - $t = unpack('v*', self::ourSubstr($in_raw, $i * 8 + $in_t, 8)); + + for ($in_t = 0; $inlen >= $rsiz; $inlen -= $rsiz, $in_t += $rsiz) { + for ($i = 0; $i < $rsizw; $i++) { + $t = unpack('v*', mb_substr($in_raw, $i * 8 + $in_t, 8, self::ENCODING)); + $st[$i] = [ $st[$i][0] ^ $t[4], $st[$i][1] ^ $t[3], @@ -240,15 +264,19 @@ private static function keccak32($in_raw, $capacity, $outputlength, $suffix, $ra $st[$i][3] ^ $t[1] ]; } + self::keccakf32($st, self::KECCAK_ROUNDS); } - $temp = self::ourSubstr($in_raw, $in_t, $inlen); - $temp = str_pad($temp, $rsiz, "\x0", STR_PAD_RIGHT); - $temp[$inlen] = chr($suffix); - $temp[$rsiz - 1] = chr($temp[$rsiz - 1] | 0x80); - for ($i = 0; $i < $rsizw; $i++) - { - $t = unpack('v*', self::ourSubstr($temp, $i * 8, 8)); + + $temp = mb_substr($in_raw, $in_t, $inlen, self::ENCODING); + $temp = str_pad($temp, $rsiz, "\x0", STR_PAD_RIGHT); + + $temp[$inlen] = chr($suffix); + $temp[$rsiz - 1] = chr((int) $temp[$rsiz - 1] | 0x80); + + for ($i = 0; $i < $rsizw; $i++) { + $t = unpack('v*', mb_substr($temp, $i * 8, 8, self::ENCODING)); + $st[$i] = [ $st[$i][0] ^ $t[4], $st[$i][1] ^ $t[3], @@ -256,136 +284,37 @@ private static function keccak32($in_raw, $capacity, $outputlength, $suffix, $ra $st[$i][3] ^ $t[1] ]; } - self::keccakf32($st, self::KECCAK_ROUNDS); - $out = ''; - for ($i = 0; $i < 25; $i++) - { - $out .= $t = pack('v*', $st[$i][3], $st[$i][2], $st[$i][1], $st[$i][0]); - } - $r = self::ourSubstr($out, 0, $outputlength / 8); - return $raw_output ? $r : bin2hex($r); - } - - // 0 = not run, 1 = 64 bit passed, 2 = 32 bit passed, 3 = failed - private static $test_state = 0; - - private static function selfTest() - { - if (self::$test_state === 1 || self::$test_state === 2) - { - return; - } - if (self::$test_state === 3) - { - throw new \Exception('Sha3 previous self test failed!'); - } - $in = ''; - $md = '6b4e03423667dbb73b6e15454f0eb1abd4597f9a1b078e3f5b5a6bc7'; - if (self::keccak64($in, 224, 224, 0x06, false) === $md) - { - self::$test_state = 1; + self::keccakf32($st, self::KECCAK_ROUNDS); - return; + $out = ''; + for ($i = 0; $i < 25; $i++) { + $out .= $t = pack('v*', $st[$i][3],$st[$i][2], $st[$i][1], $st[$i][0]); } - if (self::keccak32($in, 224, 224, 0x06, false) === $md) - { - self::$test_state = 2; + $r = mb_substr($out, 0, $outputlength / 8, self::ENCODING); - return; - } - self::$test_state = 3; - throw new \Exception('Sha3 self test failed!'); + return $raw_output ? $r: bin2hex($r); } - private static function keccak($in_raw, $capacity, $outputlength, $suffix, $raw_output) - { - self::selfTest(); - if (self::$test_state === 1) - { - return self::keccak64($in_raw, $capacity, $outputlength, $suffix, $raw_output); - } - - return self::keccak32($in_raw, $capacity, $outputlength, $suffix, $raw_output); + private static function keccak($in_raw, int $capacity, int $outputlength, $suffix, bool $raw_output): string { + return self::$x64 + ? self::keccak64($in_raw, $capacity, $outputlength, $suffix, $raw_output) + : self::keccak32($in_raw, $capacity, $outputlength, $suffix, $raw_output); } - public static function hash($in, $mdlen, $raw_output = false) - { - if (!in_array($mdlen, [224, 256, 384, 512], true)) - { - throw new \Exception('Unsupported Sha3 Hash output size.'); + public static function hash($in, int $mdlen, bool $raw_output = false): string { + if (!in_array($mdlen, [224, 256, 384, 512], true)) { + throw new Exception('Unsupported Keccak Hash output size.'); } - return self::keccak($in, $mdlen, $mdlen, 0x06, $raw_output); + return self::keccak($in, $mdlen, $mdlen, self::LFSR, $raw_output); } - public static function shake($in, $security_level, $outlen, $raw_output = false) - { - if (!in_array($security_level, [128, 256], true)) - { - throw new \Exception('Unsupported Sha3 Shake security level.'); + public static function shake($in, int $security_level, int $outlen, bool $raw_output = false): string { + if (!in_array($security_level, [128, 256], true)) { + throw new Exception('Unsupported Keccak Shake security level.'); } return self::keccak($in, $security_level, $outlen, 0x1f, $raw_output); } - /** - * Multi-byte-safe string functions borrowed from https://github.com/sarciszewski/php-future - */ - /** - * Multi-byte-safe string length calculation - * - * @param string $str - * @return int - */ - private static function ourStrlen($str) - { - // Premature optimization: cache the function_exists() result - static $exists = null; - if ($exists === null) - { - $exists = \function_exists('\\mb_strlen'); - } - // If it exists, we need to make sure we're using 8bit mode - if ($exists) - { - $length = \mb_strlen($str, '8bit'); - if ($length === false) - { - throw new \Exception('mb_strlen() failed.'); - } - - return $length; - } - - return \strlen($str); - } - - /** - * Multi-byte-safe substring calculation - * - * @param string $str - * @param int $start - * @param int $length (optional) - * @return string - */ - private static function ourSubstr($str, $start = 0, $length = null) - { - // Premature optimization: cache the function_exists() result - static $exists = null; - if ($exists === null) - { - $exists = \function_exists('\\mb_substr'); - } - // If it exists, we need to make sure we're using 8bit mode - if ($exists) - { - return \mb_substr($str, $start, $length, '8bit'); - } - elseif ($length !== null) - { - return \substr($str, $start, $length); - } - - return \substr($str, $start); - } -} \ No newline at end of file +} diff --git a/src/Validator.php b/src/Validator.php index bfb88a2..514d8c9 100644 --- a/src/Validator.php +++ b/src/Validator.php @@ -9,7 +9,6 @@ use Merkeleon\PhpCryptocurrencyAddressValidation\Enums\CurrencyEnum; use Merkeleon\PhpCryptocurrencyAddressValidation\Exception\AddressValidationException; use function app; -use function class_exists; use function config; readonly class Validator implements Contracts\Validator @@ -38,20 +37,29 @@ public function isValid(?string $address): bool return true; } - /** @var Driver $driver */ - foreach ($drivers as $driver) { - if ($driver->match($address)) { - return $driver->check($address); - } - } - - return false; + return (bool) $this->getDriver($drivers, $address)?->check($address); } public function validate(?string $address): void { - if (!$this->isValid($address)) { - throw new AddressValidationException($this->chain, $address); + if (!$address) { + return; + } + + $drivers = $this->getDrivers(); + // if there is no drivers we force address to be valid + if (null === $drivers || !$drivers->valid()) { + return; + } + + $driver = $this->getDriver($drivers, $address); + + if ($driver === null) { + throw new AddressValidationException($this->chain, $address, false); + } + + if (!$driver->check($address)) { + throw new AddressValidationException($this->chain, $address, true); } } @@ -70,4 +78,15 @@ protected function getDrivers(): ?Generator return null; } + protected function getDriver(iterable $drivers, string $address): ?Driver + { + /** @var Driver $driver */ + foreach ($drivers as $driver) { + if ($driver->match($address)) { + return $driver; + } + } + + return null; + } } \ No newline at end of file From cb0b916d7fd3a22f247b54608066379fd09d5e02 Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Mon, 24 Apr 2023 14:41:50 +0400 Subject: [PATCH 43/65] update read me --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4b191bd..8bfb2b4 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,10 @@ One day I will add other crypto currencies. Or how about you? :) ## Usage - ```php -use Merkeleon\PhpCryptocurrencyAddressValidation\Validation; +use Merkeleon\PhpCryptocurrencyAddressValidation\Enums\CurrencyEnum;use Merkeleon\PhpCryptocurrencyAddressValidation\Validator; -$validator = Validation::make('BTC'); -var_dump($validator->validate('1QLbGuc3WGKKKpLs4pBp9H6jiQ2MgPkXRp')); +$validator = Validator::make(CurrencyEnum::BITCOIN); +var_dump($validator->isValid('1QLbGuc3WGKKKpLs4pBp9H6jiQ2MgPkXRp')); ``` From 42289f4b063a81f477ab6df3d45e483cd023f931 Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Mon, 24 Apr 2023 14:42:00 +0400 Subject: [PATCH 44/65] remove unused --- src/Enums/CurrencyEnum.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Enums/CurrencyEnum.php b/src/Enums/CurrencyEnum.php index 8ff18e7..2516dd3 100644 --- a/src/Enums/CurrencyEnum.php +++ b/src/Enums/CurrencyEnum.php @@ -18,7 +18,6 @@ enum CurrencyEnum: string case ETHEREUM = 'ethereum'; case LITECOIN = 'litecoin'; case RIPPLE = 'ripple'; - case STELLAR = 'stellar'; case TRON = 'tron'; case ZCASH = 'zcash'; } From d904002c065e8a47b39e3a455c984ad8f25e2f54 Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Wed, 17 May 2023 17:09:47 +0400 Subject: [PATCH 45/65] fix configs --- config/address_validation.php | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/config/address_validation.php b/config/address_validation.php index d90cceb..2af994f 100644 --- a/config/address_validation.php +++ b/config/address_validation.php @@ -14,9 +14,6 @@ ['tbnb' => null] ), ], - CurrencyEnum::BINANCE->value => [ - new DriverConfig(Drivers\KeccakDriver::class), - ], CurrencyEnum::BITCOIN_CASH->value => [ new DriverConfig( Drivers\Base32Driver::class, @@ -38,7 +35,7 @@ new DriverConfig( Drivers\Bech32Driver::class, ['bc' => null], - ['tb' => null] + ['tb' => null, 'bcrt' => null] ), ], CurrencyEnum::CARDANO->value => [ @@ -65,9 +62,6 @@ CurrencyEnum::EOS->value => [ new DriverConfig(Drivers\EosDriver::class), ], - CurrencyEnum::ETHEREUM_CLASSIC->value => [ - new DriverConfig(Drivers\KeccakDriver::class), - ], CurrencyEnum::ETHEREUM->value => [ new DriverConfig(Drivers\KeccakDriver::class), ], @@ -80,6 +74,7 @@ new DriverConfig( Drivers\Bech32Driver::class, ['ltc' => null], + ['tltc' => null, 'rltc' => null] ) ], CurrencyEnum::RIPPLE->value => [ From 6d092e90fdabf55675a042cd84fc26852d0bcc8b Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Mon, 22 May 2023 15:27:44 +0400 Subject: [PATCH 46/65] fix configs --- config/address_validation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/address_validation.php b/config/address_validation.php index 2af994f..d56907e 100644 --- a/config/address_validation.php +++ b/config/address_validation.php @@ -63,7 +63,7 @@ new DriverConfig(Drivers\EosDriver::class), ], CurrencyEnum::ETHEREUM->value => [ - new DriverConfig(Drivers\KeccakDriver::class), + new DriverConfig(Drivers\KeccakStrictDriver::class), ], CurrencyEnum::LITECOIN->value => [ new DriverConfig( From c4e42558ec263b4e66fc79f2ab79de0483b1e473 Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Mon, 22 May 2023 15:28:06 +0400 Subject: [PATCH 47/65] make not strict driver --- src/Drivers/KeccakDriver.php | 35 ++++++++---------- src/Drivers/KeccakStrictDriver.php | 59 ++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 19 deletions(-) create mode 100644 src/Drivers/KeccakStrictDriver.php diff --git a/src/Drivers/KeccakDriver.php b/src/Drivers/KeccakDriver.php index b44887b..c539221 100644 --- a/src/Drivers/KeccakDriver.php +++ b/src/Drivers/KeccakDriver.php @@ -6,36 +6,33 @@ use Merkeleon\PhpCryptocurrencyAddressValidation\Utils\KeccakDecoder; use function intval; -use function preg_match; -use function str_split; -use function strtolower; use function strtoupper; -use function substr; -class KeccakDriver extends AbstractDriver +class KeccakDriver extends KeccakStrictDriver { - public function match(string $address): bool + public function check(string $address): bool { - return preg_match('/^0x[a-fA-F0-9]{40}$/', $address) === 1; + $address = $this->toChecksum($address); + + return parent::check($address); } - public function check(string $address): bool + protected function toChecksum(string $address): string { - $address = substr($address, 2); - $addressHash = KeccakDecoder::hash(strtolower($address), 256); - $addressArray = str_split($address); - $addressHashArray = str_split($addressHash); + $address = str_replace('0x', '', $address); + $address = mb_strtolower($address); + + $hash = KeccakDecoder::hash($address, 256); + $checksumAddress = ''; for ($i = 0; $i < 40; $i++) { - // the nth letter should be uppercase if the nth digit of casemap is 1 - if ( - (intval($addressHashArray[$i], 16) > 7 && strtoupper($addressArray[$i]) !== $addressArray[$i]) || - (intval($addressHashArray[$i], 16) <= 7 && strtolower($addressArray[$i]) !== $addressArray[$i]) - ) { - return false; + if (intval($hash[$i], 16) >= 8) { + $checksumAddress .= strtoupper($address[$i]); + } else { + $checksumAddress .= $address[$i]; } } - return true; + return '0x' . $checksumAddress; } } \ No newline at end of file diff --git a/src/Drivers/KeccakStrictDriver.php b/src/Drivers/KeccakStrictDriver.php new file mode 100644 index 0000000..1807e5d --- /dev/null +++ b/src/Drivers/KeccakStrictDriver.php @@ -0,0 +1,59 @@ + 7 && strtoupper($addressArray[$i]) !== $addressArray[$i]) || + (intval($addressHashArray[$i], 16) <= 7 && strtolower($addressArray[$i]) !== $addressArray[$i]) + ) { + return false; + } + } + + return true; + } + + public function stripZero(string $value): string + { + if ($this->isZeroPrefixed($value)) { + return str_replace('0x', '', $value); + } + return $value; + } + + public function isZeroPrefixed(string $value): bool + { + return str_starts_with(haystack: $value, needle: '0x'); + } +} \ No newline at end of file From 827b8d1319ba81cd21616a0355b931732ea5ab79 Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Mon, 22 May 2023 15:28:16 +0400 Subject: [PATCH 48/65] tests --- tests/KeccakDriverTest.php | 45 ++++++++++++++++++++++++++++++++++++++ tests/ValidatorTest.php | 8 ++----- 2 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 tests/KeccakDriverTest.php diff --git a/tests/KeccakDriverTest.php b/tests/KeccakDriverTest.php new file mode 100644 index 0000000..6bf5fdf --- /dev/null +++ b/tests/KeccakDriverTest.php @@ -0,0 +1,45 @@ +value, $config, $net === 'mainnet'); + + self::assertEquals($expected, $validator->isValid($address)); + } + + public function addressesProvider(): array + { + return [ + 'Ethereum #1' => ['mainnet', true, '0xe80b351948D0b87EE6A53e057A91467d54468D91'], + 'Ethereum #2' => ['testnet', true, '0x799aD3Ff7Ef43DfD1473F9b8a8C4237c22D8113F'], + 'Ethereum #3' => ['mainnet', true, '0xe80b351948d0b87ee6a53e057a91467d54468d91'], + 'Ethereum #4' => ['testnet', true, '0x799ad3ff7ef43dfd1473f9b8a8c4237c22d8113f'], + ]; + } +} \ No newline at end of file diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php index 186b7a4..a6f8da8 100644 --- a/tests/ValidatorTest.php +++ b/tests/ValidatorTest.php @@ -47,9 +47,6 @@ public function currencyAddressProvider(): array 'Beacon #4' => [CurrencyEnum::BEACON, 'mainnet', false, 'bnb1nuxna8asq69jf05cldcxpx9ee0m7drd9qz3aru'], 'Beacon #5' => [CurrencyEnum::BEACON, 'testnet', false, 'bnb1nuxna8asq69jf05cldcxpx9ee0m7drd9qz3aru'], // - 'Binance #1' => [CurrencyEnum::BINANCE, 'mainnet', true, '0x3f5CE5FBFe3E9af3971dD833D26bA9b5C936f0bE'], - 'Binance #2' => [CurrencyEnum::BINANCE, 'testnet', true, '0x3f5CE5FBFe3E9af3971dD833D26bA9b5C936f0bE'], - // 'BitcoinCash #1' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, 'bitcoincash:qp009ldhprp75mgn4kgaw8jvrpadnvg8qst37j42kx'], 'BitcoinCash #2' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, 'bitcoincash:qz7032ylhvxmndkx438pd8kjd7k7zcqxzsf26q0lvr'], 'BitcoinCash #3' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, '32uLhn19ZasD5bsVhLdDthhM37JhJHiEE2'], @@ -84,11 +81,10 @@ public function currencyAddressProvider(): array 'Dogecoin #6' => [CurrencyEnum::DOGECOIN, 'testnet', false, 'DFundMr7W8PjB6ZmVwGv1L1WtZ2X3m3KgQ'], 'Dogecoin #7' => [CurrencyEnum::DOGECOIN, 'mainnet', false, 'n3TZFrdPvwGqfPC7vBb8PGgbFwc1Cnxq9h'], // - 'EthereumClassic #1' => [CurrencyEnum::ETHEREUM_CLASSIC, 'mainnet', true, '0xe80b351948D0b87EE6A53e057A91467d54468D91'], - 'EthereumClassic #2' => [CurrencyEnum::ETHEREUM_CLASSIC, 'testnet', true, '0x799aD3Ff7Ef43DfD1473F9b8a8C4237c22D8113F'], - // 'Ethereum #1' => [CurrencyEnum::ETHEREUM, 'mainnet', true, '0xe80b351948D0b87EE6A53e057A91467d54468D91'], 'Ethereum #2' => [CurrencyEnum::ETHEREUM, 'testnet', true, '0x799aD3Ff7Ef43DfD1473F9b8a8C4237c22D8113F'], + 'Ethereum #3' => [CurrencyEnum::ETHEREUM, 'mainnet', false, '0xe80b351948d0b87ee6a53e057a91467d54468d91'], + 'Ethereum #4' => [CurrencyEnum::ETHEREUM, 'testnet', false, '0x799ad3ff7ef43dfd1473f9b8a8c4237c22d8113f'], // 'Litecoin #1' => [CurrencyEnum::LITECOIN, 'mainnet', true, 'MF5yqnMuNoiCiCXbZft7iFgLK5BPG5QKbE'], 'Litecoin #2' => [CurrencyEnum::LITECOIN, 'mainnet', false, '1QLbGuc3WGKKKpLs4pBp9H6jiQ2MgPkXRp'], From 5e5af51b7213a6ae14502c2aa83029e958c26896 Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Mon, 22 May 2023 15:28:25 +0400 Subject: [PATCH 49/65] remove unused --- src/Enums/CurrencyEnum.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Enums/CurrencyEnum.php b/src/Enums/CurrencyEnum.php index 2516dd3..ec93ee2 100644 --- a/src/Enums/CurrencyEnum.php +++ b/src/Enums/CurrencyEnum.php @@ -7,14 +7,12 @@ enum CurrencyEnum: string { case BEACON = 'beacon'; - case BINANCE = 'binance'; case BITCOIN_CASH = 'bitcoin_cash'; case BITCOIN = 'bitcoin'; case CARDANO = 'cardano'; case DASHCOIN = 'dashcoin'; case DOGECOIN = 'dogecoin'; case EOS = 'eos'; - case ETHEREUM_CLASSIC = 'ethereum_classic'; case ETHEREUM = 'ethereum'; case LITECOIN = 'litecoin'; case RIPPLE = 'ripple'; From 7e21106ec305b8bff8274941cb8e78d67d343ab2 Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Thu, 29 Jun 2023 20:22:18 +0400 Subject: [PATCH 50/65] add new driver --- composer.json | 4 +- config/address_validation.php | 4 ++ src/Drivers/CborDriver.php | 82 +++++++++++++++++++++++++++++++++++ tests/ValidatorTest.php | 2 + 4 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 src/Drivers/CborDriver.php diff --git a/composer.json b/composer.json index 51458de..9eccd81 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,8 @@ "php": "^8.2", "ext-gmp": "*", "ext-bcmath": "*", - "laravel/framework": ">=v7.0.0" + "laravel/framework": ">=v7.0.0", + "spomky-labs/cbor-php": "2.0.0" }, "require-dev": { "phpunit/phpunit": "~8.0" @@ -27,4 +28,3 @@ } } } - diff --git a/config/address_validation.php b/config/address_validation.php index d56907e..e699cd9 100644 --- a/config/address_validation.php +++ b/config/address_validation.php @@ -44,6 +44,10 @@ ['addr' => null], ['addr_test' => null], ), + new DriverConfig( + Drivers\CborDriver::class, + ['A' => 33, 'D' => 66] + ) ], CurrencyEnum::DASHCOIN->value => [ new DriverConfig( diff --git a/src/Drivers/CborDriver.php b/src/Drivers/CborDriver.php new file mode 100644 index 0000000..536701a --- /dev/null +++ b/src/Drivers/CborDriver.php @@ -0,0 +1,82 @@ +add(OtherObject\SimpleObject::class); + $tagManager->add(Tag\PositiveBigIntegerTag::class); + + $this->decoder = new Decoder($tagManager, $otherObjectManager); + } + + public function match(string $address): bool + { + return Str::startsWith($address, array_keys($this->options)); + } + + public function check(string $address): bool + { + try { + $addressHex = Base58Decoder::decode($address, self::$base58Alphabet); + + $data = hex2bin($addressHex); + + $stream = new StringStream($data); + + + /** @var OtherObject\SimpleObject $object */ + $object = $this->decoder->decode($stream); + if ($object->getMajorType() !== 4) { + return false; + } + + $normalizedData = $object->getNormalizedData(); + + if (count($normalizedData) !== 2) { + return false; + } + if (!is_numeric($normalizedData[1])) { + return false; + } + if (!$normalizedData[0] instanceof ByteStringObject) { + return false; + } + + $bs = $normalizedData[0]; + if (!in_array($bs->getLength(), array_values($this->options), true)) { + return false; + } + + $crcCalculated = crc32($normalizedData[0]->getValue()); + $validCrc = $normalizedData[1]; + + return $crcCalculated === (int)$validCrc; + } catch (Throwable) { + return false; + } + } +} \ No newline at end of file diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php index a6f8da8..9961563 100644 --- a/tests/ValidatorTest.php +++ b/tests/ValidatorTest.php @@ -67,6 +67,8 @@ public function currencyAddressProvider(): array 'Cardano #1' => [CurrencyEnum::CARDANO, 'mainnet', true, 'addr1v9ywm0h3r8cnxrs04gfy7c3s2j44utjyvn5ldjdca0c2ltccgqdes'], 'Cardano #2' => [CurrencyEnum::CARDANO, 'mainnet', false, 'stake1u9f9v0z5zzlldgx58n8tklphu8mf7h4jvp2j2gddluemnssjfnkzz'], 'Cardano #3' => [CurrencyEnum::CARDANO, 'mainnet', true, 'addr1qxy3w62dupy9pzmpdfzxz4k240w5vawyagl5m9djqquyymrtm3grn7gpnjh7rwh2dy62hk8639lt6kzn32yxq960usnq9pexvt'], + 'Cardano #4' => [CurrencyEnum::CARDANO, 'mainnet', true, 'Ae2tdPwUPEYwNguM7TB3dMnZMfZxn1pjGHyGdjaF4mFqZF9L3bj6cdhiH8t'], + 'Cardano #4' => [CurrencyEnum::CARDANO, 'mainnet', true, 'DdzFFzCqrht2KYLcX8Vu53urCG52NxpgrGQvP9Mcp15Q8BkB9df9GndFDBRjoWTPuNkLW3yeQiFVet1KA7mraEkJ84AK2RwcEh3khs12'], // 'Dashcoin #1' => [CurrencyEnum::DASHCOIN, 'mainnet', true, 'XpESxaUmonkq8RaLLp46Brx2K39ggQe226'], 'Dashcoin #2' => [CurrencyEnum::DASHCOIN, 'mainnet', true, 'XmZQkfLtk3xLtbBMenTdaZMxsUBYAsRz1o'], From 5b2054a9a0836bd7ca4eff17d69da2867039d6b8 Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Thu, 29 Jun 2023 20:30:16 +0400 Subject: [PATCH 51/65] aad testnet --- config/address_validation.php | 3 ++- tests/ValidatorTest.php | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/config/address_validation.php b/config/address_validation.php index e699cd9..c65828e 100644 --- a/config/address_validation.php +++ b/config/address_validation.php @@ -46,7 +46,8 @@ ), new DriverConfig( Drivers\CborDriver::class, - ['A' => 33, 'D' => 66] + ['A' => 33, 'D' => 66], + ["2" => 40, "3" => 73], ) ], CurrencyEnum::DASHCOIN->value => [ diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php index 9961563..b02c643 100644 --- a/tests/ValidatorTest.php +++ b/tests/ValidatorTest.php @@ -68,7 +68,11 @@ public function currencyAddressProvider(): array 'Cardano #2' => [CurrencyEnum::CARDANO, 'mainnet', false, 'stake1u9f9v0z5zzlldgx58n8tklphu8mf7h4jvp2j2gddluemnssjfnkzz'], 'Cardano #3' => [CurrencyEnum::CARDANO, 'mainnet', true, 'addr1qxy3w62dupy9pzmpdfzxz4k240w5vawyagl5m9djqquyymrtm3grn7gpnjh7rwh2dy62hk8639lt6kzn32yxq960usnq9pexvt'], 'Cardano #4' => [CurrencyEnum::CARDANO, 'mainnet', true, 'Ae2tdPwUPEYwNguM7TB3dMnZMfZxn1pjGHyGdjaF4mFqZF9L3bj6cdhiH8t'], - 'Cardano #4' => [CurrencyEnum::CARDANO, 'mainnet', true, 'DdzFFzCqrht2KYLcX8Vu53urCG52NxpgrGQvP9Mcp15Q8BkB9df9GndFDBRjoWTPuNkLW3yeQiFVet1KA7mraEkJ84AK2RwcEh3khs12'], + 'Cardano #5' => [CurrencyEnum::CARDANO, 'mainnet', true, 'DdzFFzCqrht2KYLcX8Vu53urCG52NxpgrGQvP9Mcp15Q8BkB9df9GndFDBRjoWTPuNkLW3yeQiFVet1KA7mraEkJ84AK2RwcEh3khs12'], + + 'Cardano #6' => [CurrencyEnum::CARDANO, 'testnet', true, '37btjrVyb4KBbrmcxh3qQzswqDB4SCU8L68vYBJshaeYQ8rHVBfrAfuXZNyFHtR8QXUKR4CtytMyX4DwhsPYKKgFSpq8f5KxNz2s6Guqr6c6LzcHck'], + 'Cardano #7' => [CurrencyEnum::CARDANO, 'testnet', true, '2cWKMJemoBaipAW1NGegM2qWevSgpL9baiizayY4NnTBvxRGyppr2uym7F9eEtRLehFek'], + 'Cardano #8' => [CurrencyEnum::CARDANO, 'testnet', true, 'addr_test1qzfst6x8f4r47vm4qfeuj7g8r5pgkjnv5cuzjk94u8p7sd3gtlpjssk2fy95k4z5lr48tu48fcqstnzte44d8f8v8vhs9pwu4x'], // 'Dashcoin #1' => [CurrencyEnum::DASHCOIN, 'mainnet', true, 'XpESxaUmonkq8RaLLp46Brx2K39ggQe226'], 'Dashcoin #2' => [CurrencyEnum::DASHCOIN, 'mainnet', true, 'XmZQkfLtk3xLtbBMenTdaZMxsUBYAsRz1o'], From 1d87ebb74af5a794dc866b4d4423a4a0d4086441 Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Thu, 29 Jun 2023 20:32:35 +0400 Subject: [PATCH 52/65] code style --- config/address_validation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/address_validation.php b/config/address_validation.php index c65828e..7860570 100644 --- a/config/address_validation.php +++ b/config/address_validation.php @@ -47,7 +47,7 @@ new DriverConfig( Drivers\CborDriver::class, ['A' => 33, 'D' => 66], - ["2" => 40, "3" => 73], + ['2' => 40, '3' => 73], ) ], CurrencyEnum::DASHCOIN->value => [ From 14265b4bd3c2757c69a272a40ca1634f29a99b0d Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Thu, 29 Jun 2023 21:11:11 +0400 Subject: [PATCH 53/65] update cbor to make compatible with laravel 10 --- composer.json | 4 ++-- src/Drivers/CborDriver.php | 19 ++++++++++++------- tests/ValidatorTest.php | 1 - 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/composer.json b/composer.json index 9eccd81..dfa37bf 100644 --- a/composer.json +++ b/composer.json @@ -11,8 +11,8 @@ "php": "^8.2", "ext-gmp": "*", "ext-bcmath": "*", - "laravel/framework": ">=v7.0.0", - "spomky-labs/cbor-php": "2.0.0" + "laravel/framework": ">=v7.0.0|>=v10.0.0", + "spomky-labs/cbor-php": "^3.0" }, "require-dev": { "phpunit/phpunit": "~8.0" diff --git a/src/Drivers/CborDriver.php b/src/Drivers/CborDriver.php index 536701a..9caac71 100644 --- a/src/Drivers/CborDriver.php +++ b/src/Drivers/CborDriver.php @@ -9,6 +9,7 @@ use CBOR\TAg; use CBOR\StringStream; use CBOR\OtherObject; +use CBOR\Tag\GenericTag; use Illuminate\Support\Str; use Merkeleon\PhpCryptocurrencyAddressValidation\Utils\Base58Decoder; use Throwable; @@ -25,10 +26,10 @@ public function __construct(array $options) parent::__construct($options); $otherObjectManager = new OtherObject\OtherObjectManager(); - $tagManager = new Tag\TagObjectManager(); - $otherObjectManager->add(OtherObject\SimpleObject::class); - $tagManager->add(Tag\PositiveBigIntegerTag::class); + + $tagManager = new Tag\TagManager(); + $tagManager->add(Tag\UnsignedBigIntegerTag::class); $this->decoder = new Decoder($tagManager, $otherObjectManager); } @@ -54,7 +55,8 @@ public function check(string $address): bool return false; } - $normalizedData = $object->getNormalizedData(); + /** @var array $normalizedData */ + $normalizedData = $object->normalize(); if (count($normalizedData) !== 2) { return false; @@ -62,16 +64,19 @@ public function check(string $address): bool if (!is_numeric($normalizedData[1])) { return false; } - if (!$normalizedData[0] instanceof ByteStringObject) { + + if (!$normalizedData[0] instanceof GenericTag) { return false; } - $bs = $normalizedData[0]; + /** @var ByteStringObject $bs */ + $bs = $normalizedData[0]->getValue(); + if (!in_array($bs->getLength(), array_values($this->options), true)) { return false; } - $crcCalculated = crc32($normalizedData[0]->getValue()); + $crcCalculated = crc32($bs->getValue()); $validCrc = $normalizedData[1]; return $crcCalculated === (int)$validCrc; diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php index b02c643..ed6f64f 100644 --- a/tests/ValidatorTest.php +++ b/tests/ValidatorTest.php @@ -69,7 +69,6 @@ public function currencyAddressProvider(): array 'Cardano #3' => [CurrencyEnum::CARDANO, 'mainnet', true, 'addr1qxy3w62dupy9pzmpdfzxz4k240w5vawyagl5m9djqquyymrtm3grn7gpnjh7rwh2dy62hk8639lt6kzn32yxq960usnq9pexvt'], 'Cardano #4' => [CurrencyEnum::CARDANO, 'mainnet', true, 'Ae2tdPwUPEYwNguM7TB3dMnZMfZxn1pjGHyGdjaF4mFqZF9L3bj6cdhiH8t'], 'Cardano #5' => [CurrencyEnum::CARDANO, 'mainnet', true, 'DdzFFzCqrht2KYLcX8Vu53urCG52NxpgrGQvP9Mcp15Q8BkB9df9GndFDBRjoWTPuNkLW3yeQiFVet1KA7mraEkJ84AK2RwcEh3khs12'], - 'Cardano #6' => [CurrencyEnum::CARDANO, 'testnet', true, '37btjrVyb4KBbrmcxh3qQzswqDB4SCU8L68vYBJshaeYQ8rHVBfrAfuXZNyFHtR8QXUKR4CtytMyX4DwhsPYKKgFSpq8f5KxNz2s6Guqr6c6LzcHck'], 'Cardano #7' => [CurrencyEnum::CARDANO, 'testnet', true, '2cWKMJemoBaipAW1NGegM2qWevSgpL9baiizayY4NnTBvxRGyppr2uym7F9eEtRLehFek'], 'Cardano #8' => [CurrencyEnum::CARDANO, 'testnet', true, 'addr_test1qzfst6x8f4r47vm4qfeuj7g8r5pgkjnv5cuzjk94u8p7sd3gtlpjssk2fy95k4z5lr48tu48fcqstnzte44d8f8v8vhs9pwu4x'], From 1f2df04cb5ce9af5f26ed9493ee641681f157f86 Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Fri, 30 Jun 2023 14:42:08 +0400 Subject: [PATCH 54/65] lower case addresses --- src/Drivers/Bech32Driver.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Drivers/Bech32Driver.php b/src/Drivers/Bech32Driver.php index b25700e..d45c9d1 100644 --- a/src/Drivers/Bech32Driver.php +++ b/src/Drivers/Bech32Driver.php @@ -10,6 +10,7 @@ use function implode; use function preg_match; use function sprintf; +use function strtolower; class Bech32Driver extends AbstractDriver { @@ -22,6 +23,8 @@ public function match(string $address): bool public function check(string $address): bool { try { + $address = strtolower($address); + $expr = $this->getPattern(); preg_match($expr, $address, $match); From 58007f3132c3f1ed9d5b87159f11683944d90747 Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Fri, 30 Jun 2023 15:29:45 +0400 Subject: [PATCH 55/65] make validation softer for BCH --- src/Drivers/Base32Driver.php | 6 +++- src/Utils/Base32Decoder.php | 70 +++++++----------------------------- 2 files changed, 17 insertions(+), 59 deletions(-) diff --git a/src/Drivers/Base32Driver.php b/src/Drivers/Base32Driver.php index 363f465..ca15788 100644 --- a/src/Drivers/Base32Driver.php +++ b/src/Drivers/Base32Driver.php @@ -4,6 +4,7 @@ namespace Merkeleon\PhpCryptocurrencyAddressValidation\Drivers; +use Illuminate\Support\Str; use Merkeleon\PhpCryptocurrencyAddressValidation\Utils\Base32Decoder; use RuntimeException; use Throwable; @@ -15,6 +16,7 @@ use function pack; use function preg_match; use function sprintf; +use function str_contains; use function strtolower; class Base32Driver extends AbstractDriver @@ -43,9 +45,11 @@ public function match(string $address): bool public function check(string $address, array $networks = []): bool { try { + $hasPrefix = Str::contains($address, array_keys($this->options)); + $address = strtolower($address); - [,$words] = Base32Decoder::decode($address); + [,$words] = Base32Decoder::decode($address, $hasPrefix); $numWords = count($words); $bytes = Base32Decoder::fromWords($numWords, $words); diff --git a/src/Utils/Base32Decoder.php b/src/Utils/Base32Decoder.php index 6722d55..e4cdd20 100644 --- a/src/Utils/Base32Decoder.php +++ b/src/Utils/Base32Decoder.php @@ -131,7 +131,7 @@ protected static function polyModStep($prev) * * @return resource */ - protected static function prefixChk(string $prefix) + public static function prefixChk(string $prefix) { $chk = gmp_init(1); $length = strlen($prefix); @@ -139,51 +139,7 @@ protected static function prefixChk(string $prefix) $char = ord($prefix[$i]) & 0x1f; $chk = self::bitwiseXor(self::polyModStep($chk), gmp_init($char, 10)); } - - $chk = self::polyModStep($chk); - - return $chk; - } - - /** - * @param string $prefix - string prefix - * @param array $words - 5bit words (array) - * - * @return string - * @throws Base32Exception - */ - public static function encode(string $prefix, array $words): string - { - if ((strlen($prefix) + 7 + count($words)) > 90) { - throw new Base32Exception(); - } - - $prefix = strtolower($prefix); - - $chk = self::prefixChk($prefix); - $result = $prefix . self::SEPARATOR; - - foreach ($words as $iValue) { - $x = $iValue; - if ($x >> 5 !== 0) { - throw new \RuntimeException("Non 5-bit word"); - } - $chk = self::bitwiseXor(self::polyModStep($chk), gmp_init($x)); - $result .= self::$charset[$x]; - } - - for ($i = 0; $i < self::$checksumLen; ++$i) { - $chk = self::polyModStep($chk); - } - $chk = self::bitwiseXor($chk, gmp_init(1)); - - for ($i = 0; $i < self::$checksumLen; ++$i) { - $pos = 5 * (self::$checksumLen - 1 - $i); - $v2 = self::bitwiseAnd(self::rightShift($chk, $pos), gmp_init('1f', 16)); - $result .= self::$charset[(int) gmp_strval($v2, 10)]; - } - - return $result; + return self::polyModStep($chk); } /** @@ -193,7 +149,7 @@ public static function encode(string $prefix, array $words): string * @throws Base32Exception * @throws InvalidChecksumException */ - public static function decode(string $string): array + public static function decode(string $string, bool $hasPrefix = true): array { $stringLen = strlen($string); if ($stringLen < 8) { @@ -234,16 +190,15 @@ public static function decode(string $string): array throw new Base32Exception("Data contains mixture of higher/lower case characters"); } - if ($idxSeparator === -1) { + if ($hasPrefix && $idxSeparator === -1) { throw new Base32Exception("Missing separator character"); - } else { - if ($idxSeparator === 0) { - throw new Base32Exception("Missing prefix"); - } else { - if (($idxSeparator + 7) > $stringLen) { - throw new Base32Exception("Invalid location for separator character"); - } - } + } + if ($hasPrefix && $idxSeparator === 0) { + throw new Base32Exception("Missing prefix"); + } + + if (($idxSeparator + 7) > $stringLen) { + throw new Base32Exception("Invalid location for separator character"); } $prefix = ""; @@ -252,7 +207,6 @@ public static function decode(string $string): array $prefix .= pack("C*", $byte); } - $chk = self::prefixChk($prefix); $words = []; @@ -266,7 +220,7 @@ public static function decode(string $string): array $words[] = $word; } - if (gmp_cmp($chk, gmp_init(1)) !== 0) { + if ($hasPrefix && gmp_cmp($chk, gmp_init(1)) !== 0) { throw new InvalidChecksumException(); } From 3aa9112bf62074c2b1604f039cd7bc3b19df24e0 Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Fri, 30 Jun 2023 15:29:56 +0400 Subject: [PATCH 56/65] test coverage --- tests/ValidatorTest.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php index ed6f64f..89aacc4 100644 --- a/tests/ValidatorTest.php +++ b/tests/ValidatorTest.php @@ -50,11 +50,17 @@ public function currencyAddressProvider(): array 'BitcoinCash #1' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, 'bitcoincash:qp009ldhprp75mgn4kgaw8jvrpadnvg8qst37j42kx'], 'BitcoinCash #2' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, 'bitcoincash:qz7032ylhvxmndkx438pd8kjd7k7zcqxzsf26q0lvr'], 'BitcoinCash #3' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, '32uLhn19ZasD5bsVhLdDthhM37JhJHiEE2'], - 'BitcoinCash #4' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', false, 'qz52zsruu43sq7ed0srym3g0ktpyjkdkxcm949pl2z'], - 'BitcoinCash #5' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', false, 'qpf8eq7ygvhqjwydk9n29f6nyc8rcjhlwcuwngn6xk'], + 'BitcoinCash #4' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, 'qz52zsruu43sq7ed0srym3g0ktpyjkdkxcm949pl2z'], + 'BitcoinCash #5' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, 'qpf8eq7ygvhqjwydk9n29f6nyc8rcjhlwcuwngn6xk'], 'BitcoinCash #6' => [CurrencyEnum::BITCOIN_CASH, 'testnet', true, 'bchtest:qp2vjh349lcd22hu0hv6hv9d0pwlk43f6u04d5jk36'], - 'BitcoinCash #7' => [CurrencyEnum::BITCOIN_CASH, 'testnet', false, 'qp2vjh349lcd22hu0hv6hv9d0pwlk43f6u04d5jk36'], + 'BitcoinCash #7' => [CurrencyEnum::BITCOIN_CASH, 'testnet', true, 'qp2vjh349lcd22hu0hv6hv9d0pwlk43f6u04d5jk36'], 'BitcoinCash #8' => [CurrencyEnum::BITCOIN_CASH, 'testnet', false, '1KADKOasjxpNKzbfcKjnigLYWjEFPcMXqf'], + 'BitcoinCash #9' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, 'qpnxwdu09eq4gqxv0ala37yj5evmmakf5vpp770edu'], + 'BitcoinCash #10' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, 'bitcoincash:qpnxwdu09eq4gqxv0ala37yj5evmmakf5vpp770edu'], + 'BitcoinCash #11' => [CurrencyEnum::BITCOIN_CASH, 'testnet', false, 'bchtest:qpnxwdu09eq4gqxv0ala37yj5evmmakf5vpp770edu'], + 'BitcoinCash #12' => [CurrencyEnum::BITCOIN_CASH, 'testnet', false, 'bchreg:qpnxwdu09eq4gqxv0ala37yj5evmmakf5vpp770edu'], + 'BitcoinCash #13' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, 'bitcoincash:pwqwzrf7z06m7nn58tkdjyxqfewanlhyrpxysack85xvf3mt0rv02l9dxc5uf'], + 'BitcoinCash #14' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, 'bitcoincash:qr6m7j9njldwwzlg9v7v53unlr4jkmx6eylep8ekg2'], // 'Bitcoin #1' => [CurrencyEnum::BITCOIN, 'mainnet', true, '1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2'], 'Bitcoin #2' => [CurrencyEnum::BITCOIN, 'mainnet', true, 'bc1q6v096h88xmpl662af0nc7wd3vta56zv6pyccl8'], From 06b1c07f5656e13d9e13c4198feb65004431d30e Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Mon, 3 Jul 2023 20:09:41 +0400 Subject: [PATCH 57/65] fix --- src/Drivers/Bech32Driver.php | 2 +- tests/ValidatorTest.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Drivers/Bech32Driver.php b/src/Drivers/Bech32Driver.php index d45c9d1..f1a876d 100644 --- a/src/Drivers/Bech32Driver.php +++ b/src/Drivers/Bech32Driver.php @@ -17,7 +17,7 @@ class Bech32Driver extends AbstractDriver public function match(string $address): bool { $expr = $this->getPattern(); - return preg_match($expr, $address) === 1; + return preg_match($expr, strtolower($address)) === 1; } public function check(string $address): bool diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php index 89aacc4..5d86501 100644 --- a/tests/ValidatorTest.php +++ b/tests/ValidatorTest.php @@ -69,6 +69,7 @@ public function currencyAddressProvider(): array 'Bitcoin #5' => [CurrencyEnum::BITCOIN, 'mainnet', false, 'tb1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq'], 'Bitcoin #6' => [CurrencyEnum::BITCOIN, 'testnet', false, '1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2'], 'Bitcoin #7' => [CurrencyEnum::BITCOIN, 'testnet', false, 'bc1q6v096h88xmpl662af0nc7wd3vta56zv6pyccl8'], + 'Bitcoin #8' => [CurrencyEnum::BITCOIN, 'mainnet', true, 'BC1QL2725QLXHGWQ7F7XLJ8363FJCUF25XZ35SWRU5'], // 'Cardano #1' => [CurrencyEnum::CARDANO, 'mainnet', true, 'addr1v9ywm0h3r8cnxrs04gfy7c3s2j44utjyvn5ldjdca0c2ltccgqdes'], 'Cardano #2' => [CurrencyEnum::CARDANO, 'mainnet', false, 'stake1u9f9v0z5zzlldgx58n8tklphu8mf7h4jvp2j2gddluemnssjfnkzz'], From 234ba127584e2792d94fedb8178a917cef9e290d Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Mon, 3 Jul 2023 21:07:35 +0400 Subject: [PATCH 58/65] fix typo --- src/Drivers/CborDriver.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Drivers/CborDriver.php b/src/Drivers/CborDriver.php index 9caac71..94dea40 100644 --- a/src/Drivers/CborDriver.php +++ b/src/Drivers/CborDriver.php @@ -6,10 +6,12 @@ use CBOR\ByteStringObject; use CBOR\Decoder; -use CBOR\TAg; use CBOR\StringStream; -use CBOR\OtherObject; +use CBOR\OtherObject\OtherObjectManager; +use CBOR\OtherObject\SimpleObject; use CBOR\Tag\GenericTag; +use CBOR\Tag\TagManager; +use CBOR\Tag\UnsignedBigIntegerTag; use Illuminate\Support\Str; use Merkeleon\PhpCryptocurrencyAddressValidation\Utils\Base58Decoder; use Throwable; @@ -25,11 +27,11 @@ public function __construct(array $options) { parent::__construct($options); - $otherObjectManager = new OtherObject\OtherObjectManager(); - $otherObjectManager->add(OtherObject\SimpleObject::class); + $otherObjectManager = new OtherObjectManager(); + $otherObjectManager->add(SimpleObject::class); - $tagManager = new Tag\TagManager(); - $tagManager->add(Tag\UnsignedBigIntegerTag::class); + $tagManager = new TagManager(); + $tagManager->add(UnsignedBigIntegerTag::class); $this->decoder = new Decoder($tagManager, $otherObjectManager); } @@ -49,7 +51,7 @@ public function check(string $address): bool $stream = new StringStream($data); - /** @var OtherObject\SimpleObject $object */ + /** @var SimpleObject $object */ $object = $this->decoder->decode($stream); if ($object->getMajorType() !== 4) { return false; From 7b2df60337a592586b626a0a3f28174a21aa3c74 Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Tue, 4 Jul 2023 14:07:52 +0400 Subject: [PATCH 59/65] add GH action with tests --- .github/workflows/tests.yml | 36 ++++++++++++++++++++++++++++++++++++ composer.json | 3 +++ 2 files changed, 39 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..7c1fd20 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,36 @@ +name: Unit Tests + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Validate composer.json and composer.lock + run: composer validate --strict + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v3 + with: + path: vendor + key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php- + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run test suite + run: composer test diff --git a/composer.json b/composer.json index dfa37bf..f07d0f1 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,9 @@ "laravel/framework": ">=v7.0.0|>=v10.0.0", "spomky-labs/cbor-php": "^3.0" }, + "scripts": { + "test": "@php vendor/bin/phpunit" + }, "require-dev": { "phpunit/phpunit": "~8.0" }, From c4b41f44e7e777082c1d4cceef925c5249a94d60 Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Tue, 4 Jul 2023 14:30:18 +0400 Subject: [PATCH 60/65] remove composer validation --- .github/workflows/tests.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7c1fd20..5f3d41d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,9 +17,6 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Validate composer.json and composer.lock - run: composer validate --strict - - name: Cache Composer packages id: composer-cache uses: actions/cache@v3 From 097cca2e6d642be934830891538e24f6bc4c7771 Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Tue, 4 Jul 2023 14:33:24 +0400 Subject: [PATCH 61/65] add php version --- .github/workflows/tests.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5f3d41d..f74ade5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,6 +17,10 @@ jobs: steps: - uses: actions/checkout@v3 + - uses: shivammathur/setup-php@master + with: + php-version: '8.2' + - name: Cache Composer packages id: composer-cache uses: actions/cache@v3 From 928b049d3d81eca2c4fe5c8779e5ac4037ae362b Mon Sep 17 00:00:00 2001 From: "ilya.shabanov" Date: Tue, 29 Aug 2023 19:28:42 +0400 Subject: [PATCH 62/65] feature: long p-address support --- src/Drivers/Base32Driver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Drivers/Base32Driver.php b/src/Drivers/Base32Driver.php index ca15788..d19194c 100644 --- a/src/Drivers/Base32Driver.php +++ b/src/Drivers/Base32Driver.php @@ -37,7 +37,7 @@ public function match(string $address): bool $address = strtolower($address); $prefix = implode('|', array_keys($this->options)); - $pattern = sprintf('/^((%s)?([qp])[a-z0-9]{41})/', $prefix); + $pattern = sprintf('/^((%s)?([qp])[a-z0-9]{41,120})/', $prefix); return preg_match($pattern, $address) === 1; } From 25e353873953d4b461b99a1b308a800cb53442c5 Mon Sep 17 00:00:00 2001 From: Ilya Shabanov Date: Wed, 1 Nov 2023 17:34:16 +0400 Subject: [PATCH 63/65] feature(validators): add bech32m support --- src/Utils/Bech32Decoder.php | 9 +++++++-- tests/ValidatorTest.php | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Utils/Bech32Decoder.php b/src/Utils/Bech32Decoder.php index aa15b4d..b5091af 100644 --- a/src/Utils/Bech32Decoder.php +++ b/src/Utils/Bech32Decoder.php @@ -28,7 +28,12 @@ class Bech32Decoder -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1 ]; - + public const BECH32_POLY = 1; + public const BECH32M_POLY = 0x2bc830a3; + private const ALLOWED_POLY = [ + self::BECH32_POLY, + self::BECH32M_POLY, + ]; /** * Validates a bech32 string and returns [$hrp, $dataChars] if * the conversion was successful. An exception is thrown on invalid @@ -139,7 +144,7 @@ private function verifyChecksum(string $hrp, array $convertedDataChars): bool $r = array_merge($expandHrp, $convertedDataChars); $poly = $this->polyMod($r, count($r)); - return $poly === 1; + return in_array($poly, self::ALLOWED_POLY, true); } diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php index 5d86501..527d8ba 100644 --- a/tests/ValidatorTest.php +++ b/tests/ValidatorTest.php @@ -70,6 +70,7 @@ public function currencyAddressProvider(): array 'Bitcoin #6' => [CurrencyEnum::BITCOIN, 'testnet', false, '1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2'], 'Bitcoin #7' => [CurrencyEnum::BITCOIN, 'testnet', false, 'bc1q6v096h88xmpl662af0nc7wd3vta56zv6pyccl8'], 'Bitcoin #8' => [CurrencyEnum::BITCOIN, 'mainnet', true, 'BC1QL2725QLXHGWQ7F7XLJ8363FJCUF25XZ35SWRU5'], + 'Bitcoin #9' => [CurrencyEnum::BITCOIN, 'mainnet', true, 'bc1p5gyty9x2lrk65yndaeh242zm6xklgrv9nze9477dg0kyv6yvfljq0lqjkh'], // 'Cardano #1' => [CurrencyEnum::CARDANO, 'mainnet', true, 'addr1v9ywm0h3r8cnxrs04gfy7c3s2j44utjyvn5ldjdca0c2ltccgqdes'], 'Cardano #2' => [CurrencyEnum::CARDANO, 'mainnet', false, 'stake1u9f9v0z5zzlldgx58n8tklphu8mf7h4jvp2j2gddluemnssjfnkzz'], From 280ae942d0c0fc3e48dc46ae5d1abc9385a57583 Mon Sep 17 00:00:00 2001 From: Roman Habrusionok Date: Mon, 6 Nov 2023 22:37:55 +0300 Subject: [PATCH 64/65] Fix LTC testnet address validation --- config/address_validation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/address_validation.php b/config/address_validation.php index 7860570..e0d86d6 100644 --- a/config/address_validation.php +++ b/config/address_validation.php @@ -74,7 +74,7 @@ new DriverConfig( Drivers\DefaultBase58Driver::class, ['L' => '30', 'M' => '32', '3' => '05'], - ['m' => '6F', '2' => 'C4', 'Q' => '3A'] + ['m' => '6F', 'n' => '6F', '2' => 'C4', 'Q' => '3A'] ), new DriverConfig( Drivers\Bech32Driver::class, From ebf2a27b1940567ae02ef836112d6a2b75392757 Mon Sep 17 00:00:00 2001 From: Aliaksei Date: Thu, 30 Nov 2023 13:38:03 +0100 Subject: [PATCH 65/65] fix: driver config construction from cache --- src/DriverConfig.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/DriverConfig.php b/src/DriverConfig.php index f6bc2a5..d9df244 100644 --- a/src/DriverConfig.php +++ b/src/DriverConfig.php @@ -45,4 +45,13 @@ private function getDriverOptions(bool $isMainNet): array ?: $this->mainnet ?: []; } -} \ No newline at end of file + + public static function __set_state(array $state): DriverConfig + { + return new self( + $state['driver'], + $state['mainnet'], + $state['testnet'] + ); + } +}