├── .coveralls.yml ├── .github └── workflows │ ├── static-analysis.yml │ └── tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src └── Ethereum │ ├── Contract.php │ ├── Token.php │ └── Token │ ├── AE.php │ ├── BAT.php │ ├── BNB.php │ ├── BTM.php │ ├── BTMX.php │ ├── CENNZ.php │ ├── CRO.php │ ├── DAI.php │ ├── DGD.php │ ├── DX.php │ ├── ENJ.php │ ├── HEDG.php │ ├── HOT.php │ ├── HT.php │ ├── ICX.php │ ├── INB.php │ ├── INO.php │ ├── IOST.php │ ├── KCS.php │ ├── KICK.php │ ├── KNC.php │ ├── LEO.php │ ├── LINK.php │ ├── MANA.php │ ├── MATIC.php │ ├── MCO.php │ ├── MKR.php │ ├── MOF.php │ ├── NEXO.php │ ├── NOAH.php │ ├── OKB.php │ ├── OMG.php │ ├── PAX.php │ ├── QNT.php │ ├── REP.php │ ├── RLC.php │ ├── SAI.php │ ├── SNT.php │ ├── SNX.php │ ├── SXP.php │ ├── Seele.php │ ├── THETA.php │ ├── TUSD.php │ ├── USDC.php │ ├── USDT.php │ ├── VEN.php │ ├── XIN.php │ ├── ZB.php │ ├── ZIL.php │ └── ZRX.php └── test └── Ethereum └── TokenTest.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.0 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.0', '8.1', '8.2'] 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.0', '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: php -m 53 | - run: composer -V 54 | - run: composer install --no-progress 55 | - run: php vendor/bin/phpunit 56 | 57 | code_coverage: 58 | name: Code Coverage 59 | runs-on: ubuntu-latest 60 | steps: 61 | - uses: actions/checkout@v2 62 | - uses: shivammathur/setup-php@v2 63 | with: 64 | php-version: 8.0 65 | coverage: none 66 | 67 | - run: composer install --no-progress 68 | - run: mkdir -p build/logs 69 | - run: phpdbg -qrr vendor/bin/phpunit 70 | - run: wget https://github.com/php-coveralls/php-coveralls/releases/download/v2.4.3/php-coveralls.phar 71 | - env: 72 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 73 | run: php php-coveralls.phar --verbose 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | /vendor/ 3 | composer.lock 4 | 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 | nbproject/ 9 | build/ 10 | infection.log 11 | .phpunit.result.cache 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 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-token [![Tests](https://github.com/kornrunner/php-ethereum-token/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/kornrunner/php-ethereum-token/actions/workflows/tests.yml) [![Coverage Status](https://coveralls.io/repos/github/kornrunner/php-ethereum-token/badge.svg?branch=master)](https://coveralls.io/github/kornrunner/php-ethereum-token?branch=master) [![Latest Stable Version](https://poser.pugx.org/kornrunner/ethereum-token/v/stable)](https://packagist.org/packages/kornrunner/ethereum-token) 2 | 3 | PHP Ethereum Token Utils 4 | 5 | ## Installation 6 | 7 | ```sh 8 | $ composer require kornrunner/ethereum-token 9 | ``` 10 | 11 | ## Usage 12 | 13 | To prepare a offline transaction, using `kornrunner/ethereum-offline-raw-tx` 14 | 15 | ```php 16 | use kornrunner\Ethereum\Token; 17 | use kornrunner\Ethereum\Transaction; 18 | 19 | $nonce = '04'; 20 | $gasPrice = '03f5476a00'; 21 | $gasLimit = '027f4b'; 22 | $to = '1a8c8adfbe1c59e8b58cc0d515f07b7225f51c72'; 23 | 24 | $privateKey = 'b2f2698dd7343fa5afc96626dee139cb92e58e5d04e855f4c712727bf198e898'; 25 | 26 | $token = new Token; 27 | $usdt = new Token\USDT; 28 | 29 | $amount = 20; 30 | $hexAmount = $token->hexAmount($usdt, $amount); 31 | // 0x1312d00 32 | 33 | $data = $token->getTransferData($to, $hexAmount); 34 | // 0xa9059cbb0000000000000000000000001a8c8adfbe1c59e8b58cc0d515f07b7225f51c720000000000000000000000000000000000000000000000000000000001312d00 35 | 36 | $transaction = new Transaction($nonce, $gasPrice, $gasLimit, $usdt::ADDRESS, '', $data); 37 | $transaction->getRaw($privateKey); 38 | // f8a9048503f5476a0083027f4b94dac17f958d2ee523a2206206994597c13d831ec7b844a9059cbb0000000000000000000000001a8c8adfbe1c59e8b58cc0d515f07b7225f51c720000000000000000000000000000000000000000000000000000000001312d00801ba03e141ea4233ec00bb3a80d7fea5f774b736772851b7bad18453d0f3c6097c42e9fa6eb47b6bead6a76d7db12809e2c916df999d7b99b613fcaa135abd8a0078e 39 | ``` 40 | 41 | ## Crypto 42 | 43 | [![Ethereum](https://user-images.githubusercontent.com/725986/61891022-0d0c7f00-af09-11e9-829f-096c039bbbfa.png) 0x9c7b7a00972121fb843af7af74526d7eb585b171][Ethereum] 44 | 45 | [Ethereum]: https://etherscan.io/address/0x9c7b7a00972121fb843af7af74526d7eb585b171 "Donate with Ethereum" 46 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kornrunner/ethereum-token", 3 | "description": "PHP Ethereum Token Utils", 4 | "keywords": ["ethereum", "offline", "transaction", "eth", "tx"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Boris Momcilovic", 9 | "homepage": "https://github.com/kornrunner/php-ethereum-token" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=7.3", 14 | "ext-bcmath": "*" 15 | }, 16 | "autoload": { 17 | "psr-4": { 18 | "kornrunner\\": "src" 19 | } 20 | }, 21 | "autoload-dev": { 22 | "psr-4": { 23 | "kornrunner\\": "test" 24 | } 25 | }, 26 | "require-dev": { 27 | "phpunit/phpunit": "^9" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/Ethereum/Contract.php: -------------------------------------------------------------------------------- 1 | sanitizeAddress($toAddress), 32, '0', STR_PAD_LEFT), 13 | str_pad($this->sanitizeHex($hexAmount), 64, '0', STR_PAD_LEFT) 14 | ); 15 | } 16 | 17 | public function hexAmount(Contract $token, float $amount): string { 18 | return '0x' . static::bcdechex(bcmul((string) $amount, bcpow('10', strval($token::DECIMALS), 0), 0)); 19 | } 20 | 21 | private function sanitizeAddress(string $address): string { 22 | $address = $this->sanitizeHex($address); 23 | if (strlen($address) !== 40) { 24 | throw new RuntimeException('Invalid address provided'); 25 | } 26 | 27 | return $address; 28 | } 29 | 30 | private function sanitizeHex(string $hex): string { 31 | if (stripos($hex, '0x') === 0) { 32 | $hex = substr($hex, 2); 33 | } 34 | 35 | $length = strlen($hex); 36 | if (($length == 0) 37 | || (trim($hex, '0..9A..Fa..f') !== '')) { 38 | throw new RuntimeException('Invalid hex provided'); 39 | } 40 | 41 | return $hex; 42 | } 43 | 44 | private static function bcdechex(string $dec): string { 45 | $end = bcmod($dec, '16'); 46 | $remainder = bcdiv(bcsub($dec, $end), '16'); 47 | return $remainder == 0 ? dechex((int) $end) : static::bcdechex($remainder) . dechex((int) $end); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/Ethereum/Token/AE.php: -------------------------------------------------------------------------------- 1 | assertSame('0x0', $token->hexAmount(new Token\AE, 0)); 13 | $this->assertSame('0xde0b6b3a7640000', $token->hexAmount(new Token\AE, 1)); 14 | $this->assertSame('0xbb8', $token->hexAmount(new Token\USDT, 0.003)); 15 | } 16 | 17 | public function testTransferInvalidAddress(): void { 18 | $this->expectException(RuntimeException::class); 19 | $this->expectExceptionMessage('Invalid hex provided'); 20 | $token = new Token; 21 | $token->getTransferData('test', ''); 22 | } 23 | 24 | public function testTransferInvalidAddressLength(): void { 25 | $this->expectException(RuntimeException::class); 26 | $this->expectExceptionMessage('Invalid address provided'); 27 | $token = new Token; 28 | $token->getTransferData('aa', ''); 29 | } 30 | 31 | public function testTransferInvalidAmount(): void { 32 | $this->expectException(RuntimeException::class); 33 | $this->expectExceptionMessage('Invalid hex provided'); 34 | $token = new Token; 35 | $token->getTransferData('0x677a637ec8f0bb2c8d33c6ace08054e521bff4b5', ''); 36 | } 37 | 38 | public function testGetTransferData(): void { 39 | $token = new Token; 40 | $this->assertSame('0xa9059cbb000000000000000000000000677a637ec8f0bb2c8d33c6ace08054e521bff4b500000000000000000000000000000000000000000000000000000000000000ac', 41 | $token->getTransferData('0x677a637ec8f0bb2c8d33c6ace08054e521bff4b5', 'ac')); 42 | $this->assertSame('0xa9059cbb000000000000000000000000677a637ec8f0bb2c8d33c6ace08054e521bff4b500000000000000000000000000000000000000000000000000000000000000ac', 43 | $token->getTransferData('677a637ec8f0bb2c8d33c6ace08054e521bff4b5', 'ac')); 44 | $this->assertSame('0xa9059cbb000000000000000000000000677a637ec8f0bb2c8d33c6ace08054e521bff4b500000000000000000000000000000000000000000000000000000000000000ac', 45 | $token->getTransferData('677a637ec8f0bb2c8d33c6ace08054e521bff4b5', '0xac')); 46 | $this->assertSame('0xa9059cbb000000000000000000000000677a637ec8f0bb2c8d33c6ace08054e521bff4b50000000000000000000000000000000000000000000000000de0b6b3a7640000', 47 | $token->getTransferData('0x677a637ec8f0bb2c8d33c6ace08054e521bff4b5', $token->hexAmount(new Token\AE, 1))); 48 | $this->assertSame('0xa9059cbb000000000000000000000000677a637ec8f0bb2c8d33c6ace08054e521bff4b50000000000000000000000000000000000000000000000000000000000000bb8', 49 | $token->getTransferData('0x677a637ec8f0bb2c8d33c6ace08054e521bff4b5', $token->hexAmount(new Token\USDT, 0.003))); 50 | $this->assertSame('0xa9059cbb000000000000000000000000677a637ec8f0bb2c8d33c6ace08054e521bff4b500000000000000000000000000000000000000000000000000000000004c4b40', 51 | $token->getTransferData('0x677a637ec8f0bb2c8d33c6ace08054e521bff4b5', $token->hexAmount(new Token\USDT, 5))); 52 | $this->assertSame('0xa9059cbb000000000000000000000000677a637ec8f0bb2c8d33c6ace08054e521bff4b5000000000000000000000000000000000000000000000000000aa87bee538000', 53 | $token->getTransferData('0x677a637ec8f0bb2c8d33c6ace08054e521bff4b5', $token->hexAmount(new Token\NEXO, 0.003))); 54 | } 55 | 56 | } --------------------------------------------------------------------------------