├── .github
└── workflows
│ └── php.yml
├── .gitignore
├── LICENSE
├── README.md
├── composer.json
├── phpunit.xml
├── src
└── Util.php
└── test
├── TestCase.php
└── unit
└── UtilTest.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-util 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 | # Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file
6 | # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
7 | # composer.lock
8 |
--------------------------------------------------------------------------------
/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-util
2 |
3 | [](https://github.com/web3p/ethereum-util/actions/workflows/php.yml)
4 | [](https://codecov.io/gh/web3p/ethereum-util)
5 | [](https://github.com/web3p/ethereum-util/blob/master/LICENSE)
6 |
7 | A collection of utility functions for Ethereum written in php.
8 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web3p/ethereum-util",
3 | "description": "A collection of utility functions for Ethereum written 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\\EthereumUtil\\": "src/"
18 | }
19 | },
20 | "autoload-dev": {
21 | "psr-4": {
22 | "Test\\": "test/"
23 | }
24 | },
25 | "require": {
26 | "PHP": "^7.1 | ^8.0",
27 | "kornrunner/keccak": "~1",
28 | "simplito/elliptic-php": "~1.0.6",
29 | "phpseclib/phpseclib": "~2.0"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 | ./test/unit
15 |
16 |
17 |
18 |
19 | ./src
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/Util.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * @author Peter Lai
9 | * @license MIT
10 | */
11 |
12 | namespace Web3p\EthereumUtil;
13 |
14 | use InvalidArgumentException;
15 | use RuntimeException;
16 | use kornrunner\Keccak;
17 | use phpseclib\Math\BigInteger as BigNumber;
18 | use Elliptic\EC;
19 | use Elliptic\EC\KeyPair;
20 | use Elliptic\EC\Signature;
21 |
22 | class Util
23 | {
24 | /**
25 | * SHA3_NULL_HASH
26 | *
27 | * @const string
28 | */
29 | const SHA3_NULL_HASH = 'c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470';
30 |
31 | /**
32 | * secp256k1
33 | *
34 | * @var \Elliptic\EC
35 | */
36 | protected $secp256k1;
37 |
38 | /**
39 | * construct
40 | *
41 | * @return void
42 | */
43 | public function __construct()
44 | {
45 | $this->secp256k1 = new EC('secp256k1');
46 | }
47 |
48 | /**
49 | * get
50 | *
51 | * @param string $name
52 | * @return mixed
53 | */
54 | public function __get($name)
55 | {
56 | $method = 'get' . ucfirst($name);
57 |
58 | if (method_exists($this, $method)) {
59 | return call_user_func_array([$this, $method], []);
60 | }
61 | return false;
62 | }
63 |
64 | /**
65 | * set
66 | *
67 | * @param string $name
68 | * @param mixed $value
69 | * @return void
70 | */
71 | public function __set($name, $value)
72 | {
73 | $method = 'set' . ucfirst($name);
74 |
75 | if (method_exists($this, $method)) {
76 | call_user_func_array([$this, $method], [$value]);
77 | }
78 | }
79 |
80 | /**
81 | * sha3
82 | * keccak256
83 | *
84 | * @param string $value
85 | * @return null|string
86 | */
87 | public function sha3(string $value)
88 | {
89 | $hash = Keccak::hash($value, 256);
90 |
91 | if ($hash === $this::SHA3_NULL_HASH) {
92 | return null;
93 | }
94 | return $hash;
95 | }
96 |
97 | /**
98 | * isZeroPrefixed
99 | *
100 | * @param string $value
101 | * @return bool
102 | */
103 | public function isZeroPrefixed(string $value)
104 | {
105 | return (strpos($value, '0x') === 0);
106 | }
107 |
108 | /**
109 | * stripZero
110 | *
111 | * @param string $value
112 | * @return string
113 | */
114 | public function stripZero(string $value)
115 | {
116 | if ($this->isZeroPrefixed($value)) {
117 | $count = 1;
118 | return str_replace('0x', '', $value, $count);
119 | }
120 | return $value;
121 | }
122 |
123 | /**
124 | * isHex
125 | *
126 | * @param string $value
127 | * @return bool
128 | */
129 | public function isHex(string $value)
130 | {
131 | return (is_string($value) && preg_match('/^(0x)?[a-fA-F0-9]+$/', $value) === 1);
132 | }
133 |
134 | /**
135 | * publicKeyToAddress
136 | *
137 | * @param string $publicKey
138 | * @throws InvalidArgumentException
139 | * @return string
140 | */
141 | public function publicKeyToAddress(string $publicKey)
142 | {
143 | if ($this->isHex($publicKey) === false) {
144 | throw new InvalidArgumentException('Invalid public key format.');
145 | }
146 | $publicKey = $this->stripZero($publicKey);
147 |
148 | if (strlen($publicKey) !== 130) {
149 | throw new InvalidArgumentException('Invalid public key length.');
150 | }
151 | return '0x' . substr($this->sha3(substr(hex2bin($publicKey), 1)), 24);
152 | }
153 |
154 | /**
155 | * privateKeyToPublicKey
156 | *
157 | * @param string $privateKey
158 | * @throws InvalidArgumentException
159 | * @return string
160 | */
161 | public function privateKeyToPublicKey(string $privateKey)
162 | {
163 | if ($this->isHex($privateKey) === false) {
164 | throw new InvalidArgumentException('Invalid private key format.');
165 | }
166 | $privateKey = $this->stripZero($privateKey);
167 |
168 | if (strlen($privateKey) !== 64) {
169 | throw new InvalidArgumentException('Invalid private key length.');
170 | }
171 | $privateKey = $this->secp256k1->keyFromPrivate($privateKey, 'hex');
172 | $publicKey = $privateKey->getPublic(false, 'hex');
173 |
174 | return '0x' . $publicKey;
175 | }
176 |
177 | /**
178 | * recoverPublicKey
179 | *
180 | * @param string $hash
181 | * @param string $r
182 | * @param string $s
183 | * @param int $v
184 | * @throws InvalidArgumentException
185 | * @return string
186 | */
187 | public function recoverPublicKey(string $hash, string $r, string $s, int $v)
188 | {
189 | if ($this->isHex($hash) === false) {
190 | throw new InvalidArgumentException('Invalid hash format.');
191 | }
192 | $hash = $this->stripZero($hash);
193 |
194 | if ($this->isHex($r) === false || $this->isHex($s) === false) {
195 | throw new InvalidArgumentException('Invalid signature format.');
196 | }
197 | $r = $this->stripZero($r);
198 | $s = $this->stripZero($s);
199 |
200 | if (strlen($r) !== 64 || strlen($s) !== 64) {
201 | throw new InvalidArgumentException('Invalid signature length.');
202 | }
203 | $publicKey = $this->secp256k1->recoverPubKey($hash, [
204 | 'r' => $r,
205 | 's' => $s
206 | ], $v);
207 | $publicKey = $publicKey->encode('hex');
208 |
209 | return '0x' . $publicKey;
210 | }
211 |
212 | /**
213 | * ecsign
214 | *
215 | * @param string $privateKey
216 | * @param string $message
217 | * @throws InvalidArgumentException
218 | * @return \Elliptic\EC\Signature
219 | */
220 | public function ecsign(string $privateKey, string $message)
221 | {
222 | if ($this->isHex($privateKey) === false) {
223 | throw new InvalidArgumentException('Invalid private key format.');
224 | }
225 | $privateKeyLength = strlen($this->stripZero($privateKey));
226 |
227 | if ($privateKeyLength % 2 !== 0 && $privateKeyLength !== 64) {
228 | throw new InvalidArgumentException('Private key length was wrong.');
229 | }
230 | $secp256k1 = new EC('secp256k1');
231 | $privateKey = $secp256k1->keyFromPrivate($privateKey, 'hex');
232 | $signature = $privateKey->sign($message, [
233 | 'canonical' => true
234 | ]);
235 | // Ethereum v is recovery param + 35
236 | // Or recovery param + 35 + (chain id * 2)
237 | $signature->recoveryParam += 35;
238 |
239 | return $signature;
240 | }
241 |
242 | /**
243 | * hasPersonalMessage
244 | *
245 | * @param string $message
246 | * @return string
247 | */
248 | public function hashPersonalMessage(string $message)
249 | {
250 | $prefix = sprintf("\x19Ethereum Signed Message:\n%d", mb_strlen($message));
251 | return $this->sha3($prefix . $message);
252 | }
253 |
254 | /**
255 | * isNegative
256 | *
257 | * @param string
258 | * @throws InvalidArgumentException
259 | * @return bool
260 | */
261 | public function isNegative(string $value)
262 | {
263 | if (!is_string($value)) {
264 | throw new InvalidArgumentException('The value to isNegative function must be string.');
265 | }
266 | return (strpos($value, '-') === 0);
267 | }
268 |
269 | /**
270 | * toBn
271 | * Change number or number string to bignumber.
272 | *
273 | * @param BigNumber|string|int $number
274 | * @throws InvalidArgumentException
275 | * @return array|\phpseclib\Math\BigInteger
276 | */
277 | public function toBn($number)
278 | {
279 | if ($number instanceof BigNumber){
280 | $bn = $number;
281 | } elseif (is_int($number)) {
282 | $bn = new BigNumber($number);
283 | } elseif (is_numeric($number)) {
284 | $number = (string) $number;
285 |
286 | if ($this->isNegative($number)) {
287 | $count = 1;
288 | $number = str_replace('-', '', $number, $count);
289 | $negative1 = new BigNumber(-1);
290 | }
291 | if (strpos($number, '.') > 0) {
292 | $comps = explode('.', $number);
293 |
294 | if (count($comps) > 2) {
295 | throw new InvalidArgumentException('toBn number must be a valid number.');
296 | }
297 | $whole = $comps[0];
298 | $fraction = $comps[1];
299 |
300 | return [
301 | new BigNumber($whole),
302 | new BigNumber($fraction),
303 | strlen($comps[1]),
304 | isset($negative1) ? $negative1 : false
305 | ];
306 | } else {
307 | $bn = new BigNumber($number);
308 | }
309 | if (isset($negative1)) {
310 | $bn = $bn->multiply($negative1);
311 | }
312 | } elseif (is_string($number)) {
313 | $number = mb_strtolower($number);
314 |
315 | if ($this->isNegative($number)) {
316 | $count = 1;
317 | $number = str_replace('-', '', $number, $count);
318 | $negative1 = new BigNumber(-1);
319 | }
320 | if (empty($number)) {
321 | $bn = new BigNumber(0);
322 | } else if ($this->isZeroPrefixed($number) || $this->isHex($number)) {
323 | $number = $this->stripZero($number);
324 | $bn = new BigNumber($number, 16);
325 | } else {
326 | throw new InvalidArgumentException('toBn number must be valid hex string.');
327 | }
328 | if (isset($negative1)) {
329 | $bn = $bn->multiply($negative1);
330 | }
331 | } else {
332 | throw new InvalidArgumentException('toBn number must be BigNumber, string or int.');
333 | }
334 | return $bn;
335 | }
336 | }
--------------------------------------------------------------------------------
/test/TestCase.php:
--------------------------------------------------------------------------------
1 | assertNull($util->sha3(''));
23 | $this->assertEquals('47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad', $util->sha3('hello world'));
24 | }
25 |
26 | /**
27 | * testIsZeroPrefixed
28 | *
29 | * @return void
30 | */
31 | public function testIsZeroPrefixed()
32 | {
33 | $util = new Util;
34 |
35 | $this->assertTrue($util->isZeroPrefixed('0x1234'));
36 | $this->assertFalse($util->isZeroPrefixed('1234'));
37 | }
38 |
39 | /**
40 | * testStripZero
41 | *
42 | * @return void
43 | */
44 | public function testStripZero()
45 | {
46 | $util = new Util;
47 |
48 | $this->assertEquals('1234', $util->stripZero('0x1234'));
49 | $this->assertEquals('1234', $util->stripZero('1234'));
50 | }
51 |
52 | /**
53 | * testIsHex
54 | *
55 | * @return void
56 | */
57 | public function testIsHex()
58 | {
59 | $util = new Util;
60 |
61 | $this->assertTrue($util->isHex('1234'));
62 | $this->assertTrue($util->isHex('0x1234'));
63 | $this->assertFalse($util->isHex('hello world'));
64 | }
65 |
66 | /**
67 | * testPublicKeyToAddress
68 | *
69 | * @return void
70 | */
71 | public function testPublicKeyToAddress()
72 | {
73 | $util = new Util;
74 |
75 | $this->assertEquals('0x9d8a62f656a8d1615c1294fd71e9cfb3e4855a4f', $util->publicKeyToAddress('044bc2a31265153f07e70e0bab08724e6b85e217f8cd628ceb62974247bb493382ce28cab79ad7119ee1ad3ebcdb98a16805211530ecc6cfefa1b88e6dff99232a'));
76 | $this->assertEquals('0x9d8a62f656a8d1615c1294fd71e9cfb3e4855a4f', $util->publicKeyToAddress('0x044bc2a31265153f07e70e0bab08724e6b85e217f8cd628ceb62974247bb493382ce28cab79ad7119ee1ad3ebcdb98a16805211530ecc6cfefa1b88e6dff99232a'));
77 | }
78 |
79 | /**
80 | * testPrivateKeyToPublicKey
81 | *
82 | * @return void
83 | */
84 | public function testPrivateKeyToPublicKey()
85 | {
86 | $util = new Util;
87 |
88 | $this->assertEquals('0x044bc2a31265153f07e70e0bab08724e6b85e217f8cd628ceb62974247bb493382ce28cab79ad7119ee1ad3ebcdb98a16805211530ecc6cfefa1b88e6dff99232a', $util->privateKeyToPublicKey('0x4646464646464646464646464646464646464646464646464646464646464646'));
89 | $this->assertEquals('0x044bc2a31265153f07e70e0bab08724e6b85e217f8cd628ceb62974247bb493382ce28cab79ad7119ee1ad3ebcdb98a16805211530ecc6cfefa1b88e6dff99232a', $util->privateKeyToPublicKey('4646464646464646464646464646464646464646464646464646464646464646'));
90 | }
91 |
92 | /**
93 | * testRecoverPublicKey
94 | *
95 | * @return void
96 | */
97 | public function testRecoverPublicKey()
98 | {
99 | $util = new Util;
100 |
101 | $this->assertEquals('0x044bc2a31265153f07e70e0bab08724e6b85e217f8cd628ceb62974247bb493382ce28cab79ad7119ee1ad3ebcdb98a16805211530ecc6cfefa1b88e6dff99232a', $util->recoverPublicKey('0xdaf5a779ae972f972197303d7b574746c7ef83eadac0f2791ad23db92e4c8e53', '0x28ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276', '0x67cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83', 0));
102 | $this->assertEquals('0x044bc2a31265153f07e70e0bab08724e6b85e217f8cd628ceb62974247bb493382ce28cab79ad7119ee1ad3ebcdb98a16805211530ecc6cfefa1b88e6dff99232a', $util->recoverPublicKey('daf5a779ae972f972197303d7b574746c7ef83eadac0f2791ad23db92e4c8e53', '28ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276', '67cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83', 0));
103 | }
104 |
105 | /**
106 | * testEcsign
107 | *
108 | * @return void
109 | */
110 | public function testEcsign()
111 | {
112 | $util = new Util;
113 | $signature = $util->ecsign('0x4646464646464646464646464646464646464646464646464646464646464646', 'daf5a779ae972f972197303d7b574746c7ef83eadac0f2791ad23db92e4c8e53');
114 |
115 | // EIP155 test data
116 | $this->assertEquals('28ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276', $signature->r->toString(16));
117 | $this->assertEquals('67cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83', $signature->s->toString(16));
118 | $this->assertEquals(35, $signature->recoveryParam);
119 | }
120 |
121 | /**
122 | * testHashPersonalMessage
123 | *
124 | * @return void
125 | */
126 | public function testHashPersonalMessage()
127 | {
128 | $util = new Util;
129 | $hashedMessage = $util->hashPersonalMessage('Hello world');
130 |
131 | $this->assertEquals('8144a6fa26be252b86456491fbcd43c1de7e022241845ffea1c3df066f7cfede', $hashedMessage);
132 | }
133 |
134 | /**
135 | * testIsNegative
136 | *
137 | * @return void
138 | */
139 | public function testIsNegative()
140 | {
141 | $util = new Util;
142 | $isNegative = $util->isNegative('-1');
143 | $this->assertTrue($isNegative);
144 |
145 | $isNegative = $util->isNegative('1');
146 | $this->assertFalse($isNegative);
147 | }
148 |
149 | /**
150 | * testToBn
151 | *
152 | * @return void
153 | */
154 | public function testToBn()
155 | {
156 | $util = new Util;
157 | $bn = $util->toBn('');
158 | $this->assertEquals($bn->toString(), '0');
159 |
160 | $bn = $util->toBn(11);
161 | $this->assertEquals($bn->toString(), '11');
162 |
163 | $bn = $util->toBn('0x12');
164 | $this->assertEquals($bn->toString(), '18');
165 |
166 | $bn = $util->toBn('-0x12');
167 | $this->assertEquals($bn->toString(), '-18');
168 |
169 | $bn = $util->toBn(0x12);
170 | $this->assertEquals($bn->toString(), '18');
171 |
172 | $bn = $util->toBn('ae');
173 | $this->assertEquals($bn->toString(), '174');
174 |
175 | $bn = $util->toBn('-ae');
176 | $this->assertEquals($bn->toString(), '-174');
177 |
178 | $bn = $util->toBn('-1');
179 | $this->assertEquals($bn->toString(), '-1');
180 |
181 | $bn = $util->toBn('-0.1');
182 | $this->assertEquals(count($bn), 4);
183 | $this->assertEquals($bn[0]->toString(), '0');
184 | $this->assertEquals($bn[1]->toString(), '1');
185 | $this->assertEquals($bn[2], 1);
186 | $this->assertEquals($bn[3]->toString(), '-1');
187 |
188 | $bn = $util->toBn(-0.1);
189 | $this->assertEquals(count($bn), 4);
190 | $this->assertEquals($bn[0]->toString(), '0');
191 | $this->assertEquals($bn[1]->toString(), '1');
192 | $this->assertEquals($bn[2], 1);
193 | $this->assertEquals($bn[3]->toString(), '-1');
194 |
195 | $bn = $util->toBn('0.1');
196 | $this->assertEquals(count($bn), 4);
197 | $this->assertEquals($bn[0]->toString(), '0');
198 | $this->assertEquals($bn[1]->toString(), '1');
199 | $this->assertEquals($bn[2], 1);
200 | $this->assertEquals($bn[3], false);
201 |
202 | $bn = $util->toBn('-1.69');
203 | $this->assertEquals(count($bn), 4);
204 | $this->assertEquals($bn[0]->toString(), '1');
205 | $this->assertEquals($bn[1]->toString(), '69');
206 | $this->assertEquals($bn[2], 2);
207 | $this->assertEquals($bn[3]->toString(), '-1');
208 |
209 | $bn = $util->toBn(-1.69);
210 | $this->assertEquals($bn[0]->toString(), '1');
211 | $this->assertEquals($bn[1]->toString(), '69');
212 | $this->assertEquals($bn[2], 2);
213 | $this->assertEquals($bn[3]->toString(), '-1');
214 |
215 | $bn = $util->toBn('1.69');
216 | $this->assertEquals(count($bn), 4);
217 | $this->assertEquals($bn[0]->toString(), '1');
218 | $this->assertEquals($bn[1]->toString(), '69');
219 | $this->assertEquals($bn[2], 2);
220 | $this->assertEquals($bn[3], false);
221 |
222 | $bn = $util->toBn(new BigNumber(1));
223 | $this->assertEquals($bn->toString(), '1');
224 | $util->toBn(new BigNumber(1));
225 |
226 | $this->expectException(InvalidArgumentException::class);
227 | $bn = $util->toBn(new stdClass);
228 | }
229 | }
--------------------------------------------------------------------------------