├── .github └── workflows │ └── tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── config └── address_validation.php ├── phpunit.xml ├── src ├── AddressValidationServiceProvider.php ├── Contracts │ ├── Driver.php │ └── Validator.php ├── DriverConfig.php ├── Drivers │ ├── AbstractDriver.php │ ├── Base32Driver.php │ ├── Base58Driver.php │ ├── Bech32Driver.php │ ├── CardanoDriver.php │ ├── CborDriver.php │ ├── DefaultBase58Driver.php │ ├── EosDriver.php │ ├── KeccakDriver.php │ ├── KeccakStrictDriver.php │ ├── XrpBase58Driver.php │ └── XrpXAddressDriver.php ├── Enums │ └── CurrencyEnum.php ├── Exception │ ├── AddressValidationException.php │ ├── Base32Exception.php │ ├── Bech32Exception.php │ └── InvalidChecksumException.php ├── Utils │ ├── Base32Decoder.php │ ├── Base58Decoder.php │ ├── Bech32Decoder.php │ ├── HexDecoder.php │ └── KeccakDecoder.php └── Validator.php └── tests ├── KeccakDriverTest.php ├── TestCase.php └── ValidatorTest.php /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - uses: shivammathur/setup-php@master 21 | with: 22 | php-version: '8.2' 23 | 24 | - name: Cache Composer packages 25 | id: composer-cache 26 | uses: actions/cache@v3 27 | with: 28 | path: vendor 29 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 30 | restore-keys: | 31 | ${{ runner.os }}-php- 32 | 33 | - name: Install dependencies 34 | run: composer install --prefer-dist --no-progress 35 | 36 | - name: Run test suite 37 | run: composer test 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor 3 | .idea 4 | .phpunit.result.cache -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Merkeleon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # php-cryptocurrency-address-validation 2 | 3 | Easy to use PHP Bitcoin and Litecoin address validator. 4 | One day I will add other crypto currencies. Or how about you? :) 5 | 6 | ## Installation 7 | 8 | ======= 9 | ``` 10 | composer require merkeleon/php-cryptocurrency-address-validation 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```php 16 | use Merkeleon\PhpCryptocurrencyAddressValidation\Enums\CurrencyEnum;use Merkeleon\PhpCryptocurrencyAddressValidation\Validator; 17 | 18 | $validator = Validator::make(CurrencyEnum::BITCOIN); 19 | var_dump($validator->isValid('1QLbGuc3WGKKKpLs4pBp9H6jiQ2MgPkXRp')); 20 | 21 | ``` 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "merkeleon/php-cryptocurrency-address-validation", 3 | "description": "Cryptocurrency address validation. Currently supports litecoin and bitcoin.", 4 | "authors": [ 5 | { 6 | "name": "Andrey Murashkin", 7 | "email": "andrey@phpteam.pro" 8 | } 9 | ], 10 | "require": { 11 | "php": "^8.2", 12 | "ext-gmp": "*", 13 | "ext-bcmath": "*", 14 | "laravel/framework": ">=v7.0.0|>=v10.0.0", 15 | "spomky-labs/cbor-php": "^3.0" 16 | }, 17 | "scripts": { 18 | "test": "@php vendor/bin/phpunit" 19 | }, 20 | "require-dev": { 21 | "phpunit/phpunit": "~8.0" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "Merkeleon\\PhpCryptocurrencyAddressValidation\\": "src" 26 | } 27 | }, 28 | "autoload-dev": { 29 | "psr-4": { 30 | "Tests\\": "tests/" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /config/address_validation.php: -------------------------------------------------------------------------------- 1 | value => [ 11 | new DriverConfig( 12 | Drivers\Bech32Driver::class, 13 | ['bnb' => null], 14 | ['tbnb' => null] 15 | ), 16 | ], 17 | CurrencyEnum::BITCOIN_CASH->value => [ 18 | new DriverConfig( 19 | Drivers\Base32Driver::class, 20 | ['bitcoincash:' => null], 21 | ['bchtest:' => null, 'bchreg:' => null,] 22 | ), 23 | new DriverConfig( 24 | Drivers\DefaultBase58Driver::class, 25 | ['1' => '00', '3' => '05'], 26 | ['2' => 'C4', 'm' => '6F'] 27 | ), 28 | ], 29 | CurrencyEnum::BITCOIN->value => [ 30 | new DriverConfig( 31 | Drivers\DefaultBase58Driver::class, 32 | ['1' => '00', '3' => '05'], 33 | ['2' => 'C4', 'm' => '6F'] 34 | ), 35 | new DriverConfig( 36 | Drivers\Bech32Driver::class, 37 | ['bc' => null], 38 | ['tb' => null, 'bcrt' => null] 39 | ), 40 | ], 41 | CurrencyEnum::CARDANO->value => [ 42 | new DriverConfig( 43 | Drivers\CardanoDriver::class, 44 | ['addr' => null], 45 | ['addr_test' => null], 46 | ), 47 | new DriverConfig( 48 | Drivers\CborDriver::class, 49 | ['A' => 33, 'D' => 66], 50 | ['2' => 40, '3' => 73], 51 | ) 52 | ], 53 | CurrencyEnum::DASHCOIN->value => [ 54 | new DriverConfig( 55 | Drivers\DefaultBase58Driver::class, 56 | ['X' => '4C', '7' => '10'], 57 | ['y' => '8C', '8' => '13'] 58 | ), 59 | ], 60 | CurrencyEnum::DOGECOIN->value => [ 61 | new DriverConfig( 62 | Drivers\DefaultBase58Driver::class, 63 | ['D' => '1E', '9' => '16', 'A' => '16'], 64 | ['n' => '71', 'm' => '6F', '2' => 'C4',], 65 | ), 66 | ], 67 | CurrencyEnum::EOS->value => [ 68 | new DriverConfig(Drivers\EosDriver::class), 69 | ], 70 | CurrencyEnum::ETHEREUM->value => [ 71 | new DriverConfig(Drivers\KeccakStrictDriver::class), 72 | ], 73 | CurrencyEnum::LITECOIN->value => [ 74 | new DriverConfig( 75 | Drivers\DefaultBase58Driver::class, 76 | ['L' => '30', 'M' => '32', '3' => '05'], 77 | ['m' => '6F', 'n' => '6F', '2' => 'C4', 'Q' => '3A'] 78 | ), 79 | new DriverConfig( 80 | Drivers\Bech32Driver::class, 81 | ['ltc' => null], 82 | ['tltc' => null, 'rltc' => null] 83 | ) 84 | ], 85 | CurrencyEnum::RIPPLE->value => [ 86 | new DriverConfig( 87 | Drivers\XrpBase58Driver::class, 88 | ['r' => '00'] 89 | ), 90 | new DriverConfig( 91 | Drivers\XrpXAddressDriver::class, 92 | ['X' => null], 93 | ['T' => null], 94 | ), 95 | ], 96 | CurrencyEnum::TRON->value => [ 97 | new DriverConfig( 98 | Drivers\DefaultBase58Driver::class, 99 | ['T' => '41'], 100 | ), 101 | ], 102 | CurrencyEnum::ZCASH->value => [ 103 | new DriverConfig( 104 | Drivers\DefaultBase58Driver::class, 105 | ['t' => '1C'], 106 | ), 107 | ], 108 | ]; -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./tests/ 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/AddressValidationServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 14 | __DIR__.'/../config/address_validation.php' => config_path('address_validation.php'), 15 | ]); 16 | } 17 | } -------------------------------------------------------------------------------- /src/Contracts/Driver.php: -------------------------------------------------------------------------------- 1 | $driver 17 | * @param array $mainnet 18 | * @param array $testnet 19 | */ 20 | public function __construct( 21 | private string $driver, 22 | private array $mainnet = [], 23 | private array $testnet = [] 24 | ) 25 | { 26 | } 27 | 28 | public function makeDriver(bool $isMainNet): ?AbstractDriver 29 | { 30 | if (!class_exists($this->driver)) { 31 | return null; 32 | } 33 | 34 | return new $this->driver($this->getDriverOptions($isMainNet)); 35 | 36 | } 37 | 38 | private function getDriverOptions(bool $isMainNet): array 39 | { 40 | if ($isMainNet) { 41 | return $this->mainnet; 42 | } 43 | 44 | return $this->testnet 45 | ?: $this->mainnet 46 | ?: []; 47 | } 48 | 49 | public static function __set_state(array $state): DriverConfig 50 | { 51 | return new self( 52 | $state['driver'], 53 | $state['mainnet'], 54 | $state['testnet'] 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Drivers/AbstractDriver.php: -------------------------------------------------------------------------------- 1 | 0, 26 | 192 => 1, 27 | 224 => 2, 28 | 256 => 3, 29 | 320 => 4, 30 | 384 => 5, 31 | 448 => 6, 32 | 512 => 7, 33 | ]; 34 | 35 | public function match(string $address): bool 36 | { 37 | $address = strtolower($address); 38 | 39 | $prefix = implode('|', array_keys($this->options)); 40 | $pattern = sprintf('/^((%s)?([qp])[a-z0-9]{41,120})/', $prefix); 41 | 42 | return preg_match($pattern, $address) === 1; 43 | } 44 | 45 | public function check(string $address, array $networks = []): bool 46 | { 47 | try { 48 | $hasPrefix = Str::contains($address, array_keys($this->options)); 49 | 50 | $address = strtolower($address); 51 | 52 | [,$words] = Base32Decoder::decode($address, $hasPrefix); 53 | 54 | $numWords = count($words); 55 | $bytes = Base32Decoder::fromWords($numWords, $words); 56 | $numBytes = count($bytes); 57 | 58 | $this->extractPayload($numBytes, $bytes); 59 | 60 | return true; 61 | } catch (Throwable) { 62 | return false; 63 | } 64 | } 65 | 66 | /** 67 | * @return string[] - script type and hash 68 | */ 69 | protected function extractPayload(int $numBytes, array $payloadBytes): array 70 | { 71 | if ($numBytes < 1) { 72 | throw new RuntimeException("Empty base32 string"); 73 | } 74 | 75 | [$scriptType, $hashLengthBits] = $this->decodeVersion($payloadBytes[0]); 76 | 77 | if (($hashLengthBits / 8) !== $numBytes - 1) { 78 | throw new RuntimeException("Hash length does not match version"); 79 | } 80 | 81 | $hash = ""; 82 | 83 | foreach (array_slice($payloadBytes, 1) as $byte) { 84 | $hash .= pack("C*", $byte); 85 | } 86 | 87 | return [$scriptType, $hash]; 88 | } 89 | 90 | 91 | protected function decodeVersion(int $version): array 92 | { 93 | if (($version >> 7) & 1) { 94 | throw new RuntimeException("Invalid version - MSB is reserved"); 95 | } 96 | 97 | $scriptMarkerBits = ($version >> 3) & 0x1f; 98 | $hashMarkerBits = ($version & 0x07); 99 | 100 | $hashBitsMap = array_flip(self::$hashBits); 101 | if (!array_key_exists($hashMarkerBits, $hashBitsMap)) { 102 | throw new RuntimeException("Invalid version or hash length"); 103 | } 104 | $hashLength = $hashBitsMap[$hashMarkerBits]; 105 | 106 | $scriptType = match ($scriptMarkerBits) { 107 | 0 => "pubkeyhash", 108 | 1 => "scripthash", 109 | default => throw new RuntimeException('Invalid version or script type'), 110 | }; 111 | 112 | return [$scriptType, $hashLength]; 113 | } 114 | } -------------------------------------------------------------------------------- /src/Drivers/Base58Driver.php: -------------------------------------------------------------------------------- 1 | options)); 23 | $expr = sprintf('/^(%s)[a-km-zA-HJ-NP-Z1-9]{25,34}$/', $prefix); 24 | 25 | return preg_match($expr, $address) === 1; 26 | } 27 | 28 | protected function getVersion($address): ?string 29 | { 30 | $hexString = Base58Decoder::decode($address, static::$base58Alphabet); 31 | if (!$hexString) { 32 | return null; 33 | } 34 | 35 | $version = substr($hexString, 0, 2); 36 | 37 | $check = substr($hexString, 0, -8); 38 | $check = pack("H*", $check); 39 | $check = hash("sha256", $check, true); 40 | $check = hash("sha256", $check); 41 | $check = strtoupper($check); 42 | $check = substr($check, 0, 8); 43 | 44 | $isValid = str_ends_with($hexString, strtolower($check)); 45 | 46 | return $isValid ? $version : null; 47 | } 48 | } -------------------------------------------------------------------------------- /src/Drivers/Bech32Driver.php: -------------------------------------------------------------------------------- 1 | getPattern(); 20 | return preg_match($expr, strtolower($address)) === 1; 21 | } 22 | 23 | public function check(string $address): bool 24 | { 25 | try { 26 | $address = strtolower($address); 27 | 28 | $expr = $this->getPattern(); 29 | preg_match($expr, $address, $match); 30 | 31 | [$hrpGot, $data] = (new Bech32Decoder())->decode($address); 32 | if ($hrpGot !== $match[2]) { 33 | return false; 34 | } 35 | 36 | $dataLen = count($data); 37 | 38 | return !($dataLen === 0 || $dataLen > 65); 39 | } catch (Throwable) { 40 | return false; 41 | } 42 | } 43 | 44 | private function getPattern(): string 45 | { 46 | $prefix = implode('|', array_keys($this->options)); 47 | return sprintf( 48 | '/^((%s)(0([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59})|1[ac-hj-np-z02-9]{8,87}))$/', 49 | $prefix 50 | ); 51 | } 52 | } -------------------------------------------------------------------------------- /src/Drivers/CardanoDriver.php: -------------------------------------------------------------------------------- 1 | options)); 20 | $expr = sprintf('/^((%s)(0([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59})|1[ac-hj-np-z02-9]{8,}))$/', $prefix); 21 | 22 | return preg_match($expr, $address) === 1; 23 | } 24 | 25 | public function check(string $address): bool 26 | { 27 | try { 28 | $decoded = (new Bech32Decoder())->decodeRaw($address); 29 | 30 | return array_key_exists($decoded[0], $this->options); 31 | } catch (Bech32Exception) { 32 | return false; 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/Drivers/CborDriver.php: -------------------------------------------------------------------------------- 1 | add(SimpleObject::class); 32 | 33 | $tagManager = new TagManager(); 34 | $tagManager->add(UnsignedBigIntegerTag::class); 35 | 36 | $this->decoder = new Decoder($tagManager, $otherObjectManager); 37 | } 38 | 39 | public function match(string $address): bool 40 | { 41 | return Str::startsWith($address, array_keys($this->options)); 42 | } 43 | 44 | public function check(string $address): bool 45 | { 46 | try { 47 | $addressHex = Base58Decoder::decode($address, self::$base58Alphabet); 48 | 49 | $data = hex2bin($addressHex); 50 | 51 | $stream = new StringStream($data); 52 | 53 | 54 | /** @var SimpleObject $object */ 55 | $object = $this->decoder->decode($stream); 56 | if ($object->getMajorType() !== 4) { 57 | return false; 58 | } 59 | 60 | /** @var array $normalizedData */ 61 | $normalizedData = $object->normalize(); 62 | 63 | if (count($normalizedData) !== 2) { 64 | return false; 65 | } 66 | if (!is_numeric($normalizedData[1])) { 67 | return false; 68 | } 69 | 70 | if (!$normalizedData[0] instanceof GenericTag) { 71 | return false; 72 | } 73 | 74 | /** @var ByteStringObject $bs */ 75 | $bs = $normalizedData[0]->getValue(); 76 | 77 | if (!in_array($bs->getLength(), array_values($this->options), true)) { 78 | return false; 79 | } 80 | 81 | $crcCalculated = crc32($bs->getValue()); 82 | $validCrc = $normalizedData[1]; 83 | 84 | return $crcCalculated === (int)$validCrc; 85 | } catch (Throwable) { 86 | return false; 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /src/Drivers/DefaultBase58Driver.php: -------------------------------------------------------------------------------- 1 | options[$address[0]] ?? null; 16 | if (null === $addressVersion) { 17 | return false; 18 | } 19 | 20 | $calculatedAddressVersion = $this->getVersion($address); 21 | if (null === $calculatedAddressVersion) { 22 | return false; 23 | } 24 | 25 | return hexdec($addressVersion) === hexdec($calculatedAddressVersion); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Drivers/EosDriver.php: -------------------------------------------------------------------------------- 1 | toChecksum($address); 16 | 17 | return parent::check($address); 18 | } 19 | 20 | protected function toChecksum(string $address): string 21 | { 22 | $address = str_replace('0x', '', $address); 23 | $address = mb_strtolower($address); 24 | 25 | $hash = KeccakDecoder::hash($address, 256); 26 | 27 | $checksumAddress = ''; 28 | for ($i = 0; $i < 40; $i++) { 29 | if (intval($hash[$i], 16) >= 8) { 30 | $checksumAddress .= strtoupper($address[$i]); 31 | } else { 32 | $checksumAddress .= $address[$i]; 33 | } 34 | } 35 | 36 | return '0x' . $checksumAddress; 37 | } 38 | } -------------------------------------------------------------------------------- /src/Drivers/KeccakStrictDriver.php: -------------------------------------------------------------------------------- 1 | 7 && strtoupper($addressArray[$i]) !== $addressArray[$i]) || 38 | (intval($addressHashArray[$i], 16) <= 7 && strtolower($addressArray[$i]) !== $addressArray[$i]) 39 | ) { 40 | return false; 41 | } 42 | } 43 | 44 | return true; 45 | } 46 | 47 | public function stripZero(string $value): string 48 | { 49 | if ($this->isZeroPrefixed($value)) { 50 | return str_replace('0x', '', $value); 51 | } 52 | return $value; 53 | } 54 | 55 | public function isZeroPrefixed(string $value): bool 56 | { 57 | return str_starts_with(haystack: $value, needle: '0x'); 58 | } 59 | } -------------------------------------------------------------------------------- /src/Drivers/XrpBase58Driver.php: -------------------------------------------------------------------------------- 1 | getVersion($address) !== null; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Drivers/XrpXAddressDriver.php: -------------------------------------------------------------------------------- 1 | options)); 17 | $expr = sprintf('/^(%s)[a-km-zA-HJ-NP-Z1-9]{33,55}$/', $prefix); 18 | 19 | return preg_match($expr, $address) === 1; 20 | } 21 | 22 | public function check(string $address): bool 23 | { 24 | return true; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Enums/CurrencyEnum.php: -------------------------------------------------------------------------------- 1 | 0) { 120 | $value = self::$generator[$j]; 121 | } 122 | 123 | $v = self::bitwiseXor($v, gmp_init((string) $value, 10)); 124 | } 125 | 126 | return $v; 127 | } 128 | 129 | /** 130 | * @param string $prefix 131 | * 132 | * @return resource 133 | */ 134 | public static function prefixChk(string $prefix) 135 | { 136 | $chk = gmp_init(1); 137 | $length = strlen($prefix); 138 | for ($i = 0; $i < $length; $i++) { 139 | $char = ord($prefix[$i]) & 0x1f; 140 | $chk = self::bitwiseXor(self::polyModStep($chk), gmp_init($char, 10)); 141 | } 142 | return self::polyModStep($chk); 143 | } 144 | 145 | /** 146 | * @param string $string - base32 string 147 | * 148 | * @return array|string> - array> 149 | * @throws Base32Exception 150 | * @throws InvalidChecksumException 151 | */ 152 | public static function decode(string $string, bool $hasPrefix = true): array 153 | { 154 | $stringLen = strlen($string); 155 | if ($stringLen < 8) { 156 | throw new Base32Exception("Address too short"); 157 | } 158 | 159 | if ($stringLen > 90) { 160 | throw new Base32Exception("Address too long"); 161 | } 162 | 163 | $chars = array_values(unpack("C*", $string)); 164 | 165 | $haveUpper = $haveLower = false; 166 | $idxSeparator = -1; 167 | $separatorChar = ord(self::SEPARATOR); 168 | 169 | for ($i = 0; $i < $stringLen; $i++) { 170 | $x = $chars[$i]; 171 | if ($x < 33 || $x > 126) { 172 | throw new Base32Exception("Out of range character in base32 string"); 173 | } 174 | 175 | if ($x >= 0x61 && $x <= 0x7a) { 176 | $haveLower = true; 177 | } 178 | 179 | if ($x >= 0x41 && $x <= 0x5a) { 180 | $haveUpper = true; 181 | $x = $chars[$i] = $x + 0x20; 182 | } 183 | 184 | if ($x === $separatorChar) { 185 | $idxSeparator = $i; 186 | } 187 | } 188 | 189 | if ($haveUpper && $haveLower) { 190 | throw new Base32Exception("Data contains mixture of higher/lower case characters"); 191 | } 192 | 193 | if ($hasPrefix && $idxSeparator === -1) { 194 | throw new Base32Exception("Missing separator character"); 195 | } 196 | if ($hasPrefix && $idxSeparator === 0) { 197 | throw new Base32Exception("Missing prefix"); 198 | } 199 | 200 | if (($idxSeparator + 7) > $stringLen) { 201 | throw new Base32Exception("Invalid location for separator character"); 202 | } 203 | 204 | $prefix = ""; 205 | 206 | foreach (array_slice($chars, 0, $idxSeparator) as $byte) { 207 | $prefix .= pack("C*", $byte); 208 | } 209 | 210 | $chk = self::prefixChk($prefix); 211 | 212 | $words = []; 213 | for ($i = $idxSeparator + 1; $i < $stringLen; $i++) { 214 | $char = $chars[$i]; 215 | if (!array_key_exists($char, self::$charsetKey)) { 216 | throw new Base32Exception("Unknown character in address"); 217 | } 218 | $word = self::$charsetKey[$char]; 219 | $chk = self::bitwiseXor(self::polyModStep($chk), gmp_init($word)); 220 | $words[] = $word; 221 | } 222 | 223 | if ($hasPrefix && gmp_cmp($chk, gmp_init(1)) !== 0) { 224 | throw new InvalidChecksumException(); 225 | } 226 | 227 | return [ 228 | $prefix, 229 | array_slice($words, 0, -self::$checksumLen) 230 | ]; 231 | } 232 | 233 | /** 234 | * Convert $bytes, an array of 8 bit numbers, to 235 | * words, an array of 5 bit numbers. 236 | * 237 | * @param int $numBytes 238 | * @param int[] $bytes 239 | * @return int[] 240 | * @throws Base32Exception 241 | */ 242 | public static function toWords($numBytes, array $bytes): array 243 | { 244 | return self::convertBits($bytes, $numBytes, 8, 5, true); 245 | } 246 | 247 | /** 248 | * Convert $words, an array of 5 bit numbres, to 249 | * bytes, an arrayof 8 bit numbers. 250 | * 251 | * @param int $numWords 252 | * @param int[] $words 253 | * @return int[] 254 | * @throws Base32Exception 255 | */ 256 | public static function fromWords($numWords, array $words): array 257 | { 258 | return self::convertBits($words, $numWords, 5, 8, false); 259 | } 260 | 261 | /** 262 | * Converts words of $fromBits bits to $toBits bits in size. 263 | * 264 | * @param int[] $data - character array of data to convert 265 | * @param int $inLen - number of elements in array 266 | * @param int $fromBits - word (bit count) size of provided data 267 | * @param int $toBits - requested word size (bit count) 268 | * @param bool $pad - whether to pad (only when encoding) 269 | * @return int[] 270 | * @throws Base32Exception 271 | */ 272 | protected static function convertBits(array $data, $inLen, $fromBits, $toBits, $pad = true): array 273 | { 274 | $acc = 0; 275 | $bits = 0; 276 | $ret = []; 277 | $maxv = (1 << $toBits) - 1; 278 | $maxacc = (1 << ($fromBits + $toBits - 1)) - 1; 279 | 280 | for ($i = 0; $i < $inLen; $i++) { 281 | $value = $data[$i]; 282 | if ($value < 0 || $value >> $fromBits) { 283 | throw new Base32Exception('Invalid value for convert bits'); 284 | } 285 | 286 | $acc = (($acc << $fromBits) | $value) & $maxacc; 287 | $bits += $fromBits; 288 | 289 | while ($bits >= $toBits) { 290 | $bits -= $toBits; 291 | $ret[] = (($acc >> $bits) & $maxv); 292 | } 293 | } 294 | 295 | if ($pad) { 296 | if ($bits) { 297 | $ret[] = ($acc << $toBits - $bits) & $maxv; 298 | } 299 | } else { 300 | if ($bits >= $fromBits || ((($acc << ($toBits - $bits))) & $maxv)) { 301 | throw new Base32Exception('Invalid data'); 302 | } 303 | } 304 | 305 | return $ret; 306 | } 307 | } -------------------------------------------------------------------------------- /src/Utils/Base58Decoder.php: -------------------------------------------------------------------------------- 1 | 90) { 53 | throw new Bech32Exception('Bech32 string cannot exceed 90 characters in length'); 54 | } 55 | 56 | return $this->decodeRaw($sBech); 57 | } 58 | 59 | /** 60 | * @param string $sBech The bech32 encoded string 61 | * 62 | * @return array Returns [$hrp, $dataChars] 63 | * @throws Bech32Exception 64 | * @throws Bech32Exception 65 | */ 66 | public function decodeRaw(string $sBech): array 67 | { 68 | $length = strlen($sBech); 69 | 70 | if ($length < 8) { 71 | throw new Bech32Exception("Bech32 string is too short"); 72 | } 73 | 74 | $chars = array_values(unpack('C*', $sBech)); 75 | 76 | $haveUpper = false; 77 | $haveLower = false; 78 | $positionOne = -1; 79 | 80 | for ($i = 0; $i < $length; $i++) { 81 | $x = $chars[$i]; 82 | 83 | if ($x < 33 || $x > 126) { 84 | throw new Bech32Exception('Out of range character in bech32 string'); 85 | } 86 | 87 | if ($x >= 0x61 && $x <= 0x7a) { 88 | $haveLower = true; 89 | } 90 | 91 | if ($x >= 0x41 && $x <= 0x5a) { 92 | $haveUpper = true; 93 | $x = $chars[$i] = $x + 0x20; 94 | } 95 | 96 | // find location of last '1' character 97 | if ($x === 0x31) { 98 | $positionOne = $i; 99 | } 100 | } 101 | 102 | if ($haveUpper && $haveLower) { 103 | throw new Bech32Exception('Data contains mixture of higher/lower case characters'); 104 | } 105 | 106 | if ($positionOne === -1) { 107 | throw new Bech32Exception("Missing separator character"); 108 | } 109 | 110 | if ($positionOne < 1) { 111 | throw new Bech32Exception("Empty HRP"); 112 | } 113 | 114 | if (($positionOne + 7) > $length) { 115 | throw new Bech32Exception('Too short checksum'); 116 | } 117 | 118 | $hrp = pack("C*", ...array_slice($chars, 0, $positionOne)); 119 | 120 | $data = []; 121 | 122 | for ($i = $positionOne + 1; $i < $length; $i++) { 123 | $data[] = ($chars[$i] & 0x80) ? -1 : self::CHARKEY_KEY[$chars[$i]]; 124 | } 125 | 126 | if (!$this->verifyChecksum($hrp, $data)) { 127 | throw new Bech32Exception('Invalid bech32 checksum'); 128 | } 129 | 130 | return [$hrp, array_slice($data, 0, -6)]; 131 | } 132 | 133 | /** 134 | * Verifies the checksum given $hrp and $convertedDataChars. 135 | * 136 | * @param string $hrp 137 | * @param int[] $convertedDataChars 138 | * 139 | * @return bool 140 | */ 141 | private function verifyChecksum(string $hrp, array $convertedDataChars): bool 142 | { 143 | $expandHrp = $this->hrpExpand($hrp, strlen($hrp)); 144 | $r = array_merge($expandHrp, $convertedDataChars); 145 | $poly = $this->polyMod($r, count($r)); 146 | 147 | return in_array($poly, self::ALLOWED_POLY, true); 148 | } 149 | 150 | 151 | /** 152 | * Expands the human-readable part into a character array for checksumming. 153 | * 154 | * @param string $hrp 155 | * @param int $hrpLen 156 | * @return int[] 157 | */ 158 | private function hrpExpand(string $hrp, int $hrpLen): array 159 | { 160 | $expand1 = []; 161 | $expand2 = []; 162 | 163 | for ($i = 0; $i < $hrpLen; $i++) { 164 | $o = ord($hrp[$i]); 165 | $expand1[] = $o >> 5; 166 | $expand2[] = $o & 31; 167 | } 168 | 169 | return array_merge($expand1, [0], $expand2); 170 | } 171 | 172 | /** 173 | * @param int[] $values 174 | * @param int $numValues 175 | * 176 | * @return int 177 | */ 178 | private function polyMod(array $values, int $numValues): int 179 | { 180 | $chk = 1; 181 | for ($i = 0; $i < $numValues; $i++) { 182 | $top = $chk >> 25; 183 | $chk = ($chk & 0x1ffffff) << 5 ^ $values[$i]; 184 | 185 | for ($j = 0; $j < 5; $j++) { 186 | $value = (($top >> $j) & 1) ? self::GENERATOR[$j] : 0; 187 | $chk ^= $value; 188 | } 189 | } 190 | 191 | return $chk; 192 | } 193 | 194 | /** 195 | * Converts words of $fromBits bits to $toBits bits in size. 196 | * 197 | * @param int[] $data Character array of data to convert 198 | * @param int $inLen Number of elements in array 199 | * @param int $fromBits Word (bit count) size of provided data 200 | * @param int $toBits Requested word size (bit count) 201 | * @param bool $pad Whether to pad (only when encoding) 202 | * 203 | * @return int[] 204 | * 205 | * @throws Bech32Exception 206 | */ 207 | private function convertBits(array $data, int $inLen, int $fromBits, int $toBits, bool $pad = true): array 208 | { 209 | $acc = 0; 210 | $bits = 0; 211 | $ret = []; 212 | $maxv = (1 << $toBits) - 1; 213 | $maxacc = (1 << ($fromBits + $toBits - 1)) - 1; 214 | 215 | for ($i = 0; $i < $inLen; $i++) { 216 | $value = $data[$i]; 217 | 218 | if ($value < 0 || $value >> $fromBits) { 219 | throw new Bech32Exception('Invalid value for convert bits'); 220 | } 221 | 222 | $acc = (($acc << $fromBits) | $value) & $maxacc; 223 | $bits += $fromBits; 224 | 225 | while ($bits >= $toBits) { 226 | $bits -= $toBits; 227 | $ret[] = (($acc >> $bits) & $maxv); 228 | } 229 | } 230 | 231 | if ($pad && $bits) { 232 | $ret[] = ($acc << $toBits - $bits) & $maxv; 233 | } elseif ($bits >= $fromBits || ((($acc << ($toBits - $bits))) & $maxv)) { 234 | throw new Bech32Exception('Invalid data'); 235 | } 236 | 237 | return $ret; 238 | } 239 | 240 | 241 | /** 242 | * @param int $version 243 | * 244 | * @param string $program 245 | * 246 | * @throws RuntimeException 247 | */ 248 | private function validateWitnessProgram(int $version, string $program): void 249 | { 250 | if ($version < 0 || $version > 16) { 251 | throw new RuntimeException("Invalid witness version"); 252 | } 253 | 254 | $sizeProgram = strlen($program); 255 | if (($version === 0) && $sizeProgram !== 20 && $sizeProgram !== 32) { 256 | throw new RuntimeException("Invalid size for V0 witness program"); 257 | } 258 | 259 | if ($sizeProgram < 2 || $sizeProgram > 40) { 260 | throw new RuntimeException("Witness program size was out of valid range"); 261 | } 262 | } 263 | } -------------------------------------------------------------------------------- /src/Utils/HexDecoder.php: -------------------------------------------------------------------------------- 1 | 0); 44 | 45 | return $hex; 46 | } 47 | } -------------------------------------------------------------------------------- /src/Utils/KeccakDecoder.php: -------------------------------------------------------------------------------- 1 | > 31)) & (0xFFFFFFFF), 49 | $bc[($i + 4) % 5][1] ^ (($bc[($i + 1) % 5][1] << 1) | ($bc[($i + 1) % 5][0] >> 31)) & (0xFFFFFFFF) 50 | ]; 51 | 52 | for ($j = 0; $j < 25; $j += 5) { 53 | $st[$j + $i] = [ 54 | $st[$j + $i][0] ^ $t[0], 55 | $st[$j + $i][1] ^ $t[1] 56 | ]; 57 | } 58 | } 59 | 60 | // Rho Pi 61 | $t = $st[1]; 62 | for ($i = 0; $i < 24; $i++) { 63 | $j = self::$keccakf_piln[$i]; 64 | 65 | $bc[0] = $st[$j]; 66 | 67 | $n = self::$keccakf_rotc[$i]; 68 | $hi = $t[0]; 69 | $lo = $t[1]; 70 | if ($n >= 32) { 71 | $n -= 32; 72 | $hi = $t[1]; 73 | $lo = $t[0]; 74 | } 75 | 76 | $st[$j] =[ 77 | (($hi << $n) | ($lo >> (32 - $n))) & (0xFFFFFFFF), 78 | (($lo << $n) | ($hi >> (32 - $n))) & (0xFFFFFFFF) 79 | ]; 80 | 81 | $t = $bc[0]; 82 | } 83 | 84 | // Chi 85 | for ($j = 0; $j < 25; $j += 5) { 86 | for ($i = 0; $i < 5; $i++) { 87 | $bc[$i] = $st[$j + $i]; 88 | } 89 | for ($i = 0; $i < 5; $i++) { 90 | $st[$j + $i] = [ 91 | $st[$j + $i][0] ^ ~$bc[($i + 1) % 5][0] & $bc[($i + 2) % 5][0], 92 | $st[$j + $i][1] ^ ~$bc[($i + 1) % 5][1] & $bc[($i + 2) % 5][1] 93 | ]; 94 | } 95 | } 96 | 97 | // Iota 98 | $st[0] = [ 99 | $st[0][0] ^ $keccakf_rndc[$round][0], 100 | $st[0][1] ^ $keccakf_rndc[$round][1] 101 | ]; 102 | } 103 | } 104 | 105 | private static function keccak64($in_raw, int $capacity, int $outputlength, $suffix, bool $raw_output): string { 106 | $capacity /= 8; 107 | 108 | $inlen = mb_strlen($in_raw, self::ENCODING); 109 | 110 | $rsiz = 200 - 2 * $capacity; 111 | $rsizw = $rsiz / 8; 112 | 113 | $st = []; 114 | for ($i = 0; $i < 25; $i++) { 115 | $st[] = [0, 0]; 116 | } 117 | 118 | for ($in_t = 0; $inlen >= $rsiz; $inlen -= $rsiz, $in_t += $rsiz) { 119 | for ($i = 0; $i < $rsizw; $i++) { 120 | $t = unpack('V*', mb_substr($in_raw, $i * 8 + $in_t, 8, self::ENCODING)); 121 | 122 | $st[$i] = [ 123 | $st[$i][0] ^ $t[2], 124 | $st[$i][1] ^ $t[1] 125 | ]; 126 | } 127 | 128 | self::keccakf64($st, self::KECCAK_ROUNDS); 129 | } 130 | 131 | $temp = mb_substr($in_raw, $in_t, $inlen, self::ENCODING); 132 | $temp = str_pad($temp, $rsiz, "\x0", STR_PAD_RIGHT); 133 | 134 | $temp[$inlen] = chr($suffix); 135 | $temp[$rsiz - 1] = chr(ord($temp[$rsiz - 1]) | 0x80); 136 | 137 | for ($i = 0; $i < $rsizw; $i++) { 138 | $t = unpack('V*', mb_substr($temp, $i * 8, 8, self::ENCODING)); 139 | 140 | $st[$i] = [ 141 | $st[$i][0] ^ $t[2], 142 | $st[$i][1] ^ $t[1] 143 | ]; 144 | } 145 | 146 | self::keccakf64($st, self::KECCAK_ROUNDS); 147 | 148 | $out = ''; 149 | for ($i = 0; $i < 25; $i++) { 150 | $out .= $t = pack('V*', $st[$i][1], $st[$i][0]); 151 | } 152 | $r = mb_substr($out, 0, $outputlength / 8, self::ENCODING); 153 | 154 | return $raw_output ? $r : bin2hex($r); 155 | } 156 | 157 | private static function keccakf32(&$st, $rounds): void { 158 | $keccakf_rndc = [ 159 | [0x0000, 0x0000, 0x0000, 0x0001], [0x0000, 0x0000, 0x0000, 0x8082], [0x8000, 0x0000, 0x0000, 0x0808a], [0x8000, 0x0000, 0x8000, 0x8000], 160 | [0x0000, 0x0000, 0x0000, 0x808b], [0x0000, 0x0000, 0x8000, 0x0001], [0x8000, 0x0000, 0x8000, 0x08081], [0x8000, 0x0000, 0x0000, 0x8009], 161 | [0x0000, 0x0000, 0x0000, 0x008a], [0x0000, 0x0000, 0x0000, 0x0088], [0x0000, 0x0000, 0x8000, 0x08009], [0x0000, 0x0000, 0x8000, 0x000a], 162 | [0x0000, 0x0000, 0x8000, 0x808b], [0x8000, 0x0000, 0x0000, 0x008b], [0x8000, 0x0000, 0x0000, 0x08089], [0x8000, 0x0000, 0x0000, 0x8003], 163 | [0x8000, 0x0000, 0x0000, 0x8002], [0x8000, 0x0000, 0x0000, 0x0080], [0x0000, 0x0000, 0x0000, 0x0800a], [0x8000, 0x0000, 0x8000, 0x000a], 164 | [0x8000, 0x0000, 0x8000, 0x8081], [0x8000, 0x0000, 0x0000, 0x8080], [0x0000, 0x0000, 0x8000, 0x00001], [0x8000, 0x0000, 0x8000, 0x8008] 165 | ]; 166 | 167 | $bc = []; 168 | for ($round = 0; $round < $rounds; $round++) { 169 | 170 | // Theta 171 | for ($i = 0; $i < 5; $i++) { 172 | $bc[$i] = [ 173 | $st[$i][0] ^ $st[$i + 5][0] ^ $st[$i + 10][0] ^ $st[$i + 15][0] ^ $st[$i + 20][0], 174 | $st[$i][1] ^ $st[$i + 5][1] ^ $st[$i + 10][1] ^ $st[$i + 15][1] ^ $st[$i + 20][1], 175 | $st[$i][2] ^ $st[$i + 5][2] ^ $st[$i + 10][2] ^ $st[$i + 15][2] ^ $st[$i + 20][2], 176 | $st[$i][3] ^ $st[$i + 5][3] ^ $st[$i + 10][3] ^ $st[$i + 15][3] ^ $st[$i + 20][3] 177 | ]; 178 | } 179 | 180 | for ($i = 0; $i < 5; $i++) { 181 | $t = [ 182 | $bc[($i + 4) % 5][0] ^ ((($bc[($i + 1) % 5][0] << 1) | ($bc[($i + 1) % 5][1] >> 15)) & (0xFFFF)), 183 | $bc[($i + 4) % 5][1] ^ ((($bc[($i + 1) % 5][1] << 1) | ($bc[($i + 1) % 5][2] >> 15)) & (0xFFFF)), 184 | $bc[($i + 4) % 5][2] ^ ((($bc[($i + 1) % 5][2] << 1) | ($bc[($i + 1) % 5][3] >> 15)) & (0xFFFF)), 185 | $bc[($i + 4) % 5][3] ^ ((($bc[($i + 1) % 5][3] << 1) | ($bc[($i + 1) % 5][0] >> 15)) & (0xFFFF)) 186 | ]; 187 | 188 | for ($j = 0; $j < 25; $j += 5) { 189 | $st[$j + $i] = [ 190 | $st[$j + $i][0] ^ $t[0], 191 | $st[$j + $i][1] ^ $t[1], 192 | $st[$j + $i][2] ^ $t[2], 193 | $st[$j + $i][3] ^ $t[3] 194 | ]; 195 | } 196 | } 197 | 198 | // Rho Pi 199 | $t = $st[1]; 200 | for ($i = 0; $i < 24; $i++) { 201 | $j = self::$keccakf_piln[$i]; 202 | $bc[0] = $st[$j]; 203 | 204 | 205 | $n = self::$keccakf_rotc[$i] >> 4; 206 | $m = self::$keccakf_rotc[$i] % 16; 207 | 208 | $st[$j] = [ 209 | ((($t[(0+$n) %4] << $m) | ($t[(1+$n) %4] >> (16-$m))) & (0xFFFF)), 210 | ((($t[(1+$n) %4] << $m) | ($t[(2+$n) %4] >> (16-$m))) & (0xFFFF)), 211 | ((($t[(2+$n) %4] << $m) | ($t[(3+$n) %4] >> (16-$m))) & (0xFFFF)), 212 | ((($t[(3+$n) %4] << $m) | ($t[(0+$n) %4] >> (16-$m))) & (0xFFFF)) 213 | ]; 214 | 215 | $t = $bc[0]; 216 | } 217 | 218 | // Chi 219 | for ($j = 0; $j < 25; $j += 5) { 220 | for ($i = 0; $i < 5; $i++) { 221 | $bc[$i] = $st[$j + $i]; 222 | } 223 | for ($i = 0; $i < 5; $i++) { 224 | $st[$j + $i] = [ 225 | $st[$j + $i][0] ^ ~$bc[($i + 1) % 5][0] & $bc[($i + 2) % 5][0], 226 | $st[$j + $i][1] ^ ~$bc[($i + 1) % 5][1] & $bc[($i + 2) % 5][1], 227 | $st[$j + $i][2] ^ ~$bc[($i + 1) % 5][2] & $bc[($i + 2) % 5][2], 228 | $st[$j + $i][3] ^ ~$bc[($i + 1) % 5][3] & $bc[($i + 2) % 5][3] 229 | ]; 230 | } 231 | } 232 | 233 | // Iota 234 | $st[0] = [ 235 | $st[0][0] ^ $keccakf_rndc[$round][0], 236 | $st[0][1] ^ $keccakf_rndc[$round][1], 237 | $st[0][2] ^ $keccakf_rndc[$round][2], 238 | $st[0][3] ^ $keccakf_rndc[$round][3] 239 | ]; 240 | } 241 | } 242 | 243 | private static function keccak32($in_raw, int $capacity, int $outputlength, $suffix, bool $raw_output): string { 244 | $capacity /= 8; 245 | 246 | $inlen = mb_strlen($in_raw, self::ENCODING); 247 | 248 | $rsiz = 200 - 2 * $capacity; 249 | $rsizw = $rsiz / 8; 250 | 251 | $st = []; 252 | for ($i = 0; $i < 25; $i++) { 253 | $st[] = [0, 0, 0, 0]; 254 | } 255 | 256 | for ($in_t = 0; $inlen >= $rsiz; $inlen -= $rsiz, $in_t += $rsiz) { 257 | for ($i = 0; $i < $rsizw; $i++) { 258 | $t = unpack('v*', mb_substr($in_raw, $i * 8 + $in_t, 8, self::ENCODING)); 259 | 260 | $st[$i] = [ 261 | $st[$i][0] ^ $t[4], 262 | $st[$i][1] ^ $t[3], 263 | $st[$i][2] ^ $t[2], 264 | $st[$i][3] ^ $t[1] 265 | ]; 266 | } 267 | 268 | self::keccakf32($st, self::KECCAK_ROUNDS); 269 | } 270 | 271 | $temp = mb_substr($in_raw, $in_t, $inlen, self::ENCODING); 272 | $temp = str_pad($temp, $rsiz, "\x0", STR_PAD_RIGHT); 273 | 274 | $temp[$inlen] = chr($suffix); 275 | $temp[$rsiz - 1] = chr((int) $temp[$rsiz - 1] | 0x80); 276 | 277 | for ($i = 0; $i < $rsizw; $i++) { 278 | $t = unpack('v*', mb_substr($temp, $i * 8, 8, self::ENCODING)); 279 | 280 | $st[$i] = [ 281 | $st[$i][0] ^ $t[4], 282 | $st[$i][1] ^ $t[3], 283 | $st[$i][2] ^ $t[2], 284 | $st[$i][3] ^ $t[1] 285 | ]; 286 | } 287 | 288 | self::keccakf32($st, self::KECCAK_ROUNDS); 289 | 290 | $out = ''; 291 | for ($i = 0; $i < 25; $i++) { 292 | $out .= $t = pack('v*', $st[$i][3],$st[$i][2], $st[$i][1], $st[$i][0]); 293 | } 294 | $r = mb_substr($out, 0, $outputlength / 8, self::ENCODING); 295 | 296 | return $raw_output ? $r: bin2hex($r); 297 | } 298 | 299 | private static function keccak($in_raw, int $capacity, int $outputlength, $suffix, bool $raw_output): string { 300 | return self::$x64 301 | ? self::keccak64($in_raw, $capacity, $outputlength, $suffix, $raw_output) 302 | : self::keccak32($in_raw, $capacity, $outputlength, $suffix, $raw_output); 303 | } 304 | 305 | public static function hash($in, int $mdlen, bool $raw_output = false): string { 306 | if (!in_array($mdlen, [224, 256, 384, 512], true)) { 307 | throw new Exception('Unsupported Keccak Hash output size.'); 308 | } 309 | 310 | return self::keccak($in, $mdlen, $mdlen, self::LFSR, $raw_output); 311 | } 312 | 313 | public static function shake($in, int $security_level, int $outlen, bool $raw_output = false): string { 314 | if (!in_array($security_level, [128, 256], true)) { 315 | throw new Exception('Unsupported Keccak Shake security level.'); 316 | } 317 | 318 | return self::keccak($in, $security_level, $outlen, 0x1f, $raw_output); 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /src/Validator.php: -------------------------------------------------------------------------------- 1 | value, config("address_validation.{$currency->value}"), app()->isProduction()); 26 | } 27 | 28 | public function isValid(?string $address): bool 29 | { 30 | if (!$address) { 31 | return false; 32 | } 33 | 34 | $drivers = $this->getDrivers(); 35 | // if there is no drivers we force address to be valid 36 | if (null === $drivers || !$drivers->valid()) { 37 | return true; 38 | } 39 | 40 | return (bool) $this->getDriver($drivers, $address)?->check($address); 41 | } 42 | 43 | public function validate(?string $address): void 44 | { 45 | if (!$address) { 46 | return; 47 | } 48 | 49 | $drivers = $this->getDrivers(); 50 | // if there is no drivers we force address to be valid 51 | if (null === $drivers || !$drivers->valid()) { 52 | return; 53 | } 54 | 55 | $driver = $this->getDriver($drivers, $address); 56 | 57 | if ($driver === null) { 58 | throw new AddressValidationException($this->chain, $address, false); 59 | } 60 | 61 | if (!$driver->check($address)) { 62 | throw new AddressValidationException($this->chain, $address, true); 63 | } 64 | } 65 | 66 | /** 67 | * @return Generator|null 68 | */ 69 | protected function getDrivers(): ?Generator 70 | { 71 | /** @var DriverConfig $driverConfig */ 72 | foreach ($this->options as $driverConfig) { 73 | if ($driver = $driverConfig->makeDriver($this->isMainnet)) { 74 | yield $driver; 75 | } 76 | } 77 | 78 | return null; 79 | } 80 | 81 | protected function getDriver(iterable $drivers, string $address): ?Driver 82 | { 83 | /** @var Driver $driver */ 84 | foreach ($drivers as $driver) { 85 | if ($driver->match($address)) { 86 | return $driver; 87 | } 88 | } 89 | 90 | return null; 91 | } 92 | } -------------------------------------------------------------------------------- /tests/KeccakDriverTest.php: -------------------------------------------------------------------------------- 1 | value, $config, $net === 'mainnet'); 32 | 33 | self::assertEquals($expected, $validator->isValid($address)); 34 | } 35 | 36 | public function addressesProvider(): array 37 | { 38 | return [ 39 | 'Ethereum #1' => ['mainnet', true, '0xe80b351948D0b87EE6A53e057A91467d54468D91'], 40 | 'Ethereum #2' => ['testnet', true, '0x799aD3Ff7Ef43DfD1473F9b8a8C4237c22D8113F'], 41 | 'Ethereum #3' => ['mainnet', true, '0xe80b351948d0b87ee6a53e057a91467d54468d91'], 42 | 'Ethereum #4' => ['testnet', true, '0x799ad3ff7ef43dfd1473f9b8a8c4237c22d8113f'], 43 | ]; 44 | } 45 | } -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | value]; 31 | 32 | $validator = new Validator($currency->value, $options, $net === 'mainnet'); 33 | 34 | $this->assertEquals( 35 | $expected, 36 | $validator->isValid($address), 37 | "[{$currency->value}] address [{$address}] is invalid" 38 | ); 39 | } 40 | 41 | public function currencyAddressProvider(): array 42 | { 43 | return [ 44 | 'Beacon #1' => [CurrencyEnum::BEACON, 'mainnet', true, 'bnb1fnd0k5l4p3ck2j9x9dp36chk059w977pszdgdz'], 45 | 'Beacon #2' => [CurrencyEnum::BEACON, 'mainnet', true, 'bnb1xd8cn4w7q4hm4fc9a68xtpx22kqenju7ea8d3v'], 46 | 'Beacon #3' => [CurrencyEnum::BEACON, 'testnet', true, 'tbnb1nuxna8asq69jf05cldcxpx9ee0m7drd9qz3aru'], 47 | 'Beacon #4' => [CurrencyEnum::BEACON, 'mainnet', false, 'bnb1nuxna8asq69jf05cldcxpx9ee0m7drd9qz3aru'], 48 | 'Beacon #5' => [CurrencyEnum::BEACON, 'testnet', false, 'bnb1nuxna8asq69jf05cldcxpx9ee0m7drd9qz3aru'], 49 | // 50 | 'BitcoinCash #1' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, 'bitcoincash:qp009ldhprp75mgn4kgaw8jvrpadnvg8qst37j42kx'], 51 | 'BitcoinCash #2' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, 'bitcoincash:qz7032ylhvxmndkx438pd8kjd7k7zcqxzsf26q0lvr'], 52 | 'BitcoinCash #3' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, '32uLhn19ZasD5bsVhLdDthhM37JhJHiEE2'], 53 | 'BitcoinCash #4' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, 'qz52zsruu43sq7ed0srym3g0ktpyjkdkxcm949pl2z'], 54 | 'BitcoinCash #5' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, 'qpf8eq7ygvhqjwydk9n29f6nyc8rcjhlwcuwngn6xk'], 55 | 'BitcoinCash #6' => [CurrencyEnum::BITCOIN_CASH, 'testnet', true, 'bchtest:qp2vjh349lcd22hu0hv6hv9d0pwlk43f6u04d5jk36'], 56 | 'BitcoinCash #7' => [CurrencyEnum::BITCOIN_CASH, 'testnet', true, 'qp2vjh349lcd22hu0hv6hv9d0pwlk43f6u04d5jk36'], 57 | 'BitcoinCash #8' => [CurrencyEnum::BITCOIN_CASH, 'testnet', false, '1KADKOasjxpNKzbfcKjnigLYWjEFPcMXqf'], 58 | 'BitcoinCash #9' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, 'qpnxwdu09eq4gqxv0ala37yj5evmmakf5vpp770edu'], 59 | 'BitcoinCash #10' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, 'bitcoincash:qpnxwdu09eq4gqxv0ala37yj5evmmakf5vpp770edu'], 60 | 'BitcoinCash #11' => [CurrencyEnum::BITCOIN_CASH, 'testnet', false, 'bchtest:qpnxwdu09eq4gqxv0ala37yj5evmmakf5vpp770edu'], 61 | 'BitcoinCash #12' => [CurrencyEnum::BITCOIN_CASH, 'testnet', false, 'bchreg:qpnxwdu09eq4gqxv0ala37yj5evmmakf5vpp770edu'], 62 | 'BitcoinCash #13' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, 'bitcoincash:pwqwzrf7z06m7nn58tkdjyxqfewanlhyrpxysack85xvf3mt0rv02l9dxc5uf'], 63 | 'BitcoinCash #14' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, 'bitcoincash:qr6m7j9njldwwzlg9v7v53unlr4jkmx6eylep8ekg2'], 64 | // 65 | 'Bitcoin #1' => [CurrencyEnum::BITCOIN, 'mainnet', true, '1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2'], 66 | 'Bitcoin #2' => [CurrencyEnum::BITCOIN, 'mainnet', true, 'bc1q6v096h88xmpl662af0nc7wd3vta56zv6pyccl8'], 67 | 'Bitcoin #3' => [CurrencyEnum::BITCOIN, 'testnet', true, 'tb1q27dglj7x4l34mj7j2x7e6fqsexk6vf8kew6qm0'], 68 | 'Bitcoin #4' => [CurrencyEnum::BITCOIN, 'testnet', false, 'tb1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq'], 69 | 'Bitcoin #5' => [CurrencyEnum::BITCOIN, 'mainnet', false, 'tb1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq'], 70 | 'Bitcoin #6' => [CurrencyEnum::BITCOIN, 'testnet', false, '1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2'], 71 | 'Bitcoin #7' => [CurrencyEnum::BITCOIN, 'testnet', false, 'bc1q6v096h88xmpl662af0nc7wd3vta56zv6pyccl8'], 72 | 'Bitcoin #8' => [CurrencyEnum::BITCOIN, 'mainnet', true, 'BC1QL2725QLXHGWQ7F7XLJ8363FJCUF25XZ35SWRU5'], 73 | 'Bitcoin #9' => [CurrencyEnum::BITCOIN, 'mainnet', true, 'bc1p5gyty9x2lrk65yndaeh242zm6xklgrv9nze9477dg0kyv6yvfljq0lqjkh'], 74 | // 75 | 'Cardano #1' => [CurrencyEnum::CARDANO, 'mainnet', true, 'addr1v9ywm0h3r8cnxrs04gfy7c3s2j44utjyvn5ldjdca0c2ltccgqdes'], 76 | 'Cardano #2' => [CurrencyEnum::CARDANO, 'mainnet', false, 'stake1u9f9v0z5zzlldgx58n8tklphu8mf7h4jvp2j2gddluemnssjfnkzz'], 77 | 'Cardano #3' => [CurrencyEnum::CARDANO, 'mainnet', true, 'addr1qxy3w62dupy9pzmpdfzxz4k240w5vawyagl5m9djqquyymrtm3grn7gpnjh7rwh2dy62hk8639lt6kzn32yxq960usnq9pexvt'], 78 | 'Cardano #4' => [CurrencyEnum::CARDANO, 'mainnet', true, 'Ae2tdPwUPEYwNguM7TB3dMnZMfZxn1pjGHyGdjaF4mFqZF9L3bj6cdhiH8t'], 79 | 'Cardano #5' => [CurrencyEnum::CARDANO, 'mainnet', true, 'DdzFFzCqrht2KYLcX8Vu53urCG52NxpgrGQvP9Mcp15Q8BkB9df9GndFDBRjoWTPuNkLW3yeQiFVet1KA7mraEkJ84AK2RwcEh3khs12'], 80 | 'Cardano #6' => [CurrencyEnum::CARDANO, 'testnet', true, '37btjrVyb4KBbrmcxh3qQzswqDB4SCU8L68vYBJshaeYQ8rHVBfrAfuXZNyFHtR8QXUKR4CtytMyX4DwhsPYKKgFSpq8f5KxNz2s6Guqr6c6LzcHck'], 81 | 'Cardano #7' => [CurrencyEnum::CARDANO, 'testnet', true, '2cWKMJemoBaipAW1NGegM2qWevSgpL9baiizayY4NnTBvxRGyppr2uym7F9eEtRLehFek'], 82 | 'Cardano #8' => [CurrencyEnum::CARDANO, 'testnet', true, 'addr_test1qzfst6x8f4r47vm4qfeuj7g8r5pgkjnv5cuzjk94u8p7sd3gtlpjssk2fy95k4z5lr48tu48fcqstnzte44d8f8v8vhs9pwu4x'], 83 | // 84 | 'Dashcoin #1' => [CurrencyEnum::DASHCOIN, 'mainnet', true, 'XpESxaUmonkq8RaLLp46Brx2K39ggQe226'], 85 | 'Dashcoin #2' => [CurrencyEnum::DASHCOIN, 'mainnet', true, 'XmZQkfLtk3xLtbBMenTdaZMxsUBYAsRz1o'], 86 | 'Dashcoin #3' => [CurrencyEnum::DASHCOIN, 'testnet', true, 'yNpxAuCGxLkDmVRY12m4qEWx1ttgTczSMJ'], 87 | 'Dashcoin #4' => [CurrencyEnum::DASHCOIN, 'testnet', true, 'yi7GRZLiUGrJfX2aNDQ3v7pGSCTrnLa87o'], 88 | // 89 | 'Dogecoin #1' => [CurrencyEnum::DOGECOIN, 'mainnet', true, 'DFrGqzk4ZnTcK1gYtxZ9QDJsDiVM8v8gwV'], 90 | 'Dogecoin #2' => [CurrencyEnum::DOGECOIN, 'mainnet', true, 'DMzanBYjj3yYHtCcnEucn7H8LHNY9fARB8'], 91 | 'Dogecoin #3' => [CurrencyEnum::DOGECOIN, 'testnet', true, 'mketxxXxaBeH7AhCBMatdH5ATVad2XHQdj'], 92 | 'Dogecoin #4' => [CurrencyEnum::DOGECOIN, 'testnet', false, 'n3TZFrdPvwGqfPC7vBb8PGgbFwc1Cnxq9h'], 93 | 'Dogecoin #5' => [CurrencyEnum::DOGECOIN, 'testnet', true, 'nd5N1KW1waCicK1vqfwtTcBSbQCHBLv2Um'], 94 | 'Dogecoin #6' => [CurrencyEnum::DOGECOIN, 'testnet', false, 'DFundMr7W8PjB6ZmVwGv1L1WtZ2X3m3KgQ'], 95 | 'Dogecoin #7' => [CurrencyEnum::DOGECOIN, 'mainnet', false, 'n3TZFrdPvwGqfPC7vBb8PGgbFwc1Cnxq9h'], 96 | // 97 | 'Ethereum #1' => [CurrencyEnum::ETHEREUM, 'mainnet', true, '0xe80b351948D0b87EE6A53e057A91467d54468D91'], 98 | 'Ethereum #2' => [CurrencyEnum::ETHEREUM, 'testnet', true, '0x799aD3Ff7Ef43DfD1473F9b8a8C4237c22D8113F'], 99 | 'Ethereum #3' => [CurrencyEnum::ETHEREUM, 'mainnet', false, '0xe80b351948d0b87ee6a53e057a91467d54468d91'], 100 | 'Ethereum #4' => [CurrencyEnum::ETHEREUM, 'testnet', false, '0x799ad3ff7ef43dfd1473f9b8a8c4237c22d8113f'], 101 | // 102 | 'Litecoin #1' => [CurrencyEnum::LITECOIN, 'mainnet', true, 'MF5yqnMuNoiCiCXbZft7iFgLK5BPG5QKbE'], 103 | 'Litecoin #2' => [CurrencyEnum::LITECOIN, 'mainnet', false, '1QLbGuc3WGKKKpLs4pBp9H6jiQ2MgPkXRp'], 104 | 'Litecoin #3' => [CurrencyEnum::LITECOIN, 'mainnet', true, '3CDJNfdWX8m2NwuGUV3nhXHXEeLygMXoAj'], 105 | 'Litecoin #4' => [CurrencyEnum::LITECOIN, 'mainnet', true, 'LbTjMGN7gELw4KbeyQf6cTCq859hD18guE'], 106 | 'Litecoin #5' => [CurrencyEnum::LITECOIN, 'mainnet', true, 'MK9xC9sbktt6DHMF6XwA3eZPJ2Vx32AXFT'], 107 | 'Litecoin #6' => [CurrencyEnum::LITECOIN, 'testnet', true, 'mpQA36uSXDGxySjknqHFVMdsLPgPnbm7ku'], 108 | 'Litecoin #7' => [CurrencyEnum::LITECOIN, 'mainnet', true, 'ltc1qf6wcq8kc0unt3wuaszlkms3zkuerxlfaz07zmj'], 109 | // 110 | 'Ripple #1' => [CurrencyEnum::RIPPLE, 'mainnet', true, 'r4dgY6Mzob3NVq8CFYdEiPnXKboRScsXRu'], 111 | // 112 | 'Tron #1' => [CurrencyEnum::TRON, 'mainnet', true, 'TC9fKEGcBTfmvXKXLHq5MJDC8P7dhZQM92'], 113 | 'Tron #2' => [CurrencyEnum::TRON, 'testnet', true, 'TRALQkt1v9MjUVn3gT7csfpodJDmnC6q8s'], 114 | // 115 | 'Zcash #1' => [CurrencyEnum::ZCASH, 'mainnet', true, 't1YQV51DKzKP63xJcynXuRfryMjfmgTJ7Jc'], 116 | 'Zcash #2' => [CurrencyEnum::ZCASH, 'mainnet', true, 't1VJhyyvbi63Cu6nEVVgNHSCokDRa3repZB'], 117 | 'Zcash #3' => [CurrencyEnum::ZCASH, 'testnet', true, 't1VJhyyvbi63Cu6nEVVgNHSCokDRa3repZB'], 118 | // 119 | 'EOS #1' => [CurrencyEnum::EOS, 'mainnet', true, 'atticlabeosb'], 120 | 'EOS #2' => [CurrencyEnum::EOS, 'mainnet', true, 'bitfinexeos1'], 121 | ]; 122 | } 123 | } --------------------------------------------------------------------------------