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/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 diff --git a/composer.json b/composer.json index 33f95bc..058459a 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,8 +23,8 @@ } }, "require": { - "PHP": "^7.1 | ^8.0", - "web3p/rlp": "0.3.3", + "PHP": "^7.1|^8.0", + "web3p/rlp": "0.3.4", "web3p/ethereum-util": "~0.1.3", "kornrunner/keccak": "~1", "simplito/elliptic-php": "~1.0.6" 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 diff --git a/src/EIP1559Transaction.php b/src/EIP1559Transaction.php new file mode 100644 index 0000000..52e44e6 --- /dev/null +++ b/src/EIP1559Transaction.php @@ -0,0 +1,234 @@ + + * + * @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; +use Web3p\EthereumTx\TypeTransaction; + +/** + * It's a instance for generating/serializing ethereum eip1559 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 extends TypeTransaction +{ + /** + * 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 + ] + ]; + + /** + * Transaction type + * + * @var string + */ + protected $transactionType = '02'; + + /** + * construct + * + * @param array|string $txData + * @return void + */ + public function __construct($txData=[]) + { + parent::__construct($txData); + } + + /** + * 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, 12, ''); + 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, 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)); + } +} \ No newline at end of file diff --git a/src/EIP2930Transaction.php b/src/EIP2930Transaction.php new file mode 100644 index 0000000..3128897 --- /dev/null +++ b/src/EIP2930Transaction.php @@ -0,0 +1,197 @@ + + * + * @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; +use Web3p\EthereumTx\TypeTransaction; + +/** + * It's a instance for generating/serializing ethereum eip2930 transaction. + * + * ```php + * use Web3p\EthereumTx\EIP2930Transaction; + * + * // generate transaction instance with transaction parameters + * $transaction = new EIP2930Transaction([ + * '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 EIP2930Transaction('0x01f86604158504a817c8008252089435353535353535353535353535353535353535358080c001a09753969d39f6a5109095d5082d67fc99a05fd66a339ba80934504ff79474e77aa07a907eb764b72b3088a331e7b97c2bad5fd43f1d574ddc80edeb022476454adb'); + * ``` + * + * ```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 extends TypeTransaction +{ + /** + * 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 + ] + ]; + + /** + * Transaction type + * + * @var string + */ + protected $transactionType = '01'; + + /** + * construct + * + * @param array|string $txData + * @return void + */ + public function __construct($txData=[]) + { + parent::__construct($txData); + } + + /** + * 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, 11, ''); + foreach ($this->txData as $key => $data) { + if ($key >= 0) { + $txData[$key] = $data; + } + } + $transactionType = $this->transactionType; + return $transactionType . $this->rlp->encode($txData); + } + + /** + * 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, 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)); + } +} \ 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 diff --git a/test/unit/TransactionTest.php b/test/unit/TransactionTest.php index 197a7e8..f97f9d6 100644 --- a/test/unit/TransactionTest.php +++ b/test/unit/TransactionTest.php @@ -4,6 +4,8 @@ use Test\TestCase; use Web3p\EthereumTx\Transaction; +use Web3p\EthereumTx\EIP2930Transaction; +use Web3p\EthereumTx\EIP1559Transaction; class TransactionTest extends TestCase { @@ -347,4 +349,57 @@ public function testIssue26() $this->assertEquals($transaction->txData, []); } } + + /** + * testEIP2930 + * see: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2930.md + * + * @return void + */ + public function testEIP2930() + { + $transaction = new EIP2930Transaction([ + 'nonce' => '0x15', + 'to' => '0x3535353535353535353535353535353535353535', + 'gas' => '0x5208', + 'gasPrice' => '0x4a817c800', + 'value' => '0x0', + 'chainId' => 4, + 'accessList' => [ + ], + 'data' => '' + ]); + $this->assertEquals('01f86604158504a817c8008252089435353535353535353535353535353535353535358080c001a09753969d39f6a5109095d5082d67fc99a05fd66a339ba80934504ff79474e77aa07a907eb764b72b3088a331e7b97c2bad5fd43f1d574ddc80edeb022476454adb', $transaction->sign('0x4646464646464646464646464646464646464646464646464646464646464646')); + + $transaction = new EIP2930Transaction('0x01f86604158504a817c8008252089435353535353535353535353535353535353535358080c001a09753969d39f6a5109095d5082d67fc99a05fd66a339ba80934504ff79474e77aa07a907eb764b72b3088a331e7b97c2bad5fd43f1d574ddc80edeb022476454adb'); + $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')); + } }