├── .github └── workflows │ └── php.yml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── src ├── EIP1559Transaction.php ├── EIP2930Transaction.php ├── Transaction.php └── TypeTransaction.php └── test ├── TestCase.php └── unit └── TransactionTest.php /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: PHP 2 | 3 | on: ["push", "pull_request"] 4 | 5 | jobs: 6 | build_and_test: 7 | name: Build and test ethereum-tx with ${{ matrix.php-version }} 8 | strategy: 9 | matrix: 10 | php-version: ["7.3", "7.4", "8.0"] 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Setup PHP 16 | uses: shivammathur/setup-php@v2 17 | with: 18 | php-version: ${{ matrix.php-version }} 19 | 20 | - name: PHP version 21 | run: | 22 | php --version 23 | 24 | - uses: actions/checkout@v2 25 | 26 | - name: Validate composer.json and composer.lock 27 | run: composer validate 28 | 29 | - name: Cache Composer packages 30 | id: composer-cache 31 | uses: actions/cache@v2 32 | with: 33 | path: vendor 34 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 35 | restore-keys: | 36 | ${{ runner.os }}-php- 37 | 38 | - name: Install dependencies 39 | if: steps.composer-cache.outputs.cache-hit != 'true' 40 | run: composer install --prefer-dist --no-progress --no-suggest 41 | 42 | - name: Run test suite 43 | run: vendor/bin/phpunit --coverage-clover=coverage.xml 44 | 45 | - uses: codecov/codecov-action@v1 46 | with: 47 | token: ${{ secrets.CODECOV_TOKEN }} 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | /vendor/ 3 | composer.lock 4 | .DS_Store 5 | .phpunit.result.cache 6 | # Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file 7 | # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file 8 | # composer.lock 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 web3p 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ethereum-tx 2 | [![PHP](https://github.com/web3p/ethereum-tx/actions/workflows/php.yml/badge.svg)](https://github.com/web3p/ethereum-tx/actions/workflows/php.yml) 3 | [![codecov](https://codecov.io/gh/web3p/ethereum-tx/branch/master/graph/badge.svg)](https://codecov.io/gh/web3p/ethereum-tx) 4 | 5 | Ethereum transaction library in PHP. 6 | 7 | # Install 8 | 9 | ``` 10 | composer require web3p/ethereum-tx 11 | ``` 12 | 13 | # Usage 14 | 15 | ## Create a transaction 16 | ```php 17 | use Web3p\EthereumTx\Transaction; 18 | 19 | // without chainId 20 | $transaction = new Transaction([ 21 | 'nonce' => '0x01', 22 | 'from' => '0xb60e8dd61c5d32be8058bb8eb970870f07233155', 23 | 'to' => '0xd46e8dd67c5d32be8058bb8eb970870f07244567', 24 | 'gas' => '0x76c0', 25 | 'gasPrice' => '0x9184e72a000', 26 | 'value' => '0x9184e72a', 27 | 'data' => '' 28 | ]); 29 | 30 | // with chainId 31 | $transaction = new Transaction([ 32 | 'nonce' => '0x01', 33 | 'from' => '0xb60e8dd61c5d32be8058bb8eb970870f07233155', 34 | 'to' => '0xd46e8dd67c5d32be8058bb8eb970870f07244567', 35 | 'gas' => '0x76c0', 36 | 'gasPrice' => '0x9184e72a000', 37 | 'value' => '0x9184e72a', 38 | 'chainId' => 1, 39 | 'data' => '0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675' 40 | ]); 41 | 42 | // hex encoded transaction 43 | $transaction = new Transaction('0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83'); 44 | ``` 45 | 46 | ## Create a EIP1559 transaction 47 | ```php 48 | use Web3p\EthereumTx\EIP1559Transaction; 49 | 50 | // generate transaction instance with transaction parameters 51 | $transaction = new EIP1559Transaction([ 52 | 'nonce' => '0x01', 53 | 'from' => '0xb60e8dd61c5d32be8058bb8eb970870f07233155', 54 | 'to' => '0xd46e8dd67c5d32be8058bb8eb970870f07244567', 55 | 'maxPriorityFeePerGas' => '0x9184e72a000', 56 | 'maxFeePerGas' => '0x9184e72a000', 57 | 'gas' => '0x76c0', 58 | 'value' => '0x9184e72a', 59 | 'chainId' => 1, // required 60 | 'accessList' => [], 61 | 'data' => '' 62 | ]); 63 | ``` 64 | 65 | ## Create a EIP2930 transaction: 66 | ```php 67 | use Web3p\EthereumTx\EIP2930Transaction; 68 | 69 | // generate transaction instance with transaction parameters 70 | $transaction = new EIP2930Transaction([ 71 | 'nonce' => '0x01', 72 | 'from' => '0xb60e8dd61c5d32be8058bb8eb970870f07233155', 73 | 'to' => '0xd46e8dd67c5d32be8058bb8eb970870f07244567', 74 | 'gas' => '0x76c0', 75 | 'value' => '0x9184e72a', 76 | 'chainId' => 1, // required 77 | 'accessList' => [], 78 | 'data' => '' 79 | ]); 80 | ``` 81 | 82 | ## Sign a transaction: 83 | ```php 84 | use Web3p\EthereumTx\Transaction; 85 | 86 | $signedTransaction = $transaction->sign('your private key'); 87 | ``` 88 | 89 | # API 90 | 91 | https://www.web3p.xyz/ethereumtx.html 92 | 93 | # License 94 | MIT 95 | 96 | 97 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web3p/ethereum-tx", 3 | "description": "Ethereum transaction library in PHP.", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "sc0Vu", 9 | "email": "alk03073135@gmail.com" 10 | } 11 | ], 12 | "require-dev": { 13 | "phpunit/phpunit": "~7|~8.0" 14 | }, 15 | "autoload": { 16 | "psr-4": { 17 | "Web3p\\EthereumTx\\": "src/" 18 | } 19 | }, 20 | "autoload-dev": { 21 | "psr-4": { 22 | "Test\\": "test/" 23 | } 24 | }, 25 | "require": { 26 | "PHP": "^7.1|^8.0", 27 | "web3p/rlp": "0.3.5", 28 | "web3p/ethereum-util": "~0.1.3", 29 | "kornrunner/keccak": "~1", 30 | "simplito/elliptic-php": "~1.0.6" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./test/unit 14 | 15 | 16 | 17 | 18 | ./src 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/EIP1559Transaction.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * @author Peter Lai 9 | * @license MIT 10 | */ 11 | 12 | namespace Web3p\EthereumTx; 13 | 14 | use InvalidArgumentException; 15 | use RuntimeException; 16 | use Web3p\RLP\RLP; 17 | use Elliptic\EC; 18 | use Elliptic\EC\KeyPair; 19 | use ArrayAccess; 20 | use Web3p\EthereumUtil\Util; 21 | use Web3p\EthereumTx\TypeTransaction; 22 | 23 | /** 24 | * It's a instance for generating/serializing ethereum eip1559 transaction. 25 | * 26 | * ```php 27 | * use Web3p\EthereumTx\EIP1559Transaction; 28 | * 29 | * // generate transaction instance with transaction parameters 30 | * $transaction = new EIP1559Transaction([ 31 | * 'nonce' => '0x01', 32 | * 'from' => '0xb60e8dd61c5d32be8058bb8eb970870f07233155', 33 | * 'to' => '0xd46e8dd67c5d32be8058bb8eb970870f07244567', 34 | * 'maxPriorityFeePerGas' => '0x9184e72a000', 35 | * 'maxFeePerGas' => '0x9184e72a000', 36 | * 'gas' => '0x76c0', 37 | * 'value' => '0x9184e72a', 38 | * 'chainId' => 1, // required 39 | * 'accessList' => [], 40 | * 'data' => '0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675' 41 | * ]); 42 | * 43 | * // generate transaction instance with hex encoded transaction 44 | * $transaction = new EIP1559Transaction('0x02f86c04158504a817c8008504a817c8008252089435353535353535353535353535353535353535358080c080a03fd48c8a173e9669c33cb5271f03b1af4f030dc8315be8ec9442b7fbdde893c8a010af381dab1df3e7012a3c8421d65a810859a5dd9d58991ad7c07f12d0c651c7'); 45 | * ``` 46 | * 47 | * ```php 48 | * After generate transaction instance, you can sign transaction with your private key. 49 | * 50 | * $signedTransaction = $transaction->sign('your private key'); 51 | * ``` 52 | * 53 | * Then you can send serialized transaction to ethereum through http rpc with web3.php. 54 | * ```php 55 | * $hashedTx = $transaction->serialize(); 56 | * ``` 57 | * 58 | * @author Peter Lai 59 | * @link https://www.web3p.xyz 60 | * @filesource https://github.com/web3p/ethereum-tx 61 | */ 62 | class EIP1559Transaction extends TypeTransaction 63 | { 64 | /** 65 | * Attribute map for keeping order of transaction key/value 66 | * 67 | * @var array 68 | */ 69 | protected $attributeMap = [ 70 | 'from' => [ 71 | 'key' => -1 72 | ], 73 | 'chainId' => [ 74 | 'key' => 0 75 | ], 76 | 'nonce' => [ 77 | 'key' => 1, 78 | 'length' => 32, 79 | 'allowLess' => true, 80 | 'allowZero' => false 81 | ], 82 | 'maxPriorityFeePerGas' => [ 83 | 'key' => 2, 84 | 'length' => 32, 85 | 'allowLess' => true, 86 | 'allowZero' => false 87 | ], 88 | 'maxFeePerGas' => [ 89 | 'key' => 3, 90 | 'length' => 32, 91 | 'allowLess' => true, 92 | 'allowZero' => false 93 | ], 94 | 'gasLimit' => [ 95 | 'key' => 4, 96 | 'length' => 32, 97 | 'allowLess' => true, 98 | 'allowZero' => false 99 | ], 100 | 'gas' => [ 101 | 'key' => 4, 102 | 'length' => 32, 103 | 'allowLess' => true, 104 | 'allowZero' => false 105 | ], 106 | 'to' => [ 107 | 'key' => 5, 108 | 'length' => 20, 109 | 'allowZero' => true, 110 | ], 111 | 'value' => [ 112 | 'key' => 6, 113 | 'length' => 32, 114 | 'allowLess' => true, 115 | 'allowZero' => false 116 | ], 117 | 'data' => [ 118 | 'key' => 7, 119 | 'allowLess' => true, 120 | 'allowZero' => true 121 | ], 122 | 'accessList' => [ 123 | 'key' => 8, 124 | 'allowLess' => true, 125 | 'allowZero' => true, 126 | 'allowArray' => true 127 | ], 128 | 'v' => [ 129 | 'key' => 9, 130 | 'allowZero' => true 131 | ], 132 | 'r' => [ 133 | 'key' => 10, 134 | 'length' => 32, 135 | 'allowZero' => true 136 | ], 137 | 's' => [ 138 | 'key' => 11, 139 | 'length' => 32, 140 | 'allowZero' => true 141 | ] 142 | ]; 143 | 144 | /** 145 | * Transaction type 146 | * 147 | * @var string 148 | */ 149 | protected $transactionType = '02'; 150 | 151 | /** 152 | * construct 153 | * 154 | * @param array|string $txData 155 | * @return void 156 | */ 157 | public function __construct($txData=[]) 158 | { 159 | parent::__construct($txData); 160 | } 161 | 162 | /** 163 | * RLP serialize the ethereum transaction. 164 | * 165 | * @return string hex encoded of the serialized ethereum transaction 166 | */ 167 | public function serialize() 168 | { 169 | // sort tx data 170 | if (ksort($this->txData) !== true) { 171 | throw new RuntimeException('Cannot sort tx data by keys.'); 172 | } 173 | $txData = array_fill(0, 12, ''); 174 | foreach ($this->txData as $key => $data) { 175 | if ($key >= 0) { 176 | $txData[$key] = $data; 177 | } 178 | } 179 | $transactionType = $this->transactionType; 180 | return $transactionType . $this->rlp->encode($txData); 181 | } 182 | 183 | /** 184 | * Sign the transaction with given hex encoded private key. 185 | * 186 | * @param string $privateKey hex encoded private key 187 | * @return string hex encoded signed ethereum transaction 188 | */ 189 | public function sign(string $privateKey) 190 | { 191 | if ($this->util->isHex($privateKey)) { 192 | $privateKey = $this->util->stripZero($privateKey); 193 | $ecPrivateKey = $this->secp256k1->keyFromPrivate($privateKey, 'hex'); 194 | } else { 195 | throw new InvalidArgumentException('Private key should be hex encoded string'); 196 | } 197 | $txHash = $this->hash(); 198 | $signature = $ecPrivateKey->sign($txHash, [ 199 | 'canonical' => true 200 | ]); 201 | $r = $signature->r; 202 | $s = $signature->s; 203 | $v = $signature->recoveryParam; 204 | 205 | $this->offsetSet('r', '0x' . $r->toString(16)); 206 | $this->offsetSet('s', '0x' . $s->toString(16)); 207 | $this->offsetSet('v', $v); 208 | $this->privateKey = $ecPrivateKey; 209 | 210 | return $this->serialize(); 211 | } 212 | 213 | /** 214 | * Return hash of the ethereum transaction with/without signature. 215 | * 216 | * @param bool $includeSignature hash with signature 217 | * @return string hex encoded hash of the ethereum transaction 218 | */ 219 | public function hash(bool $includeSignature=false) 220 | { 221 | // sort tx data 222 | if (ksort($this->txData) !== true) { 223 | throw new RuntimeException('Cannot sort tx data by keys.'); 224 | } 225 | if ($includeSignature) { 226 | $length = 12; 227 | } else { 228 | $length = 9; 229 | } 230 | $rawTxData = array_fill(0, $length, ''); 231 | for ($key = 0; $key < $length; $key++) { 232 | if (isset($this->txData[$key])) { 233 | $rawTxData[$key] = $this->txData[$key]; 234 | } 235 | } 236 | $serializedTx = $this->rlp->encode($rawTxData); 237 | $transactionType = $this->transactionType; 238 | return $this->util->sha3(hex2bin($transactionType . $serializedTx)); 239 | } 240 | } -------------------------------------------------------------------------------- /src/EIP2930Transaction.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * @author Peter Lai 9 | * @license MIT 10 | */ 11 | 12 | namespace Web3p\EthereumTx; 13 | 14 | use InvalidArgumentException; 15 | use RuntimeException; 16 | use Web3p\RLP\RLP; 17 | use Elliptic\EC; 18 | use Elliptic\EC\KeyPair; 19 | use ArrayAccess; 20 | use Web3p\EthereumUtil\Util; 21 | use Web3p\EthereumTx\TypeTransaction; 22 | 23 | /** 24 | * It's a instance for generating/serializing ethereum eip2930 transaction. 25 | * 26 | * ```php 27 | * use Web3p\EthereumTx\EIP2930Transaction; 28 | * 29 | * // generate transaction instance with transaction parameters 30 | * $transaction = new EIP2930Transaction([ 31 | * 'nonce' => '0x01', 32 | * 'from' => '0xb60e8dd61c5d32be8058bb8eb970870f07233155', 33 | * 'to' => '0xd46e8dd67c5d32be8058bb8eb970870f07244567', 34 | * 'gas' => '0x76c0', 35 | * 'gasPrice' => '0x9184e72a000', 36 | * 'value' => '0x9184e72a', 37 | * 'chainId' => 1, // required 38 | * 'accessList' => [], 39 | * 'data' => '0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675' 40 | * ]); 41 | * 42 | * // generate transaction instance with hex encoded transaction 43 | * $transaction = new EIP2930Transaction('0x01f86604158504a817c8008252089435353535353535353535353535353535353535358080c001a09753969d39f6a5109095d5082d67fc99a05fd66a339ba80934504ff79474e77aa07a907eb764b72b3088a331e7b97c2bad5fd43f1d574ddc80edeb022476454adb'); 44 | * ``` 45 | * 46 | * ```php 47 | * After generate transaction instance, you can sign transaction with your private key. 48 | * 49 | * $signedTransaction = $transaction->sign('your private key'); 50 | * ``` 51 | * 52 | * Then you can send serialized transaction to ethereum through http rpc with web3.php. 53 | * ```php 54 | * $hashedTx = $transaction->serialize(); 55 | * ``` 56 | * 57 | * @author Peter Lai 58 | * @link https://www.web3p.xyz 59 | * @filesource https://github.com/web3p/ethereum-tx 60 | */ 61 | class EIP2930Transaction extends TypeTransaction 62 | { 63 | /** 64 | * Attribute map for keeping order of transaction key/value 65 | * 66 | * @var array 67 | */ 68 | protected $attributeMap = [ 69 | 'from' => [ 70 | 'key' => -1 71 | ], 72 | 'chainId' => [ 73 | 'key' => 0 74 | ], 75 | 'nonce' => [ 76 | 'key' => 1, 77 | 'length' => 32, 78 | 'allowLess' => true, 79 | 'allowZero' => false 80 | ], 81 | 'gasPrice' => [ 82 | 'key' => 2, 83 | 'length' => 32, 84 | 'allowLess' => true, 85 | 'allowZero' => false 86 | ], 87 | 'gasLimit' => [ 88 | 'key' => 3, 89 | 'length' => 32, 90 | 'allowLess' => true, 91 | 'allowZero' => false 92 | ], 93 | 'gas' => [ 94 | 'key' => 3, 95 | 'length' => 32, 96 | 'allowLess' => true, 97 | 'allowZero' => false 98 | ], 99 | 'to' => [ 100 | 'key' => 4, 101 | 'length' => 20, 102 | 'allowZero' => true, 103 | ], 104 | 'value' => [ 105 | 'key' => 5, 106 | 'length' => 32, 107 | 'allowLess' => true, 108 | 'allowZero' => false 109 | ], 110 | 'data' => [ 111 | 'key' => 6, 112 | 'allowLess' => true, 113 | 'allowZero' => true 114 | ], 115 | 'accessList' => [ 116 | 'key' => 7, 117 | 'allowLess' => true, 118 | 'allowZero' => true, 119 | 'allowArray' => true 120 | ], 121 | 'v' => [ 122 | 'key' => 8, 123 | 'allowZero' => true 124 | ], 125 | 'r' => [ 126 | 'key' => 9, 127 | 'length' => 32, 128 | 'allowZero' => true 129 | ], 130 | 's' => [ 131 | 'key' => 10, 132 | 'length' => 32, 133 | 'allowZero' => true 134 | ] 135 | ]; 136 | 137 | /** 138 | * Transaction type 139 | * 140 | * @var string 141 | */ 142 | protected $transactionType = '01'; 143 | 144 | /** 145 | * construct 146 | * 147 | * @param array|string $txData 148 | * @return void 149 | */ 150 | public function __construct($txData=[]) 151 | { 152 | parent::__construct($txData); 153 | } 154 | 155 | /** 156 | * RLP serialize the ethereum transaction. 157 | * 158 | * @return string hex encoded of the serialized ethereum transaction 159 | */ 160 | public function serialize() 161 | { 162 | // sort tx data 163 | if (ksort($this->txData) !== true) { 164 | throw new RuntimeException('Cannot sort tx data by keys.'); 165 | } 166 | $txData = array_fill(0, 11, ''); 167 | foreach ($this->txData as $key => $data) { 168 | if ($key >= 0) { 169 | $txData[$key] = $data; 170 | } 171 | } 172 | $transactionType = $this->transactionType; 173 | return $transactionType . $this->rlp->encode($txData); 174 | } 175 | 176 | /** 177 | * Return hash of the ethereum transaction with/without signature. 178 | * 179 | * @param bool $includeSignature hash with signature 180 | * @return string hex encoded hash of the ethereum transaction 181 | */ 182 | public function hash(bool $includeSignature=false) 183 | { 184 | // sort tx data 185 | if (ksort($this->txData) !== true) { 186 | throw new RuntimeException('Cannot sort tx data by keys.'); 187 | } 188 | if ($includeSignature) { 189 | $length = 11; 190 | } else { 191 | $length = 8; 192 | } 193 | $rawTxData = array_fill(0, $length, ''); 194 | for ($key = 0; $key < $length; $key++) { 195 | if (isset($this->txData[$key])) { 196 | $rawTxData[$key] = $this->txData[$key]; 197 | } 198 | } 199 | $serializedTx = $this->rlp->encode($rawTxData); 200 | $transactionType = $this->transactionType; 201 | return $this->util->sha3(hex2bin($transactionType . $serializedTx)); 202 | } 203 | } -------------------------------------------------------------------------------- /src/Transaction.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * @author Peter Lai 9 | * @license MIT 10 | */ 11 | 12 | namespace Web3p\EthereumTx; 13 | 14 | use InvalidArgumentException; 15 | use RuntimeException; 16 | use Web3p\RLP\RLP; 17 | use Elliptic\EC; 18 | use Elliptic\EC\KeyPair; 19 | use ArrayAccess; 20 | use Web3p\EthereumUtil\Util; 21 | 22 | /** 23 | * It's a instance for generating/serializing ethereum transaction. 24 | * 25 | * ```php 26 | * use Web3p\EthereumTx\Transaction; 27 | * 28 | * // generate transaction instance with transaction parameters 29 | * $transaction = new Transaction([ 30 | * 'nonce' => '0x01', 31 | * 'from' => '0xb60e8dd61c5d32be8058bb8eb970870f07233155', 32 | * 'to' => '0xd46e8dd67c5d32be8058bb8eb970870f07244567', 33 | * 'gas' => '0x76c0', 34 | * 'gasPrice' => '0x9184e72a000', 35 | * 'value' => '0x9184e72a', 36 | * 'chainId' => 1, // optional 37 | * 'data' => '0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675' 38 | * ]); 39 | * 40 | * // generate transaction instance with hex encoded transaction 41 | * $transaction = new Transaction('0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83'); 42 | * ``` 43 | * 44 | * ```php 45 | * After generate transaction instance, you can sign transaction with your private key. 46 | * 47 | * $signedTransaction = $transaction->sign('your private key'); 48 | * ``` 49 | * 50 | * Then you can send serialized transaction to ethereum through http rpc with web3.php. 51 | * ```php 52 | * $hashedTx = $transaction->serialize(); 53 | * ``` 54 | * 55 | * @author Peter Lai 56 | * @link https://www.web3p.xyz 57 | * @filesource https://github.com/web3p/ethereum-tx 58 | */ 59 | class Transaction implements ArrayAccess 60 | { 61 | /** 62 | * Attribute map for keeping order of transaction key/value 63 | * 64 | * @var array 65 | */ 66 | protected $attributeMap = [ 67 | 'from' => [ 68 | 'key' => -1 69 | ], 70 | 'chainId' => [ 71 | 'key' => -2 72 | ], 73 | 'nonce' => [ 74 | 'key' => 0, 75 | 'length' => 32, 76 | 'allowLess' => true, 77 | 'allowZero' => false 78 | ], 79 | 'gasPrice' => [ 80 | 'key' => 1, 81 | 'length' => 32, 82 | 'allowLess' => true, 83 | 'allowZero' => false 84 | ], 85 | 'gasLimit' => [ 86 | 'key' => 2, 87 | 'length' => 32, 88 | 'allowLess' => true, 89 | 'allowZero' => false 90 | ], 91 | 'gas' => [ 92 | 'key' => 2, 93 | 'length' => 32, 94 | 'allowLess' => true, 95 | 'allowZero' => false 96 | ], 97 | 'to' => [ 98 | 'key' => 3, 99 | 'length' => 20, 100 | 'allowZero' => true, 101 | ], 102 | 'value' => [ 103 | 'key' => 4, 104 | 'length' => 32, 105 | 'allowLess' => true, 106 | 'allowZero' => false 107 | ], 108 | 'data' => [ 109 | 'key' => 5, 110 | 'allowLess' => true, 111 | 'allowZero' => true 112 | ], 113 | 'v' => [ 114 | 'key' => 6, 115 | 'allowZero' => true 116 | ], 117 | 'r' => [ 118 | 'key' => 7, 119 | 'length' => 32, 120 | 'allowZero' => true 121 | ], 122 | 's' => [ 123 | 'key' => 8, 124 | 'length' => 32, 125 | 'allowZero' => true 126 | ] 127 | ]; 128 | 129 | /** 130 | * Raw transaction data 131 | * 132 | * @var array 133 | */ 134 | protected $txData = []; 135 | 136 | /** 137 | * RLP encoding instance 138 | * 139 | * @var \Web3p\RLP\RLP 140 | */ 141 | protected $rlp; 142 | 143 | /** 144 | * secp256k1 elliptic curve instance 145 | * 146 | * @var \Elliptic\EC 147 | */ 148 | protected $secp256k1; 149 | 150 | /** 151 | * Private key instance 152 | * 153 | * @var \Elliptic\EC\KeyPair 154 | */ 155 | protected $privateKey; 156 | 157 | /** 158 | * Ethereum util instance 159 | * 160 | * @var \Web3p\EthereumUtil\Util 161 | */ 162 | protected $util; 163 | 164 | /** 165 | * construct 166 | * 167 | * @param array|string $txData 168 | * @return void 169 | */ 170 | public function __construct($txData=[]) 171 | { 172 | $this->rlp = new RLP; 173 | $this->secp256k1 = new EC('secp256k1'); 174 | $this->util = new Util; 175 | 176 | if (is_array($txData)) { 177 | foreach ($txData as $key => $data) { 178 | $this->offsetSet($key, $data); 179 | } 180 | } elseif (is_string($txData)) { 181 | $tx = []; 182 | 183 | if ($this->util->isHex($txData)) { 184 | $txData = $this->rlp->decode($txData); 185 | 186 | foreach ($txData as $txKey => $data) { 187 | if (is_int($txKey)) { 188 | $hexData = $data; 189 | 190 | if (strlen($hexData) > 0) { 191 | $tx[$txKey] = '0x' . $hexData; 192 | } else { 193 | $tx[$txKey] = $hexData; 194 | } 195 | } 196 | } 197 | } 198 | $this->txData = $tx; 199 | } 200 | } 201 | 202 | /** 203 | * Return the value in the transaction with given key or return the protected property value if get(property_name} function is existed. 204 | * 205 | * @param string $name key or protected property name 206 | * @return mixed 207 | */ 208 | public function __get(string $name) 209 | { 210 | $method = 'get' . ucfirst($name); 211 | 212 | if (method_exists($this, $method)) { 213 | return call_user_func_array([$this, $method], []); 214 | } 215 | return $this->offsetGet($name); 216 | } 217 | 218 | /** 219 | * Set the value in the transaction with given key or return the protected value if set(property_name} function is existed. 220 | * 221 | * @param string $name key, eg: to 222 | * @param mixed value 223 | * @return void 224 | */ 225 | public function __set(string $name, $value) 226 | { 227 | $method = 'set' . ucfirst($name); 228 | 229 | if (method_exists($this, $method)) { 230 | return call_user_func_array([$this, $method], [$value]); 231 | } 232 | return $this->offsetSet($name, $value); 233 | } 234 | 235 | /** 236 | * Return hash of the ethereum transaction without signature. 237 | * 238 | * @return string hex encoded of the transaction 239 | */ 240 | public function __toString() 241 | { 242 | return $this->hash(false); 243 | } 244 | 245 | /** 246 | * Set the value in the transaction with given key. 247 | * 248 | * @param string $offset key, eg: to 249 | * @param string value 250 | * @return void 251 | */ 252 | public function offsetSet($offset, $value) 253 | { 254 | $txKey = isset($this->attributeMap[$offset]) ? $this->attributeMap[$offset] : null; 255 | 256 | if (is_array($txKey)) { 257 | $checkedValue = ($value) ? (string) $value : ''; 258 | $isHex = $this->util->isHex($checkedValue); 259 | $checkedValue = $this->util->stripZero($checkedValue); 260 | 261 | if (!isset($txKey['allowLess']) || (isset($txKey['allowLess']) && $txKey['allowLess'] === false)) { 262 | // check length 263 | if (isset($txKey['length'])) { 264 | if ($isHex) { 265 | if (strlen($checkedValue) > $txKey['length'] * 2) { 266 | throw new InvalidArgumentException($offset . ' exceeds the length limit.'); 267 | } 268 | } else { 269 | if (strlen($checkedValue) > $txKey['length']) { 270 | throw new InvalidArgumentException($offset . ' exceeds the length limit.'); 271 | } 272 | } 273 | } 274 | } 275 | if (!isset($txKey['allowZero']) || (isset($txKey['allowZero']) && $txKey['allowZero'] === false)) { 276 | // check zero 277 | if (preg_match('/^0*$/', $checkedValue) === 1) { 278 | // set value to empty string 279 | $value = ''; 280 | } 281 | } 282 | $this->txData[$txKey['key']] = $value; 283 | } 284 | } 285 | 286 | /** 287 | * Return whether the value is in the transaction with given key. 288 | * 289 | * @param string $offset key, eg: to 290 | * @return bool 291 | */ 292 | public function offsetExists($offset) 293 | { 294 | $txKey = isset($this->attributeMap[$offset]) ? $this->attributeMap[$offset] : null; 295 | 296 | if (is_array($txKey)) { 297 | return isset($this->txData[$txKey['key']]); 298 | } 299 | return false; 300 | } 301 | 302 | /** 303 | * Unset the value in the transaction with given key. 304 | * 305 | * @param string $offset key, eg: to 306 | * @return void 307 | */ 308 | public function offsetUnset($offset) 309 | { 310 | $txKey = isset($this->attributeMap[$offset]) ? $this->attributeMap[$offset] : null; 311 | 312 | if (is_array($txKey) && isset($this->txData[$txKey['key']])) { 313 | unset($this->txData[$txKey['key']]); 314 | } 315 | } 316 | 317 | /** 318 | * Return the value in the transaction with given key. 319 | * 320 | * @param string $offset key, eg: to 321 | * @return mixed value of the transaction 322 | */ 323 | public function offsetGet($offset) 324 | { 325 | $txKey = isset($this->attributeMap[$offset]) ? $this->attributeMap[$offset] : null; 326 | 327 | if (is_array($txKey) && isset($this->txData[$txKey['key']])) { 328 | return $this->txData[$txKey['key']]; 329 | } 330 | return null; 331 | } 332 | 333 | /** 334 | * Return raw ethereum transaction data. 335 | * 336 | * @return array raw ethereum transaction data 337 | */ 338 | public function getTxData() 339 | { 340 | return $this->txData; 341 | } 342 | 343 | /** 344 | * RLP serialize the ethereum transaction. 345 | * 346 | * @return string hex encoded of the serialized ethereum transaction 347 | */ 348 | public function serialize() 349 | { 350 | $chainId = $this->offsetGet('chainId'); 351 | 352 | // sort tx data 353 | if (ksort($this->txData) !== true) { 354 | throw new RuntimeException('Cannot sort tx data by keys.'); 355 | } 356 | if ($chainId && $chainId > 0) { 357 | $txData = array_fill(0, 9, ''); 358 | } else { 359 | $txData = array_fill(0, 6, ''); 360 | } 361 | foreach ($this->txData as $key => $data) { 362 | if ($key >= 0) { 363 | $txData[$key] = $data; 364 | } 365 | } 366 | return $this->rlp->encode($txData); 367 | } 368 | 369 | /** 370 | * Sign the transaction with given hex encoded private key. 371 | * 372 | * @param string $privateKey hex encoded private key 373 | * @return string hex encoded signed ethereum transaction 374 | */ 375 | public function sign(string $privateKey) 376 | { 377 | if ($this->util->isHex($privateKey)) { 378 | $privateKey = $this->util->stripZero($privateKey); 379 | $ecPrivateKey = $this->secp256k1->keyFromPrivate($privateKey, 'hex'); 380 | } else { 381 | throw new InvalidArgumentException('Private key should be hex encoded string'); 382 | } 383 | $txHash = $this->hash(false); 384 | $signature = $ecPrivateKey->sign($txHash, [ 385 | 'canonical' => true 386 | ]); 387 | $r = $signature->r; 388 | $s = $signature->s; 389 | $v = $signature->recoveryParam + 35; 390 | 391 | $chainId = $this->offsetGet('chainId'); 392 | 393 | if ($chainId && $chainId > 0) { 394 | $v += (int) $chainId * 2; 395 | } 396 | 397 | $this->offsetSet('r', '0x' . $r->toString(16)); 398 | $this->offsetSet('s', '0x' . $s->toString(16)); 399 | $this->offsetSet('v', $v); 400 | $this->privateKey = $ecPrivateKey; 401 | 402 | return $this->serialize(); 403 | } 404 | 405 | /** 406 | * Return hash of the ethereum transaction with/without signature. 407 | * 408 | * @param bool $includeSignature hash with signature 409 | * @return string hex encoded hash of the ethereum transaction 410 | */ 411 | public function hash(bool $includeSignature=false) 412 | { 413 | $chainId = $this->offsetGet('chainId'); 414 | 415 | // sort tx data 416 | if (ksort($this->txData) !== true) { 417 | throw new RuntimeException('Cannot sort tx data by keys.'); 418 | } 419 | if ($includeSignature) { 420 | $txData = $this->txData; 421 | } else { 422 | $rawTxData = $this->txData; 423 | 424 | if ($chainId && $chainId > 0) { 425 | $v = (int) $chainId; 426 | $this->offsetSet('r', ''); 427 | $this->offsetSet('s', ''); 428 | $this->offsetSet('v', $v); 429 | $txData = array_fill(0, 9, ''); 430 | } else { 431 | $txData = array_fill(0, 6, ''); 432 | } 433 | 434 | foreach ($this->txData as $key => $data) { 435 | if ($key >= 0) { 436 | $txData[$key] = $data; 437 | } 438 | } 439 | $this->txData = $rawTxData; 440 | } 441 | $serializedTx = $this->rlp->encode($txData); 442 | 443 | return $this->util->sha3(hex2bin($serializedTx)); 444 | } 445 | 446 | /** 447 | * Recover from address with given signature (r, s, v) if didn't set from. 448 | * 449 | * @return string hex encoded ethereum address 450 | */ 451 | public function getFromAddress() 452 | { 453 | $from = $this->offsetGet('from'); 454 | 455 | if ($from) { 456 | return $from; 457 | } 458 | if (!isset($this->privateKey) || !($this->privateKey instanceof KeyPair)) { 459 | // recover from hash 460 | $r = $this->offsetGet('r'); 461 | $s = $this->offsetGet('s'); 462 | $v = $this->offsetGet('v'); 463 | $chainId = $this->offsetGet('chainId'); 464 | 465 | if (!$r || !$s) { 466 | throw new RuntimeException('Invalid signature r and s.'); 467 | } 468 | $txHash = $this->hash(false); 469 | 470 | if ($chainId && $chainId > 0) { 471 | $v -= ($chainId * 2); 472 | } 473 | $v -= 35; 474 | $publicKey = $this->secp256k1->recoverPubKey($txHash, [ 475 | 'r' => $r, 476 | 's' => $s 477 | ], $v); 478 | $publicKey = $publicKey->encode('hex'); 479 | } else { 480 | $publicKey = $this->privateKey->getPublic(false, 'hex'); 481 | } 482 | $from = '0x' . substr($this->util->sha3(substr(hex2bin($publicKey), 1)), 24); 483 | 484 | $this->offsetSet('from', $from); 485 | return $from; 486 | } 487 | } -------------------------------------------------------------------------------- /src/TypeTransaction.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * @author Peter Lai 9 | * @license MIT 10 | */ 11 | 12 | namespace Web3p\EthereumTx; 13 | 14 | use InvalidArgumentException; 15 | use RuntimeException; 16 | use Web3p\RLP\RLP; 17 | use Elliptic\EC; 18 | use Elliptic\EC\KeyPair; 19 | use ArrayAccess; 20 | use Web3p\EthereumUtil\Util; 21 | 22 | /** 23 | * It's a base transaction for generating/serializing ethereum type transaction (EIP1559/EIP2930). 24 | * Only use this class to generate new type transaction 25 | * 26 | * @author Peter Lai 27 | * @link https://www.web3p.xyz 28 | * @filesource https://github.com/web3p/ethereum-tx 29 | */ 30 | class TypeTransaction implements ArrayAccess 31 | { 32 | /** 33 | * Attribute map for keeping order of transaction key/value 34 | * 35 | * @var array 36 | */ 37 | protected $attributeMap = [ 38 | 'from' => [ 39 | 'key' => -1 40 | ], 41 | 'chainId' => [ 42 | 'key' => 0 43 | ], 44 | 'nonce' => [ 45 | 'key' => 1, 46 | 'length' => 32, 47 | 'allowLess' => true, 48 | 'allowZero' => false 49 | ], 50 | 'gasPrice' => [ 51 | 'key' => 2, 52 | 'length' => 32, 53 | 'allowLess' => true, 54 | 'allowZero' => false 55 | ], 56 | 'gasLimit' => [ 57 | 'key' => 3, 58 | 'length' => 32, 59 | 'allowLess' => true, 60 | 'allowZero' => false 61 | ], 62 | 'gas' => [ 63 | 'key' => 3, 64 | 'length' => 32, 65 | 'allowLess' => true, 66 | 'allowZero' => false 67 | ], 68 | 'to' => [ 69 | 'key' => 4, 70 | 'length' => 20, 71 | 'allowZero' => true, 72 | ], 73 | 'value' => [ 74 | 'key' => 5, 75 | 'length' => 32, 76 | 'allowLess' => true, 77 | 'allowZero' => false 78 | ], 79 | 'data' => [ 80 | 'key' => 6, 81 | 'allowLess' => true, 82 | 'allowZero' => true 83 | ], 84 | 'v' => [ 85 | 'key' => 7, 86 | 'allowZero' => true 87 | ], 88 | 'r' => [ 89 | 'key' => 8, 90 | 'length' => 32, 91 | 'allowZero' => true 92 | ], 93 | 's' => [ 94 | 'key' => 9, 95 | 'length' => 32, 96 | 'allowZero' => true 97 | ] 98 | ]; 99 | 100 | /** 101 | * Raw transaction data 102 | * 103 | * @var array 104 | */ 105 | protected $txData = []; 106 | 107 | /** 108 | * RLP encoding instance 109 | * 110 | * @var \Web3p\RLP\RLP 111 | */ 112 | protected $rlp; 113 | 114 | /** 115 | * secp256k1 elliptic curve instance 116 | * 117 | * @var \Elliptic\EC 118 | */ 119 | protected $secp256k1; 120 | 121 | /** 122 | * Private key instance 123 | * 124 | * @var \Elliptic\EC\KeyPair 125 | */ 126 | protected $privateKey; 127 | 128 | /** 129 | * Ethereum util instance 130 | * 131 | * @var \Web3p\EthereumUtil\Util 132 | */ 133 | protected $util; 134 | 135 | /** 136 | * Transaction type 137 | * 138 | * @var string 139 | */ 140 | protected $transactionType = '00'; 141 | 142 | /** 143 | * construct 144 | * 145 | * @param array|string $txData 146 | * @return void 147 | */ 148 | public function __construct($txData=[]) 149 | { 150 | $this->rlp = new RLP; 151 | $this->secp256k1 = new EC('secp256k1'); 152 | $this->util = new Util; 153 | 154 | if (is_array($txData)) { 155 | foreach ($txData as $key => $data) { 156 | $this->offsetSet($key, $data); 157 | } 158 | } elseif (is_string($txData)) { 159 | $tx = []; 160 | 161 | if ($this->util->isHex($txData)) { 162 | // check first byte 163 | $txData = $this->util->stripZero($txData); 164 | $firstByteStr = substr($txData, 0, 2); 165 | $firstByte = hexdec($firstByteStr); 166 | if ($this->isTransactionTypeValid($firstByte)) { 167 | $txData = substr($txData, 2); 168 | } 169 | $txData = $this->rlp->decode($txData); 170 | 171 | foreach ($txData as $txKey => $data) { 172 | if (is_int($txKey)) { 173 | if (is_string($data) && strlen($data) > 0) { 174 | $tx[$txKey] = '0x' . $data; 175 | } else { 176 | $tx[$txKey] = $data; 177 | } 178 | } 179 | } 180 | } 181 | $this->txData = $tx; 182 | } 183 | } 184 | 185 | /** 186 | * Return the value in the transaction with given key or return the protected property value if get(property_name} function is existed. 187 | * 188 | * @param string $name key or protected property name 189 | * @return mixed 190 | */ 191 | public function __get(string $name) 192 | { 193 | $method = 'get' . ucfirst($name); 194 | 195 | if (method_exists($this, $method)) { 196 | return call_user_func_array([$this, $method], []); 197 | } 198 | return $this->offsetGet($name); 199 | } 200 | 201 | /** 202 | * Set the value in the transaction with given key or return the protected value if set(property_name} function is existed. 203 | * 204 | * @param string $name key, eg: to 205 | * @param mixed value 206 | * @return void 207 | */ 208 | public function __set(string $name, $value) 209 | { 210 | $method = 'set' . ucfirst($name); 211 | 212 | if (method_exists($this, $method)) { 213 | return call_user_func_array([$this, $method], [$value]); 214 | } 215 | return $this->offsetSet($name, $value); 216 | } 217 | 218 | /** 219 | * Return hash of the ethereum transaction without signature. 220 | * 221 | * @return string hex encoded of the transaction 222 | */ 223 | public function __toString() 224 | { 225 | return $this->hash(false); 226 | } 227 | 228 | /** 229 | * Set the value in the transaction with given key. 230 | * 231 | * @param string $offset key, eg: to 232 | * @param string value 233 | * @return void 234 | */ 235 | public function offsetSet($offset, $value) 236 | { 237 | $txKey = isset($this->attributeMap[$offset]) ? $this->attributeMap[$offset] : null; 238 | 239 | if (is_array($txKey)) { 240 | if (is_array($value)) { 241 | if (!isset($txKey['allowArray']) || (isset($txKey['allowArray']) && $txKey['allowArray'] === false)) { 242 | throw new InvalidArgumentException($offset . ' should\'t be array.'); 243 | } 244 | if (!isset($txKey['allowLess']) || (isset($txKey['allowLess']) && $txKey['allowLess'] === false)) { 245 | // check length 246 | if (isset($txKey['length'])) { 247 | if (count($value) > $txKey['length'] * 2) { 248 | throw new InvalidArgumentException($offset . ' exceeds the length limit.'); 249 | } 250 | } 251 | } 252 | if (!isset($txKey['allowZero']) || (isset($txKey['allowZero']) && $txKey['allowZero'] === false)) { 253 | // check zero 254 | foreach ($value as $key => $v) { 255 | $checkedV = $v ? (string) $v : ''; 256 | if (preg_match('/^0*$/', $checkedV) === 1) { 257 | // set value to empty string 258 | $checkedV = ''; 259 | $value[$key] = $checkedV; 260 | } 261 | } 262 | } 263 | } else { 264 | $checkedValue = ($value) ? (string) $value : ''; 265 | $isHex = $this->util->isHex($checkedValue); 266 | $checkedValue = $this->util->stripZero($checkedValue); 267 | 268 | if (!isset($txKey['allowLess']) || (isset($txKey['allowLess']) && $txKey['allowLess'] === false)) { 269 | // check length 270 | if (isset($txKey['length'])) { 271 | if ($isHex) { 272 | if (strlen($checkedValue) > $txKey['length'] * 2) { 273 | throw new InvalidArgumentException($offset . ' exceeds the length limit.'); 274 | } 275 | } else { 276 | if (strlen($checkedValue) > $txKey['length']) { 277 | throw new InvalidArgumentException($offset . ' exceeds the length limit.'); 278 | } 279 | } 280 | } 281 | } 282 | if (!isset($txKey['allowZero']) || (isset($txKey['allowZero']) && $txKey['allowZero'] === false)) { 283 | // check zero 284 | if (preg_match('/^0*$/', $checkedValue) === 1) { 285 | // set value to empty string 286 | $value = ''; 287 | } 288 | } 289 | } 290 | $this->txData[$txKey['key']] = $value; 291 | } 292 | } 293 | 294 | /** 295 | * Return whether the value is in the transaction with given key. 296 | * 297 | * @param string $offset key, eg: to 298 | * @return bool 299 | */ 300 | public function offsetExists($offset) 301 | { 302 | $txKey = isset($this->attributeMap[$offset]) ? $this->attributeMap[$offset] : null; 303 | 304 | if (is_array($txKey)) { 305 | return isset($this->txData[$txKey['key']]); 306 | } 307 | return false; 308 | } 309 | 310 | /** 311 | * Unset the value in the transaction with given key. 312 | * 313 | * @param string $offset key, eg: to 314 | * @return void 315 | */ 316 | public function offsetUnset($offset) 317 | { 318 | $txKey = isset($this->attributeMap[$offset]) ? $this->attributeMap[$offset] : null; 319 | 320 | if (is_array($txKey) && isset($this->txData[$txKey['key']])) { 321 | unset($this->txData[$txKey['key']]); 322 | } 323 | } 324 | 325 | /** 326 | * Return the value in the transaction with given key. 327 | * 328 | * @param string $offset key, eg: to 329 | * @return mixed value of the transaction 330 | */ 331 | public function offsetGet($offset) 332 | { 333 | $txKey = isset($this->attributeMap[$offset]) ? $this->attributeMap[$offset] : null; 334 | 335 | if (is_array($txKey) && isset($this->txData[$txKey['key']])) { 336 | return $this->txData[$txKey['key']]; 337 | } 338 | return null; 339 | } 340 | 341 | /** 342 | * Return raw ethereum transaction data. 343 | * 344 | * @return array raw ethereum transaction data 345 | */ 346 | public function getTxData() 347 | { 348 | return $this->txData; 349 | } 350 | 351 | /** 352 | * Return whether transaction type is valid (0x0 <= $transactionType <= 0x7f). 353 | * 354 | * @param integer $transactionType 355 | * @return boolean is transaction valid 356 | */ 357 | protected function isTransactionTypeValid(int $transactionType) 358 | { 359 | return $transactionType >= 0 && $transactionType <= 127; 360 | } 361 | 362 | /** 363 | * RLP serialize the ethereum transaction. 364 | * 365 | * @return string hex encoded of the serialized ethereum transaction 366 | */ 367 | public function serialize() 368 | { 369 | // sort tx data 370 | if (ksort($this->txData) !== true) { 371 | throw new RuntimeException('Cannot sort tx data by keys.'); 372 | } 373 | $txData = array_fill(0, 10, ''); 374 | foreach ($this->txData as $key => $data) { 375 | if ($key >= 0) { 376 | $txData[$key] = $data; 377 | } 378 | } 379 | $transactionType = $this->transactionType; 380 | return $transactionType . $this->rlp->encode($txData); 381 | } 382 | 383 | /** 384 | * Sign the transaction with given hex encoded private key. 385 | * 386 | * @param string $privateKey hex encoded private key 387 | * @return string hex encoded signed ethereum transaction 388 | */ 389 | public function sign(string $privateKey) 390 | { 391 | if ($this->util->isHex($privateKey)) { 392 | $privateKey = $this->util->stripZero($privateKey); 393 | $ecPrivateKey = $this->secp256k1->keyFromPrivate($privateKey, 'hex'); 394 | } else { 395 | throw new InvalidArgumentException('Private key should be hex encoded string'); 396 | } 397 | $txHash = $this->hash(); 398 | $signature = $ecPrivateKey->sign($txHash, [ 399 | 'canonical' => true 400 | ]); 401 | $r = $signature->r; 402 | $s = $signature->s; 403 | $v = $signature->recoveryParam; 404 | 405 | $this->offsetSet('r', '0x' . $r->toString(16)); 406 | $this->offsetSet('s', '0x' . $s->toString(16)); 407 | $this->offsetSet('v', $v); 408 | $this->privateKey = $ecPrivateKey; 409 | 410 | return $this->serialize(); 411 | } 412 | 413 | /** 414 | * Return hash of the ethereum transaction with/without signature. 415 | * 416 | * @param bool $includeSignature hash with signature 417 | * @return string hex encoded hash of the ethereum transaction 418 | */ 419 | public function hash(bool $includeSignature=false) 420 | { 421 | // sort tx data 422 | if (ksort($this->txData) !== true) { 423 | throw new RuntimeException('Cannot sort tx data by keys.'); 424 | } 425 | if ($includeSignature) { 426 | $length = 10; 427 | } else { 428 | $length = 7; 429 | } 430 | $rawTxData = array_fill(0, $length, ''); 431 | for ($key = 0; $key < $length; $key++) { 432 | if (isset($this->txData[$key])) { 433 | $rawTxData[$key] = $this->txData[$key]; 434 | } 435 | } 436 | $serializedTx = $this->rlp->encode($rawTxData); 437 | $transactionType = $this->transactionType; 438 | return $this->util->sha3(hex2bin($transactionType . $serializedTx)); 439 | } 440 | 441 | /** 442 | * Recover from address with given signature (r, s, v) if didn't set from. 443 | * 444 | * @return string hex encoded ethereum address 445 | */ 446 | public function getFromAddress() 447 | { 448 | $from = $this->offsetGet('from'); 449 | 450 | if ($from) { 451 | return $from; 452 | } 453 | if (!isset($this->privateKey) || !($this->privateKey instanceof KeyPair)) { 454 | // recover from hash 455 | $r = $this->offsetGet('r'); 456 | $s = $this->offsetGet('s'); 457 | $v = $this->offsetGet('v'); 458 | 459 | if (!$r || !$s) { 460 | throw new RuntimeException('Invalid signature r and s.'); 461 | } 462 | $txHash = $this->hash(); 463 | $publicKey = $this->secp256k1->recoverPubKey($txHash, [ 464 | 'r' => $r, 465 | 's' => $s 466 | ], $v); 467 | $publicKey = $publicKey->encode('hex'); 468 | } else { 469 | $publicKey = $this->privateKey->getPublic(false, 'hex'); 470 | } 471 | $from = '0x' . substr($this->util->sha3(substr(hex2bin($publicKey), 1)), 24); 472 | 473 | $this->offsetSet('from', $from); 474 | return $from; 475 | } 476 | } -------------------------------------------------------------------------------- /test/TestCase.php: -------------------------------------------------------------------------------- 1 | rlp = new RLP; 32 | } 33 | 34 | /** 35 | * tearDown 36 | * 37 | * @return void 38 | */ 39 | public function tearDown(): void {} 40 | } -------------------------------------------------------------------------------- /test/unit/TransactionTest.php: -------------------------------------------------------------------------------- 1 | '0xb60e8dd61c5d32be8058bb8eb970870f07233155', 21 | 'to' => '0xd46e8dd67c5d32be8058bb8eb970870f07244567', 22 | 'gas' => '0x76c0', 23 | 'gasPrice' => '0x9184e72a000', 24 | 'value' => '0x9184e72a', 25 | 'data' => '0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675' 26 | ]); 27 | 28 | $this->assertEquals('0xb60e8dd61c5d32be8058bb8eb970870f07233155', $transaction['from']); 29 | $this->assertEquals('0xd46e8dd67c5d32be8058bb8eb970870f07244567', $transaction['to']); 30 | $this->assertEquals('0x76c0', $transaction['gas']); 31 | $this->assertEquals('0x9184e72a000', $transaction['gasPrice']); 32 | $this->assertEquals('0x9184e72a', $transaction['value']); 33 | $this->assertEquals('0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675', $transaction['data']); 34 | $this->assertEquals(null, $transaction['chainId']); 35 | 36 | $this->assertEquals('0xb60e8dd61c5d32be8058bb8eb970870f07233155', $transaction->from); 37 | $this->assertEquals('0xd46e8dd67c5d32be8058bb8eb970870f07244567', $transaction->to); 38 | $this->assertEquals('0x76c0', $transaction->gas); 39 | $this->assertEquals('0x9184e72a000', $transaction->gasPrice); 40 | $this->assertEquals('0x9184e72a', $transaction->value); 41 | $this->assertEquals('0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675', $transaction->data); 42 | $this->assertEquals(null, $transaction->chainId); 43 | } 44 | 45 | /** 46 | * testSet 47 | * 48 | * @return void 49 | */ 50 | public function testSet() 51 | { 52 | $transaction = new Transaction([ 53 | 'from' => '0xb60e8dd61c5d32be8058bb8eb970870f07233155', 54 | 'to' => '0xd46e8dd67c5d32be8058bb8eb970870f07244567', 55 | 'gas' => '0x76c0', 56 | 'gasPrice' => '0x9184e72a000', 57 | 'value' => '0x9184e72a', 58 | 'data' => '0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675' 59 | ]); 60 | 61 | $transaction['from'] = '0xb60e8dd61c5d32be8058bb8eb970870f07231234'; 62 | $this->assertEquals('0xb60e8dd61c5d32be8058bb8eb970870f07231234', $transaction['from']); 63 | 64 | $transaction['to'] = '0xb60e8dd61c5d32be8058bb8eb970870f07233155'; 65 | $this->assertEquals('0xb60e8dd61c5d32be8058bb8eb970870f07233155', $transaction['to']); 66 | 67 | $transaction['gas'] = '0x76'; 68 | $this->assertEquals('0x76', $transaction['gas']); 69 | 70 | $transaction['gasPrice'] = '0x12'; 71 | $this->assertEquals('0x12', $transaction['gasPrice']); 72 | 73 | $transaction['value'] = '0x01'; 74 | $this->assertEquals('0x01', $transaction['value']); 75 | 76 | $transaction['data'] = ''; 77 | $this->assertEquals('', $transaction['data']); 78 | 79 | $transaction['chainId'] = 4; 80 | $this->assertEquals(4, $transaction['chainId']); 81 | 82 | $transaction->from = '0xb60e8dd61c5d32be8058bb8eb970870f07233155'; 83 | $this->assertEquals('0xb60e8dd61c5d32be8058bb8eb970870f07233155', $transaction->from); 84 | 85 | $transaction->to = '0xd46e8dd67c5d32be8058bb8eb970870f07244567'; 86 | $this->assertEquals('0xd46e8dd67c5d32be8058bb8eb970870f07244567', $transaction->to); 87 | 88 | $transaction->gas = '0x76c0'; 89 | $this->assertEquals('0x76c0', $transaction->gas); 90 | 91 | $transaction->gasPrice = '0x9184e72a000'; 92 | $this->assertEquals('0x9184e72a000', $transaction->gasPrice); 93 | 94 | $transaction->value = '0x9184e72a'; 95 | $this->assertEquals('0x9184e72a', $transaction->value); 96 | 97 | $transaction->data = '0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675'; 98 | $this->assertEquals('0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675', $transaction->data); 99 | 100 | $transaction->chainId = null; 101 | $this->assertEquals(null, $transaction->chainId); 102 | } 103 | 104 | /** 105 | * testHash 106 | * 107 | * @return void 108 | */ 109 | public function testHash() 110 | { 111 | $transaction = new Transaction([ 112 | 'nonce' => '0x01', 113 | 'from' => '0xb60e8dd61c5d32be8058bb8eb970870f07233155', 114 | 'to' => '0xd46e8dd67c5d32be8058bb8eb970870f07244567', 115 | 'gas' => '0x76c0', 116 | 'gasPrice' => '0x9184e72a000', 117 | 'value' => '0x9184e72a', 118 | 'data' => '0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675' 119 | ]); 120 | 121 | $this->assertEquals('79617051b33e38636c12fb761abf62c20a9dd5a743ca5f338f04f2cf5f2ec6bd', $transaction->hash()); 122 | 123 | $transaction = new Transaction([ 124 | 'nonce' => '0x01', 125 | 'from' => '0xb60e8dd61c5d32be8058bb8eb970870f07233155', 126 | 'to' => '0xd46e8dd67c5d32be8058bb8eb970870f07244567', 127 | 'gas' => '0x76c0', 128 | 'gasPrice' => '0x9184e72a000', 129 | 'value' => '0x9184e72a', 130 | 'chainId' => 4, 131 | 'data' => '0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675' 132 | ]); 133 | 134 | $this->assertEquals('8aace0c8df439c9cc9f313b116f1db03e0811ca07e582d351aad1c9d6542c23d', (string) $transaction); 135 | } 136 | 137 | /** 138 | * testSign 139 | * 140 | * @return void 141 | */ 142 | public function testSign() 143 | { 144 | $transaction = new Transaction([ 145 | 'nonce' => '0x01', 146 | 'from' => '0xb60e8dd61c5d32be8058bb8eb970870f07233155', 147 | 'to' => '0xd46e8dd67c5d32be8058bb8eb970870f07244567', 148 | 'gas' => '0x76c0', 149 | 'gasPrice' => '0x9184e72a000', 150 | 'value' => '0x9184e72a', 151 | 'data' => '0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675' 152 | ]); 153 | $this->assertEquals('f892018609184e72a0008276c094d46e8dd67c5d32be8058bb8eb970870f07244567849184e72aa9d46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f07244567523a0a48d3ce9c68bb49825aea5335bd07432823e858e8a504767d08290c28aafddf8a0416c7abc3a67080db0ad07c42de82db4e05518f99595119677398c68d431ab37', $transaction->sign($this->testPrivateKey)); 154 | 155 | // test different private keys 156 | $tests = [ 157 | 'fake private key', '0xd0459987fdde1f41e524fddbf4b646cd9d3bea7fd7d63feead3f5dfce6174a3d', 'd0459987fdde1f41e524fddbf4b646cd9d3bea7fd7d63feead3f5dfce6174a3d', 'd0459987fdde1f41e524fddbf4b646cd9d3bea7fd7d63feead3f5dfce6174a' 158 | ]; 159 | for ($i=0; $isign($tests[$i]); 162 | } catch (\InvalidArgumentException $e) { 163 | $this->assertEquals('Private key should be hex encoded string', $e->getMessage()); 164 | } 165 | } 166 | } 167 | 168 | /** 169 | * testSerialize 170 | * 171 | * @return void 172 | */ 173 | public function testSerialize() 174 | { 175 | $transaction = new Transaction([ 176 | 'nonce' => '0x01', 177 | 'from' => '0xb60e8dd61c5d32be8058bb8eb970870f07233155', 178 | 'to' => '0xd46e8dd67c5d32be8058bb8eb970870f07244567', 179 | 'gas' => '0x76c0', 180 | 'gasPrice' => '0x9184e72a000', 181 | 'value' => '0x9184e72a', 182 | 'data' => '0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675' 183 | ]); 184 | 185 | $this->assertEquals('f84f018609184e72a0008276c094d46e8dd67c5d32be8058bb8eb970870f07244567849184e72aa9d46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675', $transaction->serialize()); 186 | 187 | // sign tx 188 | $transaction->sign($this->testPrivateKey); 189 | 190 | $this->assertEquals('f892018609184e72a0008276c094d46e8dd67c5d32be8058bb8eb970870f07244567849184e72aa9d46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f07244567523a0a48d3ce9c68bb49825aea5335bd07432823e858e8a504767d08290c28aafddf8a0416c7abc3a67080db0ad07c42de82db4e05518f99595119677398c68d431ab37', $transaction->serialize()); 191 | } 192 | 193 | /** 194 | * testEIP155 195 | * you can find test case here: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md 196 | * 197 | * @return void 198 | */ 199 | public function testEIP155() 200 | { 201 | // test signing data 202 | $transaction = new Transaction([ 203 | 'nonce' => '0x09', 204 | 'to' => '0x3535353535353535353535353535353535353535', 205 | 'gas' => '0x5208', 206 | 'gasPrice' => '0x4a817c800', 207 | 'value' => '0xde0b6b3a7640000', 208 | 'chainId' => 1, 209 | 'data' => '' 210 | ]); 211 | $transaction['r'] = ''; 212 | $transaction['s'] = ''; 213 | $transaction['v'] = 1; 214 | $this->assertEquals('ec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080', $transaction->serialize()); 215 | 216 | $transaction = new Transaction([ 217 | 'nonce' => '0x09', 218 | 'to' => '0x3535353535353535353535353535353535353535', 219 | 'gas' => '0x5208', 220 | 'gasPrice' => '0x4a817c800', 221 | 'value' => '0xde0b6b3a7640000', 222 | 'chainId' => 1, 223 | 'data' => '' 224 | ]); 225 | $this->assertEquals('daf5a779ae972f972197303d7b574746c7ef83eadac0f2791ad23db92e4c8e53', $transaction->hash(false)); 226 | $this->assertEquals('f86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83', $transaction->sign('0x4646464646464646464646464646464646464646464646464646464646464646')); 227 | 228 | $transaction = new Transaction([ 229 | 'nonce' => '0x09', 230 | 'to' => '0x3535353535353535353535353535353535353535', 231 | 'gas' => '0x5208', 232 | 'gasPrice' => '0x4a817c800', 233 | 'value' => '0x0', 234 | 'chainId' => 1, 235 | 'data' => '' 236 | ]); 237 | $this->assertEquals('f864098504a817c800825208943535353535353535353535353535353535353535808025a0855ec9b7d4fcabf535fe4ac4a7c31a9e521214d05bc6efbc058d4757c35e92bba0043d7df30c8a79e5522b3de8fc169df5fa7145714100ee8ec413292d97ce4d3a', $transaction->sign('0x4646464646464646464646464646464646464646464646464646464646464646')); 238 | 239 | $transaction = new Transaction('0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83'); 240 | $this->assertEquals('f86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83', $transaction->serialize()); 241 | } 242 | 243 | /** 244 | * testGetFromAddress 245 | * 0x9d8a62f656a8d1615c1294fd71e9cfb3e4855a4f 246 | * 247 | * @return void 248 | */ 249 | public function testGetFromAddress() 250 | { 251 | $transaction = new Transaction([ 252 | 'nonce' => '0x09', 253 | 'to' => '0x3535353535353535353535353535353535353535', 254 | 'gas' => '0x5208', 255 | 'gasPrice' => '0x4a817c800', 256 | 'value' => '0xde0b6b3a7640000', 257 | 'chainId' => 1, 258 | 'data' => '' 259 | ]); 260 | // sign tx 261 | $transaction->sign('0x4646464646464646464646464646464646464646464646464646464646464646'); 262 | $r = $transaction['r']; 263 | $s = $transaction['s']; 264 | $v = $transaction['v']; 265 | 266 | // get from privatekey 267 | $fromA = $transaction->getFromAddress(); 268 | 269 | $transaction = new Transaction([ 270 | 'nonce' => '0x09', 271 | 'to' => '0x3535353535353535353535353535353535353535', 272 | 'gas' => '0x5208', 273 | 'gasPrice' => '0x4a817c800', 274 | 'value' => '0xde0b6b3a7640000', 275 | 'chainId' => 1, 276 | 'data' => '' 277 | ]); 278 | $transaction['r'] = $r; 279 | $transaction['s'] = $s; 280 | $transaction['v'] = $v; 281 | 282 | // get from r, s, v 283 | $fromB = $transaction->getFromAddress(); 284 | 285 | $transaction = new Transaction([ 286 | 'from' => '0x9d8a62f656a8d1615c1294fd71e9cfb3e4855a4f', 287 | 'nonce' => '0x09', 288 | 'to' => '0x3535353535353535353535353535353535353535', 289 | 'gas' => '0x5208', 290 | 'gasPrice' => '0x4a817c800', 291 | 'value' => '0xde0b6b3a7640000', 292 | 'chainId' => 1, 293 | 'data' => '' 294 | ]); 295 | 296 | // get from transaction 297 | $fromC = $transaction->getFromAddress(); 298 | 299 | $this->assertEquals('0x9d8a62f656a8d1615c1294fd71e9cfb3e4855a4f', $fromA); 300 | $this->assertEquals($fromA, $fromB); 301 | $this->assertEquals($fromB, $fromC); 302 | } 303 | 304 | /** 305 | * testIssue15 306 | * 307 | * @return void 308 | */ 309 | public function testIssue15() 310 | { 311 | $signedTransactions = []; 312 | $nonces = [ 313 | '0x00', '0x0', 0, '0x000', '0' 314 | ]; 315 | 316 | // push signed transaction 317 | for ($i=0; $i $nonces[$i], 320 | 'to' => '0x3535353535353535353535353535353535353535', 321 | 'gas' => '0x5208', 322 | 'gasPrice' => '0x4a817c800', 323 | 'value' => '0x0', 324 | 'chainId' => 1, 325 | 'data' => '' 326 | ]); 327 | $signedTransactions[] = $transaction->sign('0x4646464646464646464646464646464646464646464646464646464646464646'); 328 | } 329 | 330 | // compare each signed transaction 331 | for ($i=1; $iassertEquals($signedTransactions[0], $signedTransactions[$i]); 333 | } 334 | } 335 | 336 | /** 337 | * testIssue26 338 | * default $txData should be empty array 339 | * 340 | * @return void 341 | */ 342 | public function testIssue26() 343 | { 344 | $tests = [ 345 | null, [], [null] 346 | ]; 347 | for ($i=0; $iassertEquals($transaction->txData, []); 350 | } 351 | } 352 | 353 | /** 354 | * testEIP2930 355 | * see: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2930.md 356 | * 357 | * @return void 358 | */ 359 | public function testEIP2930() 360 | { 361 | $transaction = new EIP2930Transaction([ 362 | 'nonce' => '0x15', 363 | 'to' => '0x3535353535353535353535353535353535353535', 364 | 'gas' => '0x5208', 365 | 'gasPrice' => '0x4a817c800', 366 | 'value' => '0x0', 367 | 'chainId' => 4, 368 | 'accessList' => [ 369 | ], 370 | 'data' => '' 371 | ]); 372 | $this->assertEquals('01f86604158504a817c8008252089435353535353535353535353535353535353535358080c001a09753969d39f6a5109095d5082d67fc99a05fd66a339ba80934504ff79474e77aa07a907eb764b72b3088a331e7b97c2bad5fd43f1d574ddc80edeb022476454adb', $transaction->sign('0x4646464646464646464646464646464646464646464646464646464646464646')); 373 | 374 | $transaction = new EIP2930Transaction('0x01f86604158504a817c8008252089435353535353535353535353535353535353535358080c001a09753969d39f6a5109095d5082d67fc99a05fd66a339ba80934504ff79474e77aa07a907eb764b72b3088a331e7b97c2bad5fd43f1d574ddc80edeb022476454adb'); 375 | $this->assertEquals('01f86604158504a817c8008252089435353535353535353535353535353535353535358080c001a09753969d39f6a5109095d5082d67fc99a05fd66a339ba80934504ff79474e77aa07a907eb764b72b3088a331e7b97c2bad5fd43f1d574ddc80edeb022476454adb', $transaction->serialize()); 376 | $this->assertEquals('01f86604158504a817c8008252089435353535353535353535353535353535353535358080c001a09753969d39f6a5109095d5082d67fc99a05fd66a339ba80934504ff79474e77aa07a907eb764b72b3088a331e7b97c2bad5fd43f1d574ddc80edeb022476454adb', $transaction->sign('0x4646464646464646464646464646464646464646464646464646464646464646')); 377 | } 378 | 379 | /** 380 | * testEIP1559 381 | * see: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md 382 | * 383 | * @return void 384 | */ 385 | public function testEIP1559() 386 | { 387 | $transaction = new EIP1559Transaction([ 388 | 'nonce' => '0x15', 389 | 'to' => '0x3535353535353535353535353535353535353535', 390 | 'gas' => '0x5208', 391 | 'maxPriorityFeePerGas' => '0x4a817c800', 392 | 'maxFeePerGas' => '0x4a817c800', 393 | 'value' => '0x0', 394 | 'chainId' => 4, 395 | 'accessList' => [ 396 | ], 397 | 'data' => '' 398 | ]); 399 | var_dump($transaction->hash()); 400 | $this->assertEquals('02f86c04158504a817c8008504a817c8008252089435353535353535353535353535353535353535358080c080a03fd48c8a173e9669c33cb5271f03b1af4f030dc8315be8ec9442b7fbdde893c8a010af381dab1df3e7012a3c8421d65a810859a5dd9d58991ad7c07f12d0c651c7', $transaction->sign('0x4646464646464646464646464646464646464646464646464646464646464646')); 401 | 402 | $transaction = new EIP1559Transaction('0x02f86c04158504a817c8008504a817c8008252089435353535353535353535353535353535353535358080c080a03fd48c8a173e9669c33cb5271f03b1af4f030dc8315be8ec9442b7fbdde893c8a010af381dab1df3e7012a3c8421d65a810859a5dd9d58991ad7c07f12d0c651c7'); 403 | $this->assertEquals('02f86c04158504a817c8008504a817c8008252089435353535353535353535353535353535353535358080c080a03fd48c8a173e9669c33cb5271f03b1af4f030dc8315be8ec9442b7fbdde893c8a010af381dab1df3e7012a3c8421d65a810859a5dd9d58991ad7c07f12d0c651c7', $transaction->serialize()); 404 | $this->assertEquals('02f86c04158504a817c8008504a817c8008252089435353535353535353535353535353535353535358080c080a03fd48c8a173e9669c33cb5271f03b1af4f030dc8315be8ec9442b7fbdde893c8a010af381dab1df3e7012a3c8421d65a810859a5dd9d58991ad7c07f12d0c651c7', $transaction->sign('0x4646464646464646464646464646464646464646464646464646464646464646')); 405 | } 406 | } 407 | --------------------------------------------------------------------------------