├── .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 | [](https://github.com/web3p/ethereum-tx/actions/workflows/php.yml)
3 | [](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 |
--------------------------------------------------------------------------------