diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..f74ade5
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,37 @@
+name: Unit Tests
+
+on:
+ push:
+ branches: [ "master" ]
+ pull_request:
+ branches: [ "master" ]
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - uses: shivammathur/setup-php@master
+ with:
+ php-version: '8.2'
+
+ - name: Cache Composer packages
+ id: composer-cache
+ uses: actions/cache@v3
+ with:
+ path: vendor
+ key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-php-
+
+ - name: Install dependencies
+ run: composer install --prefer-dist --no-progress
+
+ - name: Run test suite
+ run: composer test
diff --git a/.gitignore b/.gitignore
index bf12543..c1285db 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
composer.lock
vendor
-.idea
\ No newline at end of file
+.idea
+.phpunit.result.cache
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..a797ec1
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2017 Merkeleon
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
index 13d9f13..2144c60 100644
--- a/README.md
+++ b/README.md
@@ -3,13 +3,19 @@
Easy to use PHP Bitcoin and Litecoin address validator.
One day I will add other crypto currencies. Or how about you? :)
-## Usage
+## Installation
+=======
+```
+composer require merkeleon/php-cryptocurrency-address-validation
+```
+
+## Usage
```php
-use Murich\PhpCryptocurrencyAddressValidation\Validation\BTC as BTCValidator;
+use Merkeleon\PhpCryptocurrencyAddressValidation\Enums\CurrencyEnum;use Merkeleon\PhpCryptocurrencyAddressValidation\Validator;
-$validator = new BTCValidator('1QLbGuc3WGKKKpLs4pBp9H6jiQ2MgPkXRp');
-var_dump($validator->validate());
+$validator = Validator::make(CurrencyEnum::BITCOIN);
+var_dump($validator->isValid('1QLbGuc3WGKKKpLs4pBp9H6jiQ2MgPkXRp'));
```
diff --git a/composer.json b/composer.json
index 9e9ed7d..f07d0f1 100644
--- a/composer.json
+++ b/composer.json
@@ -1,5 +1,5 @@
{
- "name": "murich/php-cryptocurrency-address-validation",
+ "name": "merkeleon/php-cryptocurrency-address-validation",
"description": "Cryptocurrency address validation. Currently supports litecoin and bitcoin.",
"authors": [
{
@@ -7,13 +7,27 @@
"email": "andrey@phpteam.pro"
}
],
- "require": {},
+ "require": {
+ "php": "^8.2",
+ "ext-gmp": "*",
+ "ext-bcmath": "*",
+ "laravel/framework": ">=v7.0.0|>=v10.0.0",
+ "spomky-labs/cbor-php": "^3.0"
+ },
+ "scripts": {
+ "test": "@php vendor/bin/phpunit"
+ },
"require-dev": {
- "phpunit/phpunit": "4.0.*"
+ "phpunit/phpunit": "~8.0"
},
"autoload": {
"psr-4": {
- "Murich\\PhpCryptocurrencyAddressValidation\\": "src"
+ "Merkeleon\\PhpCryptocurrencyAddressValidation\\": "src"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Tests\\": "tests/"
}
}
}
diff --git a/config/address_validation.php b/config/address_validation.php
new file mode 100644
index 0000000..e0d86d6
--- /dev/null
+++ b/config/address_validation.php
@@ -0,0 +1,108 @@
+value => [
+ new DriverConfig(
+ Drivers\Bech32Driver::class,
+ ['bnb' => null],
+ ['tbnb' => null]
+ ),
+ ],
+ CurrencyEnum::BITCOIN_CASH->value => [
+ new DriverConfig(
+ Drivers\Base32Driver::class,
+ ['bitcoincash:' => null],
+ ['bchtest:' => null, 'bchreg:' => null,]
+ ),
+ new DriverConfig(
+ Drivers\DefaultBase58Driver::class,
+ ['1' => '00', '3' => '05'],
+ ['2' => 'C4', 'm' => '6F']
+ ),
+ ],
+ CurrencyEnum::BITCOIN->value => [
+ new DriverConfig(
+ Drivers\DefaultBase58Driver::class,
+ ['1' => '00', '3' => '05'],
+ ['2' => 'C4', 'm' => '6F']
+ ),
+ new DriverConfig(
+ Drivers\Bech32Driver::class,
+ ['bc' => null],
+ ['tb' => null, 'bcrt' => null]
+ ),
+ ],
+ CurrencyEnum::CARDANO->value => [
+ new DriverConfig(
+ Drivers\CardanoDriver::class,
+ ['addr' => null],
+ ['addr_test' => null],
+ ),
+ new DriverConfig(
+ Drivers\CborDriver::class,
+ ['A' => 33, 'D' => 66],
+ ['2' => 40, '3' => 73],
+ )
+ ],
+ CurrencyEnum::DASHCOIN->value => [
+ new DriverConfig(
+ Drivers\DefaultBase58Driver::class,
+ ['X' => '4C', '7' => '10'],
+ ['y' => '8C', '8' => '13']
+ ),
+ ],
+ CurrencyEnum::DOGECOIN->value => [
+ new DriverConfig(
+ Drivers\DefaultBase58Driver::class,
+ ['D' => '1E', '9' => '16', 'A' => '16'],
+ ['n' => '71', 'm' => '6F', '2' => 'C4',],
+ ),
+ ],
+ CurrencyEnum::EOS->value => [
+ new DriverConfig(Drivers\EosDriver::class),
+ ],
+ CurrencyEnum::ETHEREUM->value => [
+ new DriverConfig(Drivers\KeccakStrictDriver::class),
+ ],
+ CurrencyEnum::LITECOIN->value => [
+ new DriverConfig(
+ Drivers\DefaultBase58Driver::class,
+ ['L' => '30', 'M' => '32', '3' => '05'],
+ ['m' => '6F', 'n' => '6F', '2' => 'C4', 'Q' => '3A']
+ ),
+ new DriverConfig(
+ Drivers\Bech32Driver::class,
+ ['ltc' => null],
+ ['tltc' => null, 'rltc' => null]
+ )
+ ],
+ CurrencyEnum::RIPPLE->value => [
+ new DriverConfig(
+ Drivers\XrpBase58Driver::class,
+ ['r' => '00']
+ ),
+ new DriverConfig(
+ Drivers\XrpXAddressDriver::class,
+ ['X' => null],
+ ['T' => null],
+ ),
+ ],
+ CurrencyEnum::TRON->value => [
+ new DriverConfig(
+ Drivers\DefaultBase58Driver::class,
+ ['T' => '41'],
+ ),
+ ],
+ CurrencyEnum::ZCASH->value => [
+ new DriverConfig(
+ Drivers\DefaultBase58Driver::class,
+ ['t' => '1C'],
+ ),
+ ],
+];
\ No newline at end of file
diff --git a/phpunit.xml b/phpunit.xml
index 0e0687b..f5abe30 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -7,11 +7,10 @@
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
- stopOnFailure="false"
- syntaxCheck="false">
+ stopOnFailure="false">
-
- ./tests/
+
+ ./tests/
\ No newline at end of file
diff --git a/src/AddressValidationServiceProvider.php b/src/AddressValidationServiceProvider.php
new file mode 100644
index 0000000..cb0cdcd
--- /dev/null
+++ b/src/AddressValidationServiceProvider.php
@@ -0,0 +1,17 @@
+publishes([
+ __DIR__.'/../config/address_validation.php' => config_path('address_validation.php'),
+ ]);
+ }
+}
\ No newline at end of file
diff --git a/src/Contracts/Driver.php b/src/Contracts/Driver.php
new file mode 100644
index 0000000..8c5361e
--- /dev/null
+++ b/src/Contracts/Driver.php
@@ -0,0 +1,11 @@
+ $driver
+ * @param array $mainnet
+ * @param array $testnet
+ */
+ public function __construct(
+ private string $driver,
+ private array $mainnet = [],
+ private array $testnet = []
+ )
+ {
+ }
+
+ public function makeDriver(bool $isMainNet): ?AbstractDriver
+ {
+ if (!class_exists($this->driver)) {
+ return null;
+ }
+
+ return new $this->driver($this->getDriverOptions($isMainNet));
+
+ }
+
+ private function getDriverOptions(bool $isMainNet): array
+ {
+ if ($isMainNet) {
+ return $this->mainnet;
+ }
+
+ return $this->testnet
+ ?: $this->mainnet
+ ?: [];
+ }
+
+ public static function __set_state(array $state): DriverConfig
+ {
+ return new self(
+ $state['driver'],
+ $state['mainnet'],
+ $state['testnet']
+ );
+ }
+}
diff --git a/src/Drivers/AbstractDriver.php b/src/Drivers/AbstractDriver.php
new file mode 100644
index 0000000..f787707
--- /dev/null
+++ b/src/Drivers/AbstractDriver.php
@@ -0,0 +1,14 @@
+ 0,
+ 192 => 1,
+ 224 => 2,
+ 256 => 3,
+ 320 => 4,
+ 384 => 5,
+ 448 => 6,
+ 512 => 7,
+ ];
+
+ public function match(string $address): bool
+ {
+ $address = strtolower($address);
+
+ $prefix = implode('|', array_keys($this->options));
+ $pattern = sprintf('/^((%s)?([qp])[a-z0-9]{41,120})/', $prefix);
+
+ return preg_match($pattern, $address) === 1;
+ }
+
+ public function check(string $address, array $networks = []): bool
+ {
+ try {
+ $hasPrefix = Str::contains($address, array_keys($this->options));
+
+ $address = strtolower($address);
+
+ [,$words] = Base32Decoder::decode($address, $hasPrefix);
+
+ $numWords = count($words);
+ $bytes = Base32Decoder::fromWords($numWords, $words);
+ $numBytes = count($bytes);
+
+ $this->extractPayload($numBytes, $bytes);
+
+ return true;
+ } catch (Throwable) {
+ return false;
+ }
+ }
+
+ /**
+ * @return string[] - script type and hash
+ */
+ protected function extractPayload(int $numBytes, array $payloadBytes): array
+ {
+ if ($numBytes < 1) {
+ throw new RuntimeException("Empty base32 string");
+ }
+
+ [$scriptType, $hashLengthBits] = $this->decodeVersion($payloadBytes[0]);
+
+ if (($hashLengthBits / 8) !== $numBytes - 1) {
+ throw new RuntimeException("Hash length does not match version");
+ }
+
+ $hash = "";
+
+ foreach (array_slice($payloadBytes, 1) as $byte) {
+ $hash .= pack("C*", $byte);
+ }
+
+ return [$scriptType, $hash];
+ }
+
+
+ protected function decodeVersion(int $version): array
+ {
+ if (($version >> 7) & 1) {
+ throw new RuntimeException("Invalid version - MSB is reserved");
+ }
+
+ $scriptMarkerBits = ($version >> 3) & 0x1f;
+ $hashMarkerBits = ($version & 0x07);
+
+ $hashBitsMap = array_flip(self::$hashBits);
+ if (!array_key_exists($hashMarkerBits, $hashBitsMap)) {
+ throw new RuntimeException("Invalid version or hash length");
+ }
+ $hashLength = $hashBitsMap[$hashMarkerBits];
+
+ $scriptType = match ($scriptMarkerBits) {
+ 0 => "pubkeyhash",
+ 1 => "scripthash",
+ default => throw new RuntimeException('Invalid version or script type'),
+ };
+
+ return [$scriptType, $hashLength];
+ }
+}
\ No newline at end of file
diff --git a/src/Drivers/Base58Driver.php b/src/Drivers/Base58Driver.php
new file mode 100644
index 0000000..c443e64
--- /dev/null
+++ b/src/Drivers/Base58Driver.php
@@ -0,0 +1,48 @@
+options));
+ $expr = sprintf('/^(%s)[a-km-zA-HJ-NP-Z1-9]{25,34}$/', $prefix);
+
+ return preg_match($expr, $address) === 1;
+ }
+
+ protected function getVersion($address): ?string
+ {
+ $hexString = Base58Decoder::decode($address, static::$base58Alphabet);
+ if (!$hexString) {
+ return null;
+ }
+
+ $version = substr($hexString, 0, 2);
+
+ $check = substr($hexString, 0, -8);
+ $check = pack("H*", $check);
+ $check = hash("sha256", $check, true);
+ $check = hash("sha256", $check);
+ $check = strtoupper($check);
+ $check = substr($check, 0, 8);
+
+ $isValid = str_ends_with($hexString, strtolower($check));
+
+ return $isValid ? $version : null;
+ }
+}
\ No newline at end of file
diff --git a/src/Drivers/Bech32Driver.php b/src/Drivers/Bech32Driver.php
new file mode 100644
index 0000000..f1a876d
--- /dev/null
+++ b/src/Drivers/Bech32Driver.php
@@ -0,0 +1,52 @@
+getPattern();
+ return preg_match($expr, strtolower($address)) === 1;
+ }
+
+ public function check(string $address): bool
+ {
+ try {
+ $address = strtolower($address);
+
+ $expr = $this->getPattern();
+ preg_match($expr, $address, $match);
+
+ [$hrpGot, $data] = (new Bech32Decoder())->decode($address);
+ if ($hrpGot !== $match[2]) {
+ return false;
+ }
+
+ $dataLen = count($data);
+
+ return !($dataLen === 0 || $dataLen > 65);
+ } catch (Throwable) {
+ return false;
+ }
+ }
+
+ private function getPattern(): string
+ {
+ $prefix = implode('|', array_keys($this->options));
+ return sprintf(
+ '/^((%s)(0([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59})|1[ac-hj-np-z02-9]{8,87}))$/',
+ $prefix
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Drivers/CardanoDriver.php b/src/Drivers/CardanoDriver.php
new file mode 100644
index 0000000..5db0bd0
--- /dev/null
+++ b/src/Drivers/CardanoDriver.php
@@ -0,0 +1,35 @@
+options));
+ $expr = sprintf('/^((%s)(0([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59})|1[ac-hj-np-z02-9]{8,}))$/', $prefix);
+
+ return preg_match($expr, $address) === 1;
+ }
+
+ public function check(string $address): bool
+ {
+ try {
+ $decoded = (new Bech32Decoder())->decodeRaw($address);
+
+ return array_key_exists($decoded[0], $this->options);
+ } catch (Bech32Exception) {
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Drivers/CborDriver.php b/src/Drivers/CborDriver.php
new file mode 100644
index 0000000..94dea40
--- /dev/null
+++ b/src/Drivers/CborDriver.php
@@ -0,0 +1,89 @@
+add(SimpleObject::class);
+
+ $tagManager = new TagManager();
+ $tagManager->add(UnsignedBigIntegerTag::class);
+
+ $this->decoder = new Decoder($tagManager, $otherObjectManager);
+ }
+
+ public function match(string $address): bool
+ {
+ return Str::startsWith($address, array_keys($this->options));
+ }
+
+ public function check(string $address): bool
+ {
+ try {
+ $addressHex = Base58Decoder::decode($address, self::$base58Alphabet);
+
+ $data = hex2bin($addressHex);
+
+ $stream = new StringStream($data);
+
+
+ /** @var SimpleObject $object */
+ $object = $this->decoder->decode($stream);
+ if ($object->getMajorType() !== 4) {
+ return false;
+ }
+
+ /** @var array $normalizedData */
+ $normalizedData = $object->normalize();
+
+ if (count($normalizedData) !== 2) {
+ return false;
+ }
+ if (!is_numeric($normalizedData[1])) {
+ return false;
+ }
+
+ if (!$normalizedData[0] instanceof GenericTag) {
+ return false;
+ }
+
+ /** @var ByteStringObject $bs */
+ $bs = $normalizedData[0]->getValue();
+
+ if (!in_array($bs->getLength(), array_values($this->options), true)) {
+ return false;
+ }
+
+ $crcCalculated = crc32($bs->getValue());
+ $validCrc = $normalizedData[1];
+
+ return $crcCalculated === (int)$validCrc;
+ } catch (Throwable) {
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Drivers/DefaultBase58Driver.php b/src/Drivers/DefaultBase58Driver.php
new file mode 100644
index 0000000..ca2bf7b
--- /dev/null
+++ b/src/Drivers/DefaultBase58Driver.php
@@ -0,0 +1,27 @@
+options[$address[0]] ?? null;
+ if (null === $addressVersion) {
+ return false;
+ }
+
+ $calculatedAddressVersion = $this->getVersion($address);
+ if (null === $calculatedAddressVersion) {
+ return false;
+ }
+
+ return hexdec($addressVersion) === hexdec($calculatedAddressVersion);
+ }
+}
diff --git a/src/Drivers/EosDriver.php b/src/Drivers/EosDriver.php
new file mode 100644
index 0000000..ddd6a96
--- /dev/null
+++ b/src/Drivers/EosDriver.php
@@ -0,0 +1,18 @@
+toChecksum($address);
+
+ return parent::check($address);
+ }
+
+ protected function toChecksum(string $address): string
+ {
+ $address = str_replace('0x', '', $address);
+ $address = mb_strtolower($address);
+
+ $hash = KeccakDecoder::hash($address, 256);
+
+ $checksumAddress = '';
+ for ($i = 0; $i < 40; $i++) {
+ if (intval($hash[$i], 16) >= 8) {
+ $checksumAddress .= strtoupper($address[$i]);
+ } else {
+ $checksumAddress .= $address[$i];
+ }
+ }
+
+ return '0x' . $checksumAddress;
+ }
+}
\ No newline at end of file
diff --git a/src/Drivers/KeccakStrictDriver.php b/src/Drivers/KeccakStrictDriver.php
new file mode 100644
index 0000000..1807e5d
--- /dev/null
+++ b/src/Drivers/KeccakStrictDriver.php
@@ -0,0 +1,59 @@
+ 7 && strtoupper($addressArray[$i]) !== $addressArray[$i]) ||
+ (intval($addressHashArray[$i], 16) <= 7 && strtolower($addressArray[$i]) !== $addressArray[$i])
+ ) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public function stripZero(string $value): string
+ {
+ if ($this->isZeroPrefixed($value)) {
+ return str_replace('0x', '', $value);
+ }
+ return $value;
+ }
+
+ public function isZeroPrefixed(string $value): bool
+ {
+ return str_starts_with(haystack: $value, needle: '0x');
+ }
+}
\ No newline at end of file
diff --git a/src/Drivers/XrpBase58Driver.php b/src/Drivers/XrpBase58Driver.php
new file mode 100644
index 0000000..aeaa8b6
--- /dev/null
+++ b/src/Drivers/XrpBase58Driver.php
@@ -0,0 +1,15 @@
+getVersion($address) !== null;
+ }
+}
diff --git a/src/Drivers/XrpXAddressDriver.php b/src/Drivers/XrpXAddressDriver.php
new file mode 100644
index 0000000..af8b207
--- /dev/null
+++ b/src/Drivers/XrpXAddressDriver.php
@@ -0,0 +1,26 @@
+options));
+ $expr = sprintf('/^(%s)[a-km-zA-HJ-NP-Z1-9]{33,55}$/', $prefix);
+
+ return preg_match($expr, $address) === 1;
+ }
+
+ public function check(string $address): bool
+ {
+ return true;
+ }
+}
diff --git a/src/Enums/CurrencyEnum.php b/src/Enums/CurrencyEnum.php
new file mode 100644
index 0000000..ec93ee2
--- /dev/null
+++ b/src/Enums/CurrencyEnum.php
@@ -0,0 +1,21 @@
+ 0) {
+ $value = self::$generator[$j];
+ }
+
+ $v = self::bitwiseXor($v, gmp_init((string) $value, 10));
+ }
+
+ return $v;
+ }
+
+ /**
+ * @param string $prefix
+ *
+ * @return resource
+ */
+ public static function prefixChk(string $prefix)
+ {
+ $chk = gmp_init(1);
+ $length = strlen($prefix);
+ for ($i = 0; $i < $length; $i++) {
+ $char = ord($prefix[$i]) & 0x1f;
+ $chk = self::bitwiseXor(self::polyModStep($chk), gmp_init($char, 10));
+ }
+ return self::polyModStep($chk);
+ }
+
+ /**
+ * @param string $string - base32 string
+ *
+ * @return array|string> - array>
+ * @throws Base32Exception
+ * @throws InvalidChecksumException
+ */
+ public static function decode(string $string, bool $hasPrefix = true): array
+ {
+ $stringLen = strlen($string);
+ if ($stringLen < 8) {
+ throw new Base32Exception("Address too short");
+ }
+
+ if ($stringLen > 90) {
+ throw new Base32Exception("Address too long");
+ }
+
+ $chars = array_values(unpack("C*", $string));
+
+ $haveUpper = $haveLower = false;
+ $idxSeparator = -1;
+ $separatorChar = ord(self::SEPARATOR);
+
+ for ($i = 0; $i < $stringLen; $i++) {
+ $x = $chars[$i];
+ if ($x < 33 || $x > 126) {
+ throw new Base32Exception("Out of range character in base32 string");
+ }
+
+ if ($x >= 0x61 && $x <= 0x7a) {
+ $haveLower = true;
+ }
+
+ if ($x >= 0x41 && $x <= 0x5a) {
+ $haveUpper = true;
+ $x = $chars[$i] = $x + 0x20;
+ }
+
+ if ($x === $separatorChar) {
+ $idxSeparator = $i;
+ }
+ }
+
+ if ($haveUpper && $haveLower) {
+ throw new Base32Exception("Data contains mixture of higher/lower case characters");
+ }
+
+ if ($hasPrefix && $idxSeparator === -1) {
+ throw new Base32Exception("Missing separator character");
+ }
+ if ($hasPrefix && $idxSeparator === 0) {
+ throw new Base32Exception("Missing prefix");
+ }
+
+ if (($idxSeparator + 7) > $stringLen) {
+ throw new Base32Exception("Invalid location for separator character");
+ }
+
+ $prefix = "";
+
+ foreach (array_slice($chars, 0, $idxSeparator) as $byte) {
+ $prefix .= pack("C*", $byte);
+ }
+
+ $chk = self::prefixChk($prefix);
+
+ $words = [];
+ for ($i = $idxSeparator + 1; $i < $stringLen; $i++) {
+ $char = $chars[$i];
+ if (!array_key_exists($char, self::$charsetKey)) {
+ throw new Base32Exception("Unknown character in address");
+ }
+ $word = self::$charsetKey[$char];
+ $chk = self::bitwiseXor(self::polyModStep($chk), gmp_init($word));
+ $words[] = $word;
+ }
+
+ if ($hasPrefix && gmp_cmp($chk, gmp_init(1)) !== 0) {
+ throw new InvalidChecksumException();
+ }
+
+ return [
+ $prefix,
+ array_slice($words, 0, -self::$checksumLen)
+ ];
+ }
+
+ /**
+ * Convert $bytes, an array of 8 bit numbers, to
+ * words, an array of 5 bit numbers.
+ *
+ * @param int $numBytes
+ * @param int[] $bytes
+ * @return int[]
+ * @throws Base32Exception
+ */
+ public static function toWords($numBytes, array $bytes): array
+ {
+ return self::convertBits($bytes, $numBytes, 8, 5, true);
+ }
+
+ /**
+ * Convert $words, an array of 5 bit numbres, to
+ * bytes, an arrayof 8 bit numbers.
+ *
+ * @param int $numWords
+ * @param int[] $words
+ * @return int[]
+ * @throws Base32Exception
+ */
+ public static function fromWords($numWords, array $words): array
+ {
+ return self::convertBits($words, $numWords, 5, 8, false);
+ }
+
+ /**
+ * Converts words of $fromBits bits to $toBits bits in size.
+ *
+ * @param int[] $data - character array of data to convert
+ * @param int $inLen - number of elements in array
+ * @param int $fromBits - word (bit count) size of provided data
+ * @param int $toBits - requested word size (bit count)
+ * @param bool $pad - whether to pad (only when encoding)
+ * @return int[]
+ * @throws Base32Exception
+ */
+ protected static function convertBits(array $data, $inLen, $fromBits, $toBits, $pad = true): array
+ {
+ $acc = 0;
+ $bits = 0;
+ $ret = [];
+ $maxv = (1 << $toBits) - 1;
+ $maxacc = (1 << ($fromBits + $toBits - 1)) - 1;
+
+ for ($i = 0; $i < $inLen; $i++) {
+ $value = $data[$i];
+ if ($value < 0 || $value >> $fromBits) {
+ throw new Base32Exception('Invalid value for convert bits');
+ }
+
+ $acc = (($acc << $fromBits) | $value) & $maxacc;
+ $bits += $fromBits;
+
+ while ($bits >= $toBits) {
+ $bits -= $toBits;
+ $ret[] = (($acc >> $bits) & $maxv);
+ }
+ }
+
+ if ($pad) {
+ if ($bits) {
+ $ret[] = ($acc << $toBits - $bits) & $maxv;
+ }
+ } else {
+ if ($bits >= $fromBits || ((($acc << ($toBits - $bits))) & $maxv)) {
+ throw new Base32Exception('Invalid data');
+ }
+ }
+
+ return $ret;
+ }
+}
\ No newline at end of file
diff --git a/src/Utils/Base58Decoder.php b/src/Utils/Base58Decoder.php
new file mode 100644
index 0000000..ff72f79
--- /dev/null
+++ b/src/Utils/Base58Decoder.php
@@ -0,0 +1,70 @@
+ 90) {
+ throw new Bech32Exception('Bech32 string cannot exceed 90 characters in length');
+ }
+
+ return $this->decodeRaw($sBech);
+ }
+
+ /**
+ * @param string $sBech The bech32 encoded string
+ *
+ * @return array Returns [$hrp, $dataChars]
+ * @throws Bech32Exception
+ * @throws Bech32Exception
+ */
+ public function decodeRaw(string $sBech): array
+ {
+ $length = strlen($sBech);
+
+ if ($length < 8) {
+ throw new Bech32Exception("Bech32 string is too short");
+ }
+
+ $chars = array_values(unpack('C*', $sBech));
+
+ $haveUpper = false;
+ $haveLower = false;
+ $positionOne = -1;
+
+ for ($i = 0; $i < $length; $i++) {
+ $x = $chars[$i];
+
+ if ($x < 33 || $x > 126) {
+ throw new Bech32Exception('Out of range character in bech32 string');
+ }
+
+ if ($x >= 0x61 && $x <= 0x7a) {
+ $haveLower = true;
+ }
+
+ if ($x >= 0x41 && $x <= 0x5a) {
+ $haveUpper = true;
+ $x = $chars[$i] = $x + 0x20;
+ }
+
+ // find location of last '1' character
+ if ($x === 0x31) {
+ $positionOne = $i;
+ }
+ }
+
+ if ($haveUpper && $haveLower) {
+ throw new Bech32Exception('Data contains mixture of higher/lower case characters');
+ }
+
+ if ($positionOne === -1) {
+ throw new Bech32Exception("Missing separator character");
+ }
+
+ if ($positionOne < 1) {
+ throw new Bech32Exception("Empty HRP");
+ }
+
+ if (($positionOne + 7) > $length) {
+ throw new Bech32Exception('Too short checksum');
+ }
+
+ $hrp = pack("C*", ...array_slice($chars, 0, $positionOne));
+
+ $data = [];
+
+ for ($i = $positionOne + 1; $i < $length; $i++) {
+ $data[] = ($chars[$i] & 0x80) ? -1 : self::CHARKEY_KEY[$chars[$i]];
+ }
+
+ if (!$this->verifyChecksum($hrp, $data)) {
+ throw new Bech32Exception('Invalid bech32 checksum');
+ }
+
+ return [$hrp, array_slice($data, 0, -6)];
+ }
+
+ /**
+ * Verifies the checksum given $hrp and $convertedDataChars.
+ *
+ * @param string $hrp
+ * @param int[] $convertedDataChars
+ *
+ * @return bool
+ */
+ private function verifyChecksum(string $hrp, array $convertedDataChars): bool
+ {
+ $expandHrp = $this->hrpExpand($hrp, strlen($hrp));
+ $r = array_merge($expandHrp, $convertedDataChars);
+ $poly = $this->polyMod($r, count($r));
+
+ return in_array($poly, self::ALLOWED_POLY, true);
+ }
+
+
+ /**
+ * Expands the human-readable part into a character array for checksumming.
+ *
+ * @param string $hrp
+ * @param int $hrpLen
+ * @return int[]
+ */
+ private function hrpExpand(string $hrp, int $hrpLen): array
+ {
+ $expand1 = [];
+ $expand2 = [];
+
+ for ($i = 0; $i < $hrpLen; $i++) {
+ $o = ord($hrp[$i]);
+ $expand1[] = $o >> 5;
+ $expand2[] = $o & 31;
+ }
+
+ return array_merge($expand1, [0], $expand2);
+ }
+
+ /**
+ * @param int[] $values
+ * @param int $numValues
+ *
+ * @return int
+ */
+ private function polyMod(array $values, int $numValues): int
+ {
+ $chk = 1;
+ for ($i = 0; $i < $numValues; $i++) {
+ $top = $chk >> 25;
+ $chk = ($chk & 0x1ffffff) << 5 ^ $values[$i];
+
+ for ($j = 0; $j < 5; $j++) {
+ $value = (($top >> $j) & 1) ? self::GENERATOR[$j] : 0;
+ $chk ^= $value;
+ }
+ }
+
+ return $chk;
+ }
+
+ /**
+ * Converts words of $fromBits bits to $toBits bits in size.
+ *
+ * @param int[] $data Character array of data to convert
+ * @param int $inLen Number of elements in array
+ * @param int $fromBits Word (bit count) size of provided data
+ * @param int $toBits Requested word size (bit count)
+ * @param bool $pad Whether to pad (only when encoding)
+ *
+ * @return int[]
+ *
+ * @throws Bech32Exception
+ */
+ private function convertBits(array $data, int $inLen, int $fromBits, int $toBits, bool $pad = true): array
+ {
+ $acc = 0;
+ $bits = 0;
+ $ret = [];
+ $maxv = (1 << $toBits) - 1;
+ $maxacc = (1 << ($fromBits + $toBits - 1)) - 1;
+
+ for ($i = 0; $i < $inLen; $i++) {
+ $value = $data[$i];
+
+ if ($value < 0 || $value >> $fromBits) {
+ throw new Bech32Exception('Invalid value for convert bits');
+ }
+
+ $acc = (($acc << $fromBits) | $value) & $maxacc;
+ $bits += $fromBits;
+
+ while ($bits >= $toBits) {
+ $bits -= $toBits;
+ $ret[] = (($acc >> $bits) & $maxv);
+ }
+ }
+
+ if ($pad && $bits) {
+ $ret[] = ($acc << $toBits - $bits) & $maxv;
+ } elseif ($bits >= $fromBits || ((($acc << ($toBits - $bits))) & $maxv)) {
+ throw new Bech32Exception('Invalid data');
+ }
+
+ return $ret;
+ }
+
+
+ /**
+ * @param int $version
+ *
+ * @param string $program
+ *
+ * @throws RuntimeException
+ */
+ private function validateWitnessProgram(int $version, string $program): void
+ {
+ if ($version < 0 || $version > 16) {
+ throw new RuntimeException("Invalid witness version");
+ }
+
+ $sizeProgram = strlen($program);
+ if (($version === 0) && $sizeProgram !== 20 && $sizeProgram !== 32) {
+ throw new RuntimeException("Invalid size for V0 witness program");
+ }
+
+ if ($sizeProgram < 2 || $sizeProgram > 40) {
+ throw new RuntimeException("Witness program size was out of valid range");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Utils/HexDecoder.php b/src/Utils/HexDecoder.php
new file mode 100644
index 0000000..fbf9c10
--- /dev/null
+++ b/src/Utils/HexDecoder.php
@@ -0,0 +1,47 @@
+ 0);
+
+ return $hex;
+ }
+}
\ No newline at end of file
diff --git a/src/Utils/KeccakDecoder.php b/src/Utils/KeccakDecoder.php
new file mode 100644
index 0000000..8ab739e
--- /dev/null
+++ b/src/Utils/KeccakDecoder.php
@@ -0,0 +1,320 @@
+> 31)) & (0xFFFFFFFF),
+ $bc[($i + 4) % 5][1] ^ (($bc[($i + 1) % 5][1] << 1) | ($bc[($i + 1) % 5][0] >> 31)) & (0xFFFFFFFF)
+ ];
+
+ for ($j = 0; $j < 25; $j += 5) {
+ $st[$j + $i] = [
+ $st[$j + $i][0] ^ $t[0],
+ $st[$j + $i][1] ^ $t[1]
+ ];
+ }
+ }
+
+ // Rho Pi
+ $t = $st[1];
+ for ($i = 0; $i < 24; $i++) {
+ $j = self::$keccakf_piln[$i];
+
+ $bc[0] = $st[$j];
+
+ $n = self::$keccakf_rotc[$i];
+ $hi = $t[0];
+ $lo = $t[1];
+ if ($n >= 32) {
+ $n -= 32;
+ $hi = $t[1];
+ $lo = $t[0];
+ }
+
+ $st[$j] =[
+ (($hi << $n) | ($lo >> (32 - $n))) & (0xFFFFFFFF),
+ (($lo << $n) | ($hi >> (32 - $n))) & (0xFFFFFFFF)
+ ];
+
+ $t = $bc[0];
+ }
+
+ // Chi
+ for ($j = 0; $j < 25; $j += 5) {
+ for ($i = 0; $i < 5; $i++) {
+ $bc[$i] = $st[$j + $i];
+ }
+ for ($i = 0; $i < 5; $i++) {
+ $st[$j + $i] = [
+ $st[$j + $i][0] ^ ~$bc[($i + 1) % 5][0] & $bc[($i + 2) % 5][0],
+ $st[$j + $i][1] ^ ~$bc[($i + 1) % 5][1] & $bc[($i + 2) % 5][1]
+ ];
+ }
+ }
+
+ // Iota
+ $st[0] = [
+ $st[0][0] ^ $keccakf_rndc[$round][0],
+ $st[0][1] ^ $keccakf_rndc[$round][1]
+ ];
+ }
+ }
+
+ private static function keccak64($in_raw, int $capacity, int $outputlength, $suffix, bool $raw_output): string {
+ $capacity /= 8;
+
+ $inlen = mb_strlen($in_raw, self::ENCODING);
+
+ $rsiz = 200 - 2 * $capacity;
+ $rsizw = $rsiz / 8;
+
+ $st = [];
+ for ($i = 0; $i < 25; $i++) {
+ $st[] = [0, 0];
+ }
+
+ for ($in_t = 0; $inlen >= $rsiz; $inlen -= $rsiz, $in_t += $rsiz) {
+ for ($i = 0; $i < $rsizw; $i++) {
+ $t = unpack('V*', mb_substr($in_raw, $i * 8 + $in_t, 8, self::ENCODING));
+
+ $st[$i] = [
+ $st[$i][0] ^ $t[2],
+ $st[$i][1] ^ $t[1]
+ ];
+ }
+
+ self::keccakf64($st, self::KECCAK_ROUNDS);
+ }
+
+ $temp = mb_substr($in_raw, $in_t, $inlen, self::ENCODING);
+ $temp = str_pad($temp, $rsiz, "\x0", STR_PAD_RIGHT);
+
+ $temp[$inlen] = chr($suffix);
+ $temp[$rsiz - 1] = chr(ord($temp[$rsiz - 1]) | 0x80);
+
+ for ($i = 0; $i < $rsizw; $i++) {
+ $t = unpack('V*', mb_substr($temp, $i * 8, 8, self::ENCODING));
+
+ $st[$i] = [
+ $st[$i][0] ^ $t[2],
+ $st[$i][1] ^ $t[1]
+ ];
+ }
+
+ self::keccakf64($st, self::KECCAK_ROUNDS);
+
+ $out = '';
+ for ($i = 0; $i < 25; $i++) {
+ $out .= $t = pack('V*', $st[$i][1], $st[$i][0]);
+ }
+ $r = mb_substr($out, 0, $outputlength / 8, self::ENCODING);
+
+ return $raw_output ? $r : bin2hex($r);
+ }
+
+ private static function keccakf32(&$st, $rounds): void {
+ $keccakf_rndc = [
+ [0x0000, 0x0000, 0x0000, 0x0001], [0x0000, 0x0000, 0x0000, 0x8082], [0x8000, 0x0000, 0x0000, 0x0808a], [0x8000, 0x0000, 0x8000, 0x8000],
+ [0x0000, 0x0000, 0x0000, 0x808b], [0x0000, 0x0000, 0x8000, 0x0001], [0x8000, 0x0000, 0x8000, 0x08081], [0x8000, 0x0000, 0x0000, 0x8009],
+ [0x0000, 0x0000, 0x0000, 0x008a], [0x0000, 0x0000, 0x0000, 0x0088], [0x0000, 0x0000, 0x8000, 0x08009], [0x0000, 0x0000, 0x8000, 0x000a],
+ [0x0000, 0x0000, 0x8000, 0x808b], [0x8000, 0x0000, 0x0000, 0x008b], [0x8000, 0x0000, 0x0000, 0x08089], [0x8000, 0x0000, 0x0000, 0x8003],
+ [0x8000, 0x0000, 0x0000, 0x8002], [0x8000, 0x0000, 0x0000, 0x0080], [0x0000, 0x0000, 0x0000, 0x0800a], [0x8000, 0x0000, 0x8000, 0x000a],
+ [0x8000, 0x0000, 0x8000, 0x8081], [0x8000, 0x0000, 0x0000, 0x8080], [0x0000, 0x0000, 0x8000, 0x00001], [0x8000, 0x0000, 0x8000, 0x8008]
+ ];
+
+ $bc = [];
+ for ($round = 0; $round < $rounds; $round++) {
+
+ // Theta
+ for ($i = 0; $i < 5; $i++) {
+ $bc[$i] = [
+ $st[$i][0] ^ $st[$i + 5][0] ^ $st[$i + 10][0] ^ $st[$i + 15][0] ^ $st[$i + 20][0],
+ $st[$i][1] ^ $st[$i + 5][1] ^ $st[$i + 10][1] ^ $st[$i + 15][1] ^ $st[$i + 20][1],
+ $st[$i][2] ^ $st[$i + 5][2] ^ $st[$i + 10][2] ^ $st[$i + 15][2] ^ $st[$i + 20][2],
+ $st[$i][3] ^ $st[$i + 5][3] ^ $st[$i + 10][3] ^ $st[$i + 15][3] ^ $st[$i + 20][3]
+ ];
+ }
+
+ for ($i = 0; $i < 5; $i++) {
+ $t = [
+ $bc[($i + 4) % 5][0] ^ ((($bc[($i + 1) % 5][0] << 1) | ($bc[($i + 1) % 5][1] >> 15)) & (0xFFFF)),
+ $bc[($i + 4) % 5][1] ^ ((($bc[($i + 1) % 5][1] << 1) | ($bc[($i + 1) % 5][2] >> 15)) & (0xFFFF)),
+ $bc[($i + 4) % 5][2] ^ ((($bc[($i + 1) % 5][2] << 1) | ($bc[($i + 1) % 5][3] >> 15)) & (0xFFFF)),
+ $bc[($i + 4) % 5][3] ^ ((($bc[($i + 1) % 5][3] << 1) | ($bc[($i + 1) % 5][0] >> 15)) & (0xFFFF))
+ ];
+
+ for ($j = 0; $j < 25; $j += 5) {
+ $st[$j + $i] = [
+ $st[$j + $i][0] ^ $t[0],
+ $st[$j + $i][1] ^ $t[1],
+ $st[$j + $i][2] ^ $t[2],
+ $st[$j + $i][3] ^ $t[3]
+ ];
+ }
+ }
+
+ // Rho Pi
+ $t = $st[1];
+ for ($i = 0; $i < 24; $i++) {
+ $j = self::$keccakf_piln[$i];
+ $bc[0] = $st[$j];
+
+
+ $n = self::$keccakf_rotc[$i] >> 4;
+ $m = self::$keccakf_rotc[$i] % 16;
+
+ $st[$j] = [
+ ((($t[(0+$n) %4] << $m) | ($t[(1+$n) %4] >> (16-$m))) & (0xFFFF)),
+ ((($t[(1+$n) %4] << $m) | ($t[(2+$n) %4] >> (16-$m))) & (0xFFFF)),
+ ((($t[(2+$n) %4] << $m) | ($t[(3+$n) %4] >> (16-$m))) & (0xFFFF)),
+ ((($t[(3+$n) %4] << $m) | ($t[(0+$n) %4] >> (16-$m))) & (0xFFFF))
+ ];
+
+ $t = $bc[0];
+ }
+
+ // Chi
+ for ($j = 0; $j < 25; $j += 5) {
+ for ($i = 0; $i < 5; $i++) {
+ $bc[$i] = $st[$j + $i];
+ }
+ for ($i = 0; $i < 5; $i++) {
+ $st[$j + $i] = [
+ $st[$j + $i][0] ^ ~$bc[($i + 1) % 5][0] & $bc[($i + 2) % 5][0],
+ $st[$j + $i][1] ^ ~$bc[($i + 1) % 5][1] & $bc[($i + 2) % 5][1],
+ $st[$j + $i][2] ^ ~$bc[($i + 1) % 5][2] & $bc[($i + 2) % 5][2],
+ $st[$j + $i][3] ^ ~$bc[($i + 1) % 5][3] & $bc[($i + 2) % 5][3]
+ ];
+ }
+ }
+
+ // Iota
+ $st[0] = [
+ $st[0][0] ^ $keccakf_rndc[$round][0],
+ $st[0][1] ^ $keccakf_rndc[$round][1],
+ $st[0][2] ^ $keccakf_rndc[$round][2],
+ $st[0][3] ^ $keccakf_rndc[$round][3]
+ ];
+ }
+ }
+
+ private static function keccak32($in_raw, int $capacity, int $outputlength, $suffix, bool $raw_output): string {
+ $capacity /= 8;
+
+ $inlen = mb_strlen($in_raw, self::ENCODING);
+
+ $rsiz = 200 - 2 * $capacity;
+ $rsizw = $rsiz / 8;
+
+ $st = [];
+ for ($i = 0; $i < 25; $i++) {
+ $st[] = [0, 0, 0, 0];
+ }
+
+ for ($in_t = 0; $inlen >= $rsiz; $inlen -= $rsiz, $in_t += $rsiz) {
+ for ($i = 0; $i < $rsizw; $i++) {
+ $t = unpack('v*', mb_substr($in_raw, $i * 8 + $in_t, 8, self::ENCODING));
+
+ $st[$i] = [
+ $st[$i][0] ^ $t[4],
+ $st[$i][1] ^ $t[3],
+ $st[$i][2] ^ $t[2],
+ $st[$i][3] ^ $t[1]
+ ];
+ }
+
+ self::keccakf32($st, self::KECCAK_ROUNDS);
+ }
+
+ $temp = mb_substr($in_raw, $in_t, $inlen, self::ENCODING);
+ $temp = str_pad($temp, $rsiz, "\x0", STR_PAD_RIGHT);
+
+ $temp[$inlen] = chr($suffix);
+ $temp[$rsiz - 1] = chr((int) $temp[$rsiz - 1] | 0x80);
+
+ for ($i = 0; $i < $rsizw; $i++) {
+ $t = unpack('v*', mb_substr($temp, $i * 8, 8, self::ENCODING));
+
+ $st[$i] = [
+ $st[$i][0] ^ $t[4],
+ $st[$i][1] ^ $t[3],
+ $st[$i][2] ^ $t[2],
+ $st[$i][3] ^ $t[1]
+ ];
+ }
+
+ self::keccakf32($st, self::KECCAK_ROUNDS);
+
+ $out = '';
+ for ($i = 0; $i < 25; $i++) {
+ $out .= $t = pack('v*', $st[$i][3],$st[$i][2], $st[$i][1], $st[$i][0]);
+ }
+ $r = mb_substr($out, 0, $outputlength / 8, self::ENCODING);
+
+ return $raw_output ? $r: bin2hex($r);
+ }
+
+ private static function keccak($in_raw, int $capacity, int $outputlength, $suffix, bool $raw_output): string {
+ return self::$x64
+ ? self::keccak64($in_raw, $capacity, $outputlength, $suffix, $raw_output)
+ : self::keccak32($in_raw, $capacity, $outputlength, $suffix, $raw_output);
+ }
+
+ public static function hash($in, int $mdlen, bool $raw_output = false): string {
+ if (!in_array($mdlen, [224, 256, 384, 512], true)) {
+ throw new Exception('Unsupported Keccak Hash output size.');
+ }
+
+ return self::keccak($in, $mdlen, $mdlen, self::LFSR, $raw_output);
+ }
+
+ public static function shake($in, int $security_level, int $outlen, bool $raw_output = false): string {
+ if (!in_array($security_level, [128, 256], true)) {
+ throw new Exception('Unsupported Keccak Shake security level.');
+ }
+
+ return self::keccak($in, $security_level, $outlen, 0x1f, $raw_output);
+ }
+}
diff --git a/src/Validation.php b/src/Validation.php
deleted file mode 100644
index 8ffbbb8..0000000
--- a/src/Validation.php
+++ /dev/null
@@ -1,159 +0,0 @@
-address = $address;
- $this->determineVersion();
- }
-
- protected static function decodeHex($hex)
- {
- $hex = strtoupper($hex);
- $chars = "0123456789ABCDEF";
- $return = "0";
- for ($i = 0; $i < strlen($hex); $i++) {
- $current = (string)strpos($chars, $hex[$i]);
- $return = (string)bcmul($return, "16", 0);
- $return = (string)bcadd($return, $current, 0);
- }
- return $return;
- }
-
- protected static function encodeHex($dec)
- {
- $chars = "0123456789ABCDEF";
- $return = "";
- while (bccomp($dec, 0) == 1) {
- $dv = (string)bcdiv($dec, "16", 0);
- $rem = (integer)bcmod($dec, "16");
- $dec = $dv;
- $return = $return . $chars[$rem];
- }
- return strrev($return);
- }
-
- protected static function base58ToHex($base58)
- {
- $origbase58 = $base58;
-
- $chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
- $return = "0";
- for ($i = 0; $i < strlen($base58); $i++) {
- $current = (string)strpos($chars, $base58[$i]);
- $return = (string)bcmul($return, "58", 0);
- $return = (string)bcadd($return, $current, 0);
- }
-
- $return = self::encodeHex($return);
-
- //leading zeros
- for ($i = 0; $i < strlen($origbase58) && $origbase58[$i] == "1"; $i++) {
- $return = "00" . $return;
- }
-
- if (strlen($return) % 2 != 0) {
- $return = "0" . $return;
- }
-
- return $return;
- }
-
- protected static function encodeBase58($hex)
- {
- $orighex = $hex;
-
- $chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
- $hex = self::decodeHex($hex);
- $return = "";
- while (bccomp($hex, 0) == 1) {
- $dv = (string)bcdiv($hex, "58", 0);
- $rem = (integer)bcmod($hex, "58");
- $hex = $dv;
- $return = $return . $chars[$rem];
- }
- $return = strrev($return);
-
- //leading zeros
- for ($i = 0; $i < strlen($orighex) && substr($orighex, $i, 2) == "00"; $i += 2) {
- $return = "1" . $return;
- }
-
- return $return;
- }
-
- protected function hash160ToAddress($hash160)
- {
- $hash160 = $this->addressVersion . $hash160;
- $check = pack("H*", $hash160);
- $check = hash("sha256", hash("sha256", $check, true));
- $check = substr($check, 0, 8);
- $hash160 = strtoupper($hash160 . $check);
-
- if (strlen($hash160) % 2 != 0) {
- $this->addressVersion = null;
- }
-
- return self::encodeBase58($hash160);
- }
-
- protected static function addressToHash160($addr)
- {
- $addr = self::base58ToHex($addr);
- $addr = substr($addr, 2, strlen($addr) - 10);
- return $addr;
- }
-
- protected static function hash160($data)
- {
- $data=pack("H*" , $data);
- return strtoupper(hash("ripemd160", hash("sha256", $data, true)));
- }
-
- protected function pubKeyToAddress($pubkey)
- {
- return $this->hash160ToAddress(self::hash160($pubkey));
- }
-
- protected function validateVersion($version)
- {
- return hexdec($version) == hexdec($this->addressVersion);
- }
-
- protected function determineVersion()
- {
- if (isset($this->base58PrefixToHexVersion[$this->address[0]])) {
- $this->addressVersion = $this->base58PrefixToHexVersion[$this->address[0]];
- }
- }
-
- function validate()
- {
- if (is_null($this->addressVersion)) {
- return false;
- }
-
- $hexAddress = self::base58ToHex($this->address);
- if (strlen($hexAddress) != 50) {
- return false;
- }
- $version = substr($hexAddress, 0, 2);
-
- if (!$this->validateVersion($version)) {
- return false;
- }
-
- $check = substr($hexAddress, 0, strlen($hexAddress) - 8);
- $check = pack("H*", $check);
- $check = strtoupper(hash("sha256", hash("sha256", $check, true)));
- $check = substr($check, 0, 8);
- return $check == substr($hexAddress, strlen($hexAddress) - 8);
- }
-}
\ No newline at end of file
diff --git a/src/Validation/BTC.php b/src/Validation/BTC.php
deleted file mode 100644
index 3a85b35..0000000
--- a/src/Validation/BTC.php
+++ /dev/null
@@ -1,14 +0,0 @@
- '00',
- '3' => '05'
- ];
-}
\ No newline at end of file
diff --git a/src/Validation/DASH.php b/src/Validation/DASH.php
deleted file mode 100644
index 06b1245..0000000
--- a/src/Validation/DASH.php
+++ /dev/null
@@ -1,17 +0,0 @@
- self::VERSION_P2PKH,
- '7' => self::VERSION_P2SH
- ];
-
-}
\ No newline at end of file
diff --git a/src/Validation/LTC.php b/src/Validation/LTC.php
deleted file mode 100644
index 566e8ea..0000000
--- a/src/Validation/LTC.php
+++ /dev/null
@@ -1,43 +0,0 @@
- '30',
- 'M' => '31',
- '3' => self::DEPRECATED_ADDRESS_VERSION // deprecated for litecoin, should not be allowed for new user's inputs
- ];
-
- protected function validateVersion($version)
- {
- if ($this->addressVersion == self::DEPRECATED_ADDRESS_VERSION && !$this->deprecatedAllowed) {
- return false;
- }
- return hexdec($version) == hexdec($this->addressVersion);
- }
-
- /**
- * @return boolean
- */
- public function isDeprecatedAllowed()
- {
- return $this->deprecatedAllowed;
- }
-
- /**
- * @param boolean $deprecatedAllowed
- */
- public function setDeprecatedAllowed($deprecatedAllowed)
- {
- $this->deprecatedAllowed = $deprecatedAllowed;
- }
-
-}
\ No newline at end of file
diff --git a/src/Validator.php b/src/Validator.php
new file mode 100644
index 0000000..514d8c9
--- /dev/null
+++ b/src/Validator.php
@@ -0,0 +1,92 @@
+value, config("address_validation.{$currency->value}"), app()->isProduction());
+ }
+
+ public function isValid(?string $address): bool
+ {
+ if (!$address) {
+ return false;
+ }
+
+ $drivers = $this->getDrivers();
+ // if there is no drivers we force address to be valid
+ if (null === $drivers || !$drivers->valid()) {
+ return true;
+ }
+
+ return (bool) $this->getDriver($drivers, $address)?->check($address);
+ }
+
+ public function validate(?string $address): void
+ {
+ if (!$address) {
+ return;
+ }
+
+ $drivers = $this->getDrivers();
+ // if there is no drivers we force address to be valid
+ if (null === $drivers || !$drivers->valid()) {
+ return;
+ }
+
+ $driver = $this->getDriver($drivers, $address);
+
+ if ($driver === null) {
+ throw new AddressValidationException($this->chain, $address, false);
+ }
+
+ if (!$driver->check($address)) {
+ throw new AddressValidationException($this->chain, $address, true);
+ }
+ }
+
+ /**
+ * @return Generator|null
+ */
+ protected function getDrivers(): ?Generator
+ {
+ /** @var DriverConfig $driverConfig */
+ foreach ($this->options as $driverConfig) {
+ if ($driver = $driverConfig->makeDriver($this->isMainnet)) {
+ yield $driver;
+ }
+ }
+
+ return null;
+ }
+
+ protected function getDriver(iterable $drivers, string $address): ?Driver
+ {
+ /** @var Driver $driver */
+ foreach ($drivers as $driver) {
+ if ($driver->match($address)) {
+ return $driver;
+ }
+ }
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/tests/BTCTest.php b/tests/BTCTest.php
deleted file mode 100644
index 1cc2363..0000000
--- a/tests/BTCTest.php
+++ /dev/null
@@ -1,22 +0,0 @@
-assertEquals($row[1], $validator->validate());
- }
-
- }
-}
\ No newline at end of file
diff --git a/tests/DASHTest.php b/tests/DASHTest.php
deleted file mode 100644
index b4fcb35..0000000
--- a/tests/DASHTest.php
+++ /dev/null
@@ -1,28 +0,0 @@
-assertEquals($row[1], $validator->validate());
- }
-
- }
-}
\ No newline at end of file
diff --git a/tests/KeccakDriverTest.php b/tests/KeccakDriverTest.php
new file mode 100644
index 0000000..6bf5fdf
--- /dev/null
+++ b/tests/KeccakDriverTest.php
@@ -0,0 +1,45 @@
+value, $config, $net === 'mainnet');
+
+ self::assertEquals($expected, $validator->isValid($address));
+ }
+
+ public function addressesProvider(): array
+ {
+ return [
+ 'Ethereum #1' => ['mainnet', true, '0xe80b351948D0b87EE6A53e057A91467d54468D91'],
+ 'Ethereum #2' => ['testnet', true, '0x799aD3Ff7Ef43DfD1473F9b8a8C4237c22D8113F'],
+ 'Ethereum #3' => ['mainnet', true, '0xe80b351948d0b87ee6a53e057a91467d54468d91'],
+ 'Ethereum #4' => ['testnet', true, '0x799ad3ff7ef43dfd1473f9b8a8c4237c22d8113f'],
+ ];
+ }
+}
\ No newline at end of file
diff --git a/tests/LTCTest.php b/tests/LTCTest.php
deleted file mode 100644
index 2d1d18d..0000000
--- a/tests/LTCTest.php
+++ /dev/null
@@ -1,35 +0,0 @@
-assertEquals($row[1], $validator->validate());
- }
-
- }
-
- public function testLitecoinDeprecatedMultisigAddress()
- {
- $validator = new LTC('3CDJNfdWX8m2NwuGUV3nhXHXEeLygMXoAj');
- $validator->setDeprecatedAllowed(true);
- $this->assertEquals(true, $validator->validate());
- }
-}
\ No newline at end of file
diff --git a/tests/TestCase.php b/tests/TestCase.php
new file mode 100644
index 0000000..05347fe
--- /dev/null
+++ b/tests/TestCase.php
@@ -0,0 +1,11 @@
+value];
+
+ $validator = new Validator($currency->value, $options, $net === 'mainnet');
+
+ $this->assertEquals(
+ $expected,
+ $validator->isValid($address),
+ "[{$currency->value}] address [{$address}] is invalid"
+ );
+ }
+
+ public function currencyAddressProvider(): array
+ {
+ return [
+ 'Beacon #1' => [CurrencyEnum::BEACON, 'mainnet', true, 'bnb1fnd0k5l4p3ck2j9x9dp36chk059w977pszdgdz'],
+ 'Beacon #2' => [CurrencyEnum::BEACON, 'mainnet', true, 'bnb1xd8cn4w7q4hm4fc9a68xtpx22kqenju7ea8d3v'],
+ 'Beacon #3' => [CurrencyEnum::BEACON, 'testnet', true, 'tbnb1nuxna8asq69jf05cldcxpx9ee0m7drd9qz3aru'],
+ 'Beacon #4' => [CurrencyEnum::BEACON, 'mainnet', false, 'bnb1nuxna8asq69jf05cldcxpx9ee0m7drd9qz3aru'],
+ 'Beacon #5' => [CurrencyEnum::BEACON, 'testnet', false, 'bnb1nuxna8asq69jf05cldcxpx9ee0m7drd9qz3aru'],
+ //
+ 'BitcoinCash #1' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, 'bitcoincash:qp009ldhprp75mgn4kgaw8jvrpadnvg8qst37j42kx'],
+ 'BitcoinCash #2' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, 'bitcoincash:qz7032ylhvxmndkx438pd8kjd7k7zcqxzsf26q0lvr'],
+ 'BitcoinCash #3' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, '32uLhn19ZasD5bsVhLdDthhM37JhJHiEE2'],
+ 'BitcoinCash #4' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, 'qz52zsruu43sq7ed0srym3g0ktpyjkdkxcm949pl2z'],
+ 'BitcoinCash #5' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, 'qpf8eq7ygvhqjwydk9n29f6nyc8rcjhlwcuwngn6xk'],
+ 'BitcoinCash #6' => [CurrencyEnum::BITCOIN_CASH, 'testnet', true, 'bchtest:qp2vjh349lcd22hu0hv6hv9d0pwlk43f6u04d5jk36'],
+ 'BitcoinCash #7' => [CurrencyEnum::BITCOIN_CASH, 'testnet', true, 'qp2vjh349lcd22hu0hv6hv9d0pwlk43f6u04d5jk36'],
+ 'BitcoinCash #8' => [CurrencyEnum::BITCOIN_CASH, 'testnet', false, '1KADKOasjxpNKzbfcKjnigLYWjEFPcMXqf'],
+ 'BitcoinCash #9' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, 'qpnxwdu09eq4gqxv0ala37yj5evmmakf5vpp770edu'],
+ 'BitcoinCash #10' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, 'bitcoincash:qpnxwdu09eq4gqxv0ala37yj5evmmakf5vpp770edu'],
+ 'BitcoinCash #11' => [CurrencyEnum::BITCOIN_CASH, 'testnet', false, 'bchtest:qpnxwdu09eq4gqxv0ala37yj5evmmakf5vpp770edu'],
+ 'BitcoinCash #12' => [CurrencyEnum::BITCOIN_CASH, 'testnet', false, 'bchreg:qpnxwdu09eq4gqxv0ala37yj5evmmakf5vpp770edu'],
+ 'BitcoinCash #13' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, 'bitcoincash:pwqwzrf7z06m7nn58tkdjyxqfewanlhyrpxysack85xvf3mt0rv02l9dxc5uf'],
+ 'BitcoinCash #14' => [CurrencyEnum::BITCOIN_CASH, 'mainnet', true, 'bitcoincash:qr6m7j9njldwwzlg9v7v53unlr4jkmx6eylep8ekg2'],
+ //
+ 'Bitcoin #1' => [CurrencyEnum::BITCOIN, 'mainnet', true, '1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2'],
+ 'Bitcoin #2' => [CurrencyEnum::BITCOIN, 'mainnet', true, 'bc1q6v096h88xmpl662af0nc7wd3vta56zv6pyccl8'],
+ 'Bitcoin #3' => [CurrencyEnum::BITCOIN, 'testnet', true, 'tb1q27dglj7x4l34mj7j2x7e6fqsexk6vf8kew6qm0'],
+ 'Bitcoin #4' => [CurrencyEnum::BITCOIN, 'testnet', false, 'tb1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq'],
+ 'Bitcoin #5' => [CurrencyEnum::BITCOIN, 'mainnet', false, 'tb1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq'],
+ 'Bitcoin #6' => [CurrencyEnum::BITCOIN, 'testnet', false, '1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2'],
+ 'Bitcoin #7' => [CurrencyEnum::BITCOIN, 'testnet', false, 'bc1q6v096h88xmpl662af0nc7wd3vta56zv6pyccl8'],
+ 'Bitcoin #8' => [CurrencyEnum::BITCOIN, 'mainnet', true, 'BC1QL2725QLXHGWQ7F7XLJ8363FJCUF25XZ35SWRU5'],
+ 'Bitcoin #9' => [CurrencyEnum::BITCOIN, 'mainnet', true, 'bc1p5gyty9x2lrk65yndaeh242zm6xklgrv9nze9477dg0kyv6yvfljq0lqjkh'],
+ //
+ 'Cardano #1' => [CurrencyEnum::CARDANO, 'mainnet', true, 'addr1v9ywm0h3r8cnxrs04gfy7c3s2j44utjyvn5ldjdca0c2ltccgqdes'],
+ 'Cardano #2' => [CurrencyEnum::CARDANO, 'mainnet', false, 'stake1u9f9v0z5zzlldgx58n8tklphu8mf7h4jvp2j2gddluemnssjfnkzz'],
+ 'Cardano #3' => [CurrencyEnum::CARDANO, 'mainnet', true, 'addr1qxy3w62dupy9pzmpdfzxz4k240w5vawyagl5m9djqquyymrtm3grn7gpnjh7rwh2dy62hk8639lt6kzn32yxq960usnq9pexvt'],
+ 'Cardano #4' => [CurrencyEnum::CARDANO, 'mainnet', true, 'Ae2tdPwUPEYwNguM7TB3dMnZMfZxn1pjGHyGdjaF4mFqZF9L3bj6cdhiH8t'],
+ 'Cardano #5' => [CurrencyEnum::CARDANO, 'mainnet', true, 'DdzFFzCqrht2KYLcX8Vu53urCG52NxpgrGQvP9Mcp15Q8BkB9df9GndFDBRjoWTPuNkLW3yeQiFVet1KA7mraEkJ84AK2RwcEh3khs12'],
+ 'Cardano #6' => [CurrencyEnum::CARDANO, 'testnet', true, '37btjrVyb4KBbrmcxh3qQzswqDB4SCU8L68vYBJshaeYQ8rHVBfrAfuXZNyFHtR8QXUKR4CtytMyX4DwhsPYKKgFSpq8f5KxNz2s6Guqr6c6LzcHck'],
+ 'Cardano #7' => [CurrencyEnum::CARDANO, 'testnet', true, '2cWKMJemoBaipAW1NGegM2qWevSgpL9baiizayY4NnTBvxRGyppr2uym7F9eEtRLehFek'],
+ 'Cardano #8' => [CurrencyEnum::CARDANO, 'testnet', true, 'addr_test1qzfst6x8f4r47vm4qfeuj7g8r5pgkjnv5cuzjk94u8p7sd3gtlpjssk2fy95k4z5lr48tu48fcqstnzte44d8f8v8vhs9pwu4x'],
+ //
+ 'Dashcoin #1' => [CurrencyEnum::DASHCOIN, 'mainnet', true, 'XpESxaUmonkq8RaLLp46Brx2K39ggQe226'],
+ 'Dashcoin #2' => [CurrencyEnum::DASHCOIN, 'mainnet', true, 'XmZQkfLtk3xLtbBMenTdaZMxsUBYAsRz1o'],
+ 'Dashcoin #3' => [CurrencyEnum::DASHCOIN, 'testnet', true, 'yNpxAuCGxLkDmVRY12m4qEWx1ttgTczSMJ'],
+ 'Dashcoin #4' => [CurrencyEnum::DASHCOIN, 'testnet', true, 'yi7GRZLiUGrJfX2aNDQ3v7pGSCTrnLa87o'],
+ //
+ 'Dogecoin #1' => [CurrencyEnum::DOGECOIN, 'mainnet', true, 'DFrGqzk4ZnTcK1gYtxZ9QDJsDiVM8v8gwV'],
+ 'Dogecoin #2' => [CurrencyEnum::DOGECOIN, 'mainnet', true, 'DMzanBYjj3yYHtCcnEucn7H8LHNY9fARB8'],
+ 'Dogecoin #3' => [CurrencyEnum::DOGECOIN, 'testnet', true, 'mketxxXxaBeH7AhCBMatdH5ATVad2XHQdj'],
+ 'Dogecoin #4' => [CurrencyEnum::DOGECOIN, 'testnet', false, 'n3TZFrdPvwGqfPC7vBb8PGgbFwc1Cnxq9h'],
+ 'Dogecoin #5' => [CurrencyEnum::DOGECOIN, 'testnet', true, 'nd5N1KW1waCicK1vqfwtTcBSbQCHBLv2Um'],
+ 'Dogecoin #6' => [CurrencyEnum::DOGECOIN, 'testnet', false, 'DFundMr7W8PjB6ZmVwGv1L1WtZ2X3m3KgQ'],
+ 'Dogecoin #7' => [CurrencyEnum::DOGECOIN, 'mainnet', false, 'n3TZFrdPvwGqfPC7vBb8PGgbFwc1Cnxq9h'],
+ //
+ 'Ethereum #1' => [CurrencyEnum::ETHEREUM, 'mainnet', true, '0xe80b351948D0b87EE6A53e057A91467d54468D91'],
+ 'Ethereum #2' => [CurrencyEnum::ETHEREUM, 'testnet', true, '0x799aD3Ff7Ef43DfD1473F9b8a8C4237c22D8113F'],
+ 'Ethereum #3' => [CurrencyEnum::ETHEREUM, 'mainnet', false, '0xe80b351948d0b87ee6a53e057a91467d54468d91'],
+ 'Ethereum #4' => [CurrencyEnum::ETHEREUM, 'testnet', false, '0x799ad3ff7ef43dfd1473f9b8a8c4237c22d8113f'],
+ //
+ 'Litecoin #1' => [CurrencyEnum::LITECOIN, 'mainnet', true, 'MF5yqnMuNoiCiCXbZft7iFgLK5BPG5QKbE'],
+ 'Litecoin #2' => [CurrencyEnum::LITECOIN, 'mainnet', false, '1QLbGuc3WGKKKpLs4pBp9H6jiQ2MgPkXRp'],
+ 'Litecoin #3' => [CurrencyEnum::LITECOIN, 'mainnet', true, '3CDJNfdWX8m2NwuGUV3nhXHXEeLygMXoAj'],
+ 'Litecoin #4' => [CurrencyEnum::LITECOIN, 'mainnet', true, 'LbTjMGN7gELw4KbeyQf6cTCq859hD18guE'],
+ 'Litecoin #5' => [CurrencyEnum::LITECOIN, 'mainnet', true, 'MK9xC9sbktt6DHMF6XwA3eZPJ2Vx32AXFT'],
+ 'Litecoin #6' => [CurrencyEnum::LITECOIN, 'testnet', true, 'mpQA36uSXDGxySjknqHFVMdsLPgPnbm7ku'],
+ 'Litecoin #7' => [CurrencyEnum::LITECOIN, 'mainnet', true, 'ltc1qf6wcq8kc0unt3wuaszlkms3zkuerxlfaz07zmj'],
+ //
+ 'Ripple #1' => [CurrencyEnum::RIPPLE, 'mainnet', true, 'r4dgY6Mzob3NVq8CFYdEiPnXKboRScsXRu'],
+ //
+ 'Tron #1' => [CurrencyEnum::TRON, 'mainnet', true, 'TC9fKEGcBTfmvXKXLHq5MJDC8P7dhZQM92'],
+ 'Tron #2' => [CurrencyEnum::TRON, 'testnet', true, 'TRALQkt1v9MjUVn3gT7csfpodJDmnC6q8s'],
+ //
+ 'Zcash #1' => [CurrencyEnum::ZCASH, 'mainnet', true, 't1YQV51DKzKP63xJcynXuRfryMjfmgTJ7Jc'],
+ 'Zcash #2' => [CurrencyEnum::ZCASH, 'mainnet', true, 't1VJhyyvbi63Cu6nEVVgNHSCokDRa3repZB'],
+ 'Zcash #3' => [CurrencyEnum::ZCASH, 'testnet', true, 't1VJhyyvbi63Cu6nEVVgNHSCokDRa3repZB'],
+ //
+ 'EOS #1' => [CurrencyEnum::EOS, 'mainnet', true, 'atticlabeosb'],
+ 'EOS #2' => [CurrencyEnum::EOS, 'mainnet', true, 'bitfinexeos1'],
+ ];
+ }
+}
\ No newline at end of file