diff --git a/src/Ethereum/EIP1559Transaction.php b/src/Ethereum/EIP1559Transaction.php index 90784c6..2d32f20 100644 --- a/src/Ethereum/EIP1559Transaction.php +++ b/src/Ethereum/EIP1559Transaction.php @@ -69,7 +69,7 @@ public function getRaw(string $privateKey, int $chainId = 0): string throw new RuntimeException('Incorrect private key'); } - $this->sign($privateKey); + $this->sign($privateKey, $chainId); return $this->serialize(); } @@ -79,9 +79,26 @@ private function serialize(): string return $this->txType . $this->RLPencode($this->getInput()); } - private function sign(string $privateKey): void + public function getUnsigned(int $chainId = 0): string { - $hash = $this->hash(); + if ($chainId < 0) { + throw new RuntimeException('ChainID must be positive'); + } + + $this->chainId = dechex($chainId); + + $input = $this->getInput(); + + unset($input['v']); + unset($input['r']); + unset($input['s']); + + return $this->txType . $this->RLPencode($input); + } + + private function sign(string $privateKey, int $chainId = 0): void + { + $hash = $this->hash($chainId); $secp256k1 = new Secp256k1(); /** @@ -93,8 +110,13 @@ private function sign(string $privateKey): void $this->v = dechex((int)$signed->getRecoveryParam()); } - private function hash(): string + public function hash(int $chainId = 0): string { + if ($chainId < 0) { + throw new RuntimeException('ChainID must be positive'); + } + + $this->chainId = dechex($chainId); $input = $this->getInput(); unset($input['v']); diff --git a/src/Ethereum/Transaction.php b/src/Ethereum/Transaction.php index b8e45a0..4cc3f23 100644 --- a/src/Ethereum/Transaction.php +++ b/src/Ethereum/Transaction.php @@ -19,7 +19,8 @@ class Transaction { protected $s = ''; protected $v = ''; - public function __construct(string $nonce = '', string $gasPrice = '', string $gasLimit = '', string $to = '', string $value = '', string $data = '') { + public function __construct(string $nonce = '', string $gasPrice = '', string $gasLimit = '', string $to = '', string $value = '', string $data = '') + { $this->nonce = $nonce; $this->gasPrice = $gasPrice; $this->gasLimit = $gasLimit; @@ -28,7 +29,8 @@ public function __construct(string $nonce = '', string $gasPrice = '', string $g $this->data = $data; } - public function getInput(): array { + public function getInput(): array + { return [ 'nonce' => $this->nonce, 'gasPrice' => $this->gasPrice, @@ -42,7 +44,8 @@ public function getInput(): array { ]; } - public function getRaw(string $privateKey, int $chainId = 0): string { + public function getRaw(string $privateKey, int $chainId = 0): string + { if ($chainId < 0) { throw new RuntimeException('ChainID must be positive'); } @@ -60,11 +63,30 @@ public function getRaw(string $privateKey, int $chainId = 0): string { return $this->serialize(); } - private function serialize(): string { + private function serialize(): string + { return $this->RLPencode($this->getInput()); } - private function sign(string $privateKey, int $chainId): void { + public function getUnsigned(int $chainId = 0): string + { + $input = $this->getInput(); + + if ($chainId > 0) { + $input['v'] = dechex($chainId); + $input['r'] = ''; + $input['s'] = ''; + } else { + unset($input['v']); + unset($input['r']); + unset($input['s']); + } + + return $this->RLPencode($input); + } + + private function sign(string $privateKey, int $chainId = 0): void + { $hash = $this->hash($chainId); $secp256k1 = new Secp256k1(); @@ -78,7 +100,8 @@ private function sign(string $privateKey, int $chainId): void { $this->v = dechex ((int) $signed->getRecoveryParam () + 27 + ($chainId ? $chainId * 2 + 8 : 0)); } - private function hash(int $chainId): string { + public function hash(int $chainId = 0): string + { $input = $this->getInput(); if ($chainId > 0) { @@ -96,7 +119,8 @@ private function hash(int $chainId): string { return Keccak::hash(hex2bin($encoded), 256); } - private function RLPencode(array $input): string { + private function RLPencode(array $input): string + { $rlp = new RLP; $data = []; @@ -108,7 +132,8 @@ private function RLPencode(array $input): string { return $rlp->encode($data); } - private function hexup(string $value): string { + private function hexup(string $value): string + { return strlen ($value) % 2 === 0 ? $value : "0{$value}"; } diff --git a/test/EIP1559TransactionTest.php b/test/EIP1559TransactionTest.php new file mode 100644 index 0000000..6767b12 --- /dev/null +++ b/test/EIP1559TransactionTest.php @@ -0,0 +1,100 @@ +assertSame($expect, $transaction->getInput()); + } + + public static function input (): array { + return [ + [ + ['chainId' => null, 'nonce' => '', 'maxPriorityFeePerGas' => '', 'maxFeePerGas' => '', 'gasLimit' => '', 'to' => '', 'value' => '', 'data' => '', 'accessList' => [], 'v' => '', 'r' => '', 's' => ''], + '', '', '', '', '', '' + ], + [ + ['chainId' => null, 'nonce' => '04', 'maxPriorityFeePerGas' => '03f5476a00', 'maxFeePerGas' => '027f4b', 'gasLimit' => '1a8c8adfbe1c59e8b58cc0d515f07b7225f51c72', 'to' => '2a45907d1bef7c00', 'value' => '', 'data' => '', 'accessList' => [], 'v' => '', 'r' => '', 's' => ''], + '04', '03f5476a00', '027f4b', '1a8c8adfbe1c59e8b58cc0d515f07b7225f51c72', '2a45907d1bef7c00', '' + ], + ]; + } + + /** + * @dataProvider getTransactionData + */ + public function testGetRaw($txid, $unsigned, $signed, $privateKey, $chainId, $nonce, $maxPriorityFeePerGas, $maxFeePerGas, $gasLimit, $to, $value, $data) + { + $transaction = new EIP1559Transaction($nonce, $maxPriorityFeePerGas, $maxFeePerGas, $gasLimit, $to, $value, $data); + $this->assertSame($signed, $transaction->getRaw($privateKey, $chainId)); + } + + /** + * @dataProvider getTransactionData + */ + public function testGetUnsigned($txid, $unsigned, $signed, $privateKey, $chainId, $nonce, $maxPriorityFeePerGas, $maxFeePerGas, $gasLimit, $to, $value, $data) + { + $transaction = new EIP1559Transaction($nonce, $maxPriorityFeePerGas, $maxFeePerGas, $gasLimit, $to, $value, $data); + $this->assertSame($unsigned, $transaction->getUnsigned($chainId)); + } + + /** + * @dataProvider getTransactionData + */ + public function testHash($txid, $unsigned, $signed, $privateKey, $chainId, $nonce, $maxPriorityFeePerGas, $maxFeePerGas, $gasLimit, $to, $value, $data) + { + $transaction = new EIP1559Transaction($nonce, $maxPriorityFeePerGas, $maxFeePerGas, $gasLimit, $to, $value, $data); + $this->assertSame($txid, $transaction->hash($chainId)); + } + + public static function getTransactionData(): array + { + return [ + [ + '4fa4b9bd743ee70bb1dc109cadaa5985d5e9461e793405261daeff7bb6419865', //txid + '02f0010284b2d05e008506fc23ac00825208941a8c8adfbe1c59e8b58cc0d515f07b7225f51c728829a2241af62c000080c0', //unsigned + '02f873010284b2d05e008506fc23ac00825208941a8c8adfbe1c59e8b58cc0d515f07b7225f51c728829a2241af62c000080c080a0dd32dc794af9a9085d6772c40656fc156a577570c6fd32f2a2d4126673373919a066cf1859672a9e6fdbb00ebd230bcfec66cac1c99f5c83598ecae6025d0e91f4', + 'b2f2698dd7343fa5afc96626dee139cb92e58e5d04e855f4c712727bf198e898', 1, '02', 'b2d05e00', '6fc23ac00', '5208', '1a8c8adfbe1c59e8b58cc0d515f07b7225f51c72', '29a2241af62c0000', '' + ], + [ + 'f470e5422f3dc01bca43f818f03de3eea876328f8a7d1c9c4fe05410f6fc3bb6', //txid + '02f0380284b2d05e008506fc23ac00825208941a8c8adfbe1c59e8b58cc0d515f07b7225f51c728829a2241af62c000080c0', //unsigned + '02f873380284b2d05e008506fc23ac00825208941a8c8adfbe1c59e8b58cc0d515f07b7225f51c728829a2241af62c000080c080a0b8ec74b95d0d8ebe4b4c7e9d5a8af16e06a7d636d20b2c632633a2abe3a0f5cca005971b1f3ec9b498d8d7b17b312477d0b1fd2232ca4b82e9c34fc43d33e5f196', + 'b2f2698dd7343fa5afc96626dee139cb92e58e5d04e855f4c712727bf198e898', 56, '02', 'b2d05e00', '6fc23ac00', '5208', '1a8c8adfbe1c59e8b58cc0d515f07b7225f51c72', '29a2241af62c0000', '' + ], + [ + '950b552d191d26ea7b2dbaf53e59b2f82177b1924f8ac06715a0fe0f058f1c20', //txid + '02f4380284b2d05e008506fc23ac00825208941a8c8adfbe1c59e8b58cc0d515f07b7225f51c728829a2241af62c00008401232213c0', //unsigned + '02f877380284b2d05e008506fc23ac00825208941a8c8adfbe1c59e8b58cc0d515f07b7225f51c728829a2241af62c00008401232213c001a05bc0caa25dd8e23adf3f79f8dbe1237e0f22980a3b15f00bbe9f5a19dd23a70ca002e9a353c9b155b0e7e796b5f966bc6dd53deafea9990398a069d93278e50644', + 'b2f2698dd7343fa5afc96626dee139cb92e58e5d04e855f4c712727bf198e898', 56, '02', 'b2d05e00', '6fc23ac00', '5208', '1a8c8adfbe1c59e8b58cc0d515f07b7225f51c72', '29a2241af62c0000', '1232213' + ] + ]; + } + + public function testGetRawBadChainId() + { + $this->expectException(RuntimeException::class); + $transaction = new EIP1559Transaction('04', '03f5476a00', '027f4b', '1a8c8adfbe1c59e8b58cc0d515f07b7225f51c72', '2a45907d1bef7c00', ''); + $transaction->getRaw ('b2f2698dd7343fa5afc96626dee139cb92e58e5d04e855f4c712727bf198e898', -1); + } + + public function testBadPrivateKey() + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Incorrect private key'); + $transaction = new EIP1559Transaction(); + $transaction->getRaw(''); + } + +} diff --git a/test/TransactionTest.php b/test/TransactionTest.php index 98c5b9c..d68ff36 100644 --- a/test/TransactionTest.php +++ b/test/TransactionTest.php @@ -2,22 +2,24 @@ namespace kornrunner; -use kornrunner\Ethereum\EIP1559Transaction; use kornrunner\Ethereum\Transaction; use PHPUnit\Framework\TestCase; use RuntimeException; -class TransactionTest extends TestCase { +class TransactionTest extends TestCase +{ /** * @dataProvider input */ - public function testGetInput ($expect, $nonce, $gasPrice, $gasLimit, $to, $value, $data) { - $transaction = new Transaction ($nonce, $gasPrice, $gasLimit, $to, $value, $data); + public function testGetInput($expect, $nonce, $gasPrice, $gasLimit, $to, $value, $data) + { + $transaction = new Transaction($nonce, $gasPrice, $gasLimit, $to, $value, $data); $this->assertSame($expect, $transaction->getInput()); } - public static function input (): array { + public static function input(): array + { return [ [ ['nonce' => '', 'gasPrice' => '', 'gasLimit' => '', 'to' => '', 'value' => '', 'data' => '', 'v' => '', 'r' => '', 's' => ''], @@ -31,65 +33,68 @@ public static function input (): array { } /** - * @dataProvider getRaw + * @dataProvider getTransactionData */ - public function testGetRaw ($expect, $privateKey, $chainId, $nonce, $gasPrice, $gasLimit, $to, $value, $data) { - $transaction = new Transaction ($nonce, $gasPrice, $gasLimit, $to, $value, $data); - $this->assertSame($expect, $transaction->getRaw($privateKey, $chainId)); + public function testGetRaw($txid, $unsigned, $signed, $privateKey, $chainId, $nonce, $gasPrice, $gasLimit, $to, $value, $data) + { + $transaction = new Transaction($nonce, $gasPrice, $gasLimit, $to, $value, $data); + $this->assertSame($signed, $transaction->getRaw($privateKey, $chainId)); } - public function testGetRawBadChainId () { - $this->expectException(RuntimeException::class); - $transaction = new Transaction ('04', '03f5476a00', '027f4b', '1a8c8adfbe1c59e8b58cc0d515f07b7225f51c72', '2a45907d1bef7c00', ''); - $transaction->getRaw ('b2f2698dd7343fa5afc96626dee139cb92e58e5d04e855f4c712727bf198e898', -1); + /** + * @dataProvider getTransactionData + */ + public function getUnsigned($txid, $unsigned, $signed, $privateKey, $chainId, $nonce, $gasPrice, $gasLimit, $to, $value, $data) + { + $transaction = new Transaction($nonce, $gasPrice, $gasLimit, $to, $value, $data); + $this->assertSame($unsigned, $transaction->getUnsigned($chainId)); + } + + /** + * @dataProvider getTransactionData + */ + public function testHash($txid, $unsigned, $signed, $privateKey, $chainId, $nonce, $gasPrice, $gasLimit, $to, $value, $data) + { + $transaction = new Transaction($nonce, $gasPrice, $gasLimit, $to, $value, $data); + $this->assertSame($txid, $transaction->hash($chainId)); } - public static function getRaw (): array { + public static function getTransactionData(): array + { return [ [ + 'a0c5887430a600aac139fc269a354b0e5d4c700c3e177ea2c227502367224e9c', //txid + 'ea048503f5476a0083027f4b941a8c8adfbe1c59e8b58cc0d515f07b7225f51c72882a45907d1bef7c0080', //unsigned 'f86d048503f5476a0083027f4b941a8c8adfbe1c59e8b58cc0d515f07b7225f51c72882a45907d1bef7c00801ba0e68be766b40702e6d9c419f53d5e053c937eda36f0e973074d174027439e2b5da0790df3e4d0294f92d69104454cd96005e21095efd5f2970c2829736ca39195d8', 'b2f2698dd7343fa5afc96626dee139cb92e58e5d04e855f4c712727bf198e898', 0, '04', '03f5476a00', '027f4b', '1a8c8adfbe1c59e8b58cc0d515f07b7225f51c72', '2a45907d1bef7c00', '' ], [ + 'db734dc3a2da9bee8baf350f7d6e6f2dc593b06534a7721385cdb588431b6136', //txid + 'ed048503f5476a0083027f4b941a8c8adfbe1c59e8b58cc0d515f07b7225f51c72882a45907d1bef7c0080018080', //unsigned 'f86d048503f5476a0083027f4b941a8c8adfbe1c59e8b58cc0d515f07b7225f51c72882a45907d1bef7c008025a0db4efcc22a7d9b2cab180ce37f81959412594798cb9af7c419abb6323763cdd5a0631a0c47d27e5b6e3906a419de2d732e290b73ead4172d8598ce4799c13bda69', 'b2f2698dd7343fa5afc96626dee139cb92e58e5d04e855f4c712727bf198e898', 1, '04', '03f5476a00', '027f4b', '1a8c8adfbe1c59e8b58cc0d515f07b7225f51c72', '2a45907d1bef7c00', '' ], [ + '8e5a5404792108371948015b678cbe6a10d0cf40c9969eeb04829a6714170ff6', //txid + 'ed048503f5476a0083027f4b941a8c8adfbe1c59e8b58cc0d515f07b7225f51c72882a45907d1bef7c0080388080', //unsigned 'f86e048503f5476a0083027f4b941a8c8adfbe1c59e8b58cc0d515f07b7225f51c72882a45907d1bef7c00808193a05c206269d6d902591b5b37ec821be99f78f1375191b8a8fb5a9e7bd87cb33999a027ca8262e92e3a5d7d2682d0394587bd7362250f4313767aa1a656c075d09911', 'b2f2698dd7343fa5afc96626dee139cb92e58e5d04e855f4c712727bf198e898', 56, '04', '03f5476a00', '027f4b', '1a8c8adfbe1c59e8b58cc0d515f07b7225f51c72', '2a45907d1bef7c00', '' ], ]; } - public function testBadPrivateKey () { + public function testGetRawBadChainId() + { $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Incorrect private key'); - $transaction = new Transaction (); - $transaction->getRaw(''); + $transaction = new Transaction('04', '03f5476a00', '027f4b', '1a8c8adfbe1c59e8b58cc0d515f07b7225f51c72', '2a45907d1bef7c00', ''); + $transaction->getRaw('b2f2698dd7343fa5afc96626dee139cb92e58e5d04e855f4c712727bf198e898', -1); } - /** - * @dataProvider getEIP1559Raw - */ - public function testGetEIP1559Raw($expect, $privateKey, $chainId, $nonce, $maxPriorityFeePerGas, $maxFeePerGas, $gasLimit, $to, $value, $data) { - $transaction = new EIP1559Transaction($nonce, $maxPriorityFeePerGas, $maxFeePerGas, $gasLimit, $to, $value, $data); - $this->assertSame($expect, $transaction->getRaw($privateKey, $chainId)); - } - - public static function getEIP1559Raw(): array { - return [ - [ - '02f873010284b2d05e008506fc23ac00825208941a8c8adfbe1c59e8b58cc0d515f07b7225f51c728829a2241af62c000080c080a0dd32dc794af9a9085d6772c40656fc156a577570c6fd32f2a2d4126673373919a066cf1859672a9e6fdbb00ebd230bcfec66cac1c99f5c83598ecae6025d0e91f4', - 'b2f2698dd7343fa5afc96626dee139cb92e58e5d04e855f4c712727bf198e898', 1, '02', 'b2d05e00', '6fc23ac00', '5208', '1a8c8adfbe1c59e8b58cc0d515f07b7225f51c72', '29a2241af62c0000', '' - ], - [ - '02f873380284b2d05e008506fc23ac00825208941a8c8adfbe1c59e8b58cc0d515f07b7225f51c728829a2241af62c000080c080a0b8ec74b95d0d8ebe4b4c7e9d5a8af16e06a7d636d20b2c632633a2abe3a0f5cca005971b1f3ec9b498d8d7b17b312477d0b1fd2232ca4b82e9c34fc43d33e5f196', - 'b2f2698dd7343fa5afc96626dee139cb92e58e5d04e855f4c712727bf198e898', 56, '02', 'b2d05e00', '6fc23ac00', '5208', '1a8c8adfbe1c59e8b58cc0d515f07b7225f51c72', '29a2241af62c0000', '' - ], - [ - '02f877380284b2d05e008506fc23ac00825208941a8c8adfbe1c59e8b58cc0d515f07b7225f51c728829a2241af62c00008401232213c001a05bc0caa25dd8e23adf3f79f8dbe1237e0f22980a3b15f00bbe9f5a19dd23a70ca002e9a353c9b155b0e7e796b5f966bc6dd53deafea9990398a069d93278e50644', - 'b2f2698dd7343fa5afc96626dee139cb92e58e5d04e855f4c712727bf198e898', 56, '02', 'b2d05e00', '6fc23ac00', '5208', '1a8c8adfbe1c59e8b58cc0d515f07b7225f51c72', '29a2241af62c0000', '1232213' - ] - ]; + public function testBadPrivateKey() + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Incorrect private key'); + $transaction = new Transaction(); + $transaction->getRaw(''); } }