diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..f74ade5 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,37 @@ +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 + + - uses: shivammathur/setup-php@master + with: + php-version: '8.2' + + - 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/.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/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 diff --git a/README.md b/README.md index 13d9f13..2144c60 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,19 @@ Easy to use PHP Bitcoin and Litecoin address validator. One day I will add other crypto currencies. Or how about you? :) -## Usage +## Installation +======= +``` +composer require merkeleon/php-cryptocurrency-address-validation +``` + +## Usage ```php -use Murich\PhpCryptocurrencyAddressValidation\Validation\BTC as BTCValidator; +use Merkeleon\PhpCryptocurrencyAddressValidation\Enums\CurrencyEnum;use Merkeleon\PhpCryptocurrencyAddressValidation\Validator; -$validator = new BTCValidator('1QLbGuc3WGKKKpLs4pBp9H6jiQ2MgPkXRp'); -var_dump($validator->validate()); +$validator = Validator::make(CurrencyEnum::BITCOIN); +var_dump($validator->isValid('1QLbGuc3WGKKKpLs4pBp9H6jiQ2MgPkXRp')); ``` diff --git a/composer.json b/composer.json index 9e9ed7d..f07d0f1 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": [ { @@ -7,13 +7,27 @@ "email": "andrey@phpteam.pro" } ], - "require": {}, + "require": { + "php": "^8.2", + "ext-gmp": "*", + "ext-bcmath": "*", + "laravel/framework": ">=v7.0.0|>=v10.0.0", + "spomky-labs/cbor-php": "^3.0" + }, + "scripts": { + "test": "@php vendor/bin/phpunit" + }, "require-dev": { - "phpunit/phpunit": "4.0.*" + "phpunit/phpunit": "~8.0" }, "autoload": { "psr-4": { - "Murich\\PhpCryptocurrencyAddressValidation\\": "src" + "Merkeleon\\PhpCryptocurrencyAddressValidation\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" } } } diff --git a/config/address_validation.php b/config/address_validation.php new file mode 100644 index 0000000..e0d86d6 --- /dev/null +++ b/config/address_validation.php @@ -0,0 +1,108 @@ +value => [ + new DriverConfig( + Drivers\Bech32Driver::class, + ['bnb' => null], + ['tbnb' => null] + ), + ], + 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\Bech32Driver::class, + ['bc' => null], + ['tb' => null, 'bcrt' => null] + ), + ], + CurrencyEnum::CARDANO->value => [ + new DriverConfig( + Drivers\CardanoDriver::class, + ['addr' => null], + ['addr_test' => null], + ), + new DriverConfig( + Drivers\CborDriver::class, + ['A' => 33, 'D' => 66], + ['2' => 40, '3' => 73], + ) + ], + 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' => '71', 'm' => '6F', '2' => 'C4',], + ), + ], + CurrencyEnum::EOS->value => [ + new DriverConfig(Drivers\EosDriver::class), + ], + CurrencyEnum::ETHEREUM->value => [ + new DriverConfig(Drivers\KeccakStrictDriver::class), + ], + CurrencyEnum::LITECOIN->value => [ + new DriverConfig( + Drivers\DefaultBase58Driver::class, + ['L' => '30', 'M' => '32', '3' => '05'], + ['m' => '6F', 'n' => '6F', '2' => 'C4', 'Q' => '3A'] + ), + new DriverConfig( + Drivers\Bech32Driver::class, + ['ltc' => null], + ['tltc' => null, 'rltc' => null] + ) + ], + CurrencyEnum::RIPPLE->value => [ + new DriverConfig( + Drivers\XrpBase58Driver::class, + ['r' => '00'] + ), + new DriverConfig( + Drivers\XrpXAddressDriver::class, + ['X' => null], + ['T' => null], + ), + ], + 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 diff --git a/phpunit.xml b/phpunit.xml index 0e0687b..f5abe30 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,11 +7,10 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" - stopOnFailure="false" - syntaxCheck="false"> + stopOnFailure="false"> - - ./tests/ + + ./tests/ \ No newline at end of file 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 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 @@ + $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 + ?: []; + } + + public static function __set_state(array $state): DriverConfig + { + return new self( + $state['driver'], + $state['mainnet'], + $state['testnet'] + ); + } +} 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,120})/', $prefix); + + return preg_match($pattern, $address) === 1; + } + + public function check(string $address, array $networks = []): bool + { + try { + $hasPrefix = Str::contains($address, array_keys($this->options)); + + $address = strtolower($address); + + [,$words] = Base32Decoder::decode($address, $hasPrefix); + + $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..c443e64 --- /dev/null +++ b/src/Drivers/Base58Driver.php @@ -0,0 +1,48 @@ +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, strtolower($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..f1a876d --- /dev/null +++ b/src/Drivers/Bech32Driver.php @@ -0,0 +1,52 @@ +getPattern(); + return preg_match($expr, strtolower($address)) === 1; + } + + public function check(string $address): bool + { + try { + $address = strtolower($address); + + $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..5db0bd0 --- /dev/null +++ b/src/Drivers/CardanoDriver.php @@ -0,0 +1,35 @@ +options)); + $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; + } + + public function check(string $address): bool + { + try { + $decoded = (new Bech32Decoder())->decodeRaw($address); + + return array_key_exists($decoded[0], $this->options); + } catch (Bech32Exception) { + return false; + } + } +} \ No newline at end of file diff --git a/src/Drivers/CborDriver.php b/src/Drivers/CborDriver.php new file mode 100644 index 0000000..94dea40 --- /dev/null +++ b/src/Drivers/CborDriver.php @@ -0,0 +1,89 @@ +add(SimpleObject::class); + + $tagManager = new TagManager(); + $tagManager->add(UnsignedBigIntegerTag::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 SimpleObject $object */ + $object = $this->decoder->decode($stream); + if ($object->getMajorType() !== 4) { + return false; + } + + /** @var array $normalizedData */ + $normalizedData = $object->normalize(); + + if (count($normalizedData) !== 2) { + return false; + } + if (!is_numeric($normalizedData[1])) { + return false; + } + + if (!$normalizedData[0] instanceof GenericTag) { + return false; + } + + /** @var ByteStringObject $bs */ + $bs = $normalizedData[0]->getValue(); + + if (!in_array($bs->getLength(), array_values($this->options), true)) { + return false; + } + + $crcCalculated = crc32($bs->getValue()); + $validCrc = $normalizedData[1]; + + return $crcCalculated === (int)$validCrc; + } catch (Throwable) { + 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..ddd6a96 --- /dev/null +++ b/src/Drivers/EosDriver.php @@ -0,0 +1,18 @@ +toChecksum($address); + + return parent::check($address); + } + + protected function toChecksum(string $address): string + { + $address = str_replace('0x', '', $address); + $address = mb_strtolower($address); + + $hash = KeccakDecoder::hash($address, 256); + + $checksumAddress = ''; + for ($i = 0; $i < 40; $i++) { + if (intval($hash[$i], 16) >= 8) { + $checksumAddress .= strtoupper($address[$i]); + } else { + $checksumAddress .= $address[$i]; + } + } + + 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 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; + } +} diff --git a/src/Enums/CurrencyEnum.php b/src/Enums/CurrencyEnum.php new file mode 100644 index 0000000..ec93ee2 --- /dev/null +++ b/src/Enums/CurrencyEnum.php @@ -0,0 +1,21 @@ + 0) { + $value = self::$generator[$j]; + } + + $v = self::bitwiseXor($v, gmp_init((string) $value, 10)); + } + + return $v; + } + + /** + * @param string $prefix + * + * @return resource + */ + public 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)); + } + return self::polyModStep($chk); + } + + /** + * @param string $string - base32 string + * + * @return array|string> - array> + * @throws Base32Exception + * @throws InvalidChecksumException + */ + public static function decode(string $string, bool $hasPrefix = true): 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 ($hasPrefix && $idxSeparator === -1) { + throw new Base32Exception("Missing separator character"); + } + if ($hasPrefix && $idxSeparator === 0) { + throw new Base32Exception("Missing prefix"); + } + + 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 ($hasPrefix && 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)); + + $data = []; + + for ($i = $positionOne + 1; $i < $length; $i++) { + $data[] = ($chars[$i] & 0x80) ? -1 : self::CHARKEY_KEY[$chars[$i]]; + } + + if (!$this->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 function verifyChecksum(string $hrp, array $convertedDataChars): bool + { + $expandHrp = $this->hrpExpand($hrp, strlen($hrp)); + $r = array_merge($expandHrp, $convertedDataChars); + $poly = $this->polyMod($r, count($r)); + + return in_array($poly, self::ALLOWED_POLY, true); + } + + + /** + * Expands the human-readable part into a character array for checksumming. + * + * @param string $hrp + * @param int $hrpLen + * @return int[] + */ + private function hrpExpand(string $hrp, int $hrpLen): array + { + $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 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/HexDecoder.php b/src/Utils/HexDecoder.php new file mode 100644 index 0000000..fbf9c10 --- /dev/null +++ b/src/Utils/HexDecoder.php @@ -0,0 +1,47 @@ + 0); + + return $hex; + } +} \ No newline at end of file diff --git a/src/Utils/KeccakDecoder.php b/src/Utils/KeccakDecoder.php new file mode 100644 index 0000000..8ab739e --- /dev/null +++ b/src/Utils/KeccakDecoder.php @@ -0,0 +1,320 @@ +> 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, int $capacity, int $outputlength, $suffix, bool $raw_output): string { + $capacity /= 8; + + $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*', 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 = 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++) { + $out .= $t = pack('V*', $st[$i][1], $st[$i][0]); + } + $r = mb_substr($out, 0, $outputlength / 8, self::ENCODING); + + return $raw_output ? $r : bin2hex($r); + } + + 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], + [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, int $capacity, int $outputlength, $suffix, bool $raw_output): string { + $capacity /= 8; + + $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*', mb_substr($in_raw, $i * 8 + $in_t, 8, self::ENCODING)); + + $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 = 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], + $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 = mb_substr($out, 0, $outputlength / 8, self::ENCODING); + + return $raw_output ? $r: bin2hex($r); + } + + 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, 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, self::LFSR, $raw_output); + } + + 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); + } +} diff --git a/src/Validation.php b/src/Validation.php deleted file mode 100644 index 8ffbbb8..0000000 --- a/src/Validation.php +++ /dev/null @@ -1,159 +0,0 @@ -address = $address; - $this->determineVersion(); - } - - 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]]; - } - } - - function validate() - { - if (is_null($this->addressVersion)) { - return false; - } - - $hexAddress = self::base58ToHex($this->address); - if (strlen($hexAddress) != 50) { - 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/BTC.php b/src/Validation/BTC.php deleted file mode 100644 index 3a85b35..0000000 --- a/src/Validation/BTC.php +++ /dev/null @@ -1,14 +0,0 @@ - '00', - '3' => '05' - ]; -} \ No newline at end of file diff --git a/src/Validation/DASH.php b/src/Validation/DASH.php deleted file mode 100644 index 06b1245..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/LTC.php b/src/Validation/LTC.php deleted file mode 100644 index 566e8ea..0000000 --- a/src/Validation/LTC.php +++ /dev/null @@ -1,43 +0,0 @@ - '30', - 'M' => '31', - '3' => self::DEPRECATED_ADDRESS_VERSION // deprecated for litecoin, should not be allowed for new user's inputs - ]; - - protected function validateVersion($version) - { - if ($this->addressVersion == self::DEPRECATED_ADDRESS_VERSION && !$this->deprecatedAllowed) { - 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 diff --git a/src/Validator.php b/src/Validator.php new file mode 100644 index 0000000..514d8c9 --- /dev/null +++ b/src/Validator.php @@ -0,0 +1,92 @@ +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; + } + + return (bool) $this->getDriver($drivers, $address)?->check($address); + } + + public function validate(?string $address): void + { + 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); + } + } + + /** + * @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; + } + + 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 diff --git a/tests/BTCTest.php b/tests/BTCTest.php deleted file mode 100644 index 1cc2363..0000000 --- a/tests/BTCTest.php +++ /dev/null @@ -1,22 +0,0 @@ -assertEquals($row[1], $validator->validate()); - } - - } -} \ No newline at end of file diff --git a/tests/DASHTest.php b/tests/DASHTest.php deleted file mode 100644 index b4fcb35..0000000 --- a/tests/DASHTest.php +++ /dev/null @@ -1,28 +0,0 @@ -assertEquals($row[1], $validator->validate()); - } - - } -} \ No newline at end of file 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/LTCTest.php b/tests/LTCTest.php deleted file mode 100644 index 2d1d18d..0000000 --- a/tests/LTCTest.php +++ /dev/null @@ -1,35 +0,0 @@ -assertEquals($row[1], $validator->validate()); - } - - } - - public function testLitecoinDeprecatedMultisigAddress() - { - $validator = new LTC('3CDJNfdWX8m2NwuGUV3nhXHXEeLygMXoAj'); - $validator->setDeprecatedAllowed(true); - $this->assertEquals(true, $validator->validate()); - } -} \ 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 @@ +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'], + // + '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', 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', 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'], + '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'], + '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'], + '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'], + // + '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'], + // + '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'], + '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