From 0d2fa4074b9cdaeedaa316bfeb57c4edb64134cc Mon Sep 17 00:00:00 2001 From: fourclub39 Date: Sun, 26 Feb 2023 17:26:52 +0200 Subject: [PATCH] init --- .gitignore | 33 + .travis.yml | 17 + _config.yml | 1 + abi.json | 1 + composer.json | 39 + examples/account.php | 24 + examples/amount.php | 13 + examples/balance.php | 15 + examples/contract.php | 29 + examples/contract_balance.php | 16 + examples/custom-nodes.php | 17 + examples/find-transaction.php | 15 + examples/hex.php | 10 + examples/is-connection.php | 14 + examples/option-trx.php | 21 + examples/send-transaction USDT-TRC20.php | 28 + examples/send-transaction.php | 23 + examples/transactionBuilder.php | 19 + examples/universal.php | 51 + phpunit.xml.dist | 22 + src/Concerns/ManagesTronscan.php | 24 + src/Concerns/ManagesUniversal.php | 85 ++ src/Exception/ErrorException.php | 8 + src/Exception/NotFoundException.php | 9 + src/Exception/TRC20Exception.php | 5 + src/Exception/TronException.php | 6 + src/Provider/HttpProvider.php | 190 +++ src/Provider/HttpProviderInterface.php | 31 + src/Support/Base58.php | 32 + src/Support/Base58Check.php | 70 ++ src/Support/BigInteger.php | 295 +++++ src/Support/Crypto.php | 80 ++ src/Support/Hash.php | 41 + src/Support/Keccak.php | 312 +++++ src/Support/Secp.php | 21 + src/Support/Utils.php | 186 +++ src/TRC20Contract.php | 405 +++++++ src/TransactionBuilder.php | 600 ++++++++++ src/Tron.php | 1389 ++++++++++++++++++++++ src/TronAddress.php | 93 ++ src/TronAwareTrait.php | 136 +++ src/TronInterface.php | 134 +++ src/TronManager.php | 204 ++++ src/trc20.json | 269 +++++ tests/TronTest.php | 52 + 45 files changed, 5085 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 _config.yml create mode 100644 abi.json create mode 100644 composer.json create mode 100644 examples/account.php create mode 100644 examples/amount.php create mode 100644 examples/balance.php create mode 100644 examples/contract.php create mode 100644 examples/contract_balance.php create mode 100644 examples/custom-nodes.php create mode 100644 examples/find-transaction.php create mode 100644 examples/hex.php create mode 100644 examples/is-connection.php create mode 100644 examples/option-trx.php create mode 100644 examples/send-transaction USDT-TRC20.php create mode 100644 examples/send-transaction.php create mode 100644 examples/transactionBuilder.php create mode 100644 examples/universal.php create mode 100644 phpunit.xml.dist create mode 100644 src/Concerns/ManagesTronscan.php create mode 100644 src/Concerns/ManagesUniversal.php create mode 100644 src/Exception/ErrorException.php create mode 100644 src/Exception/NotFoundException.php create mode 100644 src/Exception/TRC20Exception.php create mode 100644 src/Exception/TronException.php create mode 100644 src/Provider/HttpProvider.php create mode 100644 src/Provider/HttpProviderInterface.php create mode 100644 src/Support/Base58.php create mode 100644 src/Support/Base58Check.php create mode 100644 src/Support/BigInteger.php create mode 100644 src/Support/Crypto.php create mode 100644 src/Support/Hash.php create mode 100644 src/Support/Keccak.php create mode 100644 src/Support/Secp.php create mode 100644 src/Support/Utils.php create mode 100644 src/TRC20Contract.php create mode 100644 src/TransactionBuilder.php create mode 100644 src/Tron.php create mode 100644 src/TronAddress.php create mode 100644 src/TronAwareTrait.php create mode 100644 src/TronInterface.php create mode 100644 src/TronManager.php create mode 100644 src/trc20.json create mode 100644 tests/TronTest.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ef424e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# User-specific stuff +.idea +# CMake +cmake-build-*/ + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +examples/index.php +composer.lock +examples/index2.php +vendor/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..727cfa2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,17 @@ +language: php + +php: + - 7.4 + +matrix: + fast_finish: true + +cache: + directories: + - $HOME/.composer/cache + +before_install: + - travis_retry composer self-update + +install: + - travis_retry composer install --no-interaction --prefer-dist diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..c419263 --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-cayman \ No newline at end of file diff --git a/abi.json b/abi.json new file mode 100644 index 0000000..f2a4c99 --- /dev/null +++ b/abi.json @@ -0,0 +1 @@ +[{"constant":false,"inputs":[{"name":"number","type":"uint256"}],"name":"fibonacciNotify","outputs":[{"name":"result","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"number","type":"uint256"}],"name":"fibonacci","outputs":[{"name":"result","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"name":"input","type":"uint256"},{"indexed":false,"name":"result","type":"uint256"}],"name":"Notify","type":"event"}] diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..56620fd --- /dev/null +++ b/composer.json @@ -0,0 +1,39 @@ +{ + "name": "fourclub/tron-api", + "description": "PHP API for interacting with Tron (Trx)", + "license": "MIT", + "type": "library", + "homepage": "https://github.com/fourclub/tron-api", + "require": { + "php": "^7.4", + "comely-io/data-types": "^1.0", + "guzzlehttp/guzzle": "^7.0", + "iexbase/web3.php": "^2.0.1", + "kornrunner/secp256k1": "^0.1.2", + "simplito/elliptic-php": "^1.0", + "ext-json": "*", + "ext-bcmath": "*" + }, + + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "autoload": { + "psr-4": { + "fourclub\\TronAPI\\": "src" + } + }, + + "autoload-dev": { + "psr-4": { + "fourclub\\TronAPI\\Test\\": "tests" + } + }, + + "config": { + "sort-packages": true + }, + + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/examples/account.php b/examples/account.php new file mode 100644 index 0000000..130ccda --- /dev/null +++ b/examples/account.php @@ -0,0 +1,24 @@ +generateAddress(); // or createAddress() + $isValid = $tron->isAddress($generateAddress->getAddress()); + + + echo 'Address hex: ' . $generateAddress->getAddress(); + echo 'Address base58: ' . $generateAddress->getAddress(true); + echo 'Private key: ' . $generateAddress->getPrivateKey(); + echo 'Public key: ' . $generateAddress->getPublicKey(); + echo 'Is Validate: ' . $isValid; + + echo 'Raw data: ' . $generateAddress->getRawData(); + +} catch (\fourclub\TronAPI\Exception\TronException $e) { + echo $e->getMessage(); +} + + + diff --git a/examples/amount.php b/examples/amount.php new file mode 100644 index 0000000..35729a8 --- /dev/null +++ b/examples/amount.php @@ -0,0 +1,13 @@ +toTron(1.15); //1150000 +$to = $tron->fromTron(11500000); //11.5000000 diff --git a/examples/balance.php b/examples/balance.php new file mode 100644 index 0000000..43b9b7a --- /dev/null +++ b/examples/balance.php @@ -0,0 +1,15 @@ +getMessage()); +} + +$tron->setAddress('address'); +$balance = $tron->getBalance(null, true); \ No newline at end of file diff --git a/examples/contract.php b/examples/contract.php new file mode 100644 index 0000000..f267761 --- /dev/null +++ b/examples/contract.php @@ -0,0 +1,29 @@ +getMessage(); +} + +try { + $tron = new Tron($fullNode, $solidityNode, $eventServer, null, true); + $contract = $tron->contract('TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t'); // Tether USDT https://tronscan.org/#/token20/TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t + + // Data + echo $contract->name(); + echo $contract->symbol(); + echo $contract->balanceOf(); + echo $contract->totalSupply(); + //echo $contract->transfer('to', 'amount', 'from'); + + +} catch (\fourclub\TronAPI\Exception\TronException $e) { + echo $e->getMessage(); +} \ No newline at end of file diff --git a/examples/contract_balance.php b/examples/contract_balance.php new file mode 100644 index 0000000..79d365e --- /dev/null +++ b/examples/contract_balance.php @@ -0,0 +1,16 @@ +getMessage()); +} + +$balance = $tron->getTransactionBuilder()->contractbalance($tron->getAddress); +foreach ($balance as $key => $item) { + echo $item["name"] . " (" . $item["symbol"] . ") => " . $item["balance"] . "\n"; +} + diff --git a/examples/custom-nodes.php b/examples/custom-nodes.php new file mode 100644 index 0000000..3714f81 --- /dev/null +++ b/examples/custom-nodes.php @@ -0,0 +1,17 @@ +getMessage()); +} \ No newline at end of file diff --git a/examples/find-transaction.php b/examples/find-transaction.php new file mode 100644 index 0000000..903f6f9 --- /dev/null +++ b/examples/find-transaction.php @@ -0,0 +1,15 @@ +getMessage()); +} + +$detail = $tron->getTransaction('TxId'); +var_dump($detail); \ No newline at end of file diff --git a/examples/hex.php b/examples/hex.php new file mode 100644 index 0000000..23e3d06 --- /dev/null +++ b/examples/hex.php @@ -0,0 +1,10 @@ +toHex('TT67rPNwgmpeimvHUMVzFfKsjL9GZ1wGw8'); +//result: 41BBC8C05F1B09839E72DB044A6AA57E2A5D414A10 + +$tron->fromHex('41BBC8C05F1B09839E72DB044A6AA57E2A5D414A10'); +//result: TT67rPNwgmpeimvHUMVzFfKsjL9GZ1wGw8 \ No newline at end of file diff --git a/examples/is-connection.php b/examples/is-connection.php new file mode 100644 index 0000000..7c796ba --- /dev/null +++ b/examples/is-connection.php @@ -0,0 +1,14 @@ +getMessage()); +} + +$tron->isConnected(); diff --git a/examples/option-trx.php b/examples/option-trx.php new file mode 100644 index 0000000..009ec05 --- /dev/null +++ b/examples/option-trx.php @@ -0,0 +1,21 @@ +getMessage()); +} + +//option 1 +$tron->sendTransaction('to', 0.1, 'hello'); + +//option 2 +$tron->send('to', 0.1); + +//option 3 +$tron->sendTrx('to', 0.1); diff --git a/examples/send-transaction USDT-TRC20.php b/examples/send-transaction USDT-TRC20.php new file mode 100644 index 0000000..663c12c --- /dev/null +++ b/examples/send-transaction USDT-TRC20.php @@ -0,0 +1,28 @@ +getMessage()); +} + +$tron->setAddress('address'); +$tron->setPrivateKey('privateKey'); + +try { + $transfer = $tron->send('ToAddress', 1); + $transfer = $tron + ->contract('TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t') // tether contract address + ->setFeeLimit(15); // fee limit 15 trx o more AND + // send "USDT amount" to " USDT address" + $result = $transfer->transfer('address To', 'USDT amount'); +} catch (\fourclub\TronAPI\Exception\TronException $e) { + die($e->getMessage()); +} + +var_dump($transfer); \ No newline at end of file diff --git a/examples/send-transaction.php b/examples/send-transaction.php new file mode 100644 index 0000000..d72eb81 --- /dev/null +++ b/examples/send-transaction.php @@ -0,0 +1,23 @@ +getMessage()); +} + +$tron->setAddress('address'); +$tron->setPrivateKey('privateKey'); + +try { + $transfer = $tron->send('ToAddress', 1); +} catch (\fourclub\TronAPI\Exception\TronException $e) { + die($e->getMessage()); +} + +var_dump($transfer); \ No newline at end of file diff --git a/examples/transactionBuilder.php b/examples/transactionBuilder.php new file mode 100644 index 0000000..30085ff --- /dev/null +++ b/examples/transactionBuilder.php @@ -0,0 +1,19 @@ +getMessage()); +} + + +try { + $transaction = $tron->getTransactionBuilder()->sendTrx('to', 2, 'fromAddress'); + $signedTransaction = $tron->signTransaction($transaction); + $response = $tron->sendRawTransaction($signedTransaction); +} catch (\fourclub\TronAPI\Exception\TronException $e) { + die($e->getMessage()); +} diff --git a/examples/universal.php b/examples/universal.php new file mode 100644 index 0000000..d3353cf --- /dev/null +++ b/examples/universal.php @@ -0,0 +1,51 @@ +setPrivateKey('...'); + + +/** + * check multi balances + * + * $address = [ + * ['address', 'isFromTron'], + * ['address', 'isFromTron'], + * ] + */ + +//address one -> TRWBqiqoFZysoAeyR1J35ibuyc8EvhUAoY +$addresses = [ + ['address one', true], + ['address two', true], + ['address three', false], +]; + +//isValid (tron address) - default false +$check = $tron->balances($addresses); +var_dump($check); + + +/** + * send one to many + * + * $address = [ + * ['to address', 'amount float'], + * ['to address', 'amount float'], + * ] + * + * toAddress format: TRWBqiqoFZysoAeyR1J35ibuyc8EvhUAoY + */ + + +$toArray = [ + ['TRWBqiqoFZysoAeyR1J35ibuyc8EvhUAoY', 0.1], + ['TRWBqiqoFZysoAeyR1J35ibuyc8EvhUAoY', 0.2], + ['other address', 0.001] +]; + +//default: $this->setPrivateKey(); +$send = $tron->sendOneToMany('from_address', $toArray, 'private_key alt'); +var_dump($send); diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..0acdf54 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,22 @@ + + + + + tests + + + + + src/ + + + \ No newline at end of file diff --git a/src/Concerns/ManagesTronscan.php b/src/Concerns/ManagesTronscan.php new file mode 100644 index 0000000..bf98d93 --- /dev/null +++ b/src/Concerns/ManagesTronscan.php @@ -0,0 +1,24 @@ +manager->request('api/transaction', $options); + } +} \ No newline at end of file diff --git a/src/Concerns/ManagesUniversal.php b/src/Concerns/ManagesUniversal.php new file mode 100644 index 0000000..489bb6c --- /dev/null +++ b/src/Concerns/ManagesUniversal.php @@ -0,0 +1,85 @@ + [], + 'one_to_many' => [] + ]; + + /** + * Check multiple balances + * + * @param array $accounts + * @param bool $isValid + * @return array + * @throws ErrorException + */ + public function balances(array $accounts, $isValid = false): array + { + if(!is_array($accounts)) { + throw new ErrorException('Data must be an array'); + } + + if(count($accounts) > 20) { + throw new ErrorException('Once you can check 20 accounts'); + } + + foreach ($accounts as $item) + { + if($isValid && $this->validateAddress($item[0])['result'] == false) { + throw new ErrorException($item[0].' invalid address'); + } + + array_push($this->attribute['balances'], [ + 'address' => $item[0], + 'balance' => $this->getBalance($item[0], $item[1]) + ]); + } + + return $this->attribute['balances']; + } + + /** + * We send funds to several addresses at once. + * + * @param string $from + * @param array $to + * @param null $private_key + * @param bool $isValid + * @return array + * @throws ErrorException + */ + public function sendOneToMany(array $to, $private_key = null, $isValid = false, string $from): array + { + if(!is_null($private_key)) { + $this->privateKey = $private_key; + } + + if(count($to) > 10) { + throw new ErrorException('Allowed to send to "10" accounts'); + } + + foreach ($to as $item) + { + if($isValid && $this->validateAddress($item[0])['result'] == false) { + throw new ErrorException($item[0].' invalid address'); + } + + array_push($this->attribute['one_to_many'], + $this->send($item[0], $item[1], $from) + ); + } + + return $this->attribute['one_to_many']; + } +} diff --git a/src/Exception/ErrorException.php b/src/Exception/ErrorException.php new file mode 100644 index 0000000..7fa4d39 --- /dev/null +++ b/src/Exception/ErrorException.php @@ -0,0 +1,8 @@ +host = $host; + $this->timeout = $timeout; + $this->statusPage = $statusPage; + $this->headers = $headers; + + $this->httpClient = new Client([ + 'base_uri' => $host, + 'timeout' => $timeout, + 'auth' => $user && [$user, $password] + ]); + } + + /** + * Enter a new page + * + * @param string $page + */ + public function setStatusPage(string $page = '/'): void + { + $this->statusPage = $page; + } + + /** + * Check connection + * + * @return bool + * @throws TronException + */ + public function isConnected() : bool + { + $response = $this->request($this->statusPage); + + if(array_key_exists('blockID', $response)) { + return true; + } elseif(array_key_exists('status', $response)) { + return true; + } + return false; + } + + /** + * Getting a host + * + * @return string + */ + public function getHost(): string + { + return $this->host; + } + + /** + * Getting timeout + * + * @return int + */ + public function getTimeout(): int + { + return $this->timeout; + } + + /** + * We send requests to the server + * + * @param $url + * @param array $payload + * @param string $method + * @return array|mixed + * @throws TronException + */ + public function request($url, array $payload = [], string $method = 'get'): array + { + $method = strtoupper($method); + + if(!in_array($method, ['GET', 'POST'])) { + throw new TronException('The method is not defined'); + } + + $options = [ + 'headers' => $this->headers, + 'body' => json_encode($payload) + ]; + + $request = new Request($method, $url, $options['headers'], $options['body']); + $rawResponse = $this->httpClient->send($request, $options); + + return $this->decodeBody( + $rawResponse->getBody(), + $rawResponse->getStatusCode() + ); + } + + /** + * Convert the original answer to an array + * + * @param StreamInterface $stream + * @param int $status + * @return array|mixed + */ + protected function decodeBody(StreamInterface $stream, int $status): array + { + $decodedBody = json_decode($stream->getContents(),true); + + if((string)$stream == 'OK') { + $decodedBody = [ + 'status' => 1 + ]; + }elseif ($decodedBody == null or !is_array($decodedBody)) { + $decodedBody = []; + } + + if($status == 404) { + throw new NotFoundException('Page not found'); + } + + return $decodedBody; + } +} diff --git a/src/Provider/HttpProviderInterface.php b/src/Provider/HttpProviderInterface.php new file mode 100644 index 0000000..2265c62 --- /dev/null +++ b/src/Provider/HttpProviderInterface.php @@ -0,0 +1,31 @@ +value = $this->initValue($value); + $this->mutable = $mutable; + } + + /** + * Gets the value of the big integer. + * + * @return string + */ + public function getValue(): string + { + return gmp_strval($this->value); + } + + /** + * Sets the value. + * + * @param string $value The value to set. + * @return BigInteger + */ + public function setValue(string $value): BigInteger + { + if (!$this->isMutable()) { + throw new RuntimeException('Cannot set the value since the number is immutable.'); + } + + $this->value = $this->initValue($value); + + return $this; + } + + /** + * Converts the value to an absolute number. + * + * @return BigInteger + */ + public function abs(): BigInteger + { + $value = gmp_abs($this->value); + + return $this->assignValue($value); + } + + /** + * Adds the given value to this value. + * + * @param string $value The value to add. + * @return BigInteger + */ + public function add(string $value): BigInteger + { + $gmp = $this->initValue($value); + + $calculatedValue = gmp_add($this->value, $gmp); + + return $this->assignValue($calculatedValue); + } + + /** + * Compares this number and the given number. + * + * @param string $value The value to compare. + * @return int Returns -1 is the number is less than this number. 0 if equal and 1 when greater. + */ + public function cmp($value): int + { + $value = $this->initValue($value); + + $result = gmp_cmp($this->value, $value); + + // It could happen that gmp_cmp returns a value greater than one (e.g. gmp_cmp('123', '-123')). That's why + // we do an additional check to make sure to return the correct value. + + if ($result > 0) { + return 1; + } elseif ($result < 0) { + return -1; + } + + return 0; + } + + /** + * Divides this value by the given value. + * + * @param string $value The value to divide by. + * @return BigInteger + */ + public function divide(string $value): BigInteger + { + $gmp = $this->initValue($value); + + $calculatedValue = gmp_div_q($this->value, $gmp, GMP_ROUND_ZERO); + + return $this->assignValue($calculatedValue); + } + + /** + * Calculates factorial of this value. + * + * @return BigInteger + */ + public function factorial(): BigInteger + { + $calculatedValue = gmp_fact($this->getValue()); + + return $this->assignValue($calculatedValue); + } + + /** + * Performs a modulo operation with the given number. + * + * @param string $value The value to perform a modulo operation with. + * @return BigInteger + */ + public function mod(string $value): BigInteger + { + $gmp = $this->initValue($value); + + $calculatedValue = gmp_mod($this->value, $gmp); + + return $this->assignValue($calculatedValue); + } + + /** + * Multiplies the given value with this value. + * + * @param string $value The value to multiply with. + * @return BigInteger + */ + public function multiply(string $value): BigInteger + { + $gmp = $this->initValue($value); + + $calculatedValue = gmp_mul($this->value, $gmp); + + return $this->assignValue($calculatedValue); + } + + /** + * Negates the value. + * + * @return BigInteger + */ + public function negate(): BigInteger + { + $calculatedValue = gmp_neg($this->value); + + return $this->assignValue($calculatedValue); + } + + /** + * Performs a power operation with the given number. + * + * @param int $value The value to perform a power operation with. + * @return BigInteger + */ + public function pow(int $value): BigInteger + { + $calculatedValue = gmp_pow($this->value, $value); + + return $this->assignValue($calculatedValue); + } + + /** + * Subtracts the given value from this value. + * + * @param string $value The value to subtract. + * @return BigInteger + */ + public function subtract(string $value): BigInteger + { + $gmp = $this->initValue($value); + + $calculatedValue = gmp_sub($this->value, $gmp); + + return $this->assignValue($calculatedValue); + } + + /** + * Checks if the big integr is the prime number. + * + * @param float $probabilityFactor A normalized factor between 0 and 1 used for checking the probability. + * @return bool Returns true if the number is a prime number false if not. + */ + public function isPrimeNumber(float $probabilityFactor = 1.0): bool + { + $reps = (int)floor(($probabilityFactor * 5.0) + 5.0); + + if ($reps < 5 || $reps > 10) { + throw new InvalidArgumentException('The provided probability number should be 5 to 10.'); + } + + return gmp_prob_prime($this->value, $reps) !== 0; + } + + /** + * Checks if this object is mutable. + * + * @return bool + */ + public function isMutable(): bool + { + return $this->mutable; + } + + /** + * Converts this class to a string. + * + * @return string + */ + public function toString(): string + { + return $this->getValue(); + } + + /** + * Converts this class to a string. + * + * @return string + */ + public function __toString(): string + { + return $this->toString(); + } + + /** + * A helper method to assign the given value. + * + * @param GMP $value The value to assign. + * @return BigInteger + */ + private function assignValue(GMP $value): BigInteger + { + $rawValue = gmp_strval($value); + + if ($this->isMutable()) { + $this->value = gmp_init($rawValue); + + return $this; + } + + return new BigInteger($rawValue, false); + } + + /** + * Creates a new GMP object. + * + * @param string $value The value to initialize with. + * @return GMP + * @throws InvalidArgumentException Thrown when the value is invalid. + */ + private function initValue(string $value): GMP + { + $result = @gmp_init($value); + + if ($result === false) { + throw new InvalidArgumentException('The provided number is invalid.'); + } + + return $result; + } +} diff --git a/src/Support/Crypto.php b/src/Support/Crypto.php new file mode 100644 index 0000000..baa4726 --- /dev/null +++ b/src/Support/Crypto.php @@ -0,0 +1,80 @@ + 256) { + die("Invalid Base: " . $base); + } + bcscale(0); + $value = ""; + if (!$digits) { + $digits = self::digits($base); + } + while ($dec > $base - 1) { + $rest = bcmod($dec, $base); + $dec = bcdiv($dec, $base); + $value = $digits[$rest] . $value; + } + $value = $digits[intval($dec)] . $value; + return (string)$value; + } else { + die('Please install BCMATH'); + } + } + + public static function base2dec($value, $base, $digits = false) + { + if (extension_loaded('bcmath')) { + if ($base < 2 || $base > 256) { + die("Invalid Base: " . $base); + } + bcscale(0); + if ($base < 37) { + $value = strtolower($value); + } + if (!$digits) { + $digits = self::digits($base); + } + $size = strlen($value); + $dec = "0"; + for ($loop = 0; $loop < $size; $loop++) { + $element = strpos($digits, $value[$loop]); + $power = bcpow($base, $size - $loop - 1); + $dec = bcadd($dec, bcmul($element, $power)); + } + return (string)$dec; + } else { + die('Please install BCMATH'); + } + } + + public static function digits($base) + { + if ($base > 64) { + $digits = ""; + for ($loop = 0; $loop < 256; $loop++) { + $digits .= chr($loop); + } + } else { + $digits = "0123456789abcdefghijklmnopqrstuvwxyz"; + $digits .= "ABCDEFGHIJKLMNOPQRSTUVWXYZ-_"; + } + $digits = substr($digits, 0, $base); + return (string)$digits; + } + + public static function bin2bc($num) + { + return self::base2dec($num, 256); + } +} diff --git a/src/Support/Hash.php b/src/Support/Hash.php new file mode 100644 index 0000000..1496ec3 --- /dev/null +++ b/src/Support/Hash.php @@ -0,0 +1,41 @@ +> 31)) & (0xFFFFFFFF), + $bc[($i + 4) % 5][1] ^ (($bc[($i + 1) % 5][1] << 1) | ($bc[($i + 1) % 5][0] >> 31)) & (0xFFFFFFFF) + ]; + + for ($j = 0; $j < 25; $j += 5) { + $st[$j + $i] = [ + $st[$j + $i][0] ^ $t[0], + $st[$j + $i][1] ^ $t[1] + ]; + } + } + + // Rho Pi + $t = $st[1]; + for ($i = 0; $i < 24; $i++) { + $j = self::$keccakf_piln[$i]; + + $bc[0] = $st[$j]; + + $n = self::$keccakf_rotc[$i]; + $hi = $t[0]; + $lo = $t[1]; + if ($n >= 32) { + $n -= 32; + $hi = $t[1]; + $lo = $t[0]; + } + + $st[$j] =[ + (($hi << $n) | ($lo >> (32 - $n))) & (0xFFFFFFFF), + (($lo << $n) | ($hi >> (32 - $n))) & (0xFFFFFFFF) + ]; + + $t = $bc[0]; + } + + // Chi + for ($j = 0; $j < 25; $j += 5) { + for ($i = 0; $i < 5; $i++) { + $bc[$i] = $st[$j + $i]; + } + for ($i = 0; $i < 5; $i++) { + $st[$j + $i] = [ + $st[$j + $i][0] ^ ~$bc[($i + 1) % 5][0] & $bc[($i + 2) % 5][0], + $st[$j + $i][1] ^ ~$bc[($i + 1) % 5][1] & $bc[($i + 2) % 5][1] + ]; + } + } + + // Iota + $st[0] = [ + $st[0][0] ^ $keccakf_rndc[$round][0], + $st[0][1] ^ $keccakf_rndc[$round][1] + ]; + } + } + + private static function keccak64($in_raw, int $capacity, int $outputlength, $suffix, bool $raw_output): string { + $capacity /= 8; + + $inlen = mb_strlen($in_raw, self::ENCODING); + + $rsiz = 200 - 2 * $capacity; + $rsizw = $rsiz / 8; + + $st = []; + for ($i = 0; $i < 25; $i++) { + $st[] = [0, 0]; + } + + for ($in_t = 0; $inlen >= $rsiz; $inlen -= $rsiz, $in_t += $rsiz) { + for ($i = 0; $i < $rsizw; $i++) { + $t = unpack('V*', mb_substr($in_raw, $i * 8 + $in_t, 8, self::ENCODING)); + + $st[$i] = [ + $st[$i][0] ^ $t[2], + $st[$i][1] ^ $t[1] + ]; + } + + self::keccakf64($st, self::KECCAK_ROUNDS); + } + + $temp = mb_substr($in_raw, $in_t, $inlen, self::ENCODING); + $temp = str_pad($temp, $rsiz, "\x0", STR_PAD_RIGHT); + + $temp[$inlen] = chr($suffix); + $temp[$rsiz - 1] = chr(ord($temp[$rsiz - 1]) | 0x80); + + for ($i = 0; $i < $rsizw; $i++) { + $t = unpack('V*', mb_substr($temp, $i * 8, 8, self::ENCODING)); + + $st[$i] = [ + $st[$i][0] ^ $t[2], + $st[$i][1] ^ $t[1] + ]; + } + + self::keccakf64($st, self::KECCAK_ROUNDS); + + $out = ''; + for ($i = 0; $i < 25; $i++) { + $out .= $t = pack('V*', $st[$i][1], $st[$i][0]); + } + $r = mb_substr($out, 0, $outputlength / 8, self::ENCODING); + + return $raw_output ? $r : bin2hex($r); + } + + private static function keccakf32(&$st, $rounds): void { + $keccakf_rndc = [ + [0x0000, 0x0000, 0x0000, 0x0001], [0x0000, 0x0000, 0x0000, 0x8082], [0x8000, 0x0000, 0x0000, 0x0808a], [0x8000, 0x0000, 0x8000, 0x8000], + [0x0000, 0x0000, 0x0000, 0x808b], [0x0000, 0x0000, 0x8000, 0x0001], [0x8000, 0x0000, 0x8000, 0x08081], [0x8000, 0x0000, 0x0000, 0x8009], + [0x0000, 0x0000, 0x0000, 0x008a], [0x0000, 0x0000, 0x0000, 0x0088], [0x0000, 0x0000, 0x8000, 0x08009], [0x0000, 0x0000, 0x8000, 0x000a], + [0x0000, 0x0000, 0x8000, 0x808b], [0x8000, 0x0000, 0x0000, 0x008b], [0x8000, 0x0000, 0x0000, 0x08089], [0x8000, 0x0000, 0x0000, 0x8003], + [0x8000, 0x0000, 0x0000, 0x8002], [0x8000, 0x0000, 0x0000, 0x0080], [0x0000, 0x0000, 0x0000, 0x0800a], [0x8000, 0x0000, 0x8000, 0x000a], + [0x8000, 0x0000, 0x8000, 0x8081], [0x8000, 0x0000, 0x0000, 0x8080], [0x0000, 0x0000, 0x8000, 0x00001], [0x8000, 0x0000, 0x8000, 0x8008] + ]; + + $bc = []; + for ($round = 0; $round < $rounds; $round++) { + + // Theta + for ($i = 0; $i < 5; $i++) { + $bc[$i] = [ + $st[$i][0] ^ $st[$i + 5][0] ^ $st[$i + 10][0] ^ $st[$i + 15][0] ^ $st[$i + 20][0], + $st[$i][1] ^ $st[$i + 5][1] ^ $st[$i + 10][1] ^ $st[$i + 15][1] ^ $st[$i + 20][1], + $st[$i][2] ^ $st[$i + 5][2] ^ $st[$i + 10][2] ^ $st[$i + 15][2] ^ $st[$i + 20][2], + $st[$i][3] ^ $st[$i + 5][3] ^ $st[$i + 10][3] ^ $st[$i + 15][3] ^ $st[$i + 20][3] + ]; + } + + for ($i = 0; $i < 5; $i++) { + $t = [ + $bc[($i + 4) % 5][0] ^ ((($bc[($i + 1) % 5][0] << 1) | ($bc[($i + 1) % 5][1] >> 15)) & (0xFFFF)), + $bc[($i + 4) % 5][1] ^ ((($bc[($i + 1) % 5][1] << 1) | ($bc[($i + 1) % 5][2] >> 15)) & (0xFFFF)), + $bc[($i + 4) % 5][2] ^ ((($bc[($i + 1) % 5][2] << 1) | ($bc[($i + 1) % 5][3] >> 15)) & (0xFFFF)), + $bc[($i + 4) % 5][3] ^ ((($bc[($i + 1) % 5][3] << 1) | ($bc[($i + 1) % 5][0] >> 15)) & (0xFFFF)) + ]; + + for ($j = 0; $j < 25; $j += 5) { + $st[$j + $i] = [ + $st[$j + $i][0] ^ $t[0], + $st[$j + $i][1] ^ $t[1], + $st[$j + $i][2] ^ $t[2], + $st[$j + $i][3] ^ $t[3] + ]; + } + } + + // Rho Pi + $t = $st[1]; + for ($i = 0; $i < 24; $i++) { + $j = self::$keccakf_piln[$i]; + $bc[0] = $st[$j]; + + + $n = self::$keccakf_rotc[$i] >> 4; + $m = self::$keccakf_rotc[$i] % 16; + + $st[$j] = [ + ((($t[(0+$n) %4] << $m) | ($t[(1+$n) %4] >> (16-$m))) & (0xFFFF)), + ((($t[(1+$n) %4] << $m) | ($t[(2+$n) %4] >> (16-$m))) & (0xFFFF)), + ((($t[(2+$n) %4] << $m) | ($t[(3+$n) %4] >> (16-$m))) & (0xFFFF)), + ((($t[(3+$n) %4] << $m) | ($t[(0+$n) %4] >> (16-$m))) & (0xFFFF)) + ]; + + $t = $bc[0]; + } + + // Chi + for ($j = 0; $j < 25; $j += 5) { + for ($i = 0; $i < 5; $i++) { + $bc[$i] = $st[$j + $i]; + } + for ($i = 0; $i < 5; $i++) { + $st[$j + $i] = [ + $st[$j + $i][0] ^ ~$bc[($i + 1) % 5][0] & $bc[($i + 2) % 5][0], + $st[$j + $i][1] ^ ~$bc[($i + 1) % 5][1] & $bc[($i + 2) % 5][1], + $st[$j + $i][2] ^ ~$bc[($i + 1) % 5][2] & $bc[($i + 2) % 5][2], + $st[$j + $i][3] ^ ~$bc[($i + 1) % 5][3] & $bc[($i + 2) % 5][3] + ]; + } + } + + // Iota + $st[0] = [ + $st[0][0] ^ $keccakf_rndc[$round][0], + $st[0][1] ^ $keccakf_rndc[$round][1], + $st[0][2] ^ $keccakf_rndc[$round][2], + $st[0][3] ^ $keccakf_rndc[$round][3] + ]; + } + } + + private static function keccak32($in_raw, int $capacity, int $outputlength, $suffix, bool $raw_output): string { + $capacity /= 8; + + $inlen = mb_strlen($in_raw, self::ENCODING); + + $rsiz = 200 - 2 * $capacity; + $rsizw = $rsiz / 8; + + $st = []; + for ($i = 0; $i < 25; $i++) { + $st[] = [0, 0, 0, 0]; + } + + for ($in_t = 0; $inlen >= $rsiz; $inlen -= $rsiz, $in_t += $rsiz) { + for ($i = 0; $i < $rsizw; $i++) { + $t = unpack('v*', mb_substr($in_raw, $i * 8 + $in_t, 8, self::ENCODING)); + + $st[$i] = [ + $st[$i][0] ^ $t[4], + $st[$i][1] ^ $t[3], + $st[$i][2] ^ $t[2], + $st[$i][3] ^ $t[1] + ]; + } + + self::keccakf32($st, self::KECCAK_ROUNDS); + } + + $temp = mb_substr($in_raw, $in_t, $inlen, self::ENCODING); + $temp = str_pad($temp, $rsiz, "\x0", STR_PAD_RIGHT); + + $temp[$inlen] = chr($suffix); + $temp[$rsiz - 1] = chr((int) $temp[$rsiz - 1] | 0x80); + + for ($i = 0; $i < $rsizw; $i++) { + $t = unpack('v*', mb_substr($temp, $i * 8, 8, self::ENCODING)); + + $st[$i] = [ + $st[$i][0] ^ $t[4], + $st[$i][1] ^ $t[3], + $st[$i][2] ^ $t[2], + $st[$i][3] ^ $t[1] + ]; + } + + self::keccakf32($st, self::KECCAK_ROUNDS); + + $out = ''; + for ($i = 0; $i < 25; $i++) { + $out .= $t = pack('v*', $st[$i][3],$st[$i][2], $st[$i][1], $st[$i][0]); + } + $r = mb_substr($out, 0, $outputlength / 8, self::ENCODING); + + return $raw_output ? $r: bin2hex($r); + } + + private static function keccak($in_raw, int $capacity, int $outputlength, $suffix, bool $raw_output): string { + return self::$x64 + ? self::keccak64($in_raw, $capacity, $outputlength, $suffix, $raw_output) + : self::keccak32($in_raw, $capacity, $outputlength, $suffix, $raw_output); + } + + public static function hash($in, int $mdlen, bool $raw_output = false): string { + if (!in_array($mdlen, [224, 256, 384, 512], true)) { + throw new Exception('Unsupported Keccak Hash output size.'); + } + + return self::keccak($in, $mdlen, $mdlen, self::LFSR, $raw_output); + } + + public static function shake($in, int $security_level, int $outlen, bool $raw_output = false): string { + if (!in_array($security_level, [128, 256], true)) { + throw new Exception('Unsupported Keccak Shake security level.'); + } + + return self::keccak($in, $security_level, $outlen, 0x1f, $raw_output); + } + +} \ No newline at end of file diff --git a/src/Support/Secp.php b/src/Support/Secp.php new file mode 100644 index 0000000..848b1e0 --- /dev/null +++ b/src/Support/Secp.php @@ -0,0 +1,21 @@ +sign($message, $privateKey, ['canonical' => false]); + + return $sign->toHex() . bin2hex(implode('', array_map('chr', [$sign->getRecoveryParam()]))); + } +} diff --git a/src/Support/Utils.php b/src/Support/Utils.php new file mode 100644 index 0000000..0f5be78 --- /dev/null +++ b/src/Support/Utils.php @@ -0,0 +1,186 @@ + + * @license https://github.com/fourclub/tron-api/blob/master/LICENSE (MIT License) + * @version 1.3.4 + * @link https://github.com/fourclub/tron-api + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace fourclub\TronAPI; + +use Comely\DataTypes\BcNumber; +use fourclub\TronAPI\Exception\TRC20Exception; +use fourclub\TronAPI\Exception\TronException; + +/** + * Class TRC20Contract + * @package TronAPI + */ +class TRC20Contract +{ + const TRX_TO_SUN = 1000000; + + /*** + * Maximum decimal supported by the Token + * + * @var integer|null + */ + private ?int $_decimals = null; + + /*** + * Token Name + * + * @var string|null + */ + private ?string $_name = null; + + /*** + * Token Symbol + * + * @var string|null + */ + private ?string $_symbol = null; + + /** + * The smart contract which issued TRC20 Token + * + * @var string + */ + private string $contractAddress; + + /** + * ABI Data + * + * @var string|null + */ + private $abiData; + + /** + * Fee Limit + * + * @var integer + */ + private int $feeLimit = 10; + + /** + * Base Tron object + * + * @var Tron + */ + protected Tron $_tron; + + /** + * Total Supply + * + * @var string|null + */ + private ?string $_totalSupply = null; + + /** + * Create Trc20 Contract + * + * @param Tron $tron + * @param string $contractAddress + * @param string|null $abi + */ + public function __construct(Tron $tron, string $contractAddress, string $abi = null) + { + $this->_tron = $tron; + + // If abi is absent, then it takes by default + if (is_null($abi)) { + $abi = file_get_contents(__DIR__ . '/trc20.json'); + } + + $this->abiData = json_decode($abi, true); + $this->contractAddress = $contractAddress; + } + + /** + * Debug Info + * + * @return array + * @throws TronException + */ + public function __debugInfo(): array + { + return $this->array(); + } + + /** + * Clears cached values + * + * @return void + */ + public function clearCached(): void + { + $this->_name = null; + $this->_symbol = null; + $this->_decimals = null; + $this->_totalSupply = null; + } + + /** + * All data + * + * @throws TronException + */ + public function array(): array + { + return [ + 'name' => $this->name(), + 'symbol' => $this->symbol(), + 'decimals' => $this->decimals(), + 'totalSupply' => $this->totalSupply(true) + ]; + } + + /** + * Get token name + * + * @return string + * @throws TronException + */ + public function name(): string + { + if ($this->_name) { + return $this->_name; + } + + $result = $this->trigger('name', null, []); + $name = $result[0] ?? null; + + if (!is_string($name)) { + throw new TRC20Exception('Failed to retrieve TRC20 token name'); + } + + $this->_name = $this->cleanStr($name); + return $this->_name; + } + + /** + * Get symbol name + * + * @return string + * @throws TronException + */ + public function symbol(): string + { + if ($this->_symbol) { + return $this->_symbol; + } + $result = $this->trigger('symbol', null, []); + $code = $result[0] ?? null; + + if (!is_string($code)) { + throw new TRC20Exception('Failed to retrieve TRRC20 token symbol'); + } + + $this->_symbol = $this->cleanStr($code); + return $this->_symbol; + } + + /** + * The total number of tokens issued on the main network + * + * @param bool $scaled + * @return string + * @throws Exception\TronException + * @throws TRC20Exception + */ + public function totalSupply(bool $scaled = true): string + { + if (!$this->_totalSupply) { + + $result = $this->trigger('totalSupply', null, []); + $totalSupply = $result[0]->toString() ?? null; + + if (!is_string($totalSupply) || !preg_match('/^[0-9]+$/', $totalSupply)) { + throw new TRC20Exception('Failed to retrieve TRC20 token totalSupply'); + } + + $this->_totalSupply = $totalSupply; + } + + return $scaled ? $this->decimalValue($this->_totalSupply, $this->decimals()) : $this->_totalSupply; + } + + /** + * Maximum decimal supported by the Token + * + * @throws TRC20Exception + * @throws TronException + */ + public function decimals(): int + { + if ($this->_decimals) { + return $this->_decimals; + } + + $result = $this->trigger('decimals', null, []); + $scale = intval($result[0]->toString() ?? null); + + if (is_null($scale)) { + throw new TRC20Exception('Failed to retrieve TRC20 token decimals/scale value'); + } + + $this->_decimals = $scale; + return $this->_decimals; + } + + /** + * Balance TRC20 contract + * + * @param string|null $address + * @param bool $scaled + * @return string + * @throws TRC20Exception + * @throws TronException + */ + public function balanceOf(string $address = null, bool $scaled = true): string + { + if (is_null($address)) + $address = $this->_tron->address['base58']; + + $addr = str_pad($this->_tron->address2HexString($address), 64, "0", STR_PAD_LEFT); + $result = $this->trigger('balanceOf', $address, [$addr]); + $balance = $result[0]->toString(); + + if (!is_string($balance) || !preg_match('/^[0-9]+$/', $balance)) { + throw new TRC20Exception( + sprintf('Failed to retrieve TRC20 token balance of address "%s"', $addr) + ); + } + + return $scaled ? $this->decimalValue($balance, $this->decimals()) : $balance; + } + + /** + * Send TRC20 contract + * + * @param string $to + * @param string $amount + * @param string|null $from + * @return array + * @throws TRC20Exception + * @throws TronException + */ + public function transfer(string $to, string $amount, string $from = null): array + { + if ($from == null) { + $from = $this->_tron->address['base58']; + } + + $feeLimitInSun = bcmul((string)$this->feeLimit, (string)self::TRX_TO_SUN); + + if (!is_numeric($this->feeLimit) or $this->feeLimit <= 0) { + throw new TRC20Exception('fee_limit is required.'); + } else if ($this->feeLimit > 1000) { + throw new TRC20Exception('fee_limit must not be greater than 1000 TRX.'); + } + + $tokenAmount = bcmul($amount, bcpow("10", (string)$this->decimals(), 0), 0); + + $transfer = $this->_tron->getTransactionBuilder() + ->triggerSmartContract( + $this->abiData, + $this->_tron->address2HexString($this->contractAddress), + 'transfer', + [$this->_tron->address2HexString($to), $tokenAmount], + $feeLimitInSun, + $this->_tron->address2HexString($from) + ); + + $signedTransaction = $this->_tron->signTransaction($transfer); + $response = $this->_tron->sendRawTransaction($signedTransaction); + + return array_merge($response, $signedTransaction); + } + + /** + * TRC20 All transactions + * + * @param string $address + * @param int $limit + * @return array + * + * @throws TronException + */ + public function getTransactions(string $address, int $limit = 100): array + { + return $this->_tron->getManager() + ->request("v1/accounts/{$address}/transactions/trc20?limit={$limit}&contract_address={$this->contractAddress}", [], 'get'); + } + + /** + * Get transaction info by contract address + * + * @throws TronException + */ + public function getTransactionInfoByContract(array $options = []): array + { + return $this->_tron->getManager() + ->request("v1/contracts/{$this->contractAddress}/transactions?" . http_build_query($options), [], 'get'); + } + + /** + * Get TRC20 token holder balances + * + * @throws TronException + */ + public function getTRC20TokenHolderBalance(array $options = []): array + { + return $this->_tron->getManager() + ->request("v1/contracts/{$this->contractAddress}/tokens?" . http_build_query($options), [], 'get'); + } + + /** + * Find transaction + * + * @param string $transaction_id + * @return array + * @throws TronException + */ + public function getTransaction(string $transaction_id): array + { + return $this->_tron->getManager() + ->request('/wallet/gettransactioninfobyid', ['value' => $transaction_id], 'post'); + } + + /** + * Config trigger + * + * @param $function + * @param null $address + * @param array $params + * @return mixed + * @throws TronException + */ + private function trigger($function, $address = null, array $params = []) + { + $owner_address = is_null($address) ? '410000000000000000000000000000000000000000' : $this->_tron->address2HexString($address); + + return $this->_tron->getTransactionBuilder() + ->triggerConstantContract($this->abiData, $this->_tron->address2HexString($this->contractAddress), $function, $params, $owner_address); + } + + /** + * @param string $int + * @param int $scale + * @return string + */ + protected function decimalValue(string $int, int $scale = 18): string + { + return (new BcNumber($int))->divide(pow(10, $scale), $scale)->value(); + } + + /** + * @param string $str + * @return string + */ + public function cleanStr(string $str): string + { + return preg_replace('/[^\w.-]/', '', trim($str)); + } + + /** + * Set fee limit + * + * @param int $fee_limit + * @return TRC20Contract + */ + public function setFeeLimit(int $fee_limit): TRC20Contract + { + $this->feeLimit = $fee_limit; + return $this; + } +} diff --git a/src/TransactionBuilder.php b/src/TransactionBuilder.php new file mode 100644 index 0000000..6a61236 --- /dev/null +++ b/src/TransactionBuilder.php @@ -0,0 +1,600 @@ +tron = $tron; + } + + /** + * Creates a transaction of transfer. + * If the recipient address does not exist, a corresponding account will be created on the blockchain. + * + * @param string $to + * @param float $amount + * @param string $from + * @return array + * @throws TronException + */ + public function sendTrx($to, $amount, string $from = null) + { + if ($amount < 0) { + throw new TronException('Invalid amount provided'); + } + + if(is_null($from)) { + $from = $this->tron->address['hex']; + } + + $to = $this->tron->address2HexString($to); + $from = $this->tron->address2HexString($from); + + if ($from === $to) { + throw new TronException('Cannot transfer TRX to the same account'); + } + + $response = $this->tron->getManager()->request('wallet/createtransaction', [ + 'to_address' => $to, + 'owner_address' => $from, + 'amount' => $this->tron->toTron($amount), + ]); + + return $response; + } + + /** + * Transfer Token + * + * @param string $to + * @param int $amount + * @param string $tokenID + * @param string|null $from + * @return array + * @throws TronException + */ + public function sendToken(string $to, int $amount, string $tokenID, string $from) + { + if (!is_integer($amount) or $amount <= 0) { + throw new TronException('Invalid amount provided'); + } + + if (!is_string($tokenID)) { + throw new TronException('Invalid token ID provided'); + } + + if ($to === $from) { + throw new TronException('Cannot transfer tokens to the same account'); + } + + $transfer = $this->tron->getManager()->request('wallet/transferasset', [ + 'owner_address' => $this->tron->address2HexString($from), + 'to_address' => $this->tron->address2HexString($to), + 'asset_name' => $this->tron->stringUtf8toHex($tokenID), + 'amount' => intval($amount) + ]); + + if (array_key_exists('Error', $transfer)) { + throw new TronException($transfer['Error']); + } + return $transfer; + } + + /** + * Purchase a Token + * + * @param $issuerAddress + * @param $tokenID + * @param $amount + * @param $buyer + * @return array + * @throws TronException + */ + public function purchaseToken($issuerAddress, $tokenID, $amount, $buyer) + { + if (!is_string($tokenID)) { + throw new TronException('Invalid token ID provided'); + } + + if (!is_integer($amount) and $amount <= 0) { + throw new TronException('Invalid amount provided'); + } + + $purchase = $this->tron->getManager()->request('wallet/participateassetissue', [ + 'to_address' => $this->tron->address2HexString($issuerAddress), + 'owner_address' => $this->tron->address2HexString($buyer), + 'asset_name' => $this->tron->stringUtf8toHex($tokenID), + 'amount' => $this->tron->toTron($amount) + ]); + + if (array_key_exists('Error', $purchase)) { + throw new TronException($purchase['Error']); + } + return $purchase; + } + + /** + * createToken + * + * @param array $options + * @param null $issuerAddress + * @return array + * @throws TronException + */ + public function createToken($options = [], $issuerAddress = null) + { + $startDate = new \DateTime(); + $startTimeStamp = $startDate->getTimestamp() * 1000; + + // Create default parameters in case of their absence + if(!$options['totalSupply']) $options['totalSupply'] = 0; + if(!$options['trxRatio']) $options['trxRatio'] = 1; + if(!$options['tokenRatio']) $options['tokenRatio'] = 1; + if(!$options['freeBandwidth']) $options['freeBandwidth'] = 0; + if(!$options['freeBandwidthLimit']) $options['freeBandwidthLimit'] = 0; + if(!$options['frozenAmount']) $options['frozenAmount'] = 0; + if(!$options['frozenDuration']) $options['frozenDuration'] = 0; + + if (is_null($issuerAddress)) { + $issuerAddress = $this->tron->address['hex']; + } + + if(!$options['name'] or !is_string($options['name'])) { + throw new TronException('Invalid token name provided'); + } + + if(!$options['abbreviation'] or !is_string($options['abbreviation'])) { + throw new TronException('Invalid token abbreviation provided'); + } + + if(!is_integer($options['totalSupply']) or $options['totalSupply'] <= 0) { + throw new TronException('Invalid supply amount provided'); + } + + if(!is_integer($options['trxRatio']) or $options['trxRatio'] <= 0) { + throw new TronException('TRX ratio must be a positive integer'); + } + + if(!is_integer($options['saleStart']) or $options['saleStart'] <= $startTimeStamp) { + throw new TronException('Invalid sale start timestamp provided'); + } + + if(!is_integer($options['saleEnd']) or $options['saleEnd'] <= $options['saleStart']) { + throw new TronException('Invalid sale end timestamp provided'); + } + + if(!$options['description'] or !is_string($options['description'])) { + throw new TronException('Invalid token description provided'); + } + + if(!is_string($options['url']) || !filter_var($options['url'], FILTER_VALIDATE_URL)) { + throw new TronException('Invalid token url provided'); + } + + if(!is_integer($options['freeBandwidth']) || $options['freeBandwidth'] < 0) { + throw new TronException('Invalid free bandwidth amount provided'); + } + + if(!is_integer($options['freeBandwidthLimit']) || $options['freeBandwidthLimit '] < 0 || + ($options['freeBandwidth'] && !$options['freeBandwidthLimit']) + ) { + throw new TronException('Invalid free bandwidth limit provided'); + } + + if(!is_integer($options['frozenAmount']) || $options['frozenAmount '] < 0 || + (!$options['frozenDuration'] && $options['frozenAmount']) + ) { + throw new TronException('Invalid frozen supply provided'); + } + + if(!is_integer($options['frozenDuration']) || $options['frozenDuration '] < 0 || + ($options['frozenDuration'] && !$options['frozenAmount']) + ) { + throw new TronException('Invalid frozen duration provided'); + } + + $data = [ + 'owner_address' => $this->tron->address2HexString($issuerAddress), + 'name' => $this->tron->stringUtf8toHex($options['name']), + 'abbr' => $this->tron->stringUtf8toHex($options['abbreviation']), + 'description' => $this->tron->stringUtf8toHex($options['description']), + 'url' => $this->tron->stringUtf8toHex($options['url']), + 'total_supply' => intval($options['totalSupply']), + 'trx_num' => intval($options['trxRatio']), + 'num' => intval($options['tokenRatio']), + 'start_time' => intval($options['saleStart']), + 'end_time' => intval($options['saleEnd']), + 'free_asset_net_limit' => intval($options['freeBandwidth']), + 'public_free_asset_net_limit' => intval($options['freeBandwidthLimit']), + 'frozen_supply' => [ + 'frozen_amount' => intval($options['frozenAmount']), + 'frozen_days' => intval($options['frozenDuration']), + ] + ]; + + if($options['precision'] && !is_nan(intval($options['precision']))) { + $data['precision'] = intval($options['precision']); + } + + if($options['voteScore'] && !is_nan(intval($options['voteScore']))) { + $data['vote_score'] = intval($options['voteScore']); + } + + return $this->tron->getManager()->request('wallet/createassetissue', $data); + } + + /** + * Freezes an amount of TRX. + * Will give bandwidth OR Energy and TRON Power(voting rights) to the owner of the frozen tokens. + * + * @param float $amount + * @param int $duration + * @param string $resource + * @param string|null $address + * @return array + * @throws TronException + */ + public function freezeBalance(float $amount = 0, int $duration = 3, string $resource = 'BANDWIDTH', string $address) + { + if (!in_array($resource, ['BANDWIDTH', 'ENERGY'])) { + throw new TronException('Invalid resource provided: Expected "BANDWIDTH" or "ENERGY"'); + } + + if (!is_float($amount)) { + throw new TronException('Invalid amount provided'); + } + + if(!is_integer($duration) and $duration < 3) { + throw new TronException('Invalid duration provided, minimum of 3 days'); + } + + return $this->tron->getManager()->request('wallet/freezebalance', [ + 'owner_address' => $this->tron->address2HexString($address), + 'frozen_balance' => $this->tron->toTron($amount), + 'frozen_duration' => $duration, + 'resource' => $resource + ]); + } + + /** + * Unfreeze TRX that has passed the minimum freeze duration. + * Unfreezing will remove bandwidth and TRON Power. + * + * @param string $resource + * @param string $owner_address + * @return array + * @throws TronException + */ + public function unfreezeBalance(string $resource = 'BANDWIDTH', string $owner_address) + { + if (!in_array($resource, ['BANDWIDTH', 'ENERGY'])) { + throw new TronException('Invalid resource provided: Expected "BANDWIDTH" or "ENERGY"'); + } + + return $this->tron->getManager()->request('wallet/unfreezebalance', [ + 'owner_address' => $this->tron->address2HexString($owner_address), + 'resource' => $resource + ]); + } + + /** + * Withdraw Super Representative rewards, useable every 24 hours. + * + * @param string $owner_address + * @return array + * @throws TronException + */ + public function withdrawBlockRewards($owner_address = null) + { + $withdraw = $this->tron->getManager()->request('wallet/withdrawbalance', [ + 'owner_address' => $this->tron->address2HexString($owner_address) + ]); + + if (array_key_exists('Error', $withdraw)) { + throw new TronException($withdraw['Error']); + } + return $withdraw; + } + + /** + * Update a Token's information + * + * @param string $description + * @param string $url + * @param int $freeBandwidth + * @param int $freeBandwidthLimit + * @param $address + * @return array + * @throws TronException + */ + public function updateToken(string $description, string $url, int $freeBandwidth = 0, int $freeBandwidthLimit = 0, $address) + { + if (!is_integer($freeBandwidth) || $freeBandwidth < 0) { + throw new TronException('Invalid free bandwidth amount provided'); + } + + if (!is_integer($freeBandwidthLimit) || $freeBandwidthLimit < 0 && ($freeBandwidth && !$freeBandwidthLimit)) { + throw new TronException('Invalid free bandwidth limit provided'); + } + + return $this->tron->getManager()->request('wallet/updateasset', [ + 'owner_address' => $this->tron->address2HexString($address), + 'description' => $this->tron->stringUtf8toHex($description), + 'url' => $this->tron->stringUtf8toHex($url), + 'new_limit' => intval($freeBandwidth), + 'new_public_limit' => intval($freeBandwidthLimit) + ]); + } + + /** + * updateEnergyLimit + * + * @param string $contractAddress + * @param int $originEnergyLimit + * @param string $ownerAddress + * @return array + * @throws TronException + */ + public function updateEnergyLimit(string $contractAddress, int $originEnergyLimit, string $ownerAddress) + { + $contractAddress = $this->tron->address2HexString($contractAddress); + $ownerAddress = $this->tron->address2HexString($ownerAddress); + + if($originEnergyLimit < 0 || $originEnergyLimit > 10000000) { + throw new TronException('Invalid originEnergyLimit provided'); + } + + return $this->tron->getManager()->request('wallet/updateenergylimit', [ + 'owner_address' => $this->tron->address2HexString($ownerAddress), + 'contract_address' => $this->tron->address2HexString($contractAddress), + 'origin_energy_limit' => $originEnergyLimit + ]); + } + + /** + * updateSetting + * + * @param string $contractAddress + * @param int $userFeePercentage + * @param string $ownerAddress + * @return array + * @throws TronException + */ + public function updateSetting(string $contractAddress, int $userFeePercentage, string $ownerAddress) + { + $contractAddress = $this->tron->address2HexString($contractAddress); + $ownerAddress = $this->tron->address2HexString($ownerAddress); + + if($userFeePercentage < 0 || $userFeePercentage > 1000) { + throw new TronException('Invalid userFeePercentage provided'); + } + + return $this->tron->getManager()->request('wallet/updatesetting', [ + 'owner_address' => $this->tron->address2HexString($ownerAddress), + 'contract_address' => $this->tron->address2HexString($contractAddress), + 'consume_user_resource_percent' => $userFeePercentage + ]); + } +/** + * Contract Balance + * @param string $address $tron->toHex('Txxxxx'); + * + * @return array + */ +public function contractbalance($adres) +{ + $trc20=array(); + $abi=json_decode('{"entrys": [{"constant": true,"name": "name","outputs": [{"type": "string"}],"type": "Function","stateMutability": "View"},{"name": "approve","inputs": [{"name": "_spender","type": "address"},{"name": "_value","type": "uint256"}],"outputs": [{"type": "bool"}],"type": "Function","stateMutability": "Nonpayable"},{"name": "setCanApproveCall","inputs": [{"name": "_val","type": "bool"}],"type": "Function","stateMutability": "Nonpayable"},{"constant": true,"name": "totalSupply","outputs": [{"type": "uint256"}],"type": "Function","stateMutability": "View"},{"name": "transferFrom","inputs": [{"name": "_from","type": "address"},{"name": "_to","type": "address"},{"name": "_value","type": "uint256"}],"outputs": [{"type": "bool"}],"type": "Function","stateMutability": "Nonpayable"},{"constant": true,"name": "decimals","outputs": [{"type": "uint8"}],"type": "Function","stateMutability": "View"},{"name": "setCanBurn","inputs": [{"name": "_val","type": "bool"}],"type": "Function","stateMutability": "Nonpayable"},{"name": "burn","inputs": [{"name": "_value","type": "uint256"}],"outputs": [{"name": "success","type": "bool"}],"type": "Function","stateMutability": "Nonpayable"},{"constant": true,"name": "balanceOf","inputs": [{"name": "_owner","type": "address"}],"outputs": [{"type": "uint256"}],"type": "Function","stateMutability": "View"},{"constant": true,"name": "symbol","outputs": [{"type": "string"}],"type": "Function","stateMutability": "View"},{"name": "transfer","inputs": [{"name": "_to","type": "address"},{"name": "_value","type": "uint256"}],"outputs": [{"type": "bool"}],"type": "Function","stateMutability": "Nonpayable"},{"constant": true,"name": "canBurn","outputs": [{"type": "bool"}],"type": "Function","stateMutability": "View"},{"name": "approveAndCall","inputs": [{"name": "_spender","type": "address"},{"name": "_value","type": "uint256"},{"name": "_extraData","type": "bytes"}],"outputs": [{"name": "success","type": "bool"}],"type": "Function","stateMutability": "Nonpayable"},{"constant": true,"name": "allowance","inputs": [{"name": "_owner","type": "address"},{"name": "_spender","type": "address"}],"outputs": [{"type": "uint256"}],"type": "Function","stateMutability": "View"},{"name": "transferOwnership","inputs": [{"name": "_newOwner","type": "address"}],"type": "Function","stateMutability": "Nonpayable"},{"constant": true,"name": "canApproveCall","outputs": [{"type": "bool"}],"type": "Function","stateMutability": "View"},{"type": "Constructor","stateMutability": "Nonpayable"},{"name": "Transfer","inputs": [{"indexed": true,"name": "_from","type": "address"},{"indexed": true,"name": "_to","type": "address"},{"name": "_value","type": "uint256"}],"type": "Event"},{"name": "Approval","inputs": [{"indexed": true,"name": "_owner","type": "address"},{"indexed": true,"name": "_spender","type": "address"},{"name": "_value","type": "uint256"}],"type": "Event"},{"name": "Burn","inputs": [{"indexed": true,"name": "_from","type": "address"},{"name": "_value","type": "uint256"}],"type": "Event"}]}',true); + $feeLimit=1000000; + $func="balanceOf"; + $jsonData = json_decode(file_get_contents("https://apilist.tronscan.org/api/token_trc20?sort=issue_time&limit=100&start=0"),true); + foreach($jsonData["trc20_tokens"] as $key =>$item) + { + $owner=$item["contract_address"]; + $params=array("0"=>$this->tron->toHex($adres)); + $result = $this->tron->getTransactionBuilder()->triggerSmartContract( + $abi['entrys'], + $this->tron->toHex($owner), + $func, + $params, + $feeLimit, + $this->tron->toHex($adres), + 0, + 0); + $balance_hex=$result["0"]; + $balance=0+(float)number_format($balance_hex->value/pow(10,$item["decimals"]),$item["decimals"],".",""); + if($balance>0) + { + $trc20[]=array( + "name"=>$item["name"], + "symbol"=>$item["symbol"], + "balance"=>$balance, + "value"=>$balance_hex->value, + "decimals"=>$item["decimals"], + ); + } + } +return $trc20; +} + + /** + * Triggers smart contract + * + * @param mixed $abi + * @param string $contract $tron->toHex('Txxxxx'); + * @param string $function + * @param array $params array("0"=>$value); + * @param integer $feeLimit + * @param string $address $tron->toHex('Txxxxx'); + * @param int $callValue + * @param int $bandwidthLimit + * + * @return mixed + * @throws TronException + */ + public function triggerSmartContract($abi, + $contract, + $function, + $params, + $feeLimit, + $address, + $callValue = 0, + $bandwidthLimit = 0) + { + $func_abi = []; + foreach($abi as $key =>$item) { + if(isset($item['name']) && $item['name'] === $function) { + $func_abi = $item; + break; + } + } + + if(count($func_abi) === 0) + throw new TronException("Function $function not defined in ABI"); + + if(!is_array($params)) + throw new TronException("Function params must be an array"); + + if(count($func_abi['inputs']) !== count($params)) + throw new TronException("Count of params and abi inputs must be identical"); + + if($feeLimit > 1000000000) + throw new TronException('fee_limit must not be greater than 1000000000'); + + + $inputs = array_map(function($item){ return $item['type']; },$func_abi['inputs']); + $signature = $func_abi['name'].'('; + if(count($inputs) > 0) + $signature .= implode(',',$inputs); + $signature .= ')'; + + $eth_abi = new Ethabi([ + 'address' => new Address, + 'bool' => new Boolean, + 'bytes' => new Bytes, + 'dynamicBytes' => new DynamicBytes, + 'int' => new Integer, + 'string' => new Str, + 'uint' => new Uinteger, + ]); + $parameters = substr($eth_abi->encodeParameters($func_abi, $params),2); + + $result = $this->tron->getManager()->request('wallet/triggersmartcontract', [ + 'contract_address' => $contract, + 'function_selector' => $signature, + 'parameter' => $parameters, + 'owner_address' => $address, + 'fee_limit' => $feeLimit, + 'call_value' => $callValue, + 'consume_user_resource_percent' => $bandwidthLimit, + ]); + + if(!isset($result['result'])){ + throw new TronException('No result field in response. Raw response:'.print_r($result,true)); + } + if(isset($result['result']['result'])) { + if(count($func_abi['outputs']) >= 0 && isset($result['constant_result'])) { + return $eth_abi->decodeParameters($func_abi, $result['constant_result'][0]); + } + return $result['transaction']; + } + $message = isset($result['result']['message']) ? + $this->tron->hexString2Utf8($result['result']['message']) : ''; + + throw new TronException('Failed to execute. Error:'.$message); + } + + /** + * Triggers constant contract + * + * @param mixed $abi + * @param string $contract $tron->toHex('Txxxxx'); + * @param string $function + * @param array $params array("0"=>$value); + * @param string $address $tron->toHex('Txxxxx'); + * + * @return mixed + * @throws TronException + */ + public function triggerConstantContract($abi, + $contract, + $function, + $params = [], + $address = '410000000000000000000000000000000000000000') + { + $func_abi = []; + foreach($abi as $key =>$item) { + if(isset($item['name']) && $item['name'] === $function) { + $func_abi = $item + ['inputs' => []]; + break; + } + } + + if(count($func_abi) === 0) + throw new TronException("Function $function not defined in ABI"); + + if(!is_array($params)) + throw new TronException("Function params must be an array"); + + if(count($func_abi['inputs']) !== count($params)) + throw new TronException("Count of params and abi inputs must be identical"); + + + $inputs = array_map(function($item){ return $item['type']; },$func_abi['inputs']); + $signature = $func_abi['name'].'('; + if(count($inputs) > 0) + $signature .= implode(',',$inputs); + $signature .= ')'; + + $eth_abi = new Ethabi([ + 'address' => new Address, + 'bool' => new Boolean, + 'bytes' => new Bytes, + 'dynamicBytes' => new DynamicBytes, + 'int' => new Integer, + 'string' => new Str, + 'uint' => new Uinteger, + ]); + $parameters = substr($eth_abi->encodeParameters($func_abi, $params),2); + + $result = $this->tron->getManager()->request('wallet/triggerconstantcontract', [ + 'contract_address' => $contract, + 'function_selector' => $signature, + 'parameter' => $parameters, + 'owner_address' => $address, + ]); + + if(!isset($result['result'])){ + throw new TronException('No result field in response. Raw response:'.print_r($result,true)); + } + if(isset($result['result']['result'])) { + if(count($func_abi['outputs']) >= 0 && isset($result['constant_result'])) { + return $eth_abi->decodeParameters($func_abi, $result['constant_result'][0]); + } + return $result['transaction']; + } + $message = isset($result['result']['message']) ? + $this->tron->hexString2Utf8($result['result']['message']) : ''; + + throw new TronException('Failed to execute. Error:'.$message); + } +} diff --git a/src/Tron.php b/src/Tron.php new file mode 100644 index 0000000..f7b744e --- /dev/null +++ b/src/Tron.php @@ -0,0 +1,1389 @@ + + * @license https://github.com/fourclub/tron-api/blob/master/LICENSE (MIT License) + * @version 1.3.4 + * @link https://github.com/fourclub/tron-api + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace fourclub\TronAPI; + +use Elliptic\EC; +use fourclub\TronAPI\Exception\TRC20Exception; +use fourclub\TronAPI\Support\Base58; +use fourclub\TronAPI\Support\Base58Check; +use fourclub\TronAPI\Support\Crypto; +use fourclub\TronAPI\Support\Hash; +use fourclub\TronAPI\Support\Keccak; +use fourclub\TronAPI\Support\Utils; +use fourclub\TronAPI\Provider\HttpProviderInterface; +use fourclub\TronAPI\Exception\TronException; + +/** + * A PHP API for interacting with the Tron (TRX) + * + * @package TronAPI + * @author Shamsudin Serderov + * @since 1.0.0 + */ +class Tron implements TronInterface +{ + use TronAwareTrait, + Concerns\ManagesUniversal, + Concerns\ManagesTronscan; + + const ADDRESS_SIZE = 34; + const ADDRESS_PREFIX = "41"; + const ADDRESS_PREFIX_BYTE = 0x41; + + /** + * Default Address: + * Example: + * - base58: T**** + * - hex: 41**** + * + * @var array + */ + public $address = [ + 'base58' => null, + 'hex' => null + ]; + + /** + * Private key + * + * @var string + */ + protected $privateKey; + + /** + * Default block + * + * @var string|integer|bool + */ + protected $defaultBlock = 'latest'; + + /** + * Transaction Builder + * + * @var TransactionBuilder + */ + protected $transactionBuilder; + + /** + * Transaction Builder + * + * @var TransactionBuilder + */ + protected $trc20Contract; + + /** + * Provider manager + * + * @var TronManager + */ + protected $manager; + + /** + * Object Result + * + * @var bool + */ + protected $isObject = false; + + /** + * Create a new Tron object + * + * @param HttpProviderInterface $fullNode + * @param HttpProviderInterface $solidityNode + * @param HttpProviderInterface|null $eventServer + * @param HttpProviderInterface|null $signServer + * @param HttpProviderInterface|null $explorer + * @param string $privateKey + + * @throws TronException + */ + public function __construct(?HttpProviderInterface $fullNode = null, + ?HttpProviderInterface $solidityNode = null, + ?HttpProviderInterface $eventServer = null, + ?HttpProviderInterface $signServer = null, + ?HttpProviderInterface $explorer = null, + ?string $privateKey = null) + { + if(!is_null($privateKey)) { + $this->setPrivateKey($privateKey); + } + + $this->setManager(new TronManager($this, [ + 'fullNode' => $fullNode, + 'solidityNode' => $solidityNode, + 'eventServer' => $eventServer, + 'signServer' => $signServer, + ])); + + $this->transactionBuilder = new TransactionBuilder($this); + } + + /** + * Create a new tron instance if the value isn't one already. + * + * @param HttpProviderInterface|null $fullNode + * @param HttpProviderInterface|null $solidityNode + * @param HttpProviderInterface|null $eventServer + * @param HttpProviderInterface|null $signServer + * @param string|null $privateKey + * @return static + * @throws TronException + */ + public static function make(?HttpProviderInterface $fullNode = null, + ?HttpProviderInterface $solidityNode = null, + ?HttpProviderInterface $eventServer = null, + ?HttpProviderInterface $signServer = null, + string $privateKey = null) { + return new static($fullNode, $solidityNode, $eventServer, $signServer, $privateKey); + } + + /** + * Фасад для Laravel + * + * @return Tron + */ + public function getFacade(): Tron { + return $this; + } + + /** + * Enter the link to the manager nodes + * + * @param $providers + */ + public function setManager($providers) { + $this->manager = $providers; + } + + /** + * Get provider manager + * + * @return TronManager + */ + public function getManager(): TronManager { + return $this->manager; + } + + + /** + * Contract module + * + * @param string $contractAddress + * @param string|null $abi + * @return TRC20Contract + */ + public function contract(string $contractAddress, string $abi = null) + { + return new TRC20Contract($this, $contractAddress, $abi); + } + + /** + * Set is object + * + * @param bool $value + * @return Tron + */ + public function setIsObject(bool $value) + { + $this->isObject = boolval($value); + return $this; + } + + /** + * Get Transaction Builder + * + * @return TransactionBuilder + */ + public function getTransactionBuilder(): TransactionBuilder + { + return $this->transactionBuilder; + } + + /** + * Check connected provider + * + * @param $provider + * @return bool + */ + public function isValidProvider($provider): bool + { + return ($provider instanceof HttpProviderInterface); + } + + /** + * Enter the default block + * + * @param bool $blockID + * @return void + * @throws TronException + */ + public function setDefaultBlock($blockID = false): void + { + if($blockID === false || $blockID == 'latest' || $blockID == 'earliest' || $blockID === 0) { + $this->defaultBlock = $blockID; + return; + } + + if(!is_integer($blockID)) { + throw new TronException('Invalid block ID provided'); + } + + $this->defaultBlock = abs($blockID); + } + + /** + * Get default block + * + * @return string|integer|bool + */ + public function getDefaultBlock() + { + return $this->defaultBlock; + } + + /** + * Enter your private account key + * + * @param string $privateKey + */ + public function setPrivateKey(string $privateKey): void + { + $this->privateKey = $privateKey; + } + + /** + * Enter your account address + * + * @param string $address + */ + public function setAddress(string $address): void + { + $_toHex = $this->address2HexString($address); + $_fromHex = $this->hexString2Address($address); + + $this->address = [ + 'hex' => $_toHex, + 'base58' => $_fromHex + ]; + } + + /** + * Get account address + * + * @return array + */ + public function getAddress(): array + { + return $this->address; + } + + /** + * Get customized provider data + * + * @return array + */ + public function providers(): array + { + return $this->manager->getProviders(); + } + + /** + * Check Connection Providers + * + * @return array + */ + public function isConnected(): array + { + return $this->manager->isConnected(); + } + + /** + * Last block number + * + * @return array + * @throws TronException + */ + public function getCurrentBlock(): array + { + return $this->manager->request('wallet/getnowblock'); + } + + /** + * Will return all events matching the filters. + * + * @param $contractAddress + * @param int $sinceTimestamp + * @param string|null $eventName + * @param int $blockNumber + * @return array + * @throws TronException + */ + public function getEventResult($contractAddress, int $sinceTimestamp = 0, string $eventName = null, int $blockNumber = 0) + { + if (!$this->isValidProvider($this->manager->eventServer())) { + throw new TronException('No event server configured'); + } + + $routeParams = []; + if($eventName && !$contractAddress) { + throw new TronException('Usage of event name filtering requires a contract address'); + } + + if($blockNumber && !$eventName) + throw new TronException('Usage of block number filtering requires an event name'); + + if($contractAddress) { + array_push($routeParams, $contractAddress); + } + if($eventName) { + array_push($routeParams, $eventName); + } + if($blockNumber) { + array_push($routeParams, $blockNumber); + } + + $routeParams = implode('/', $routeParams); + return $this->manager->request("event/contract/{$routeParams}?since={$sinceTimestamp}"); + } + + + /** + * Will return all events within a transactionID. + * + * @param string $transactionID + * @return array + * @throws TronException + */ + public function getEventByTransactionID(string $transactionID) + { + if (!$this->isValidProvider($this->manager->eventServer())) { + throw new TronException('No event server configured'); + } + return $this->manager->request("event/transaction/{$transactionID}"); + } + + /** + * Get block details using HashString or blockNumber + * + * @param null $block + * @return array + * @throws TronException + */ + public function getBlock($block = null): array + { + $block = (is_null($block) ? $this->defaultBlock : $block); + + if($block === false) { + throw new TronException('No block identifier provided'); + } + + if($block == 'earliest') { + $block = 0; + } + + if($block == 'latest') { + return $this->getCurrentBlock(); + } + + if(Utils::isHex($block)) { + return $this->getBlockByHash($block); + } + return $this->getBlockByNumber($block); + } + + /** + * Query block by ID + * + * @param $hashBlock + * @return array + * @throws TronException + */ + public function getBlockByHash(string $hashBlock): array + { + return $this->manager->request('wallet/getblockbyid', [ + 'value' => $hashBlock + ]); + } + + /** + * Query block by height + * + * @param $blockID + * @return array + * @throws TronException + */ + public function getBlockByNumber(int $blockID): array + { + if(!is_integer($blockID) || $blockID < 0) { + throw new TronException('Invalid block number provided'); + } + + $response = $this->manager->request('wallet/getblockbynum', [ + 'num' => intval($blockID) + ]); + + if (empty($response)) { + throw new TronException('Block not found'); + } + return $response; + } + + /** + * Total number of transactions in a block + * + * @param $block + * @return int + * @throws TronException + */ + public function getBlockTransactionCount($block): int + { + $transaction = $this->getBlock($block)['transactions']; + if(!$transaction) { + return 0; + } + + return count($transaction); + } + + /** + * Get transaction details from Block + * + * @param null $block + * @param int $index + * @return array | string + * @throws TronException + */ + public function getTransactionFromBlock($block = null, $index = 0) + { + if(!is_integer($index) || $index < 0) { + throw new TronException('Invalid transaction index provided'); + } + + $transactions = $this->getBlock($block)['transactions']; + if(!$transactions || count($transactions) < $index) { + throw new TronException('Transaction not found in block'); + } + + return $transactions[$index]; + } + + /** + * Query transaction based on id + * + * @param $transactionID + * @return array + * @throws TronException + */ + public function getTransaction(string $transactionID): array + { + $response = $this->manager->request('wallet/gettransactionbyid', [ + 'value' => $transactionID + ]); + + if(!$response) { + throw new TronException('Transaction not found'); + } + + return $response; + } + + /** + * Query transaction fee based on id + * + * @param $transactionID + * @return array + * @throws TronException + */ + public function getTransactionInfo(string $transactionID): array + { + return $this->manager->request('walletsolidity/gettransactioninfobyid', [ + 'value' => $transactionID + ]); + } + + /** + * Query the list of transactions received by an address + * + * @param string $address + * @param int $limit + * @param int $offset + * @return array + * @throws TronException + */ + public function getTransactionsToAddress(string $address, int $limit = 30, int $offset = 0) + { + return $this->getTransactionsRelated($address,'to', $limit, $offset); + } + + /** + * Query the list of transactions sent by an address + * + * @param string $address + * @param int $limit + * @param int $offset + * @return array + * @throws TronException + */ + public function getTransactionsFromAddress(string $address, int $limit = 30, int $offset = 0) + { + return $this->getTransactionsRelated($address,'from', $limit, $offset); + } + + /** + * Query information about an account + * + * @param $address + * @return array + * @throws TronException + */ + public function getAccount(string $address = null): array + { + $address = (!is_null($address) ? $this->toHex($address) : $this->address['hex']); + + return $this->manager->request('walletsolidity/getaccount', [ + 'address' => $address + ]); + } + + /** + * Getting a balance + * + * @param string $address + * @param bool $fromTron + * @return float + * @throws TronException + */ + public function getBalance(string $address = null, bool $fromTron = false): float + { + $account = $this->getAccount($address); + + if(!array_key_exists('balance', $account)) { + return 0; + } + + return ($fromTron == true ? + $this->fromTron($account['balance']) : + $account['balance']); + } + + + /** + * Get token balance + * + * @param string $address + * @param int $tokenId + * @param bool $fromTron + * @return array|int + * @throws TronException + */ + public function getTokenBalance(int $tokenId, string $address, bool $fromTron = false) + { + $account = $this->getAccount($address); + + if(isset($account['assetV2']) and !empty($account['assetV2']) ) + { + $value = array_filter($account['assetV2'], function($item) use ($tokenId) { + return $item['key'] == $tokenId; + }); + + if(empty($value)) { + throw new TronException('Token id not found'); + } + + $first = array_shift($value); + return ($fromTron == true ? $this->fromTron($first['value']) : $first['value']); + } + + return 0; + } + + /** + * Query bandwidth information. + * + * @param $address + * @return array + * @throws TronException + */ + public function getBandwidth(string $address = null) + { + $address = (!is_null($address) ? $this->toHex($address) : $this->address['hex']); + return $this->manager->request('wallet/getaccountnet', [ + 'address' => $address + ]); + } + + /** + * Getting data in the "from","to" directions + * + * @param string $address + * @param string $direction + * @param int $limit + * @param int $offset + * @return array + * @throws TronException + */ + public function getTransactionsRelated(string $address, string $direction = 'to', int $limit = 30, int $offset = 0) + { + if(!in_array($direction, ['to', 'from'])) { + throw new TronException('Invalid direction provided: Expected "to", "from"'); + } + + if(!is_integer($limit) || $limit < 0 || ($offset && $limit < 1)) { + throw new TronException('Invalid limit provided'); + } + + if(!is_integer($offset) || $offset < 0) { + throw new TronException('Invalid offset provided'); + } + + $response = $this->manager->request(sprintf('walletextension/gettransactions%sthis', $direction), [ + 'account' => ['address' => $this->toHex($address)], + 'limit' => $limit, + 'offset' => $offset + ]); + + return array_merge($response, ['direction' => $direction]); + } + + /** + * Count all transactions on the network + * + * @return integer + * @throws TronException + */ + public function getTransactionCount(): int + { + $response = $this->manager->request('wallet/totaltransaction'); + return $response['num']; + } + + /** + * Send transaction to Blockchain + * + * @param string $to + * @param float $amount + * @param string|null $message + * @param string $from + * + * @return array + * @throws TronException + */ + public function sendTransaction(string $to, float $amount, string $message= null, string $from = null): array + { + if (is_null($from)) { + $from = $this->address['hex']; + } + + $transaction = $this->transactionBuilder->sendTrx($to, $amount, $from); + $signedTransaction = $this->signTransaction($transaction, $message); + + + $response = $this->sendRawTransaction($signedTransaction); + return array_merge($response, $signedTransaction); + } + + /** + * Send token transaction to Blockchain + * + * @param string $to + * @param float $amount + * @param int $tokenID + * @param string $from + * + * @return array + * @throws TronException + */ + public function sendTokenTransaction(string $to, float $amount, int $tokenID = null, string $from = null): array + { + if (is_null($from)) { + $from = $this->address['hex']; + } + + $transaction = $this->transactionBuilder->sendToken($to, $this->toTron($amount), (string)$tokenID, $from); + $signedTransaction = $this->signTransaction($transaction); + + $response = $this->sendRawTransaction($signedTransaction); + + return array_merge($response, $signedTransaction); + } + + /** + * Sign the transaction, the api has the risk of leaking the private key, + * please make sure to call the api in a secure environment + * + * @param $transaction + * @param string|null $message + * @return array + * @throws TronException + */ + public function signTransaction($transaction, string $message = null): array + { + if(!$this->privateKey) { + throw new TronException('Missing private key'); + } + + if(!is_array($transaction)) { + throw new TronException('Invalid transaction provided'); + } + + if(isset($transaction['Error'])) + throw new TronException($transaction['Error']); + + + if(isset($transaction['signature'])) { + throw new TronException('Transaction is already signed'); + } + + if(!is_null($message)) { + $transaction['raw_data']['data'] = $this->stringUtf8toHex($message); + } + + + $signature = Support\Secp::sign($transaction['txID'], $this->privateKey); + $transaction['signature'] = [$signature]; + + return $transaction; + } + + /** + * Broadcast the signed transaction + * + * @param $signedTransaction + * @return array + * @throws TronException + */ + public function sendRawTransaction($signedTransaction): array + { + if(!is_array($signedTransaction)) { + throw new TronException('Invalid transaction provided'); + } + + if(!array_key_exists('signature', $signedTransaction) || !is_array($signedTransaction['signature'])) { + throw new TronException('Transaction is not signed'); + } + + return $this->manager->request('wallet/broadcasttransaction', + $signedTransaction); + } + + /** + * Modify account name + * Note: Username is allowed to edit only once. + * + * @param $address + * @param $account_name + * @return array + * @throws TronException + */ + public function changeAccountName(string $address = null, string $account_name) + { + $address = (!is_null($address) ? $address : $this->address['hex']); + + $transaction = $this->manager->request('wallet/updateaccount', [ + 'account_name' => $this->stringUtf8toHex($account_name), + 'owner_address' => $this->toHex($address) + ]); + + $signedTransaction = $this->signTransaction($transaction); + $response = $this->sendRawTransaction($signedTransaction); + + return $response; + } + + /** + * Send funds to the Tron account (option 2) + * + * @param array $args + * @return array + * @throws TronException + */ + public function send(...$args): array { + return $this->sendTransaction(...$args); + } + + /** + * Send funds to the Tron account (option 3) + * + * @param array $args + * @return array + * @throws TronException + */ + public function sendTrx(...$args): array { + return $this->sendTransaction(...$args); + } + + /** + * Creating a new token based on Tron + * + * @param array token { + * "owner_address": "41e552f6487585c2b58bc2c9bb4492bc1f17132cd0", + * "name": "0x6173736574497373756531353330383934333132313538", + * "abbr": "0x6162627231353330383934333132313538", + * "total_supply": 4321, + * "trx_num": 1, + * "num": 1, + * "start_time": 1530894315158, + * "end_time": 1533894312158, + * "description": "007570646174654e616d6531353330363038383733343633", + * "url": "007570646174654e616d6531353330363038383733343633", + * "free_asset_net_limit": 10000, + * "public_free_asset_net_limit": 10000, + * "frozen_supply": { "frozen_amount": 1, "frozen_days": 2 } + * + * @return array + * @throws TronException + */ + public function createToken($token = []) + { + return $this->manager->request('wallet/createassetissue', [ + 'owner_address' => $this->toHex($token['owner_address']), + 'name' => $this->stringUtf8toHex($token['name']), + 'abbr' => $this->stringUtf8toHex($token['abbr']), + 'description' => $this->stringUtf8toHex($token['description']), + 'url' => $this->stringUtf8toHex($token['url']), + 'total_supply' => $token['total_supply'], + 'trx_num' => $token['trx_num'], + 'num' => $token['num'], + 'start_time' => $token['start_time'], + 'end_time' => $token['end_time'], + 'free_asset_net_limit' => $token['free_asset_net_limit'], + 'public_free_asset_net_limit' => $token['public_free_asset_net_limit'], + 'frozen_supply' => $token['frozen_supply'] + ]); + } + + /** + * Create an account. + * Uses an already activated account to create a new account + * + * @param $address + * @param $newAccountAddress + * @return array + * @throws TronException + */ + public function registerAccount(string $address, string $newAccountAddress): array + { + return $this->manager->request('wallet/createaccount', [ + 'owner_address' => $this->toHex($address), + 'account_address' => $this->toHex($newAccountAddress) + ]); + } + + /** + * Apply to become a super representative + * + * @param $address + * @param $url + * @return array + * @throws TronException + */ + public function applyForSuperRepresentative(string $address, string $url) + { + return $this->manager->request('wallet/createwitness', [ + 'owner_address' => $this->toHex($address), + 'url' => $this->stringUtf8toHex($url) + ]); + } + + /** + * Transfer Token + * + * @param $to + * @param $amount + * @param $tokenID + * @param $from + * @return array + * @throws TronException + */ + public function sendToken(string $to, int $amount, string $tokenID, string $from = null) + { + if($from == null) { + $from = $this->address['hex']; + } + + $transfer = $this->transactionBuilder->sendToken($to, $amount, $tokenID, $from); + $signedTransaction = $this->signTransaction($transfer); + $response = $this->sendRawTransaction($signedTransaction); + + return array_merge($response, $signedTransaction); + } + + /** + * Purchase a Token + * @param $issuerAddress + * @param $tokenID + * @param $amount + * @param null $buyer + * @return array + * @throws TronException + */ + public function purchaseToken($issuerAddress, $tokenID, $amount, $buyer = null) + { + if($buyer == null) { + $buyer = $this->address['hex']; + } + + $purchase = $this->transactionBuilder->purchaseToken($issuerAddress, $tokenID, $amount, $buyer); + $signedTransaction = $this->signTransaction($purchase); + $response = $this->sendRawTransaction($signedTransaction); + + return array_merge($response, $signedTransaction); + } + + /** + * Freezes an amount of TRX. + * Will give bandwidth OR Energy and TRON Power(voting rights) to the owner of the frozen tokens. + * + * @param float $amount + * @param int $duration + * @param string $resource + * @param string $owner_address + * @return array + * @throws TronException + */ + public function freezeBalance(float $amount = 0, int $duration = 3, string $resource = 'BANDWIDTH', string $owner_address = null) + { + if($owner_address == null) { + $owner_address = $this->address['hex']; + } + + $freeze = $this->transactionBuilder->freezeBalance($amount, $duration, $resource, $owner_address); + $signedTransaction = $this->signTransaction($freeze); + $response = $this->sendRawTransaction($signedTransaction); + + return array_merge($response, $signedTransaction); + } + + /** + * Unfreeze TRX that has passed the minimum freeze duration. + * Unfreezing will remove bandwidth and TRON Power. + * + * @param string $resource + * @param string $owner_address + * @return array + * @throws TronException + */ + public function unfreezeBalance(string $resource = 'BANDWIDTH', string $owner_address = null) + { + if($owner_address == null) { + $owner_address = $this->address['hex']; + } + + $unfreeze = $this->transactionBuilder->unfreezeBalance($resource, $owner_address); + $signedTransaction = $this->signTransaction($unfreeze); + $response = $this->sendRawTransaction($signedTransaction); + + return array_merge($response, $signedTransaction); + } + + /** + * Withdraw Super Representative rewards, useable every 24 hours. + * + * @param string $owner_address + * @return array + * @throws TronException + */ + public function withdrawBlockRewards(string $owner_address = null) + { + if($owner_address == null) { + $owner_address = $this->address['hex']; + } + + $withdraw = $this->transactionBuilder->withdrawBlockRewards($owner_address); + $signedTransaction = $this->signTransaction($withdraw); + $response = $this->sendRawTransaction($signedTransaction); + + return array_merge($response, $signedTransaction); + } + + /** + * Update a Token's information + * + * @param string $description + * @param string $url + * @param int $freeBandwidth + * @param int $freeBandwidthLimit + * @param $owner_address + * @return array + * @throws TronException + */ + public function updateToken(string $description, + string $url, + int $freeBandwidth = 0, + int $freeBandwidthLimit = 0, + string $owner_address = null) + { + if($owner_address == null) { + $owner_address = $this->address['hex']; + } + + $withdraw = $this->transactionBuilder->updateToken($description, $url, $freeBandwidth, $freeBandwidthLimit, $owner_address); + $signedTransaction = $this->signTransaction($withdraw); + $response = $this->sendRawTransaction($signedTransaction); + + return array_merge($response, $signedTransaction); + } + + /** + * Node list + * + * @return array + * @throws TronException + */ + public function listNodes(): array + { + $nodes = $this->manager->request('wallet/listnodes'); + return array_map(function($item) { + $address = $item['address']; + return sprintf('%s:%s', $this->toUtf8($address['host']), $address['port']); + }, $nodes['nodes']); + } + + + /** + * List the tokens issued by an account. + * + * @param string $address + * @return array + * @throws TronException + */ + public function getTokensIssuedByAddress(string $address = null) + { + $address = (!is_null($address) ? $this->toHex($address) : $this->address['hex']); + return $this->manager->request('wallet/getassetissuebyaccount',[ + 'address' => $address + ]); + } + + /** + * Query token by name. + * + * @param $tokenID + * @return array + * @throws TronException + */ + public function getTokenFromID($tokenID = null) + { + return $this->manager->request('wallet/getassetissuebyname', [ + 'value' => $this->stringUtf8toHex($tokenID) + ]); + } + + /** + * Query a range of blocks by block height + * + * @param int $start + * @param int $end + * @return array + * @throws TronException + */ + public function getBlockRange(int $start = 0, int $end = 30) + { + if(!is_integer($start) || $start < 0) { + throw new TronException('Invalid start of range provided'); + } + + if(!is_integer($end) || $end <= $start) { + throw new TronException('Invalid end of range provided'); + } + + return $this->manager->request('wallet/getblockbylimitnext', [ + 'startNum' => intval($start), + 'endNum' => intval($end) + 1 + ])['block']; + } + + /** + * Query the latest blocks + * + * @param int $limit + * @return array + * @throws TronException + */ + public function getLatestBlocks(int $limit = 1): array + { + if(!is_integer($limit) || $limit <= 0) { + throw new TronException('Invalid limit provided'); + } + + return $this->manager->request('wallet/getblockbylatestnum', [ + 'num' => $limit + ])['block']; + } + + /** + * Query the list of Super Representatives + * + * @return array + * @throws TronException + */ + public function listSuperRepresentatives(): array + { + return $this->manager->request('wallet/listwitnesses')['witnesses']; + } + + /** + * Query the list of Tokens with pagination + * + * @param int $limit + * @param int $offset + * @return array + * @throws TronException + */ + public function listTokens(int $limit = 0, int $offset = 0) + { + if(!is_integer($limit) || $limit < 0 || ($offset && $limit < 1)) { + throw new TronException('Invalid limit provided'); + } + + if(!is_integer($offset) || $offset < 0) { + throw new TronException('Invalid offset provided'); + } + + if(!$limit) { + return $this->manager->request('wallet/getassetissuelist')['assetIssue']; + } + + return $this->manager->request('wallet/getpaginatedassetissuelist', [ + 'offset' => intval($offset), + 'limit' => intval($limit) + ])['assetIssue']; + } + + /** + * Get the time of the next Super Representative vote + * + * @return float + * @throws TronException + */ + public function timeUntilNextVoteCycle(): float + { + $num = $this->manager->request('wallet/getnextmaintenancetime')['num']; + + if($num == -1) { + throw new TronException('Failed to get time until next vote cycle'); + } + + return floor($num / 1000); + } + + /** + * Validate address + * + * @param string $address + * @param bool $hex + * @return array + * @throws TronException + */ + public function validateAddress(string $address = null, bool $hex = false): array + { + $address = (!is_null($address) ? $address : $this->address['hex']); + if($hex) { + $address = $this->toHex($address); + } + return $this->manager->request('wallet/validateaddress', [ + 'address' => $address + ]); + } + + /** + * Validate Tron Address (Locale) + * + * @param string|null $address + * @return bool + */ + public function isAddress(string $address = null): bool + { + if(strlen($address) !== self::ADDRESS_SIZE) + return false; + + $address = Base58Check::decode($address, 0, 0, false); + $utf8 = hex2bin($address); + + if(strlen($utf8) !== 25) return false; + if(strpos($utf8 , chr(self::ADDRESS_PREFIX_BYTE)) !== 0) return false; + + $checkSum = substr($utf8, 21); + $address = substr($utf8, 0, 21); + + $hash0 = Hash::SHA256($address); + $hash1 = Hash::SHA256($hash0); + $checkSum1 = substr($hash1, 0, 4); + + if ($checkSum === $checkSum1) + return true; + return false; + } + + /** + * Deploys a contract + * + * @param $abi + * @param $bytecode + * @param $feeLimit + * @param $address + * @param int $callValue + * @param int $bandwidthLimit + * @return array + * @throws TronException + */ + public function deployContract($abi, $bytecode, $feeLimit, $address, $callValue = 0, $bandwidthLimit = 0) + { + $payable = array_filter(json_decode($abi, true), function($v) + { + if($v['type'] == 'constructor' && $v['payable']) { + return $v['payable']; + } + return null; + }); + + if($feeLimit > 1000000000) { + throw new TronException('fee_limit must not be greater than 1000000000'); + } + + if($payable && $callValue == 0) { + throw new TronException('call_value must be greater than 0 if contract is type payable'); + } + + if(!$payable && $callValue > 0) { + throw new TronException('call_value can only equal to 0 if contract type isn‘t payable'); + } + + return $this->manager->request('wallet/deploycontract', [ + 'owner_address' => $this->toHex($address), + 'fee_limit' => $feeLimit, + 'call_value' => $callValue, + 'consume_user_resource_percent' => $bandwidthLimit, + 'abi' => $abi, + 'bytecode' => $bytecode + ]); + } + + /** + * Get a list of exchanges + * + * @return array + * @throws TronException + */ + public function listExchanges() + { + return $this->manager->request('/wallet/listexchanges', []); + } + + /** + * Query the resource information of the account + * + * @param string $address + * @return array + * @throws TronException + */ + public function getAccountResources(string $address = null) + { + $address = (!is_null($address) ? $address : $this->address['hex']); + + return $this->manager->request('/wallet/getaccountresource', [ + 'address' => $this->toHex($address) + ]); + } + + /** + * Create a new account + * + * @return TronAddress + * @throws TronException + */ + public function createAccount(): TronAddress + { + return $this->generateAddress(); + } + + public function getAddressHex(string $pubKeyBin): string + { + if (strlen($pubKeyBin) == 65) { + $pubKeyBin = substr($pubKeyBin, 1); + } + + $hash = Keccak::hash($pubKeyBin, 256); + + return self::ADDRESS_PREFIX . substr($hash, 24); + } + + public function getBase58CheckAddress(string $addressBin): string + { + $hash0 = Hash::SHA256($addressBin); + $hash1 = Hash::SHA256($hash0); + $checksum = substr($hash1, 0, 4); + $checksum = $addressBin . $checksum; + + return Base58::encode(Crypto::bin2bc($checksum)); + } + + /** + * Generate new address + * + * @return TronAddress + * @throws TronException + */ + public function generateAddress(): TronAddress + { + $ec = new EC('secp256k1'); + + // Generate keys + $key = $ec->genKeyPair(); + $priv = $ec->keyFromPrivate($key->priv); + $pubKeyHex = $priv->getPublic(false, "hex"); + + $pubKeyBin = hex2bin($pubKeyHex); + $addressHex = $this->getAddressHex($pubKeyBin); + $addressBin = hex2bin($addressHex); + $addressBase58 = $this->getBase58CheckAddress($addressBin); + + return new TronAddress([ + 'private_key' => $priv->getPrivate('hex'), + 'public_key' => $pubKeyHex, + 'address_hex' => $addressHex, + 'address_base58' => $addressBase58 + ]); + } + + /** + * Helper function that will convert HEX to UTF8 + * + * @param $str + * @return string + */ + public function toUtf8($str): string { + return pack('H*', $str); + } + + /** + * Query token by id. + * + * @param string $token_id + * @return array + * @throws TronException + */ + public function getTokenByID(string $token_id): array + { + if(!is_string($token_id)) + throw new TronException('Invalid token ID provided'); + + return $this->manager->request('/wallet/getassetissuebyid', [ + 'value' => $token_id + ]); + } +} diff --git a/src/TronAddress.php b/src/TronAddress.php new file mode 100644 index 0000000..732f028 --- /dev/null +++ b/src/TronAddress.php @@ -0,0 +1,93 @@ +response = $data; + + // Проверяем ключи, перед выводом результатов + if(!$this->array_keys_exist($this->response, ['address_hex', 'private_key', 'public_key'])) { + throw new TronException('Incorrectly generated address'); + } + } + + /** + * Получение адреса + * + * @param bool $is_base58 + * @return string + */ + public function getAddress(bool $is_base58 = false): string + { + return $this->response[($is_base58 == false) ? 'address_hex' : 'address_base58']; + } + + /** + * Получение публичного ключа + * + * @return string + */ + public function getPublicKey(): string + { + return $this->response['public_key']; + } + + /** + * Получение приватного ключа + * + * @return string + */ + public function getPrivateKey(): string + { + return $this->response['private_key']; + } + + /** + * Получение результатов в массике + * + * @return array + */ + public function getRawData(): array + { + return $this->response; + } + + /** + * Проверка нескольких ключей + * + * @param array $array + * @param array $keys + * @return bool + */ + private function array_keys_exist(array $array, array $keys = []): bool + { + $count = 0; + if (!is_array($keys)) { + $keys = func_get_args(); + array_shift($keys); + } + foreach ($keys as $key) { + if (isset( $array[$key]) || array_key_exists($key, $array)) { + $count ++; + } + } + + return count($keys) === $count; + } +} \ No newline at end of file diff --git a/src/TronAwareTrait.php b/src/TronAwareTrait.php new file mode 100644 index 0000000..ca70ed6 --- /dev/null +++ b/src/TronAwareTrait.php @@ -0,0 +1,136 @@ +hexString2Address($string); + } + + return $this->hexString2Utf8($string); + } + + /** + * Convert to Hex + * + * @param $str + * @return string + */ + public function toHex($str) + { + if(mb_strlen($str) == 34 && mb_substr($str, 0, 1) === 'T') { + return $this->address2HexString($str); + }; + + return $this->stringUtf8toHex($str); + } + + /** + * Check the address before converting to Hex + * + * @param $sHexAddress + * @return string + */ + public function address2HexString($sHexAddress) + { + if(strlen($sHexAddress) == 42 && mb_strpos($sHexAddress, '41') == 0) { + return $sHexAddress; + } + return Base58Check::decode($sHexAddress,0,3); + } + + /** + * Check Hex address before converting to Base58 + * + * @param $sHexString + * @return string + */ + public function hexString2Address($sHexString) + { + if(!ctype_xdigit($sHexString)) { + return $sHexString; + } + + if(strlen($sHexString) < 2 || (strlen($sHexString) & 1) != 0) { + return ''; + } + + return Base58Check::encode($sHexString,0,false); + } + + /** + * Convert string to hex + * + * @param $sUtf8 + * @return string + */ + public function stringUtf8toHex($sUtf8) + { + return bin2hex($sUtf8); + } + + /** + * Convert hex to string + * + * @param $sHexString + * @return string + */ + public function hexString2Utf8($sHexString) + { + return hex2bin($sHexString); + } + + /** + * Convert to great value + * + * @param $str + * @return BigInteger + */ + public function toBigNumber($str) { + return new BigInteger($str); + } + + /** + * Convert trx to float + * + * @param $amount + * @return float + */ + public function fromTron($amount): float { + return (float) bcdiv((string)$amount, (string)1e6, 8); + } + + /** + * Convert float to trx format + * + * @param $double + * @return int + */ + public function toTron($double): int { + return (int) bcmul((string)$double, (string)1e6,0); + } + + /** + * Convert to SHA3 + * + * @param $string + * @param bool $prefix + * @return string + * @throws \Exception + */ + public function sha3($string, $prefix = true) + { + return ($prefix ? '0x' : ''). Keccak::hash($string, 256); + } +} \ No newline at end of file diff --git a/src/TronInterface.php b/src/TronInterface.php new file mode 100644 index 0000000..df55721 --- /dev/null +++ b/src/TronInterface.php @@ -0,0 +1,134 @@ + 'https://api.trongrid.io', + 'solidityNode' => 'https://api.trongrid.io', + 'eventServer' => 'https://api.trongrid.io', + 'explorer' => 'https://apilist.tronscan.org', + 'signServer' => '' + ]; + + /** + * Providers + * + * @var array + */ + protected $providers = [ + 'fullNode' => [], + 'solidityNode' => [], + 'eventServer' => [], + 'explorer' => [], + 'signServer' => [] + ]; + + /** + * Status Page + * + * @var array + */ + protected $statusPage = [ + 'fullNode' => 'wallet/getnowblock', + 'solidityNode' => 'walletsolidity/getnowblock', + 'eventServer' => 'healthcheck', + 'explorer' => 'api/system/status' + ]; + + /** + * @param $tron + * @param $providers + * @throws Exception\TronException + */ + public function __construct($tron, $providers) + { + $this->providers = $providers; + + foreach ($providers as $key => $value) + { + //Do not skip the supplier is empty + if ($value == null) { + $this->providers[$key] = new HttpProvider( + $this->defaultNodes[$key] + ); + }; + + if(is_string($providers[$key])) + $this->providers[$key] = new HttpProvider($value); + + if(in_array($key, ['signServer'])) + continue; + + $this->providers[$key]->setStatusPage($this->statusPage[$key]); + } + } + + /** + * List of providers + * + * @return array + */ + public function getProviders() { + return $this->providers; + } + + /** + * Full Node + * + * @throws TronException + * @return HttpProviderInterface + */ + public function fullNode() : HttpProviderInterface + { + if (!array_key_exists('fullNode', $this->providers)) { + throw new TronException('Full node is not activated.'); + } + + return $this->providers['fullNode']; + } + + /** + * Solidity Node + * + * @throws TronException + * @return HttpProviderInterface + */ + public function solidityNode() : HttpProviderInterface + { + if (!array_key_exists('solidityNode', $this->providers)) { + throw new TronException('Solidity node is not activated.'); + } + + return $this->providers['solidityNode']; + } + + /** + * Sign server + * + * @throws TronException + * @return HttpProviderInterface + */ + public function signServer(): HttpProviderInterface + { + if (!array_key_exists('signServer', $this->providers)) { + throw new TronException('Sign server is not activated.'); + } + + return $this->providers['signServer']; + } + + /** + * TronScan server + * + * @throws TronException + * @return HttpProviderInterface + */ + public function explorer(): HttpProviderInterface + { + if (!array_key_exists('explorer', $this->providers)) { + throw new TronException('explorer is not activated.'); + } + + return $this->providers['explorer']; + } + + /** + * Event server + * + * @throws TronException + * @return HttpProviderInterface + */ + public function eventServer(): HttpProviderInterface + { + if (!array_key_exists('eventServer', $this->providers)) { + throw new TronException('Event server is not activated.'); + } + + return $this->providers['eventServer']; + } + + /** + * Basic query to nodes + * + * @param $url + * @param $params + * @param string $method + * @return array + * @throws TronException + */ + public function request($url, $params = [], $method = 'post') + { + $split = explode('/', $url); + if(in_array($split[0], ['walletsolidity', 'walletextension'])) { + $response = $this->solidityNode()->request($url, $params, $method); + } elseif(in_array($split[0], ['event'])) { + $response = $this->eventServer()->request($url, $params, 'get'); + } elseif (in_array($split[0], ['trx-sign'])) { + $response = $this->signServer()->request($url, $params, 'post'); + } elseif(in_array($split[0], ['api'])) { + $response = $this->explorer()->request($url, $params, 'get'); + }else { + $response = $this->fullNode()->request($url, $params, $method); + } + + return $response; + } + + /** + * Check connections + * + * @return array + */ + public function isConnected() + { + $array = []; + foreach ($this->providers as $key => $value) { + array_push($array, [ + $key => boolval($value->isConnected()) + ]); + } + + return $array; + } +} \ No newline at end of file diff --git a/src/trc20.json b/src/trc20.json new file mode 100644 index 0000000..62e8f2f --- /dev/null +++ b/src/trc20.json @@ -0,0 +1,269 @@ +[ + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "spender", + "type": "address" + }, + { + "name": "value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "sender", + "type": "address" + }, + { + "name": "recipient", + "type": "address" + }, + { + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "name": "", + "type": "uint8" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "spender", + "type": "address" + }, + { + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "spender", + "type": "address" + }, + { + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "recipient", + "type": "address" + }, + { + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "owner", + "type": "address" + }, + { + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "from", + "type": "address" + }, + { + "indexed": true, + "name": "to", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + } +] \ No newline at end of file diff --git a/tests/TronTest.php b/tests/TronTest.php new file mode 100644 index 0000000..1417a2e --- /dev/null +++ b/tests/TronTest.php @@ -0,0 +1,52 @@ +assertEquals($tron->isValidProvider($provider), true); + } + + public function test_setAddress() + { + $tron = new Tron(new HttpProvider(self::FULL_NODE_API), new HttpProvider(self::SOLIDITY_NODE_API)); + $tron->setAddress(self::ADDRESS_HEX); + + $this->assertEquals($tron->getAddress()['hex'],self::ADDRESS_HEX); + $this->assertEquals($tron->getAddress()['base58'], self::ADDRESS_BASE58); + } + + public function test_setDefaultBlock() + { + $tron = new Tron(new HttpProvider(self::FULL_NODE_API),new HttpProvider(self::SOLIDITY_NODE_API)); + $tron->setDefaultBlock(1); + $this->assertEquals($tron->getDefaultBlock(), 1); + + $tron->setDefaultBlock(-2); + $this->assertEquals($tron->getDefaultBlock(),2); + + $tron->setDefaultBlock(0); + $this->assertEquals($tron->getDefaultBlock(),0); + + $tron->setDefaultBlock(); + $this->assertEquals($tron->getDefaultBlock(),false); + + $tron->setDefaultBlock('latest'); + $this->assertEquals($tron->getDefaultBlock(),'latest'); + } +} \ No newline at end of file