diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 28acd015..8fda1c98 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,10 +13,26 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} - - name: Install - run: composer update --no-interaction --no-suggest --ignore-platform-reqs + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.4" + extensions: mbstring, dom, fileinfo, intl, gd, imagick, bcmath, soap, zip, sqlite, pcov + coverage: pcov - - name: Unit tests - run: composer test + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ~/.composer/cache/files + key: dependencies-composer-${{ hashFiles('composer.json') }} + + - name: Install Composer dependencies + run: composer install --no-ansi --no-interaction --no-suggest --no-progress --prefer-dist --optimize-autoloader + + - name: Run Tests + run: ./vendor/bin/pest --coverage --coverage-html=.coverage --coverage-clover=coverage.xml diff --git a/.gitignore b/.gitignore index 9d8b2a4f..d5d2eba4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -phpunit.xml vendor clover.xml .php_cs.cache diff --git a/composer.json b/composer.json index db4af42b..896bf1f5 100755 --- a/composer.json +++ b/composer.json @@ -57,7 +57,7 @@ "vendor/bin/php-cs-fixer fix" ], "test": [ - "./vendor/bin/pest --parallel" + "./vendor/bin/pest --coverage --min=100 --coverage-html=.coverage --coverage-clover=coverage.xml" ] }, "minimum-stability": "dev", diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 00000000..0c12bb9f --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,17 @@ + + + + + ./tests + + + + + ./src + + + diff --git a/src/ByteBuffer/Concerns/Writes/UnsignedInteger.php b/src/ByteBuffer/Concerns/Writes/UnsignedInteger.php index db9f8d18..31d2b6d5 100644 --- a/src/ByteBuffer/Concerns/Writes/UnsignedInteger.php +++ b/src/ByteBuffer/Concerns/Writes/UnsignedInteger.php @@ -75,7 +75,7 @@ public function writeUInt64(int $value, int $offset = 0): self public function writeUInt256($value, int $offset = 0): self { // Convert the value to a GMP object for handling large numbers - if (is_numeric($value) || is_string($value)) { + if (is_numeric($value)) { $gmpValue = gmp_init($value); } elseif ($value instanceof \GMP) { $gmpValue = $value; diff --git a/src/Networks/AbstractNetwork.php b/src/Networks/AbstractNetwork.php index e1310cae..0d94bb39 100644 --- a/src/Networks/AbstractNetwork.php +++ b/src/Networks/AbstractNetwork.php @@ -14,17 +14,9 @@ abstract class AbstractNetwork extends Network implements NetworkInterface * @see Network::$base58PrefixMap */ protected $base58PrefixMap = [ - self::BASE58_WIF => 'aa', // 170 + self::BASE58_WIF => 'aa', // 170 ]; - /** - * {@inheritdoc} - */ - public static function __callStatic(string $method, array $args) - { - return static::factory()->{$method}(...$args); - } - /** * Create a new network instance. * diff --git a/src/Utils/Abi/ArgumentDecoder.php b/src/Utils/Abi/ArgumentDecoder.php index eac3fbc1..3efc708e 100644 --- a/src/Utils/Abi/ArgumentDecoder.php +++ b/src/Utils/Abi/ArgumentDecoder.php @@ -12,7 +12,13 @@ final class ArgumentDecoder public function __construct(string $bytes) { - $bytes = hex2bin($bytes); + try { + $bytes = hex2bin($bytes); + } catch (\Throwable $e) { + // Handle the case where hex2bin fails, e.g., invalid hex string + $bytes = false; + } + if ($bytes === false) { $bytes = ''; } diff --git a/src/Utils/Message.php b/src/Utils/Message.php index 3e1ef91a..311f0dfb 100644 --- a/src/Utils/Message.php +++ b/src/Utils/Message.php @@ -44,16 +44,11 @@ class Message */ public function __construct(object $message) { - if (property_exists($message, 'publickey')) { - $this->publicKey = $message->publickey; - } elseif (property_exists($message, 'publicKey')) { - $this->publicKey = $message->publicKey; - } elseif (property_exists($message, 'signatory')) { - $this->publicKey = $message->signatory; - } else { + if (! property_exists($message, 'publicKey')) { throw new InvalidArgumentException('The given message did not contain a valid public key.'); } + $this->publicKey = $message->publicKey; $this->signature = $message->signature; $this->message = $message->message; } @@ -113,7 +108,7 @@ public static function sign(string $message, string $passphrase): self $v = dechex($signature->getRecoveryId() + 27); return static::new([ - 'publickey' => $privateKey->publicKey, + 'publicKey' => $privateKey->publicKey, 'signature' => $r.$s.$v, 'message' => $message, ]); @@ -144,7 +139,7 @@ public function verify(): bool public function toArray(): array { return [ - 'publickey' => $this->publicKey, + 'publicKey' => $this->publicKey, 'signature' => $this->signature, 'message' => $this->message, ]; diff --git a/tests/Unit/ByteBuffer/ByteBufferTest.php b/tests/Unit/ByteBuffer/ByteBufferTest.php index 5bebcecf..3d670635 100644 --- a/tests/Unit/ByteBuffer/ByteBufferTest.php +++ b/tests/Unit/ByteBuffer/ByteBufferTest.php @@ -177,6 +177,13 @@ expect($buffer->internalSize())->toBe(4 + 11); }); +it('should fill the buffer starting from a different start point', function () { + $buffer = ByteBuffer::new('hello'); + $buffer->fill(11, 4); + + expect($buffer->internalSize())->toBe(4 + 11); +}); + it('should flip the buffer contents', function () { $buffer = ByteBuffer::new('Hello World'); $buffer->flip(); diff --git a/tests/Unit/ByteBuffer/Concerns/Reads/UnsignedIntegerTest.php b/tests/Unit/ByteBuffer/Concerns/Reads/UnsignedIntegerTest.php index b86e4421..a5c3d7ff 100644 --- a/tests/Unit/ByteBuffer/Concerns/Reads/UnsignedIntegerTest.php +++ b/tests/Unit/ByteBuffer/Concerns/Reads/UnsignedIntegerTest.php @@ -71,3 +71,13 @@ expect($buffer->readULong())->toBe(64); }); + +test('it should read uint256', function () { + // 256-bit unsigned integer (32 bytes) + $value = '1157920892373161954235709850086879078532699846656405640323232344'; // max uint256 + $buffer = ByteBuffer::new(0); + $buffer->writeUInt256($value); + $buffer->position(0); + + expect($buffer->readUInt256())->toBe($value); +}); diff --git a/tests/Unit/ByteBuffer/Concerns/Writes/UnsignedIntegerTest.php b/tests/Unit/ByteBuffer/Concerns/Writes/UnsignedIntegerTest.php index 645797f6..b6db0e4c 100644 --- a/tests/Unit/ByteBuffer/Concerns/Writes/UnsignedIntegerTest.php +++ b/tests/Unit/ByteBuffer/Concerns/Writes/UnsignedIntegerTest.php @@ -63,3 +63,35 @@ expect($buffer->internalSize())->toBe(8); }); + +test('it should write uint256', function () { + // 256-bit unsigned integer (32 bytes) + $value = '1157920892373161954235709850086879078532699846656405640323232344'; // max uint256 + $buffer = ByteBuffer::new(0); + $buffer->writeUInt256($value); + + expect($buffer->internalSize())->toBe(32); +}); + +test('it should write uint256 gmp value', function () { + // 256-bit unsigned integer (32 bytes) + $value = gmp_init('1157920892373161954235709850086879078532699846656405640323232344'); // max uint256 + $buffer = ByteBuffer::new(0); + $buffer->writeUInt256($value); + + expect($buffer->internalSize())->toBe(32); +}); + +test('it should throw exception when writing invalid uint256', function () { + // 256-bit unsigned integer (32 bytes) + $value = 'asd'; + $buffer = ByteBuffer::new(0); + $buffer->writeUInt256($value); +})->throws(InvalidArgumentException::class, 'The value must be a numeric string, integer, or GMP object.'); + +test('it should throw exception when writing uint256 which is too long', function () { + // 256-bit unsigned integer (32 bytes) + $value = '1157920892373161954235709850086879078532699846656405640323232344444411579208923731619542357098500868790785326998466564056403232323444444'; + $buffer = ByteBuffer::new(0); + $buffer->writeUInt256($value); +})->throws(InvalidArgumentException::class, 'The value must fit into 256 bits.'); diff --git a/tests/Unit/Enums/AbiFunctionTest.php b/tests/Unit/Enums/AbiFunctionTest.php new file mode 100644 index 00000000..bc91bf3b --- /dev/null +++ b/tests/Unit/Enums/AbiFunctionTest.php @@ -0,0 +1,24 @@ +transactionClass())->toEqual($class); +})->with([ + 'Vote' => ['VOTE', Vote::class], + 'Unvote' => ['UNVOTE', Unvote::class], + 'ValidatorRegistration' => ['VALIDATOR_REGISTRATION', ValidatorRegistration::class], + 'ValidatorResignation' => ['VALIDATOR_RESIGNATION', ValidatorResignation::class], + 'UsernameRegistration' => ['USERNAME_REGISTRATION', UsernameRegistration::class], + 'UsernameResignation' => ['USERNAME_RESIGNATION', UsernameResignation::class], + 'Multipayment' => ['MULTIPAYMENT', Multipayment::class], +]); diff --git a/tests/Unit/Identities/AddressTest.php b/tests/Unit/Identities/AddressTest.php index 240d0c72..31bbf76b 100644 --- a/tests/Unit/Identities/AddressTest.php +++ b/tests/Unit/Identities/AddressTest.php @@ -38,3 +38,17 @@ expect($actual)->toBe($fixture['data']['address']); }); + +it('should validate an address', function () { + $fixture = $this->getFixture('identity'); + + $actual = Address::validate($fixture['data']['address']); + + expect($actual)->toBeTrue(); +}); + +it('should return false for invalid an address', function () { + $actual = Address::validate('invalid-address'); + + expect($actual)->toBeFalse(); +}); diff --git a/tests/Unit/Transactions/DeserializerTest.php b/tests/Unit/Transactions/DeserializerTest.php index 11ef54f2..93a4e374 100644 --- a/tests/Unit/Transactions/DeserializerTest.php +++ b/tests/Unit/Transactions/DeserializerTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use ArkEcosystem\Crypto\Transactions\Deserializer; use ArkEcosystem\Crypto\Transactions\Types\EvmCall; use ArkEcosystem\Crypto\Transactions\Types\Multipayment; use ArkEcosystem\Crypto\Transactions\Types\Transfer; @@ -88,3 +89,45 @@ expect($transaction)->toBeInstanceOf(Multipayment::class); }); + +it('should use ByteBuffer::fromHex when there is no null-byte in the string', function () { + // The string does not contain a null-byte + $hexString = 'abcdef1234567890'; + $deserializer = new Deserializer($hexString); + + // Use reflection to access the private buffer property + $reflection = new ReflectionClass($deserializer); + $bufferProperty = $reflection->getProperty('buffer'); + $bufferProperty->setAccessible(true); + $buffer = $bufferProperty->getValue($deserializer); + + // The buffer should be an instance of ByteBuffer + expect($buffer)->toBeInstanceOf(ArkEcosystem\Crypto\ByteBuffer\ByteBuffer::class); + + // The buffer should contain the hex string (converted to binary) + expect($buffer->toString('hex'))->toContain($hexString); +}); + +it('should use ByteBuffer::fromBinary when there is a null-byte in the string', function () { + // The string contains a null-byte + $binaryString = "abc\0def"; // hex: 61626300646566 + $hexString = '61626300646566'; + $deserializer = new Deserializer($binaryString); + + // Use reflection to access the private buffer property + $reflection = new ReflectionClass($deserializer); + $bufferProperty = $reflection->getProperty('buffer'); + $bufferProperty->setAccessible(true); + $buffer = $bufferProperty->getValue($deserializer); + + // The buffer should be an instance of ByteBuffer + expect($buffer)->toBeInstanceOf(ArkEcosystem\Crypto\ByteBuffer\ByteBuffer::class); + + // The buffer should contain the binary string + expect($buffer->toString('hex'))->toBe($hexString); +}); + +it('should return null if no data value in transaction data', function () { + expect(Deserializer::decodePayload([]))->toBeNull(); + expect(Deserializer::decodePayload(['data' => '']))->toBeNull(); +}); diff --git a/tests/Unit/Utils/Abi/ArgumentDecoderTest.php b/tests/Unit/Utils/Abi/ArgumentDecoderTest.php index 0c388b50..df0d7cb7 100644 --- a/tests/Unit/Utils/Abi/ArgumentDecoderTest.php +++ b/tests/Unit/Utils/Abi/ArgumentDecoderTest.php @@ -19,6 +19,15 @@ expect($decoder->decodeAddress())->toBe($expected); }); +it('should decode a string', function () { + $payload = '0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000474657374'; + $expected = 'test'; + + $decoder = new ArgumentDecoder($payload); + + expect($decoder->decodeString())->toBe($expected); +}); + it('should decode unsigned int', function () { $payload = '000000000000000000000000000000000000000000000000016345785d8a0000'; $expected = '100000000000000000'; @@ -54,3 +63,12 @@ expect($decoder->decodeBool())->toBe($expected); }); + +it('should handle issue converting hex to binary', function () { + $decoder = new ArgumentDecoder('invalid'); + + $reflectionProperty = new \ReflectionProperty(ArgumentDecoder::class, 'bytes'); + $reflectionProperty->setAccessible(true); + + expect($reflectionProperty->getValue($decoder))->toBe(''); +}); diff --git a/tests/Unit/Utils/AddressTest.php b/tests/Unit/Utils/AddressTest.php index 6f0ba7bb..b70e8daa 100644 --- a/tests/Unit/Utils/AddressTest.php +++ b/tests/Unit/Utils/AddressTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use ArkEcosystem\Crypto\ByteBuffer\ByteBuffer; use ArkEcosystem\Crypto\Utils\Address as TestClass; test('it should validate the address', function () { @@ -17,3 +18,19 @@ expect($actual)->toBeFalse(); }); + +it('should convert to hex string', function () { + $fixture = $this->getFixture('identity'); + + $actual = TestClass::toBufferHexString($fixture['data']['address']); + + expect($actual)->toBe(substr($fixture['data']['address'], 2)); +}); + +it('should extract address from a byte buffer', function () { + $fixture = $this->getFixture('identity'); + + $actual = TestClass::fromByteBuffer(ByteBuffer::fromHex(substr($fixture['data']['address'], 2))); + + expect($actual)->toBe($fixture['data']['address']); +}); diff --git a/tests/Unit/Utils/MessageTest.php b/tests/Unit/Utils/MessageTest.php index 2ce906b2..35093fee 100644 --- a/tests/Unit/Utils/MessageTest.php +++ b/tests/Unit/Utils/MessageTest.php @@ -34,6 +34,14 @@ expect($message->message)->toBe($fixture['message']); }); +test('it should throw if no public key is provided', function () { + $fixture = $this->getFixture('message-sign'); + + unset($fixture['publicKey']); + + Message::new($fixture); +})->throws(InvalidArgumentException::class, 'The given message did not contain a valid public key.'); + test('it should create a message from a string', function () { $fixture = $this->getFixture('message-sign'); diff --git a/tests/Unit/Utils/TransactionUtilsTest.php b/tests/Unit/Utils/TransactionUtilsTest.php new file mode 100644 index 00000000..c45005a7 --- /dev/null +++ b/tests/Unit/Utils/TransactionUtilsTest.php @@ -0,0 +1,80 @@ +getTransactionFixture('evm_call', 'username-resignation'); + + $transaction = TransactionUtils::toBuffer($fixture['data']); + + expect($transaction->getHex())->toBe($fixture['serialized']); +}); + +it('should convert a transaction to a buffer when data starts with 0x', function () { + $fixture = $this->getTransactionFixture('evm_call', 'username-resignation'); + + $fixture['data']['data'] = '0x'.$fixture['data']['data']; + + $transaction = TransactionUtils::toBuffer($fixture['data']); + + expect($transaction->getHex())->toBe($fixture['serialized']); +}); + +it('should get the hash for a transaction', function () { + $fixture = $this->getTransactionFixture('evm_call', 'username-resignation'); + + $transaction = TransactionUtils::toHash($fixture['data']); + + expect($transaction->getHex())->toBe($fixture['data']['hash']); +}); + +it('should handle string data starting with 0x', function () { + $fixture = $this->getTransactionFixture('evm_call', 'username-resignation'); + + $fixture['data']['gasPrice'] = '0x'.dechex($fixture['data']['gasPrice']); + + $transaction = TransactionUtils::toBuffer($fixture['data']); + + expect($transaction->getHex())->toBe($fixture['serialized']); +}); + +it('should handle BigDecimal value', function () { + $fixture = $this->getTransactionFixture('evm_call', 'username-resignation'); + + $fixture['data']['gasPrice'] = BigDecimal::of($fixture['data']['gasPrice']); + + $transaction = TransactionUtils::toBuffer($fixture['data']); + + expect($transaction->getHex())->toBe($fixture['serialized']); +}); + +it('should handle zero BigDecimal value', function () { + $fixture = $this->getTransactionFixture('evm_call', 'username-resignation'); + + $fixture['data']['gasPrice'] = BigDecimal::zero(); + + $transaction = TransactionUtils::toBuffer($fixture['data'], true); + + $decoded = RlpDecoder::decode('0x'.substr($transaction->getHex(), 2)); + + expect($decoded[3])->toBe('0x'); +}); + +it('should handle unknown value value', function () { + $fixture = $this->getTransactionFixture('evm_call', 'username-resignation'); + + $fixture['data']['gasPrice'] = 123.456; + + $transaction = TransactionUtils::toBuffer($fixture['data'], true); + + $decoded = RlpDecoder::decode('0x'.substr($transaction->getHex(), 2)); + + expect($decoded[3])->toBe('0x'); +}); + +// toBuffer +// toHash