├── .coveralls.yml
├── .github
└── workflows
│ ├── static-analysis.yml
│ └── tests.yml
├── .gitignore
├── LICENSE
├── README.md
├── composer.json
├── infection.json.dist
├── phpunit.xml.dist
├── psalm.xml
├── src
└── Address.php
└── test
└── AddressTest.php
/.coveralls.yml:
--------------------------------------------------------------------------------
1 | service_name: travis-ci
2 | coverage_clover: build/logs/clover.xml
3 | json_path: coveralls-upload.json
4 |
--------------------------------------------------------------------------------
/.github/workflows/static-analysis.yml:
--------------------------------------------------------------------------------
1 | name: Static Analysis (informative)
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | phpstan:
10 | name: PHPStan
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 | - uses: shivammathur/setup-php@v2
15 | with:
16 | php-version: 8.1
17 | coverage: none
18 |
19 | - run: composer install --no-progress --prefer-dist
20 | - run: composer require phpstan/phpstan --no-progress --dev
21 | - run: vendor/bin/phpstan analyse src/
22 | continue-on-error: true # is only informative
23 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 |
7 | linux:
8 | name: Test on Linux
9 | runs-on: ubuntu-20.04
10 | strategy:
11 | fail-fast: false
12 | matrix:
13 | php-version: ['8.1', '8.2', '8.3']
14 |
15 | steps:
16 | - uses: actions/checkout@v2
17 | with:
18 | fetch-depth: 1
19 |
20 | - run: php${{ matrix.php-version }} -v
21 | - run: php${{ matrix.php-version }} -m
22 | - run: composer -V
23 | - run: composer install --no-progress
24 | - run: php${{ matrix.php-version }} vendor/bin/phpunit
25 |
26 | windows:
27 | name: Test on Windows
28 | defaults:
29 | run:
30 | shell: cmd
31 | runs-on: windows-latest
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | php-version: ['8.1', '8.2']
36 | arch: [x64]
37 | ts: [nts]
38 |
39 | steps:
40 | - name: Setup PHP
41 | id: setup-php
42 | uses: cmb69/setup-php-sdk@v0.7
43 | with:
44 | version: ${{matrix.php-version}}
45 | arch: ${{matrix.arch}}
46 | ts: ${{matrix.ts}}
47 | - uses: actions/checkout@v2
48 | with:
49 | fetch-depth: 1
50 |
51 | - run: php -v
52 | - run: echo extension=gmp>>C:\tools\php\php.ini
53 | - run: php -m
54 | - run: composer -V
55 | - run: composer install --no-progress
56 | - run: php vendor/bin/phpunit
57 |
58 | code_coverage:
59 | name: Code Coverage
60 | runs-on: ubuntu-latest
61 | steps:
62 | - uses: actions/checkout@v2
63 | - uses: shivammathur/setup-php@v2
64 | with:
65 | php-version: 8.1
66 | coverage: none
67 |
68 | - run: composer install --no-progress
69 | - run: mkdir -p build/logs
70 | - run: phpdbg -qrr vendor/bin/phpunit
71 | - run: wget https://github.com/php-coveralls/php-coveralls/releases/download/v2.4.3/php-coveralls.phar
72 | - env:
73 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
74 | run: php php-coveralls.phar --verbose
75 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | composer.phar
2 | /vendor/
3 | composer.lock
4 |
5 | # Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control
6 | # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
7 | # composer.lock
8 | nbproject/
9 | build/
10 | .phpunit.result.cache
11 | infection.log
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Boris Momčilović
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 | # php-ethereum-address [](https://github.com/kornrunner/php-ethereum-address/actions/workflows/tests.yml) [](https://coveralls.io/github/kornrunner/php-ethereum-address?branch=master) [](https://packagist.org/packages/kornrunner/ethereum-address)
2 |
3 |
4 | ```lang=bash
5 | $ composer require kornrunner/ethereum-address
6 | ```
7 |
8 | ## Usage
9 |
10 | Create a new address:
11 |
12 | ```php
13 | get();
23 | // 4e1c45599f667b4dc3604d69e43722d4ace6b770
24 |
25 | $address->getPrivateKey();
26 | // 33eb576d927573cff6ae50a9e09fc60b672a8dafdfbe3045c7f62955fc55ccb4
27 |
28 | $address->getPublicKey();
29 | // 20876c03fff2b09ea01861f3b3789ada54a20a8c5e90170618364cbb02d8e6408401e120158f489376a1db3f8cde24f9432976d2f89aeb193fb5becc094a28b9
30 | ```
31 |
32 | Or load one from private key:
33 |
34 | ```php
35 | get();
46 | // 4e1c45599f667b4dc3604d69e43722d4ace6b770
47 |
48 | $address->getPrivateKey();
49 | // 33eb576d927573cff6ae50a9e09fc60b672a8dafdfbe3045c7f62955fc55ccb4
50 |
51 | $address->getPublicKey();
52 | // 20876c03fff2b09ea01861f3b3789ada54a20a8c5e90170618364cbb02d8e6408401e120158f489376a1db3f8cde24f9432976d2f89aeb193fb5becc094a28b9
53 | ```
54 |
55 | ## License
56 |
57 | MIT
58 |
59 | ## Crypto
60 |
61 |
62 | [ 0x9c7b7a00972121fb843af7af74526d7eb585b171][Ethereum]
63 |
64 | [Ethereum]: https://etherscan.io/address/0x9c7b7a00972121fb843af7af74526d7eb585b171 "Donate with Ethereum"
65 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "kornrunner/ethereum-address",
3 | "description": "Pure PHP Ethereum Address Generator / Validator",
4 | "type": "library",
5 | "require": {
6 | "php": ">=8.1",
7 | "kornrunner/keccak": "^1.0",
8 | "paragonie/ecc": "^2"
9 | },
10 | "require-dev": {
11 | "phpunit/phpunit": "^9"
12 | },
13 | "autoload": {
14 | "psr-4": {
15 | "kornrunner\\Ethereum\\": "src"
16 | }
17 | },
18 | "autoload-dev": {
19 | "psr-4": {
20 | "kornrunner\\Ethereum\\": "test"
21 | }
22 | },
23 | "license": "MIT",
24 | "authors": [
25 | {
26 | "name": "Boris Momčilović",
27 | "email": "boris.momcilovic@gmail.com"
28 | }
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/infection.json.dist:
--------------------------------------------------------------------------------
1 | {
2 | "timeout": 10,
3 | "source": {
4 | "directories": [
5 | "src"
6 | ]
7 | },
8 | "logs": {
9 | "text": "infection.log"
10 | },
11 | "mutators": {
12 | "@default": true
13 | }
14 | }
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | src
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | test
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/src/Address.php:
--------------------------------------------------------------------------------
1 | generator256k1();
15 | if (empty ($privateKey)) {
16 | $this->privateKey = $generator->createPrivateKey();
17 | } else {
18 | if (!ctype_xdigit($privateKey)) {
19 | throw new InvalidArgumentException('Private key must be a hexadecimal number');
20 | }
21 | if (strlen($privateKey) != self::SIZE) {
22 | throw new InvalidArgumentException(sprintf('Private key should be exactly %d chars long', self::SIZE));
23 | }
24 |
25 | $key = gmp_init($privateKey, 16);
26 | $this->privateKey = $generator->getPrivateKeyFrom($key);
27 | }
28 | }
29 |
30 | public function getPrivateKey(): string {
31 | return str_pad(gmp_strval($this->privateKey->getSecret(), 16), self::SIZE, '0', STR_PAD_LEFT);
32 | }
33 |
34 | public function getPublicKey(): string {
35 | $publicKey = $this->privateKey->getPublicKey();
36 | $publicKeySerializer = new DerPublicKeySerializer(EccFactory::getAdapter());
37 | return substr($publicKeySerializer->getUncompressedKey($publicKey), 2);
38 | }
39 |
40 | public function get(): string {
41 | $hash = Keccak::hash(hex2bin($this->getPublicKey()), 256);
42 | return substr($hash, -40);
43 | }
44 |
45 | /**
46 | * @var PrivateKeyInterface
47 | */
48 | private $privateKey;
49 |
50 | private const SIZE = 64;
51 | }
52 |
--------------------------------------------------------------------------------
/test/AddressTest.php:
--------------------------------------------------------------------------------
1 | assertNotEmpty($address->getPrivateKey());
13 | $this->assertIsString($address->getPrivateKey());
14 | $this->assertSame(64, strlen($address->getPrivateKey()));
15 | $this->assertSame(40, strlen($address->get()));
16 | }
17 |
18 | public function testCreateFromPrivateKey(): void {
19 | $key = '996b7de9c371b0ca9f916d6c264c04a57e350e84addc286ac3f91e8937113f63';
20 | $address = new Address($key);
21 | $this->assertSame($key, $address->getPrivateKey());
22 | $this->assertSame('677a637ec8f0bb2c8d33c6ace08054e521bff4b5', $address->get());
23 | $this->assertSame('5f65c9c32a4e38393b79ccf94913c1e5dbe7071d4264aad290d936c4bb2a7c0e3a71ebc855aaadd38f477320d54cd88e5133bfcf97bbf037252db4cd824ab902', $address->getPublicKey());
24 | }
25 | /**
26 | * @dataProvider privateKeyPading
27 | */
28 | public function testPrivateKeyPadding($key, $public): void {
29 | $address = new Address($key);
30 | $this->assertSame($public, $address->get());
31 | }
32 |
33 | public static function privateKeyPading(): array {
34 | return [
35 | ['93262d84237f92dc8e4409062dcc9dfc8cdc211ec32b18aa073af15841cd8440', '669d9098736e33b8a0ee0470c10357b66caac548'],
36 | ['093262d84237f92dc8e4409062dcc9dfc8cdc211ec32b18aa073af15841cd844', '2c10383ae14f59415979d7c232ca2c85b62c18a9'],
37 | ['07a51d7d4445c567c12639ca38e4c9fc4b12f6ec9f0aab82f98c28acaae446a3', 'f81153ba99e401149c6d028eb39fd657e474e7c0'],
38 | ['7a51d7d4445c567c12639ca38e4c9fc4b12f6ec9f0aab82f98c28acaae446a30', 'f783c3bccfcc24a3731eb25b9587bf5071aab592'],
39 | ];
40 | }
41 |
42 | public function testThrowsNotHex(): void {
43 | $this->expectException(InvalidArgumentException::class);
44 | $this->expectExceptionMessage('Private key must be a hexadecimal number');
45 | new Address('xxxx');
46 | }
47 |
48 | public function testThrowsWrongSize(): void {
49 | $this->expectException(InvalidArgumentException::class);
50 | $this->expectExceptionMessage('Private key should be exactly 64 chars long');
51 | new Address(dechex(1));
52 | }
53 |
54 | }
--------------------------------------------------------------------------------