├── .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 [![Tests](https://github.com/kornrunner/php-ethereum-address/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/kornrunner/php-ethereum-address/actions/workflows/tests.yml) [![Coverage Status](https://coveralls.io/repos/github/kornrunner/php-ethereum-address/badge.svg?branch=master)](https://coveralls.io/github/kornrunner/php-ethereum-address?branch=master) [![Latest Stable Version](https://poser.pugx.org/kornrunner/ethereum-address/v/stable)](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 | [![Ethereum](https://user-images.githubusercontent.com/725986/61891022-0d0c7f00-af09-11e9-829f-096c039bbbfa.png) 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 | } --------------------------------------------------------------------------------