From 10bfcf0a0ba9f1130158e08ab99fde5e9e450124 Mon Sep 17 00:00:00 2001 From: sc0vu Date: Sun, 15 Aug 2021 04:43:35 +0000 Subject: [PATCH 1/8] Clean up - Remove space from composer.json - Remove syntaxCheck from phpunit.xml - Update .gitignore --- .gitignore | 1 + composer.json | 4 ++-- phpunit.xml | 3 +-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 084ea1b..8b7f132 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ composer.phar /vendor/ composer.lock .DS_Store +.phpunit.result.cache # Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file # composer.lock diff --git a/composer.json b/composer.json index 33f95bc..748e386 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,7 @@ } ], "require-dev": { - "phpunit/phpunit": "~7 | ~8.0" + "phpunit/phpunit": "~7|~8.0" }, "autoload": { "psr-4": { @@ -23,7 +23,7 @@ } }, "require": { - "PHP": "^7.1 | ^8.0", + "PHP": "^7.1|^8.0", "web3p/rlp": "0.3.3", "web3p/ethereum-util": "~0.1.3", "kornrunner/keccak": "~1", diff --git a/phpunit.xml b/phpunit.xml index e2c2311..379e15c 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,8 +7,7 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" - stopOnFailure="false" - syntaxCheck="false"> + stopOnFailure="false"> ./test/unit From 0de3effc6b9f954db425b9fb0e939d52c60c3bb3 Mon Sep 17 00:00:00 2001 From: sc0vu Date: Sat, 28 Aug 2021 17:43:19 +0000 Subject: [PATCH 2/8] Add EIP2718 transaction type --- src/Transaction.php | 37 +++++++++++++++++++++++++++++++++++ test/unit/TransactionTest.php | 24 +++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/src/Transaction.php b/src/Transaction.php index bca994d..a3ef26b 100644 --- a/src/Transaction.php +++ b/src/Transaction.php @@ -70,6 +70,11 @@ class Transaction implements ArrayAccess 'chainId' => [ 'key' => -2 ], + 'transactionType' => [ + 'key' => -3, + 'length' => 2, + 'allowZero' => true + ], 'nonce' => [ 'key' => 0, 'length' => 32, @@ -181,6 +186,15 @@ public function __construct($txData=[]) $tx = []; if ($this->util->isHex($txData)) { + // check first byte + $txData = $this->util->stripZero($txData); + $firstByteStr = substr($txData, 0, 2); + $firstByte = hexdec($firstByteStr); + if ($firstByte >= 0 && $firstByte <= 127) { + // first byte is transaction type + $tx[$this->attributeMap['transactionType']['key']] = $firstByteStr; + $txData = substr($txData, 2); + } $txData = $this->rlp->decode($txData); foreach ($txData as $txKey => $data) { @@ -340,6 +354,17 @@ public function getTxData() return $this->txData; } + /** + * Return whether transaction type is valid (0x0 <= $transactionType <= 0x7f). + * + * @param integer $transactionType + * @return boolean is transaction valid + */ + protected function isTransactionTypeValid(int $transactionType) + { + return $transactionType >= 0 && $transactionType <= 127; + } + /** * RLP serialize the ethereum transaction. * @@ -363,6 +388,18 @@ public function serialize() $txData[$key] = $data; } } + $transactionType = $this->offsetGet('transactionType'); + if ( + $transactionType + ) { + $transactionType = $this->util->stripZero($transactionType); + if ($this->isTransactionTypeValid(hexdec($transactionType))) { + if (strlen($transactionType) % 2 != 0) { + $transactionType = '0' . $transactionType; + } + return $transactionType . $this->rlp->encode($txData); + } + } return $this->rlp->encode($txData); } diff --git a/test/unit/TransactionTest.php b/test/unit/TransactionTest.php index 197a7e8..d20c1cb 100644 --- a/test/unit/TransactionTest.php +++ b/test/unit/TransactionTest.php @@ -347,4 +347,28 @@ public function testIssue26() $this->assertEquals($transaction->txData, []); } } + + /** + * testEIP2718 + * see: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2718.md + * + * @return void + */ + public function testEIP2718() + { + $transaction = new Transaction([ + 'transactionType' => '0x0', + 'nonce' => '0x09', + 'to' => '0x3535353535353535353535353535353535353535', + 'gas' => '0x5208', + 'gasPrice' => '0x4a817c800', + 'value' => '0x0', + 'chainId' => 1, + 'data' => '' + ]); + $this->assertEquals('00f864098504a817c800825208943535353535353535353535353535353535353535808025a0855ec9b7d4fcabf535fe4ac4a7c31a9e521214d05bc6efbc058d4757c35e92bba0043d7df30c8a79e5522b3de8fc169df5fa7145714100ee8ec413292d97ce4d3a', $transaction->sign('0x4646464646464646464646464646464646464646464646464646464646464646')); + + $transaction = new Transaction('0x00f86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83'); + $this->assertEquals('00f86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83', $transaction->serialize()); + } } From 03eb24bc2e029694c2297f12d1aefafd612833f7 Mon Sep 17 00:00:00 2001 From: sc0vu Date: Sun, 29 Aug 2021 04:24:00 +0000 Subject: [PATCH 3/8] Add EIP2930 type 1 transaction --- src/EIP2930Transaction.php | 514 ++++++++++++++++++++++++++++++++++ src/Transaction.php | 37 --- test/unit/TransactionTest.php | 23 +- 3 files changed, 527 insertions(+), 47 deletions(-) create mode 100644 src/EIP2930Transaction.php diff --git a/src/EIP2930Transaction.php b/src/EIP2930Transaction.php new file mode 100644 index 0000000..82787ac --- /dev/null +++ b/src/EIP2930Transaction.php @@ -0,0 +1,514 @@ + + * + * @author Peter Lai + * @license MIT + */ + +namespace Web3p\EthereumTx; + +use InvalidArgumentException; +use RuntimeException; +use Web3p\RLP\RLP; +use Elliptic\EC; +use Elliptic\EC\KeyPair; +use ArrayAccess; +use Web3p\EthereumUtil\Util; + +/** + * It's a instance for generating/serializing ethereum transaction. + * + * ```php + * use Web3p\EthereumTx\EIP2930Transaction; + * + * // generate transaction instance with transaction parameters + * $transaction = new Transaction([ + * 'nonce' => '0x01', + * 'from' => '0xb60e8dd61c5d32be8058bb8eb970870f07233155', + * 'to' => '0xd46e8dd67c5d32be8058bb8eb970870f07244567', + * 'gas' => '0x76c0', + * 'gasPrice' => '0x9184e72a000', + * 'value' => '0x9184e72a', + * 'chainId' => 1, // required + * 'accessList' => [], + * 'data' => '0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675' + * ]); + * + * // generate transaction instance with hex encoded transaction + * $transaction = new Transaction('0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83'); + * ``` + * + * ```php + * After generate transaction instance, you can sign transaction with your private key. + * + * $signedTransaction = $transaction->sign('your private key'); + * ``` + * + * Then you can send serialized transaction to ethereum through http rpc with web3.php. + * ```php + * $hashedTx = $transaction->serialize(); + * ``` + * + * @author Peter Lai + * @link https://www.web3p.xyz + * @filesource https://github.com/web3p/ethereum-tx + */ +class EIP2930Transaction implements ArrayAccess +{ + /** + * Attribute map for keeping order of transaction key/value + * + * @var array + */ + protected $attributeMap = [ + 'from' => [ + 'key' => -1 + ], + 'chainId' => [ + 'key' => 0 + ], + 'nonce' => [ + 'key' => 1, + 'length' => 32, + 'allowLess' => true, + 'allowZero' => false + ], + 'gasPrice' => [ + 'key' => 2, + 'length' => 32, + 'allowLess' => true, + 'allowZero' => false + ], + 'gasLimit' => [ + 'key' => 3, + 'length' => 32, + 'allowLess' => true, + 'allowZero' => false + ], + 'gas' => [ + 'key' => 3, + 'length' => 32, + 'allowLess' => true, + 'allowZero' => false + ], + 'to' => [ + 'key' => 4, + 'length' => 20, + 'allowZero' => true, + ], + 'value' => [ + 'key' => 5, + 'length' => 32, + 'allowLess' => true, + 'allowZero' => false + ], + 'data' => [ + 'key' => 6, + 'allowLess' => true, + 'allowZero' => true + ], + 'accessList' => [ + 'key' => 7, + 'allowLess' => true, + 'allowZero' => true, + 'allowArray' => true + ], + 'v' => [ + 'key' => 8, + 'allowZero' => true + ], + 'r' => [ + 'key' => 9, + 'length' => 32, + 'allowZero' => true + ], + 's' => [ + 'key' => 10, + 'length' => 32, + 'allowZero' => true + ] + ]; + + /** + * Raw transaction data + * + * @var array + */ + protected $txData = []; + + /** + * RLP encoding instance + * + * @var \Web3p\RLP\RLP + */ + protected $rlp; + + /** + * secp256k1 elliptic curve instance + * + * @var \Elliptic\EC + */ + protected $secp256k1; + + /** + * Private key instance + * + * @var \Elliptic\EC\KeyPair + */ + protected $privateKey; + + /** + * Ethereum util instance + * + * @var \Web3p\EthereumUtil\Util + */ + protected $util; + + /** + * Transaction type + * + * @var string + */ + protected $transactionType = '01'; + + /** + * construct + * + * @param array|string $txData + * @return void + */ + public function __construct($txData=[]) + { + $this->rlp = new RLP; + $this->secp256k1 = new EC('secp256k1'); + $this->util = new Util; + + if (is_array($txData)) { + foreach ($txData as $key => $data) { + $this->offsetSet($key, $data); + } + } elseif (is_string($txData)) { + $tx = []; + + if ($this->util->isHex($txData)) { + // check first byte + $txData = $this->util->stripZero($txData); + $firstByteStr = substr($txData, 0, 2); + $firstByte = hexdec($firstByteStr); + if ($this->isTransactionTypeValid($firstByte)) { + $txData = substr($txData, 2); + } + $txData = $this->rlp->decode($txData); + + foreach ($txData as $txKey => $data) { + if (is_int($txKey)) { + if (is_string($data) && strlen($data) > 0) { + $tx[$txKey] = '0x' . $data; + } else { + $tx[$txKey] = $data; + } + } + } + } + $this->txData = $tx; + } + } + + /** + * Return the value in the transaction with given key or return the protected property value if get(property_name} function is existed. + * + * @param string $name key or protected property name + * @return mixed + */ + public function __get(string $name) + { + $method = 'get' . ucfirst($name); + + if (method_exists($this, $method)) { + return call_user_func_array([$this, $method], []); + } + return $this->offsetGet($name); + } + + /** + * Set the value in the transaction with given key or return the protected value if set(property_name} function is existed. + * + * @param string $name key, eg: to + * @param mixed value + * @return void + */ + public function __set(string $name, $value) + { + $method = 'set' . ucfirst($name); + + if (method_exists($this, $method)) { + return call_user_func_array([$this, $method], [$value]); + } + return $this->offsetSet($name, $value); + } + + /** + * Return hash of the ethereum transaction without signature. + * + * @return string hex encoded of the transaction + */ + public function __toString() + { + return $this->hash(false); + } + + /** + * Set the value in the transaction with given key. + * + * @param string $offset key, eg: to + * @param string value + * @return void + */ + public function offsetSet($offset, $value) + { + $txKey = isset($this->attributeMap[$offset]) ? $this->attributeMap[$offset] : null; + + if (is_array($txKey)) { + if (is_array($value)) { + if (!isset($txKey['allowArray']) || (isset($txKey['allowArray']) && $txKey['allowArray'] === false)) { + throw new InvalidArgumentException($offset . ' should\'t be array.'); + } + if (!isset($txKey['allowLess']) || (isset($txKey['allowLess']) && $txKey['allowLess'] === false)) { + // check length + if (isset($txKey['length'])) { + if (count($value) > $txKey['length'] * 2) { + throw new InvalidArgumentException($offset . ' exceeds the length limit.'); + } + } + } + if (!isset($txKey['allowZero']) || (isset($txKey['allowZero']) && $txKey['allowZero'] === false)) { + // check zero + foreach ($value as $key => $v) { + $checkedV = $v ? (string) $v : ''; + if (preg_match('/^0*$/', $checkedV) === 1) { + // set value to empty string + $checkedV = ''; + $value[$key] = $checkedV; + } + } + } + } else { + $checkedValue = ($value) ? (string) $value : ''; + $isHex = $this->util->isHex($checkedValue); + $checkedValue = $this->util->stripZero($checkedValue); + + if (!isset($txKey['allowLess']) || (isset($txKey['allowLess']) && $txKey['allowLess'] === false)) { + // check length + if (isset($txKey['length'])) { + if ($isHex) { + if (strlen($checkedValue) > $txKey['length'] * 2) { + throw new InvalidArgumentException($offset . ' exceeds the length limit.'); + } + } else { + if (strlen($checkedValue) > $txKey['length']) { + throw new InvalidArgumentException($offset . ' exceeds the length limit.'); + } + } + } + } + if (!isset($txKey['allowZero']) || (isset($txKey['allowZero']) && $txKey['allowZero'] === false)) { + // check zero + if (preg_match('/^0*$/', $checkedValue) === 1) { + // set value to empty string + $value = ''; + } + } + } + $this->txData[$txKey['key']] = $value; + } + } + + /** + * Return whether the value is in the transaction with given key. + * + * @param string $offset key, eg: to + * @return bool + */ + public function offsetExists($offset) + { + $txKey = isset($this->attributeMap[$offset]) ? $this->attributeMap[$offset] : null; + + if (is_array($txKey)) { + return isset($this->txData[$txKey['key']]); + } + return false; + } + + /** + * Unset the value in the transaction with given key. + * + * @param string $offset key, eg: to + * @return void + */ + public function offsetUnset($offset) + { + $txKey = isset($this->attributeMap[$offset]) ? $this->attributeMap[$offset] : null; + + if (is_array($txKey) && isset($this->txData[$txKey['key']])) { + unset($this->txData[$txKey['key']]); + } + } + + /** + * Return the value in the transaction with given key. + * + * @param string $offset key, eg: to + * @return mixed value of the transaction + */ + public function offsetGet($offset) + { + $txKey = isset($this->attributeMap[$offset]) ? $this->attributeMap[$offset] : null; + + if (is_array($txKey) && isset($this->txData[$txKey['key']])) { + return $this->txData[$txKey['key']]; + } + return null; + } + + /** + * Return raw ethereum transaction data. + * + * @return array raw ethereum transaction data + */ + public function getTxData() + { + return $this->txData; + } + + /** + * Return whether transaction type is valid (0x0 <= $transactionType <= 0x7f). + * + * @param integer $transactionType + * @return boolean is transaction valid + */ + protected function isTransactionTypeValid(int $transactionType) + { + return $transactionType >= 0 && $transactionType <= 127; + } + + /** + * RLP serialize the ethereum transaction. + * + * @return \Web3p\RLP\RLP\Buffer serialized ethereum transaction + */ + public function serialize() + { + $chainId = $this->offsetGet('chainId'); + + // sort tx data + if (ksort($this->txData) !== true) { + throw new RuntimeException('Cannot sort tx data by keys.'); + } + if ($chainId && $chainId > 0) { + $txData = array_fill(0, 11, ''); + } else { + $txData = array_fill(0, 8, ''); + } + foreach ($this->txData as $key => $data) { + if ($key >= 0) { + $txData[$key] = $data; + } + } + $transactionType = $this->transactionType; + return $transactionType . $this->rlp->encode($txData); + } + + /** + * Sign the transaction with given hex encoded private key. + * + * @param string $privateKey hex encoded private key + * @return string hex encoded signed ethereum transaction + */ + public function sign(string $privateKey) + { + if ($this->util->isHex($privateKey)) { + $privateKey = $this->util->stripZero($privateKey); + $ecPrivateKey = $this->secp256k1->keyFromPrivate($privateKey, 'hex'); + } else { + throw new InvalidArgumentException('Private key should be hex encoded string'); + } + $txHash = $this->hash(); + $signature = $ecPrivateKey->sign($txHash, [ + 'canonical' => true + ]); + $r = $signature->r; + $s = $signature->s; + $v = $signature->recoveryParam; + + $this->offsetSet('r', '0x' . $r->toString(16)); + $this->offsetSet('s', '0x' . $s->toString(16)); + $this->offsetSet('v', $v); + $this->privateKey = $ecPrivateKey; + + return $this->serialize(); + } + + /** + * Return hash of the ethereum transaction with/without signature. + * + * @return string hex encoded hash of the ethereum transaction + */ + public function hash() + { + $chainId = $this->offsetGet('chainId'); + + // sort tx data + if (ksort($this->txData) !== true) { + throw new RuntimeException('Cannot sort tx data by keys.'); + } + $rawTxData = array_fill(0, 8, ''); + foreach ($this->txData as $key => $data) { + if ($key >= 0 && $key < 8) { + $rawTxData[$key] = $data; + } + } + $serializedTx = $this->rlp->encode($rawTxData); + $transactionType = $this->transactionType; + return $this->util->sha3(hex2bin($transactionType . $serializedTx)); + } + + /** + * Recover from address with given signature (r, s, v) if didn't set from. + * + * @return string hex encoded ethereum address + */ + public function getFromAddress() + { + $from = $this->offsetGet('from'); + + if ($from) { + return $from; + } + if (!isset($this->privateKey) || !($this->privateKey instanceof KeyPair)) { + // recover from hash + $r = $this->offsetGet('r'); + $s = $this->offsetGet('s'); + $v = $this->offsetGet('v'); + + if (!$r || !$s) { + throw new RuntimeException('Invalid signature r and s.'); + } + $txHash = $this->hash(); + $publicKey = $this->secp256k1->recoverPubKey($txHash, [ + 'r' => $r, + 's' => $s + ], $v); + $publicKey = $publicKey->encode('hex'); + } else { + $publicKey = $this->privateKey->getPublic(false, 'hex'); + } + $from = '0x' . substr($this->util->sha3(substr(hex2bin($publicKey), 1)), 24); + + $this->offsetSet('from', $from); + return $from; + } +} \ No newline at end of file diff --git a/src/Transaction.php b/src/Transaction.php index a3ef26b..bca994d 100644 --- a/src/Transaction.php +++ b/src/Transaction.php @@ -70,11 +70,6 @@ class Transaction implements ArrayAccess 'chainId' => [ 'key' => -2 ], - 'transactionType' => [ - 'key' => -3, - 'length' => 2, - 'allowZero' => true - ], 'nonce' => [ 'key' => 0, 'length' => 32, @@ -186,15 +181,6 @@ public function __construct($txData=[]) $tx = []; if ($this->util->isHex($txData)) { - // check first byte - $txData = $this->util->stripZero($txData); - $firstByteStr = substr($txData, 0, 2); - $firstByte = hexdec($firstByteStr); - if ($firstByte >= 0 && $firstByte <= 127) { - // first byte is transaction type - $tx[$this->attributeMap['transactionType']['key']] = $firstByteStr; - $txData = substr($txData, 2); - } $txData = $this->rlp->decode($txData); foreach ($txData as $txKey => $data) { @@ -354,17 +340,6 @@ public function getTxData() return $this->txData; } - /** - * Return whether transaction type is valid (0x0 <= $transactionType <= 0x7f). - * - * @param integer $transactionType - * @return boolean is transaction valid - */ - protected function isTransactionTypeValid(int $transactionType) - { - return $transactionType >= 0 && $transactionType <= 127; - } - /** * RLP serialize the ethereum transaction. * @@ -388,18 +363,6 @@ public function serialize() $txData[$key] = $data; } } - $transactionType = $this->offsetGet('transactionType'); - if ( - $transactionType - ) { - $transactionType = $this->util->stripZero($transactionType); - if ($this->isTransactionTypeValid(hexdec($transactionType))) { - if (strlen($transactionType) % 2 != 0) { - $transactionType = '0' . $transactionType; - } - return $transactionType . $this->rlp->encode($txData); - } - } return $this->rlp->encode($txData); } diff --git a/test/unit/TransactionTest.php b/test/unit/TransactionTest.php index d20c1cb..fc17b93 100644 --- a/test/unit/TransactionTest.php +++ b/test/unit/TransactionTest.php @@ -4,6 +4,7 @@ use Test\TestCase; use Web3p\EthereumTx\Transaction; +use Web3p\EthereumTx\EIP2930Transaction; class TransactionTest extends TestCase { @@ -349,26 +350,28 @@ public function testIssue26() } /** - * testEIP2718 - * see: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2718.md + * testEIP2930 + * see: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2930.md * * @return void */ - public function testEIP2718() + public function testEIP2930() { - $transaction = new Transaction([ - 'transactionType' => '0x0', - 'nonce' => '0x09', + $transaction = new EIP2930Transaction([ + 'nonce' => '0x15', 'to' => '0x3535353535353535353535353535353535353535', 'gas' => '0x5208', 'gasPrice' => '0x4a817c800', 'value' => '0x0', - 'chainId' => 1, + 'chainId' => 4, + 'accessList' => [ + ], 'data' => '' ]); - $this->assertEquals('00f864098504a817c800825208943535353535353535353535353535353535353535808025a0855ec9b7d4fcabf535fe4ac4a7c31a9e521214d05bc6efbc058d4757c35e92bba0043d7df30c8a79e5522b3de8fc169df5fa7145714100ee8ec413292d97ce4d3a', $transaction->sign('0x4646464646464646464646464646464646464646464646464646464646464646')); + $this->assertEquals('01f86604158504a817c8008252089435353535353535353535353535353535353535358080c001a09753969d39f6a5109095d5082d67fc99a05fd66a339ba80934504ff79474e77aa07a907eb764b72b3088a331e7b97c2bad5fd43f1d574ddc80edeb022476454adb', $transaction->sign('0x4646464646464646464646464646464646464646464646464646464646464646')); - $transaction = new Transaction('0x00f86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83'); - $this->assertEquals('00f86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83', $transaction->serialize()); + $transaction = new EIP2930Transaction('0x01f86604158504a817c8008252089435353535353535353535353535353535353535358080c001a09753969d39f6a5109095d5082d67fc99a05fd66a339ba80934504ff79474e77aa07a907eb764b72b3088a331e7b97c2bad5fd43f1d574ddc80edeb022476454adb'); + $this->assertEquals('01f86604158504a817c8008252089435353535353535353535353535353535353535358080c001a09753969d39f6a5109095d5082d67fc99a05fd66a339ba80934504ff79474e77aa07a907eb764b72b3088a331e7b97c2bad5fd43f1d574ddc80edeb022476454adb', $transaction->serialize()); + $this->assertEquals('01f86604158504a817c8008252089435353535353535353535353535353535353535358080c001a09753969d39f6a5109095d5082d67fc99a05fd66a339ba80934504ff79474e77aa07a907eb764b72b3088a331e7b97c2bad5fd43f1d574ddc80edeb022476454adb', $transaction->sign('0x4646464646464646464646464646464646464646464646464646464646464646')); } } From 3f2f865b2e36fda86fb160c04f51eaf057b0c3b8 Mon Sep 17 00:00:00 2001 From: sc0vu Date: Sun, 29 Aug 2021 04:48:37 +0000 Subject: [PATCH 4/8] Add EIP1559 type 2 transaction --- src/EIP1559Transaction.php | 521 ++++++++++++++++++++++++++++++++++ test/unit/TransactionTest.php | 28 ++ 2 files changed, 549 insertions(+) create mode 100644 src/EIP1559Transaction.php diff --git a/src/EIP1559Transaction.php b/src/EIP1559Transaction.php new file mode 100644 index 0000000..0da02b8 --- /dev/null +++ b/src/EIP1559Transaction.php @@ -0,0 +1,521 @@ + + * + * @author Peter Lai + * @license MIT + */ + +namespace Web3p\EthereumTx; + +use InvalidArgumentException; +use RuntimeException; +use Web3p\RLP\RLP; +use Elliptic\EC; +use Elliptic\EC\KeyPair; +use ArrayAccess; +use Web3p\EthereumUtil\Util; + +/** + * It's a instance for generating/serializing ethereum transaction. + * + * ```php + * use Web3p\EthereumTx\EIP1559Transaction; + * + * // generate transaction instance with transaction parameters + * $transaction = new EIP1559Transaction([ + * 'nonce' => '0x01', + * 'from' => '0xb60e8dd61c5d32be8058bb8eb970870f07233155', + * 'to' => '0xd46e8dd67c5d32be8058bb8eb970870f07244567', + * 'maxPriorityFeePerGas' => '0x9184e72a000', + * 'maxFeePerGas' => '0x9184e72a000', + * 'gas' => '0x76c0', + * 'value' => '0x9184e72a', + * 'chainId' => 1, // required + * 'accessList' => [], + * 'data' => '0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675' + * ]); + * + * // generate transaction instance with hex encoded transaction + * $transaction = new EIP1559Transaction('0x02f86c04158504a817c8008504a817c8008252089435353535353535353535353535353535353535358080c080a03fd48c8a173e9669c33cb5271f03b1af4f030dc8315be8ec9442b7fbdde893c8a010af381dab1df3e7012a3c8421d65a810859a5dd9d58991ad7c07f12d0c651c7'); + * ``` + * + * ```php + * After generate transaction instance, you can sign transaction with your private key. + * + * $signedTransaction = $transaction->sign('your private key'); + * ``` + * + * Then you can send serialized transaction to ethereum through http rpc with web3.php. + * ```php + * $hashedTx = $transaction->serialize(); + * ``` + * + * @author Peter Lai + * @link https://www.web3p.xyz + * @filesource https://github.com/web3p/ethereum-tx + */ +class EIP1559Transaction implements ArrayAccess +{ + /** + * Attribute map for keeping order of transaction key/value + * + * @var array + */ + protected $attributeMap = [ + 'from' => [ + 'key' => -1 + ], + 'chainId' => [ + 'key' => 0 + ], + 'nonce' => [ + 'key' => 1, + 'length' => 32, + 'allowLess' => true, + 'allowZero' => false + ], + 'maxPriorityFeePerGas' => [ + 'key' => 2, + 'length' => 32, + 'allowLess' => true, + 'allowZero' => false + ], + 'maxFeePerGas' => [ + 'key' => 3, + 'length' => 32, + 'allowLess' => true, + 'allowZero' => false + ], + 'gasLimit' => [ + 'key' => 4, + 'length' => 32, + 'allowLess' => true, + 'allowZero' => false + ], + 'gas' => [ + 'key' => 4, + 'length' => 32, + 'allowLess' => true, + 'allowZero' => false + ], + 'to' => [ + 'key' => 5, + 'length' => 20, + 'allowZero' => true, + ], + 'value' => [ + 'key' => 6, + 'length' => 32, + 'allowLess' => true, + 'allowZero' => false + ], + 'data' => [ + 'key' => 7, + 'allowLess' => true, + 'allowZero' => true + ], + 'accessList' => [ + 'key' => 8, + 'allowLess' => true, + 'allowZero' => true, + 'allowArray' => true + ], + 'v' => [ + 'key' => 9, + 'allowZero' => true + ], + 'r' => [ + 'key' => 10, + 'length' => 32, + 'allowZero' => true + ], + 's' => [ + 'key' => 11, + 'length' => 32, + 'allowZero' => true + ] + ]; + + /** + * Raw transaction data + * + * @var array + */ + protected $txData = []; + + /** + * RLP encoding instance + * + * @var \Web3p\RLP\RLP + */ + protected $rlp; + + /** + * secp256k1 elliptic curve instance + * + * @var \Elliptic\EC + */ + protected $secp256k1; + + /** + * Private key instance + * + * @var \Elliptic\EC\KeyPair + */ + protected $privateKey; + + /** + * Ethereum util instance + * + * @var \Web3p\EthereumUtil\Util + */ + protected $util; + + /** + * Transaction type + * + * @var string + */ + protected $transactionType = '02'; + + /** + * construct + * + * @param array|string $txData + * @return void + */ + public function __construct($txData=[]) + { + $this->rlp = new RLP; + $this->secp256k1 = new EC('secp256k1'); + $this->util = new Util; + + if (is_array($txData)) { + foreach ($txData as $key => $data) { + $this->offsetSet($key, $data); + } + } elseif (is_string($txData)) { + $tx = []; + + if ($this->util->isHex($txData)) { + // check first byte + $txData = $this->util->stripZero($txData); + $firstByteStr = substr($txData, 0, 2); + $firstByte = hexdec($firstByteStr); + if ($this->isTransactionTypeValid($firstByte)) { + $txData = substr($txData, 2); + } + $txData = $this->rlp->decode($txData); + + foreach ($txData as $txKey => $data) { + if (is_int($txKey)) { + if (is_string($data) && strlen($data) > 0) { + $tx[$txKey] = '0x' . $data; + } else { + $tx[$txKey] = $data; + } + } + } + } + $this->txData = $tx; + } + } + + /** + * Return the value in the transaction with given key or return the protected property value if get(property_name} function is existed. + * + * @param string $name key or protected property name + * @return mixed + */ + public function __get(string $name) + { + $method = 'get' . ucfirst($name); + + if (method_exists($this, $method)) { + return call_user_func_array([$this, $method], []); + } + return $this->offsetGet($name); + } + + /** + * Set the value in the transaction with given key or return the protected value if set(property_name} function is existed. + * + * @param string $name key, eg: to + * @param mixed value + * @return void + */ + public function __set(string $name, $value) + { + $method = 'set' . ucfirst($name); + + if (method_exists($this, $method)) { + return call_user_func_array([$this, $method], [$value]); + } + return $this->offsetSet($name, $value); + } + + /** + * Return hash of the ethereum transaction without signature. + * + * @return string hex encoded of the transaction + */ + public function __toString() + { + return $this->hash(false); + } + + /** + * Set the value in the transaction with given key. + * + * @param string $offset key, eg: to + * @param string value + * @return void + */ + public function offsetSet($offset, $value) + { + $txKey = isset($this->attributeMap[$offset]) ? $this->attributeMap[$offset] : null; + + if (is_array($txKey)) { + if (is_array($value)) { + if (!isset($txKey['allowArray']) || (isset($txKey['allowArray']) && $txKey['allowArray'] === false)) { + throw new InvalidArgumentException($offset . ' should\'t be array.'); + } + if (!isset($txKey['allowLess']) || (isset($txKey['allowLess']) && $txKey['allowLess'] === false)) { + // check length + if (isset($txKey['length'])) { + if (count($value) > $txKey['length'] * 2) { + throw new InvalidArgumentException($offset . ' exceeds the length limit.'); + } + } + } + if (!isset($txKey['allowZero']) || (isset($txKey['allowZero']) && $txKey['allowZero'] === false)) { + // check zero + foreach ($value as $key => $v) { + $checkedV = $v ? (string) $v : ''; + if (preg_match('/^0*$/', $checkedV) === 1) { + // set value to empty string + $checkedV = ''; + $value[$key] = $checkedV; + } + } + } + } else { + $checkedValue = ($value) ? (string) $value : ''; + $isHex = $this->util->isHex($checkedValue); + $checkedValue = $this->util->stripZero($checkedValue); + + if (!isset($txKey['allowLess']) || (isset($txKey['allowLess']) && $txKey['allowLess'] === false)) { + // check length + if (isset($txKey['length'])) { + if ($isHex) { + if (strlen($checkedValue) > $txKey['length'] * 2) { + throw new InvalidArgumentException($offset . ' exceeds the length limit.'); + } + } else { + if (strlen($checkedValue) > $txKey['length']) { + throw new InvalidArgumentException($offset . ' exceeds the length limit.'); + } + } + } + } + if (!isset($txKey['allowZero']) || (isset($txKey['allowZero']) && $txKey['allowZero'] === false)) { + // check zero + if (preg_match('/^0*$/', $checkedValue) === 1) { + // set value to empty string + $value = ''; + } + } + } + $this->txData[$txKey['key']] = $value; + } + } + + /** + * Return whether the value is in the transaction with given key. + * + * @param string $offset key, eg: to + * @return bool + */ + public function offsetExists($offset) + { + $txKey = isset($this->attributeMap[$offset]) ? $this->attributeMap[$offset] : null; + + if (is_array($txKey)) { + return isset($this->txData[$txKey['key']]); + } + return false; + } + + /** + * Unset the value in the transaction with given key. + * + * @param string $offset key, eg: to + * @return void + */ + public function offsetUnset($offset) + { + $txKey = isset($this->attributeMap[$offset]) ? $this->attributeMap[$offset] : null; + + if (is_array($txKey) && isset($this->txData[$txKey['key']])) { + unset($this->txData[$txKey['key']]); + } + } + + /** + * Return the value in the transaction with given key. + * + * @param string $offset key, eg: to + * @return mixed value of the transaction + */ + public function offsetGet($offset) + { + $txKey = isset($this->attributeMap[$offset]) ? $this->attributeMap[$offset] : null; + + if (is_array($txKey) && isset($this->txData[$txKey['key']])) { + return $this->txData[$txKey['key']]; + } + return null; + } + + /** + * Return raw ethereum transaction data. + * + * @return array raw ethereum transaction data + */ + public function getTxData() + { + return $this->txData; + } + + /** + * Return whether transaction type is valid (0x0 <= $transactionType <= 0x7f). + * + * @param integer $transactionType + * @return boolean is transaction valid + */ + protected function isTransactionTypeValid(int $transactionType) + { + return $transactionType >= 0 && $transactionType <= 127; + } + + /** + * RLP serialize the ethereum transaction. + * + * @return \Web3p\RLP\RLP\Buffer serialized ethereum transaction + */ + public function serialize() + { + $chainId = $this->offsetGet('chainId'); + + // sort tx data + if (ksort($this->txData) !== true) { + throw new RuntimeException('Cannot sort tx data by keys.'); + } + if ($chainId && $chainId > 0) { + $txData = array_fill(0, 12, ''); + } else { + $txData = array_fill(0, 9, ''); + } + foreach ($this->txData as $key => $data) { + if ($key >= 0) { + $txData[$key] = $data; + } + } + $transactionType = $this->transactionType; + return $transactionType . $this->rlp->encode($txData); + } + + /** + * Sign the transaction with given hex encoded private key. + * + * @param string $privateKey hex encoded private key + * @return string hex encoded signed ethereum transaction + */ + public function sign(string $privateKey) + { + if ($this->util->isHex($privateKey)) { + $privateKey = $this->util->stripZero($privateKey); + $ecPrivateKey = $this->secp256k1->keyFromPrivate($privateKey, 'hex'); + } else { + throw new InvalidArgumentException('Private key should be hex encoded string'); + } + $txHash = $this->hash(); + $signature = $ecPrivateKey->sign($txHash, [ + 'canonical' => true + ]); + $r = $signature->r; + $s = $signature->s; + $v = $signature->recoveryParam; + + $this->offsetSet('r', '0x' . $r->toString(16)); + $this->offsetSet('s', '0x' . $s->toString(16)); + $this->offsetSet('v', $v); + $this->privateKey = $ecPrivateKey; + + return $this->serialize(); + } + + /** + * Return hash of the ethereum transaction with/without signature. + * + * @return string hex encoded hash of the ethereum transaction + */ + public function hash() + { + $chainId = $this->offsetGet('chainId'); + + // sort tx data + if (ksort($this->txData) !== true) { + throw new RuntimeException('Cannot sort tx data by keys.'); + } + $rawTxData = array_fill(0, 9, ''); + foreach ($this->txData as $key => $data) { + if ($key >= 0 && $key < 9) { + $rawTxData[$key] = $data; + } + } + $serializedTx = $this->rlp->encode($rawTxData); + $transactionType = $this->transactionType; + return $this->util->sha3(hex2bin($transactionType . $serializedTx)); + } + + /** + * Recover from address with given signature (r, s, v) if didn't set from. + * + * @return string hex encoded ethereum address + */ + public function getFromAddress() + { + $from = $this->offsetGet('from'); + + if ($from) { + return $from; + } + if (!isset($this->privateKey) || !($this->privateKey instanceof KeyPair)) { + // recover from hash + $r = $this->offsetGet('r'); + $s = $this->offsetGet('s'); + $v = $this->offsetGet('v'); + + if (!$r || !$s) { + throw new RuntimeException('Invalid signature r and s.'); + } + $txHash = $this->hash(); + $publicKey = $this->secp256k1->recoverPubKey($txHash, [ + 'r' => $r, + 's' => $s + ], $v); + $publicKey = $publicKey->encode('hex'); + } else { + $publicKey = $this->privateKey->getPublic(false, 'hex'); + } + $from = '0x' . substr($this->util->sha3(substr(hex2bin($publicKey), 1)), 24); + + $this->offsetSet('from', $from); + return $from; + } +} \ No newline at end of file diff --git a/test/unit/TransactionTest.php b/test/unit/TransactionTest.php index fc17b93..f97f9d6 100644 --- a/test/unit/TransactionTest.php +++ b/test/unit/TransactionTest.php @@ -5,6 +5,7 @@ use Test\TestCase; use Web3p\EthereumTx\Transaction; use Web3p\EthereumTx\EIP2930Transaction; +use Web3p\EthereumTx\EIP1559Transaction; class TransactionTest extends TestCase { @@ -374,4 +375,31 @@ public function testEIP2930() $this->assertEquals('01f86604158504a817c8008252089435353535353535353535353535353535353535358080c001a09753969d39f6a5109095d5082d67fc99a05fd66a339ba80934504ff79474e77aa07a907eb764b72b3088a331e7b97c2bad5fd43f1d574ddc80edeb022476454adb', $transaction->serialize()); $this->assertEquals('01f86604158504a817c8008252089435353535353535353535353535353535353535358080c001a09753969d39f6a5109095d5082d67fc99a05fd66a339ba80934504ff79474e77aa07a907eb764b72b3088a331e7b97c2bad5fd43f1d574ddc80edeb022476454adb', $transaction->sign('0x4646464646464646464646464646464646464646464646464646464646464646')); } + + /** + * testEIP1559 + * see: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md + * + * @return void + */ + public function testEIP1559() + { + $transaction = new EIP1559Transaction([ + 'nonce' => '0x15', + 'to' => '0x3535353535353535353535353535353535353535', + 'gas' => '0x5208', + 'maxPriorityFeePerGas' => '0x4a817c800', + 'maxFeePerGas' => '0x4a817c800', + 'value' => '0x0', + 'chainId' => 4, + 'accessList' => [ + ], + 'data' => '' + ]); + $this->assertEquals('02f86c04158504a817c8008504a817c8008252089435353535353535353535353535353535353535358080c080a03fd48c8a173e9669c33cb5271f03b1af4f030dc8315be8ec9442b7fbdde893c8a010af381dab1df3e7012a3c8421d65a810859a5dd9d58991ad7c07f12d0c651c7', $transaction->sign('0x4646464646464646464646464646464646464646464646464646464646464646')); + + $transaction = new EIP1559Transaction('0x02f86c04158504a817c8008504a817c8008252089435353535353535353535353535353535353535358080c080a03fd48c8a173e9669c33cb5271f03b1af4f030dc8315be8ec9442b7fbdde893c8a010af381dab1df3e7012a3c8421d65a810859a5dd9d58991ad7c07f12d0c651c7'); + $this->assertEquals('02f86c04158504a817c8008504a817c8008252089435353535353535353535353535353535353535358080c080a03fd48c8a173e9669c33cb5271f03b1af4f030dc8315be8ec9442b7fbdde893c8a010af381dab1df3e7012a3c8421d65a810859a5dd9d58991ad7c07f12d0c651c7', $transaction->serialize()); + $this->assertEquals('02f86c04158504a817c8008504a817c8008252089435353535353535353535353535353535353535358080c080a03fd48c8a173e9669c33cb5271f03b1af4f030dc8315be8ec9442b7fbdde893c8a010af381dab1df3e7012a3c8421d65a810859a5dd9d58991ad7c07f12d0c651c7', $transaction->sign('0x4646464646464646464646464646464646464646464646464646464646464646')); + } } From 20b622e1e558f14bdf6657cfd6348b408c40c057 Mon Sep 17 00:00:00 2001 From: sc0vu Date: Sun, 29 Aug 2021 04:50:04 +0000 Subject: [PATCH 5/8] Update EIP2930 transaction --- src/EIP2930Transaction.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EIP2930Transaction.php b/src/EIP2930Transaction.php index 82787ac..c83bc87 100644 --- a/src/EIP2930Transaction.php +++ b/src/EIP2930Transaction.php @@ -26,7 +26,7 @@ * use Web3p\EthereumTx\EIP2930Transaction; * * // generate transaction instance with transaction parameters - * $transaction = new Transaction([ + * $transaction = new EIP2930Transaction([ * 'nonce' => '0x01', * 'from' => '0xb60e8dd61c5d32be8058bb8eb970870f07233155', * 'to' => '0xd46e8dd67c5d32be8058bb8eb970870f07244567', @@ -39,7 +39,7 @@ * ]); * * // generate transaction instance with hex encoded transaction - * $transaction = new Transaction('0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83'); + * $transaction = new EIP2930Transaction('0x01f86604158504a817c8008252089435353535353535353535353535353535353535358080c001a09753969d39f6a5109095d5082d67fc99a05fd66a339ba80934504ff79474e77aa07a907eb764b72b3088a331e7b97c2bad5fd43f1d574ddc80edeb022476454adb'); * ``` * * ```php From 94e077550b24d903f70a8060ce92bbcfb3e8fceb Mon Sep 17 00:00:00 2001 From: sc0vu Date: Sun, 29 Aug 2021 06:21:02 +0000 Subject: [PATCH 6/8] Refactor EIP1559/EIP2930 transaction --- src/EIP1559Transaction.php | 297 +---------------------- src/EIP2930Transaction.php | 327 +------------------------- src/TypeTransaction.php | 470 +++++++++++++++++++++++++++++++++++++ 3 files changed, 480 insertions(+), 614 deletions(-) create mode 100755 src/TypeTransaction.php diff --git a/src/EIP1559Transaction.php b/src/EIP1559Transaction.php index 0da02b8..52e44e6 100644 --- a/src/EIP1559Transaction.php +++ b/src/EIP1559Transaction.php @@ -18,9 +18,10 @@ use Elliptic\EC\KeyPair; use ArrayAccess; use Web3p\EthereumUtil\Util; +use Web3p\EthereumTx\TypeTransaction; /** - * It's a instance for generating/serializing ethereum transaction. + * It's a instance for generating/serializing ethereum eip1559 transaction. * * ```php * use Web3p\EthereumTx\EIP1559Transaction; @@ -58,7 +59,7 @@ * @link https://www.web3p.xyz * @filesource https://github.com/web3p/ethereum-tx */ -class EIP1559Transaction implements ArrayAccess +class EIP1559Transaction extends TypeTransaction { /** * Attribute map for keeping order of transaction key/value @@ -140,41 +141,6 @@ class EIP1559Transaction implements ArrayAccess ] ]; - /** - * Raw transaction data - * - * @var array - */ - protected $txData = []; - - /** - * RLP encoding instance - * - * @var \Web3p\RLP\RLP - */ - protected $rlp; - - /** - * secp256k1 elliptic curve instance - * - * @var \Elliptic\EC - */ - protected $secp256k1; - - /** - * Private key instance - * - * @var \Elliptic\EC\KeyPair - */ - protected $privateKey; - - /** - * Ethereum util instance - * - * @var \Web3p\EthereumUtil\Util - */ - protected $util; - /** * Transaction type * @@ -190,216 +156,7 @@ class EIP1559Transaction implements ArrayAccess */ public function __construct($txData=[]) { - $this->rlp = new RLP; - $this->secp256k1 = new EC('secp256k1'); - $this->util = new Util; - - if (is_array($txData)) { - foreach ($txData as $key => $data) { - $this->offsetSet($key, $data); - } - } elseif (is_string($txData)) { - $tx = []; - - if ($this->util->isHex($txData)) { - // check first byte - $txData = $this->util->stripZero($txData); - $firstByteStr = substr($txData, 0, 2); - $firstByte = hexdec($firstByteStr); - if ($this->isTransactionTypeValid($firstByte)) { - $txData = substr($txData, 2); - } - $txData = $this->rlp->decode($txData); - - foreach ($txData as $txKey => $data) { - if (is_int($txKey)) { - if (is_string($data) && strlen($data) > 0) { - $tx[$txKey] = '0x' . $data; - } else { - $tx[$txKey] = $data; - } - } - } - } - $this->txData = $tx; - } - } - - /** - * Return the value in the transaction with given key or return the protected property value if get(property_name} function is existed. - * - * @param string $name key or protected property name - * @return mixed - */ - public function __get(string $name) - { - $method = 'get' . ucfirst($name); - - if (method_exists($this, $method)) { - return call_user_func_array([$this, $method], []); - } - return $this->offsetGet($name); - } - - /** - * Set the value in the transaction with given key or return the protected value if set(property_name} function is existed. - * - * @param string $name key, eg: to - * @param mixed value - * @return void - */ - public function __set(string $name, $value) - { - $method = 'set' . ucfirst($name); - - if (method_exists($this, $method)) { - return call_user_func_array([$this, $method], [$value]); - } - return $this->offsetSet($name, $value); - } - - /** - * Return hash of the ethereum transaction without signature. - * - * @return string hex encoded of the transaction - */ - public function __toString() - { - return $this->hash(false); - } - - /** - * Set the value in the transaction with given key. - * - * @param string $offset key, eg: to - * @param string value - * @return void - */ - public function offsetSet($offset, $value) - { - $txKey = isset($this->attributeMap[$offset]) ? $this->attributeMap[$offset] : null; - - if (is_array($txKey)) { - if (is_array($value)) { - if (!isset($txKey['allowArray']) || (isset($txKey['allowArray']) && $txKey['allowArray'] === false)) { - throw new InvalidArgumentException($offset . ' should\'t be array.'); - } - if (!isset($txKey['allowLess']) || (isset($txKey['allowLess']) && $txKey['allowLess'] === false)) { - // check length - if (isset($txKey['length'])) { - if (count($value) > $txKey['length'] * 2) { - throw new InvalidArgumentException($offset . ' exceeds the length limit.'); - } - } - } - if (!isset($txKey['allowZero']) || (isset($txKey['allowZero']) && $txKey['allowZero'] === false)) { - // check zero - foreach ($value as $key => $v) { - $checkedV = $v ? (string) $v : ''; - if (preg_match('/^0*$/', $checkedV) === 1) { - // set value to empty string - $checkedV = ''; - $value[$key] = $checkedV; - } - } - } - } else { - $checkedValue = ($value) ? (string) $value : ''; - $isHex = $this->util->isHex($checkedValue); - $checkedValue = $this->util->stripZero($checkedValue); - - if (!isset($txKey['allowLess']) || (isset($txKey['allowLess']) && $txKey['allowLess'] === false)) { - // check length - if (isset($txKey['length'])) { - if ($isHex) { - if (strlen($checkedValue) > $txKey['length'] * 2) { - throw new InvalidArgumentException($offset . ' exceeds the length limit.'); - } - } else { - if (strlen($checkedValue) > $txKey['length']) { - throw new InvalidArgumentException($offset . ' exceeds the length limit.'); - } - } - } - } - if (!isset($txKey['allowZero']) || (isset($txKey['allowZero']) && $txKey['allowZero'] === false)) { - // check zero - if (preg_match('/^0*$/', $checkedValue) === 1) { - // set value to empty string - $value = ''; - } - } - } - $this->txData[$txKey['key']] = $value; - } - } - - /** - * Return whether the value is in the transaction with given key. - * - * @param string $offset key, eg: to - * @return bool - */ - public function offsetExists($offset) - { - $txKey = isset($this->attributeMap[$offset]) ? $this->attributeMap[$offset] : null; - - if (is_array($txKey)) { - return isset($this->txData[$txKey['key']]); - } - return false; - } - - /** - * Unset the value in the transaction with given key. - * - * @param string $offset key, eg: to - * @return void - */ - public function offsetUnset($offset) - { - $txKey = isset($this->attributeMap[$offset]) ? $this->attributeMap[$offset] : null; - - if (is_array($txKey) && isset($this->txData[$txKey['key']])) { - unset($this->txData[$txKey['key']]); - } - } - - /** - * Return the value in the transaction with given key. - * - * @param string $offset key, eg: to - * @return mixed value of the transaction - */ - public function offsetGet($offset) - { - $txKey = isset($this->attributeMap[$offset]) ? $this->attributeMap[$offset] : null; - - if (is_array($txKey) && isset($this->txData[$txKey['key']])) { - return $this->txData[$txKey['key']]; - } - return null; - } - - /** - * Return raw ethereum transaction data. - * - * @return array raw ethereum transaction data - */ - public function getTxData() - { - return $this->txData; - } - - /** - * Return whether transaction type is valid (0x0 <= $transactionType <= 0x7f). - * - * @param integer $transactionType - * @return boolean is transaction valid - */ - protected function isTransactionTypeValid(int $transactionType) - { - return $transactionType >= 0 && $transactionType <= 127; + parent::__construct($txData); } /** @@ -409,17 +166,11 @@ protected function isTransactionTypeValid(int $transactionType) */ public function serialize() { - $chainId = $this->offsetGet('chainId'); - // sort tx data if (ksort($this->txData) !== true) { throw new RuntimeException('Cannot sort tx data by keys.'); } - if ($chainId && $chainId > 0) { - $txData = array_fill(0, 12, ''); - } else { - $txData = array_fill(0, 9, ''); - } + $txData = array_fill(0, 12, ''); foreach ($this->txData as $key => $data) { if ($key >= 0) { $txData[$key] = $data; @@ -466,8 +217,6 @@ public function sign(string $privateKey) */ public function hash() { - $chainId = $this->offsetGet('chainId'); - // sort tx data if (ksort($this->txData) !== true) { throw new RuntimeException('Cannot sort tx data by keys.'); @@ -482,40 +231,4 @@ public function hash() $transactionType = $this->transactionType; return $this->util->sha3(hex2bin($transactionType . $serializedTx)); } - - /** - * Recover from address with given signature (r, s, v) if didn't set from. - * - * @return string hex encoded ethereum address - */ - public function getFromAddress() - { - $from = $this->offsetGet('from'); - - if ($from) { - return $from; - } - if (!isset($this->privateKey) || !($this->privateKey instanceof KeyPair)) { - // recover from hash - $r = $this->offsetGet('r'); - $s = $this->offsetGet('s'); - $v = $this->offsetGet('v'); - - if (!$r || !$s) { - throw new RuntimeException('Invalid signature r and s.'); - } - $txHash = $this->hash(); - $publicKey = $this->secp256k1->recoverPubKey($txHash, [ - 'r' => $r, - 's' => $s - ], $v); - $publicKey = $publicKey->encode('hex'); - } else { - $publicKey = $this->privateKey->getPublic(false, 'hex'); - } - $from = '0x' . substr($this->util->sha3(substr(hex2bin($publicKey), 1)), 24); - - $this->offsetSet('from', $from); - return $from; - } } \ No newline at end of file diff --git a/src/EIP2930Transaction.php b/src/EIP2930Transaction.php index c83bc87..3128897 100644 --- a/src/EIP2930Transaction.php +++ b/src/EIP2930Transaction.php @@ -18,9 +18,10 @@ use Elliptic\EC\KeyPair; use ArrayAccess; use Web3p\EthereumUtil\Util; +use Web3p\EthereumTx\TypeTransaction; /** - * It's a instance for generating/serializing ethereum transaction. + * It's a instance for generating/serializing ethereum eip2930 transaction. * * ```php * use Web3p\EthereumTx\EIP2930Transaction; @@ -57,7 +58,7 @@ * @link https://www.web3p.xyz * @filesource https://github.com/web3p/ethereum-tx */ -class EIP2930Transaction implements ArrayAccess +class EIP2930Transaction extends TypeTransaction { /** * Attribute map for keeping order of transaction key/value @@ -133,41 +134,6 @@ class EIP2930Transaction implements ArrayAccess ] ]; - /** - * Raw transaction data - * - * @var array - */ - protected $txData = []; - - /** - * RLP encoding instance - * - * @var \Web3p\RLP\RLP - */ - protected $rlp; - - /** - * secp256k1 elliptic curve instance - * - * @var \Elliptic\EC - */ - protected $secp256k1; - - /** - * Private key instance - * - * @var \Elliptic\EC\KeyPair - */ - protected $privateKey; - - /** - * Ethereum util instance - * - * @var \Web3p\EthereumUtil\Util - */ - protected $util; - /** * Transaction type * @@ -183,216 +149,7 @@ class EIP2930Transaction implements ArrayAccess */ public function __construct($txData=[]) { - $this->rlp = new RLP; - $this->secp256k1 = new EC('secp256k1'); - $this->util = new Util; - - if (is_array($txData)) { - foreach ($txData as $key => $data) { - $this->offsetSet($key, $data); - } - } elseif (is_string($txData)) { - $tx = []; - - if ($this->util->isHex($txData)) { - // check first byte - $txData = $this->util->stripZero($txData); - $firstByteStr = substr($txData, 0, 2); - $firstByte = hexdec($firstByteStr); - if ($this->isTransactionTypeValid($firstByte)) { - $txData = substr($txData, 2); - } - $txData = $this->rlp->decode($txData); - - foreach ($txData as $txKey => $data) { - if (is_int($txKey)) { - if (is_string($data) && strlen($data) > 0) { - $tx[$txKey] = '0x' . $data; - } else { - $tx[$txKey] = $data; - } - } - } - } - $this->txData = $tx; - } - } - - /** - * Return the value in the transaction with given key or return the protected property value if get(property_name} function is existed. - * - * @param string $name key or protected property name - * @return mixed - */ - public function __get(string $name) - { - $method = 'get' . ucfirst($name); - - if (method_exists($this, $method)) { - return call_user_func_array([$this, $method], []); - } - return $this->offsetGet($name); - } - - /** - * Set the value in the transaction with given key or return the protected value if set(property_name} function is existed. - * - * @param string $name key, eg: to - * @param mixed value - * @return void - */ - public function __set(string $name, $value) - { - $method = 'set' . ucfirst($name); - - if (method_exists($this, $method)) { - return call_user_func_array([$this, $method], [$value]); - } - return $this->offsetSet($name, $value); - } - - /** - * Return hash of the ethereum transaction without signature. - * - * @return string hex encoded of the transaction - */ - public function __toString() - { - return $this->hash(false); - } - - /** - * Set the value in the transaction with given key. - * - * @param string $offset key, eg: to - * @param string value - * @return void - */ - public function offsetSet($offset, $value) - { - $txKey = isset($this->attributeMap[$offset]) ? $this->attributeMap[$offset] : null; - - if (is_array($txKey)) { - if (is_array($value)) { - if (!isset($txKey['allowArray']) || (isset($txKey['allowArray']) && $txKey['allowArray'] === false)) { - throw new InvalidArgumentException($offset . ' should\'t be array.'); - } - if (!isset($txKey['allowLess']) || (isset($txKey['allowLess']) && $txKey['allowLess'] === false)) { - // check length - if (isset($txKey['length'])) { - if (count($value) > $txKey['length'] * 2) { - throw new InvalidArgumentException($offset . ' exceeds the length limit.'); - } - } - } - if (!isset($txKey['allowZero']) || (isset($txKey['allowZero']) && $txKey['allowZero'] === false)) { - // check zero - foreach ($value as $key => $v) { - $checkedV = $v ? (string) $v : ''; - if (preg_match('/^0*$/', $checkedV) === 1) { - // set value to empty string - $checkedV = ''; - $value[$key] = $checkedV; - } - } - } - } else { - $checkedValue = ($value) ? (string) $value : ''; - $isHex = $this->util->isHex($checkedValue); - $checkedValue = $this->util->stripZero($checkedValue); - - if (!isset($txKey['allowLess']) || (isset($txKey['allowLess']) && $txKey['allowLess'] === false)) { - // check length - if (isset($txKey['length'])) { - if ($isHex) { - if (strlen($checkedValue) > $txKey['length'] * 2) { - throw new InvalidArgumentException($offset . ' exceeds the length limit.'); - } - } else { - if (strlen($checkedValue) > $txKey['length']) { - throw new InvalidArgumentException($offset . ' exceeds the length limit.'); - } - } - } - } - if (!isset($txKey['allowZero']) || (isset($txKey['allowZero']) && $txKey['allowZero'] === false)) { - // check zero - if (preg_match('/^0*$/', $checkedValue) === 1) { - // set value to empty string - $value = ''; - } - } - } - $this->txData[$txKey['key']] = $value; - } - } - - /** - * Return whether the value is in the transaction with given key. - * - * @param string $offset key, eg: to - * @return bool - */ - public function offsetExists($offset) - { - $txKey = isset($this->attributeMap[$offset]) ? $this->attributeMap[$offset] : null; - - if (is_array($txKey)) { - return isset($this->txData[$txKey['key']]); - } - return false; - } - - /** - * Unset the value in the transaction with given key. - * - * @param string $offset key, eg: to - * @return void - */ - public function offsetUnset($offset) - { - $txKey = isset($this->attributeMap[$offset]) ? $this->attributeMap[$offset] : null; - - if (is_array($txKey) && isset($this->txData[$txKey['key']])) { - unset($this->txData[$txKey['key']]); - } - } - - /** - * Return the value in the transaction with given key. - * - * @param string $offset key, eg: to - * @return mixed value of the transaction - */ - public function offsetGet($offset) - { - $txKey = isset($this->attributeMap[$offset]) ? $this->attributeMap[$offset] : null; - - if (is_array($txKey) && isset($this->txData[$txKey['key']])) { - return $this->txData[$txKey['key']]; - } - return null; - } - - /** - * Return raw ethereum transaction data. - * - * @return array raw ethereum transaction data - */ - public function getTxData() - { - return $this->txData; - } - - /** - * Return whether transaction type is valid (0x0 <= $transactionType <= 0x7f). - * - * @param integer $transactionType - * @return boolean is transaction valid - */ - protected function isTransactionTypeValid(int $transactionType) - { - return $transactionType >= 0 && $transactionType <= 127; + parent::__construct($txData); } /** @@ -402,17 +159,11 @@ protected function isTransactionTypeValid(int $transactionType) */ public function serialize() { - $chainId = $this->offsetGet('chainId'); - // sort tx data if (ksort($this->txData) !== true) { throw new RuntimeException('Cannot sort tx data by keys.'); } - if ($chainId && $chainId > 0) { - $txData = array_fill(0, 11, ''); - } else { - $txData = array_fill(0, 8, ''); - } + $txData = array_fill(0, 11, ''); foreach ($this->txData as $key => $data) { if ($key >= 0) { $txData[$key] = $data; @@ -422,36 +173,6 @@ public function serialize() return $transactionType . $this->rlp->encode($txData); } - /** - * Sign the transaction with given hex encoded private key. - * - * @param string $privateKey hex encoded private key - * @return string hex encoded signed ethereum transaction - */ - public function sign(string $privateKey) - { - if ($this->util->isHex($privateKey)) { - $privateKey = $this->util->stripZero($privateKey); - $ecPrivateKey = $this->secp256k1->keyFromPrivate($privateKey, 'hex'); - } else { - throw new InvalidArgumentException('Private key should be hex encoded string'); - } - $txHash = $this->hash(); - $signature = $ecPrivateKey->sign($txHash, [ - 'canonical' => true - ]); - $r = $signature->r; - $s = $signature->s; - $v = $signature->recoveryParam; - - $this->offsetSet('r', '0x' . $r->toString(16)); - $this->offsetSet('s', '0x' . $s->toString(16)); - $this->offsetSet('v', $v); - $this->privateKey = $ecPrivateKey; - - return $this->serialize(); - } - /** * Return hash of the ethereum transaction with/without signature. * @@ -459,8 +180,6 @@ public function sign(string $privateKey) */ public function hash() { - $chainId = $this->offsetGet('chainId'); - // sort tx data if (ksort($this->txData) !== true) { throw new RuntimeException('Cannot sort tx data by keys.'); @@ -475,40 +194,4 @@ public function hash() $transactionType = $this->transactionType; return $this->util->sha3(hex2bin($transactionType . $serializedTx)); } - - /** - * Recover from address with given signature (r, s, v) if didn't set from. - * - * @return string hex encoded ethereum address - */ - public function getFromAddress() - { - $from = $this->offsetGet('from'); - - if ($from) { - return $from; - } - if (!isset($this->privateKey) || !($this->privateKey instanceof KeyPair)) { - // recover from hash - $r = $this->offsetGet('r'); - $s = $this->offsetGet('s'); - $v = $this->offsetGet('v'); - - if (!$r || !$s) { - throw new RuntimeException('Invalid signature r and s.'); - } - $txHash = $this->hash(); - $publicKey = $this->secp256k1->recoverPubKey($txHash, [ - 'r' => $r, - 's' => $s - ], $v); - $publicKey = $publicKey->encode('hex'); - } else { - $publicKey = $this->privateKey->getPublic(false, 'hex'); - } - $from = '0x' . substr($this->util->sha3(substr(hex2bin($publicKey), 1)), 24); - - $this->offsetSet('from', $from); - return $from; - } } \ No newline at end of file diff --git a/src/TypeTransaction.php b/src/TypeTransaction.php new file mode 100755 index 0000000..4d2cbc0 --- /dev/null +++ b/src/TypeTransaction.php @@ -0,0 +1,470 @@ + + * + * @author Peter Lai + * @license MIT + */ + +namespace Web3p\EthereumTx; + +use InvalidArgumentException; +use RuntimeException; +use Web3p\RLP\RLP; +use Elliptic\EC; +use Elliptic\EC\KeyPair; +use ArrayAccess; +use Web3p\EthereumUtil\Util; + +/** + * It's a base transaction for generating/serializing ethereum type transaction (EIP1559/EIP2930). + * Only use this class to generate new type transaction + * + * @author Peter Lai + * @link https://www.web3p.xyz + * @filesource https://github.com/web3p/ethereum-tx + */ +class TypeTransaction implements ArrayAccess +{ + /** + * Attribute map for keeping order of transaction key/value + * + * @var array + */ + protected $attributeMap = [ + 'from' => [ + 'key' => -1 + ], + 'chainId' => [ + 'key' => 0 + ], + 'nonce' => [ + 'key' => 1, + 'length' => 32, + 'allowLess' => true, + 'allowZero' => false + ], + 'gasPrice' => [ + 'key' => 2, + 'length' => 32, + 'allowLess' => true, + 'allowZero' => false + ], + 'gasLimit' => [ + 'key' => 3, + 'length' => 32, + 'allowLess' => true, + 'allowZero' => false + ], + 'gas' => [ + 'key' => 3, + 'length' => 32, + 'allowLess' => true, + 'allowZero' => false + ], + 'to' => [ + 'key' => 4, + 'length' => 20, + 'allowZero' => true, + ], + 'value' => [ + 'key' => 5, + 'length' => 32, + 'allowLess' => true, + 'allowZero' => false + ], + 'data' => [ + 'key' => 6, + 'allowLess' => true, + 'allowZero' => true + ], + 'v' => [ + 'key' => 7, + 'allowZero' => true + ], + 'r' => [ + 'key' => 8, + 'length' => 32, + 'allowZero' => true + ], + 's' => [ + 'key' => 9, + 'length' => 32, + 'allowZero' => true + ] + ]; + + /** + * Raw transaction data + * + * @var array + */ + protected $txData = []; + + /** + * RLP encoding instance + * + * @var \Web3p\RLP\RLP + */ + protected $rlp; + + /** + * secp256k1 elliptic curve instance + * + * @var \Elliptic\EC + */ + protected $secp256k1; + + /** + * Private key instance + * + * @var \Elliptic\EC\KeyPair + */ + protected $privateKey; + + /** + * Ethereum util instance + * + * @var \Web3p\EthereumUtil\Util + */ + protected $util; + + /** + * Transaction type + * + * @var string + */ + protected $transactionType = '00'; + + /** + * construct + * + * @param array|string $txData + * @return void + */ + public function __construct($txData=[]) + { + $this->rlp = new RLP; + $this->secp256k1 = new EC('secp256k1'); + $this->util = new Util; + + if (is_array($txData)) { + foreach ($txData as $key => $data) { + $this->offsetSet($key, $data); + } + } elseif (is_string($txData)) { + $tx = []; + + if ($this->util->isHex($txData)) { + // check first byte + $txData = $this->util->stripZero($txData); + $firstByteStr = substr($txData, 0, 2); + $firstByte = hexdec($firstByteStr); + if ($this->isTransactionTypeValid($firstByte)) { + $txData = substr($txData, 2); + } + $txData = $this->rlp->decode($txData); + + foreach ($txData as $txKey => $data) { + if (is_int($txKey)) { + if (is_string($data) && strlen($data) > 0) { + $tx[$txKey] = '0x' . $data; + } else { + $tx[$txKey] = $data; + } + } + } + } + $this->txData = $tx; + } + } + + /** + * Return the value in the transaction with given key or return the protected property value if get(property_name} function is existed. + * + * @param string $name key or protected property name + * @return mixed + */ + public function __get(string $name) + { + $method = 'get' . ucfirst($name); + + if (method_exists($this, $method)) { + return call_user_func_array([$this, $method], []); + } + return $this->offsetGet($name); + } + + /** + * Set the value in the transaction with given key or return the protected value if set(property_name} function is existed. + * + * @param string $name key, eg: to + * @param mixed value + * @return void + */ + public function __set(string $name, $value) + { + $method = 'set' . ucfirst($name); + + if (method_exists($this, $method)) { + return call_user_func_array([$this, $method], [$value]); + } + return $this->offsetSet($name, $value); + } + + /** + * Return hash of the ethereum transaction without signature. + * + * @return string hex encoded of the transaction + */ + public function __toString() + { + return $this->hash(false); + } + + /** + * Set the value in the transaction with given key. + * + * @param string $offset key, eg: to + * @param string value + * @return void + */ + public function offsetSet($offset, $value) + { + $txKey = isset($this->attributeMap[$offset]) ? $this->attributeMap[$offset] : null; + + if (is_array($txKey)) { + if (is_array($value)) { + if (!isset($txKey['allowArray']) || (isset($txKey['allowArray']) && $txKey['allowArray'] === false)) { + throw new InvalidArgumentException($offset . ' should\'t be array.'); + } + if (!isset($txKey['allowLess']) || (isset($txKey['allowLess']) && $txKey['allowLess'] === false)) { + // check length + if (isset($txKey['length'])) { + if (count($value) > $txKey['length'] * 2) { + throw new InvalidArgumentException($offset . ' exceeds the length limit.'); + } + } + } + if (!isset($txKey['allowZero']) || (isset($txKey['allowZero']) && $txKey['allowZero'] === false)) { + // check zero + foreach ($value as $key => $v) { + $checkedV = $v ? (string) $v : ''; + if (preg_match('/^0*$/', $checkedV) === 1) { + // set value to empty string + $checkedV = ''; + $value[$key] = $checkedV; + } + } + } + } else { + $checkedValue = ($value) ? (string) $value : ''; + $isHex = $this->util->isHex($checkedValue); + $checkedValue = $this->util->stripZero($checkedValue); + + if (!isset($txKey['allowLess']) || (isset($txKey['allowLess']) && $txKey['allowLess'] === false)) { + // check length + if (isset($txKey['length'])) { + if ($isHex) { + if (strlen($checkedValue) > $txKey['length'] * 2) { + throw new InvalidArgumentException($offset . ' exceeds the length limit.'); + } + } else { + if (strlen($checkedValue) > $txKey['length']) { + throw new InvalidArgumentException($offset . ' exceeds the length limit.'); + } + } + } + } + if (!isset($txKey['allowZero']) || (isset($txKey['allowZero']) && $txKey['allowZero'] === false)) { + // check zero + if (preg_match('/^0*$/', $checkedValue) === 1) { + // set value to empty string + $value = ''; + } + } + } + $this->txData[$txKey['key']] = $value; + } + } + + /** + * Return whether the value is in the transaction with given key. + * + * @param string $offset key, eg: to + * @return bool + */ + public function offsetExists($offset) + { + $txKey = isset($this->attributeMap[$offset]) ? $this->attributeMap[$offset] : null; + + if (is_array($txKey)) { + return isset($this->txData[$txKey['key']]); + } + return false; + } + + /** + * Unset the value in the transaction with given key. + * + * @param string $offset key, eg: to + * @return void + */ + public function offsetUnset($offset) + { + $txKey = isset($this->attributeMap[$offset]) ? $this->attributeMap[$offset] : null; + + if (is_array($txKey) && isset($this->txData[$txKey['key']])) { + unset($this->txData[$txKey['key']]); + } + } + + /** + * Return the value in the transaction with given key. + * + * @param string $offset key, eg: to + * @return mixed value of the transaction + */ + public function offsetGet($offset) + { + $txKey = isset($this->attributeMap[$offset]) ? $this->attributeMap[$offset] : null; + + if (is_array($txKey) && isset($this->txData[$txKey['key']])) { + return $this->txData[$txKey['key']]; + } + return null; + } + + /** + * Return raw ethereum transaction data. + * + * @return array raw ethereum transaction data + */ + public function getTxData() + { + return $this->txData; + } + + /** + * Return whether transaction type is valid (0x0 <= $transactionType <= 0x7f). + * + * @param integer $transactionType + * @return boolean is transaction valid + */ + protected function isTransactionTypeValid(int $transactionType) + { + return $transactionType >= 0 && $transactionType <= 127; + } + + /** + * RLP serialize the ethereum transaction. + * + * @return \Web3p\RLP\RLP\Buffer serialized ethereum transaction + */ + public function serialize() + { + // sort tx data + if (ksort($this->txData) !== true) { + throw new RuntimeException('Cannot sort tx data by keys.'); + } + $txData = array_fill(0, 10, ''); + foreach ($this->txData as $key => $data) { + if ($key >= 0) { + $txData[$key] = $data; + } + } + $transactionType = $this->transactionType; + return $transactionType . $this->rlp->encode($txData); + } + + /** + * Sign the transaction with given hex encoded private key. + * + * @param string $privateKey hex encoded private key + * @return string hex encoded signed ethereum transaction + */ + public function sign(string $privateKey) + { + if ($this->util->isHex($privateKey)) { + $privateKey = $this->util->stripZero($privateKey); + $ecPrivateKey = $this->secp256k1->keyFromPrivate($privateKey, 'hex'); + } else { + throw new InvalidArgumentException('Private key should be hex encoded string'); + } + $txHash = $this->hash(); + $signature = $ecPrivateKey->sign($txHash, [ + 'canonical' => true + ]); + $r = $signature->r; + $s = $signature->s; + $v = $signature->recoveryParam; + + $this->offsetSet('r', '0x' . $r->toString(16)); + $this->offsetSet('s', '0x' . $s->toString(16)); + $this->offsetSet('v', $v); + $this->privateKey = $ecPrivateKey; + + return $this->serialize(); + } + + /** + * Return hash of the ethereum transaction with/without signature. + * + * @return string hex encoded hash of the ethereum transaction + */ + public function hash() + { + // sort tx data + if (ksort($this->txData) !== true) { + throw new RuntimeException('Cannot sort tx data by keys.'); + } + $rawTxData = array_fill(0, 7, ''); + foreach ($this->txData as $key => $data) { + if ($key >= 0 && $key < 8) { + $rawTxData[$key] = $data; + } + } + $serializedTx = $this->rlp->encode($rawTxData); + $transactionType = $this->transactionType; + return $this->util->sha3(hex2bin($transactionType . $serializedTx)); + } + + /** + * Recover from address with given signature (r, s, v) if didn't set from. + * + * @return string hex encoded ethereum address + */ + public function getFromAddress() + { + $from = $this->offsetGet('from'); + + if ($from) { + return $from; + } + if (!isset($this->privateKey) || !($this->privateKey instanceof KeyPair)) { + // recover from hash + $r = $this->offsetGet('r'); + $s = $this->offsetGet('s'); + $v = $this->offsetGet('v'); + + if (!$r || !$s) { + throw new RuntimeException('Invalid signature r and s.'); + } + $txHash = $this->hash(); + $publicKey = $this->secp256k1->recoverPubKey($txHash, [ + 'r' => $r, + 's' => $s + ], $v); + $publicKey = $publicKey->encode('hex'); + } else { + $publicKey = $this->privateKey->getPublic(false, 'hex'); + } + $from = '0x' . substr($this->util->sha3(substr(hex2bin($publicKey), 1)), 24); + + $this->offsetSet('from', $from); + return $from; + } +} \ No newline at end of file From 88ea684d26353f8880a3e8aa5f4680a5ea8c53e8 Mon Sep 17 00:00:00 2001 From: sc0vu Date: Sun, 29 Aug 2021 06:27:03 +0000 Subject: [PATCH 7/8] Update README.md --- README.md | 114 +++++++++++------------------------------------------- 1 file changed, 22 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index 6826d12..1d9746c 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ composer require web3p/ethereum-tx # Usage -Create a transaction: +## Create a transaction ```php use Web3p\EthereumTx\Transaction; @@ -24,7 +24,7 @@ $transaction = new Transaction([ 'gas' => '0x76c0', 'gasPrice' => '0x9184e72a000', 'value' => '0x9184e72a', - 'data' => '0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675' + 'data' => '' ]); // with chainId @@ -43,122 +43,52 @@ $transaction = new Transaction([ $transaction = new Transaction('0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83'); ``` -Sign a transaction: -```php -use Web3p\EthereumTx\Transaction; - -$signedTransaction = $transaction->sign('your private key'); -``` - -# API - -### Web3p\EthereumTx\Transaction - -#### sha3 - -Returns keccak256 encoding of given data. - -> It will be removed in the next version. - -`sha3(string $input)` - -String input - -###### Example - -* Encode string. - +## Create a EIP1559 transaction ```php -use Web3p\EthereumTx\Transaction; +use Web3p\EthereumTx\EIP1559Transaction; -$transaction = new Transaction([ +// generate transaction instance with transaction parameters +$transaction = new EIP1559Transaction([ 'nonce' => '0x01', 'from' => '0xb60e8dd61c5d32be8058bb8eb970870f07233155', 'to' => '0xd46e8dd67c5d32be8058bb8eb970870f07244567', + 'maxPriorityFeePerGas' => '0x9184e72a000', + 'maxFeePerGas' => '0x9184e72a000', 'gas' => '0x76c0', - 'gasPrice' => '0x9184e72a000', 'value' => '0x9184e72a', - 'data' => '0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675' + 'chainId' => 1, // required + 'accessList' => [], + 'data' => '' ]); -$hashedString = $transaction->sha3('web3p'); ``` -#### serialize - -Returns recursive length prefix encoding of transaction data. - -`serialize()` - -###### Example - -* Serialize the transaction data. - +## Create a EIP2930 transaction: ```php -use Web3p\EthereumTx\Transaction; +use Web3p\EthereumTx\EIP2930Transaction; -$transaction = new Transaction([ +// generate transaction instance with transaction parameters +$transaction = new EIP2930Transaction([ 'nonce' => '0x01', 'from' => '0xb60e8dd61c5d32be8058bb8eb970870f07233155', 'to' => '0xd46e8dd67c5d32be8058bb8eb970870f07244567', 'gas' => '0x76c0', - 'gasPrice' => '0x9184e72a000', 'value' => '0x9184e72a', - 'data' => '0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675' + 'chainId' => 1, // required + 'accessList' => [], + 'data' => '' ]); -$serializedTx = $transaction->serialize(); ``` -#### sign - -Returns signed of transaction data. - -`sign(string $privateKey)` - -String privateKey - hexed private key with zero prefixed. - -###### Example - -* Sign the transaction data. - +## Sign a transaction: ```php use Web3p\EthereumTx\Transaction; -$transaction = new Transaction([ - 'nonce' => '0x01', - 'from' => '0xb60e8dd61c5d32be8058bb8eb970870f07233155', - 'to' => '0xd46e8dd67c5d32be8058bb8eb970870f07244567', - 'gas' => '0x76c0', - 'gasPrice' => '0x9184e72a000', - 'value' => '0x9184e72a', - 'data' => '0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675' -]); -$signedTx = $transaction->sign($stringPrivateKey); +$signedTransaction = $transaction->sign('your private key'); ``` -#### hash - -Returns keccak256 encoding of serialized transaction data. - -`hash()` - -###### Example - -* Hash serialized transaction data. - -```php -use Web3p\EthereumTx\Transaction; +# API -$transaction = new Transaction([ - 'nonce' => '0x01', - 'from' => '0xb60e8dd61c5d32be8058bb8eb970870f07233155', - 'to' => '0xd46e8dd67c5d32be8058bb8eb970870f07244567', - 'gas' => '0x76c0', - 'gasPrice' => '0x9184e72a000', - 'value' => '0x9184e72a', - 'data' => '0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675' -]); -$hashedTx = $transaction->serialize(); -``` +https://www.web3p.xyz/ethereumtx.html # License MIT From b98241f194ab3268fa5cb95825d60b172b5341bf Mon Sep 17 00:00:00 2001 From: sc0vu Date: Mon, 30 Aug 2021 10:28:26 +0000 Subject: [PATCH 8/8] Update web3p/rlp to 0.3.4 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 748e386..058459a 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ }, "require": { "PHP": "^7.1|^8.0", - "web3p/rlp": "0.3.3", + "web3p/rlp": "0.3.4", "web3p/ethereum-util": "~0.1.3", "kornrunner/keccak": "~1", "simplito/elliptic-php": "~1.0.6"