├── .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 | }
--------------------------------------------------------------------------------