diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml new file mode 100644 index 0000000..78a4bee --- /dev/null +++ b/.github/workflows/codecov.yml @@ -0,0 +1,44 @@ +name: Codecov + +on: [ push, pull_request ] + +jobs: + codecov: + name: Codecov + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest ] + php-versions: [ '8.1' ] + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 1 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: gmp, pdo_sqlite, xdebug + + - name: PHP version + run: php -v + + - name: Composer install + run: composer install + + - name: PHPUnit coverage + env: + XDEBUG_MODE: coverage + WAVES_CONFIG: 7b2257415645535f4e4f4445223a2268747470733a5c2f5c2f73746167652d6e6f64652e77382e696f222c2257415645535f464155434554223a2277617665732070726976617465206e6f64652073656564207769746820776176657320746f6b656e73227d + run: php vendor/bin/phpunit tests --coverage-clover ./coverage.xml + + - name: Codecov + uses: codecov/codecov-action@v2 + with: + files: ./coverage.xml + \ No newline at end of file diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml new file mode 100644 index 0000000..8cfb19e --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,35 @@ +name: PHPStan + +on: [ push, pull_request ] + +jobs: + phpstan: + name: PHPStan + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest ] + php-versions: [ '8.1' ] + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 1 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: gmp, pdo_sqlite + + - name: PHP version + run: php -v + + - name: Composer install + run: composer install + + - name: PHPStan analyse + run: php vendor/bin/phpstan analyse src tests --level 9 diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml new file mode 100644 index 0000000..42798d8 --- /dev/null +++ b/.github/workflows/phpunit.yml @@ -0,0 +1,40 @@ +name: PHPUnit + +on: [ push, pull_request ] + +jobs: + tests: + name: Tests + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest, windows-latest, macos-latest ] + php-versions: [ '7.4', '8.1' ] + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 1 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: gmp, pdo_sqlite + + - name: PHP version + run: php -v + + - name: Composer validate + run: composer validate + + - name: Composer install + run: composer install + + - name: PHPUnit tests + env: + WAVES_CONFIG: 7b2257415645535f4e4f4445223a2268747470733a5c2f5c2f73746167652d6e6f64652e77382e696f222c2257415645535f464155434554223a2277617665732070726976617465206e6f64652073656564207769746820776176657320746f6b656e73227d + run: php vendor/bin/phpunit tests diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af12b5d --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/vendor +/.phpunit.cache +/.vscode +/coverage.xml +/composer.lock +/tests/config.php diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..00b396c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 WavesPlatform + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9a09a44 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# Waves-PHP + +PHP client library for interacting with Waves blockchain platform. + +## Installation +```bash +composer require waves/client +``` + +## Usage +- Transfer: +```php +$account = PrivateKey::fromSeed( 'manage manual recall harvest series desert melt police rose hollow moral pledge kitten position add' ); +$tx = TransferTransaction::build( $account->publicKey(), Recipient::fromAddressOrAlias( 'test' ), Amount::of( 1 ) ); +$txId = Node::TESTNET()->broadcast( $tx->addProof( $account ) )->id(); +$txOnChain = Node::TESTNET()->waitForTransaction( $txId ); +``` + +## Requirements +- [PHP](http://php.net) >= 7.4 diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..0b32b00 --- /dev/null +++ b/composer.json @@ -0,0 +1,28 @@ +{ + "name": "waves/client", + "description": "PHP client library for interacting with Waves blockchain platform", + "keywords": [ "waves", "wavesplatform", "blockchain", "client" ], + "homepage": "https://github.com/wavesplatform/waves-php", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Dmitrii Pichulin", + "email": "deem@deem.ru" + } + ], + "autoload": { + "psr-4": { + "Waves\\": "src" + } + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "phpstan/phpstan": "^1.8" + }, + "require": { + "deemru/waveskit": "^1.0", + "deemru/abcode": "^1.0", + "waves/protobuf": "^1.4" + } +} diff --git a/example.php b/example.php new file mode 100644 index 0000000..12ea856 --- /dev/null +++ b/example.php @@ -0,0 +1,18 @@ +publicKey(), Recipient::fromAddressOrAlias( 'test' ), Amount::of( 1 ) ); +$txId = Node::TESTNET()->broadcast( $tx->addProof( $account ) )->id(); +$txOnChain = Node::TESTNET()->waitForTransaction( $txId ); diff --git a/phpunit.php b/phpunit.php new file mode 100644 index 0000000..6ca8d3c --- /dev/null +++ b/phpunit.php @@ -0,0 +1,3 @@ + + + + + tests + + + + + + src + + + diff --git a/src/API/Node.php b/src/API/Node.php new file mode 100644 index 0000000..7a9a49b --- /dev/null +++ b/src/API/Node.php @@ -0,0 +1,734 @@ +uri = $uri; + $this->wk = new \deemru\WavesKit( '?', function( string $wklevel, string $wkmessage ) + { + $this->wklevel = $wklevel; + $this->wkmessage = $wkmessage; + } ); + $this->wk->setNodeAddress( $uri, 0 ); + + if( !isset( $chainId ) ) + { + if( $uri === Node::MAINNET ) + $this->chainId = ChainId::MAINNET(); + else + if( $uri === Node::TESTNET ) + $this->chainId = ChainId::TESTNET(); + else + if( $uri === Node::STAGENET ) + $this->chainId = ChainId::STAGENET(); + else + $this->chainId = $this->getAddresses()[0]->chainId(); + } + else + { + $this->chainId = $chainId; + } + + $this->wk->chainId = $this->chainId->asString(); // @phpstan-ignore-line // accept workaround + } + + static function MAINNET(): Node + { + return new Node( Node::MAINNET ); + } + + static function TESTNET(): Node + { + return new Node( Node::TESTNET ); + } + + static function STAGENET(): Node + { + return new Node( Node::STAGENET ); + } + + static function LOCAL(): Node + { + return new Node( Node::LOCAL ); + } + + function chainId(): ChainId + { + return $this->chainId; + } + + function uri(): string + { + return $this->uri; + } + + /** + * Fetches a custom REST API request + * + * @param string $uri + * @param Json|string|null $data + * @return Json + */ + private function fetch( string $uri, $data = null ) + { + if( isset( $data ) ) + { + if( is_string( $data ) ) + $fetch = $this->wk->fetch( $uri, true, $data, null, [ 'Content-Type: text/plain', 'Accept: application/json' ] ); + else + $fetch = $this->wk->fetch( $uri, true, $data->toString() ); + } + else + $fetch = $this->wk->fetch( $uri ); + if( $fetch === false ) + { + $message = __FUNCTION__ . ' failed at `' . $uri . '`'; + if( $this->wklevel === 'e' ) + $message .= ' (' . $this->wkmessage . ')'; + throw new Exception( $message, ExceptionCode::FETCH_URI ); + } + $fetch = $this->wk->json_decode( $fetch ); + if( $fetch === false ) + throw new Exception( __FUNCTION__ . ' failed to decode `' . $uri . '`', ExceptionCode::JSON_DECODE ); + return Json::as( $fetch ); + } + + /** + * GETs a custom REST API request + * + * @param string $uri + * @return Json + */ + function get( string $uri ): Json + { + return $this->fetch( $uri ); + } + + /** + * POSTs a custom REST API request + * + * @param string $uri + * @param Json|string $data + * @return Json + */ + function post( string $uri, $data ): Json + { + return $this->fetch( $uri, $data ); + } + + //=============== + // ADDRESSES + //=============== + + /** + * Return addresses of the node + * + * @return array + */ + function getAddresses(): array + { + return $this->get( '/addresses' )->asArrayAddress(); + } + + /** + * Return addresses of the node by indexes + * + * @return array + */ + function getAddressesByIndexes( int $fromIndex, int $toIndex ): array + { + return $this->get( '/addresses/seq/' . $fromIndex . '/' . $toIndex )->asArrayAddress(); + } + + function getBalance( Address $address, int $confirmations = null ): int + { + $uri = '/addresses/balance/' . $address->toString(); + if( isset( $confirmations ) ) + $uri .= '/' . $confirmations; + return $this->get( $uri )->get( 'balance' )->asInt(); + } + + /** + * Gets addresses balances + * + * @param array $addresses + * @param int|null $height (default: null) + * @return array + */ + function getBalances( array $addresses, int $height = null ): array + { + $json = Json::emptyJson(); + + $array = []; + foreach( $addresses as $address ) + $array[] = $address->toString(); + $json->put( 'addresses', $array ); + + if( isset( $height ) ) + $json->put( 'height', $height ); + + return $this->post( '/addresses/balance', $json )->asArrayBalance(); + } + + function getBalanceDetails( Address $address ): BalanceDetails + { + return $this->get( '/addresses/balance/details/' . $address->toString() )->asBalanceDetails(); + } + + /** + * Gets DataEntry array of address + * + * @param Address $address + * @param string|null $regex (default: null) + * @return array + */ + function getData( Address $address, string $regex = null ): array + { + $uri = '/addresses/data/' . $address->toString(); + if( isset( $regex ) ) + $uri .= '?matches=' . urlencode( $regex ); + return $this->get( $uri )->asArrayDataEntry(); + } + + /** + * Gets DataEntry array of address by keys + * + * @param Address $address + * @param array $keys + * @return array + */ + function getDataByKeys( Address $address, array $keys ): array + { + $json = Json::emptyJson(); + + $array = []; + foreach( $keys as $key ) + $array[] = $key; + $json->put( 'keys', $array ); + + return $this->post( '/addresses/data/' . $address->toString(), $json )->asArrayDataEntry(); + } + + /** + * Gets a single DataEntry of address by a key + * + * @param Address $address + * @param string $key + * @return DataEntry + */ + function getDataByKey( Address $address, string $key ): DataEntry + { + return $this->get( '/addresses/data/' . $address->toString() . '/' . $key )->asDataEntry(); + } + + function getScriptInfo( Address $address ): ScriptInfo + { + return $this->get( '/addresses/scriptInfo/' . $address->toString() )->asScriptInfo(); + } + + function getScriptMeta( Address $address ): ScriptMeta + { + $json = $this->get( '/addresses/scriptInfo/' . $address->toString() . '/meta' ); + if( !$json->exists( 'meta' ) ) + $json->put( 'meta', [ 'version' => 0, 'callableFuncTypes' => [] ] ); + return $json->get( 'meta' )->asJson()->asScriptMeta(); + } + + //=============== + // ALIAS + //=============== + + /** + * Gets an array of aliases by address + * + * @param Address $address + * @return array + */ + function getAliasesByAddress( Address $address ): array + { + return $this->get( '/alias/by-address/' . $address->toString() )->asArrayAlias(); + } + + function getAddressByAlias( Alias $alias ): Address + { + return $this->get( '/alias/by-alias/' . $alias->name() )->get( 'address' )->asAddress(); + } + + //=============== + // ASSETS + //=============== + + function getAssetDistribution( AssetId $assetId, int $height, int $limit = 1000, string $after = null ): AssetDistribution + { + $uri = '/assets/' . $assetId->toString() . '/distribution/' . $height . '/limit/' . $limit; + if( isset( $after ) ) + $uri .= '?after=' . $after; + return $this->get( $uri )->asAssetDistribution(); + } + + /** + * Gets an array of AssetBalance for an address + * + * @param Address $address + * @return array + */ + function getAssetsBalance( Address $address ): array + { + return $this->get( '/assets/balance/' . $address->toString() )->get( 'balances' )->asJson()->asArrayAssetBalance(); + } + + function getAssetBalance( Address $address, AssetId $assetId ): int + { + return $assetId->isWaves() ? + $this->getBalance( $address ) : + $this->get( '/assets/balance/' . $address->toString() . '/' . $assetId->toString() )->get( 'balance' )->asInt(); + } + + function getAssetDetails( AssetId $assetId ): AssetDetails + { + return $this->get( '/assets/details/' . $assetId->toString() . '?full=true' )->asAssetDetails(); + } + + /** + * @param array $assetIds + * @return array + */ + function getAssetsDetails( array $assetIds ): array + { + $json = Json::emptyJson(); + + $array = []; + foreach( $assetIds as $assetId ) + $array[] = $assetId->toString(); + $json->put( 'ids', $array ); + + return $this->post( '/assets/details?full=true', $json )->asArrayAssetDetails(); + } + + /** + * @return array + */ + function getNft( Address $address, int $limit = 1000, AssetId $after = null ): array + { + $uri = '/assets/nft/' . $address->toString() . '/limit/' . $limit; + if( isset( $after ) ) + $uri .= '?after=' . $after->toString(); + return $this->get( $uri )->asArrayAssetDetails(); + } + + //=============== + // BLOCKCHAIN + //=============== + + function getBlockchainRewards( int $height = null ): BlockchainRewards + { + $uri = '/blockchain/rewards'; + if( isset( $height ) ) + $uri .= '/' . $height; + return $this->get( $uri )->asBlockchainRewards(); + } + + //=============== + // BLOCKS + //=============== + + function getHeight(): int + { + return $this->get( '/blocks/height' )->get( 'height' )->asInt(); + } + + function getBlockHeightById( string $blockId ): int + { + return $this->get( '/blocks/height/' . $blockId )->get( 'height' )->asInt(); + } + + function getBlockHeightByTimestamp( int $timestamp ): int + { + return $this->get( "/blocks/heightByTimestamp/" . $timestamp )->get( "height" )->asInt(); + } + + function getBlocksDelay( string $startBlockId, int $blocksNum ): int + { + return $this->get( "/blocks/delay/" . $startBlockId . "/" . $blocksNum )->get( "delay" )->asInt(); + } + + function getBlockHeadersByHeight( int $height ): BlockHeaders + { + return $this->get( "/blocks/headers/at/" . $height )->asBlockHeaders(); + } + + function getBlockHeadersById( string $blockId ): BlockHeaders + { + return $this->get( "/blocks/headers/" . $blockId )->asBlockHeaders(); + } + + /** + * Get an array of BlockHeaders from fromHeight to toHeight + * + * @param integer $fromHeight + * @param integer $toHeight + * @return array + */ + function getBlocksHeaders( int $fromHeight, int $toHeight ): array + { + return $this->get( "/blocks/headers/seq/" . $fromHeight . "/" . $toHeight )->asArrayBlockHeaders(); + } + + function getLastBlockHeaders(): BlockHeaders + { + return $this->get( "/blocks/headers/last" )->asBlockHeaders(); + } + + function getBlockByHeight( int $height ): Block + { + return $this->get( '/blocks/at/' . $height )->asBlock(); + } + + function getBlockById( Id $id ): Block + { + return $this->get( '/blocks/' . $id->toString() )->asBlock(); + } + + /** + * @return array + */ + function getBlocks( int $fromHeight, int $toHeight ): array + { + return $this->get( '/blocks/seq/' . $fromHeight . '/' . $toHeight )->asArrayBlock(); + } + + function getGenesisBlock(): Block + { + return $this->get( '/blocks/first' )->asBlock(); + } + + function getLastBlock(): Block + { + return $this->get( '/blocks/last' )->asBlock(); + } + + /** + * @return array + */ + function getBlocksGeneratedBy( Address $generator, int $fromHeight, int $toHeight ): array + { + return $this->get( '/blocks/address/' . $generator->toString() . '/' . $fromHeight . '/' . $toHeight )->asArrayBlock(); + } + + //=============== + // NODE + //=============== + + function getVersion(): string + { + return $this->get( '/node/version')->get( 'version' )->asString(); + } + + //=============== + // DEBUG + //=============== + + /** + * @param Address $address + * @return array + */ + function getBalanceHistory( Address $address ): array + { + return $this->get( '/debug/balances/history/' . $address->toString() )->asArrayHistoryBalance(); + } + + function validateTransaction( Transaction $transaction ): Validation + { + return $this->post( '/debug/validate', $transaction->json() )->asValidation(); + } + + //=============== + // LEASING + //=============== + + /** + * @return array + */ + function getActiveLeases( Address $address ): array + { + return $this->get( '/leasing/active/' . $address->toString() )->asArrayLeaseInfo(); + } + + function getLeaseInfo( Id $leaseId ): LeaseInfo + { + return $this->get( '/leasing/info/' . $leaseId->toString() )->asLeaseInfo(); + } + + /** + * @param array $leaseIds + * @return array + */ + function getLeasesInfo( array $leaseIds ): array + { + $json = Json::emptyJson(); + + $array = []; + foreach( $leaseIds as $leaseId ) + $array[] = $leaseId->toString(); + $json->put( 'ids', $array ); + + return $this->post( '/leasing/info', $json )->asArrayLeaseInfo(); + } + + //=============== + // TRANSACTIONS + //=============== + + function calculateTransactionFee( Transaction $transaction ): Amount + { + $json = $this->post( '/transactions/calculateFee', $transaction->json() ); + return Amount::fromJson( $json, 'feeAmount', 'feeAssetId' ); + } + + function serializeTransaction( Transaction $transaction ): string + { + $json = $this->post( '/utils/transactionSerialize', $transaction->json() ); + $bytes = ''; + foreach( $json->get( 'bytes' )->asArrayInt() as $byte ) + $bytes .= chr( $byte ); + return $bytes; + } + + function broadcast( Transaction $transaction ): Transaction + { + return $this->post( '/transactions/broadcast', $transaction->json() )->asTransaction(); + } + + function getTransactionInfo( Id $txId ): TransactionInfo + { + return $this->get( '/transactions/info/' . $txId->toString() )->asTransactionInfo(); + } + + /** + * @return array + */ + function getTransactionsByAddress( Address $address, int $limit = 100, Id $afterTxId = null ): array + { + $uri = '/transactions/address/' . $address->toString() . '/limit/' . $limit; + if( isset( $afterTxId ) ) + $uri .= '?after=' . $afterTxId->toString(); + return $this->get( $uri )->get( 0 )->asJson()->asArrayTransactionInfo(); + } + + function getTransactionStatus( Id $txId ): TransactionStatus + { + return $this->get( '/transactions/status?id=' . $txId->toString() )->get( 0 )->asJson()->asTransactionStatus(); + } + + /** + * @param array $txIds + * @return array + */ + function getTransactionsStatus( array $txIds ): array + { + $json = Json::emptyJson(); + + $array = []; + foreach( $txIds as $txId ) + $array[] = $txId->toString(); + $json->put( 'ids', $array ); + + return $this->post( '/transactions/status', $json )->asArrayTransactionStatus(); + } + + function getUnconfirmedTransaction( Id $txId ): Transaction + { + return $this->get( '/transactions/unconfirmed/info/' . $txId->toString() )->asTransaction(); + } + + /** + * @return array + */ + function getUnconfirmedTransactions(): array + { + return $this->get( '/transactions/unconfirmed' )->asArrayTransaction(); + } + + function getUtxSize(): int + { + return $this->get( '/transactions/unconfirmed/size' )->get( 'size' )->asInt(); + } + + //=============== + // UTILS + //=============== + + function compileScript( string $source, bool $enableCompaction = null ): ScriptInfo + { + $uri = '/utils/script/compileCode'; + if( isset( $enableCompaction ) ) + $uri .= '?compact=' . ( $enableCompaction ? 'true' : 'false' ); + return $this->post( $uri, $source )->asScriptInfo(); + } + + function ethToWavesAsset( string $asset ): string + { + return $this->get( '/eth/assets?id=' . $asset )->get( 0 )->asJson()->asAssetDetails()->assetId()->encoded(); + } + + //=============== + // WAITINGS + //=============== + + const blockInterval = 60; + + function waitForTransaction( Id $id, int $waitingInSeconds = Node::blockInterval ): TransactionInfo + { + if( $waitingInSeconds < 1 ) + $waitingInSeconds = 1; + + $pollingIntervalInMillis = 100; + $pollingIntervalInMicros = $pollingIntervalInMillis * 1000; + $waitingInMillis = $waitingInSeconds * 1000; + + for( $spentMillis = 0; $spentMillis < $waitingInMillis; $spentMillis += $pollingIntervalInMillis ) + { + try + { + return $this->getTransactionInfo( $id ); + } + catch( Exception $e ) + { + if( $e->getCode() !== ExceptionCode::FETCH_URI ) + throw new Exception( __FUNCTION__ . ' unexpected exception `' . $e->getCode() . '`:`' . $e->getMessage() . '`', ExceptionCode::UNEXPECTED ); + + usleep( $pollingIntervalInMicros ); + } + } + + throw new Exception( __FUNCTION__ . ' could not wait for transaction `' . $id->toString() . '` in ' . $waitingInSeconds . ' seconds', ExceptionCode::TIMEOUT ); + } + + /** + * @param array $ids + * @param int $waitingInSeconds + * @return void + */ + function waitForTransactions( array $ids, int $waitingInSeconds = Node::blockInterval ): void + { + if( $waitingInSeconds < 1 ) + $waitingInSeconds = 1; + + $pollingIntervalInMillis = 1000; + $pollingIntervalInMicros = $pollingIntervalInMillis * 1000; + $waitingInMillis = $waitingInSeconds * 1000; + + for( $spentMillis = 0; $spentMillis < $waitingInMillis; $spentMillis += $pollingIntervalInMillis ) + { + try + { + $isOK = true; + $statuses = $this->getTransactionsStatus( $ids ); + foreach( $statuses as $status ) + if( $status->status() !== Status::CONFIRMED ) + { + $isOK = false; + break; + } + + if( $isOK ) + return; + + usleep( $pollingIntervalInMicros ); + } + catch( Exception $e ) + { + if( $e->getCode() !== ExceptionCode::FETCH_URI ) + throw new Exception( __FUNCTION__ . ' unexpected exception `' . $e->getCode() . '`:`' . $e->getMessage() . '`', ExceptionCode::UNEXPECTED ); + + usleep( $pollingIntervalInMicros ); + } + } + + throw new Exception( __FUNCTION__ . ' could not wait for transactions', ExceptionCode::TIMEOUT ); + } + + function waitForHeight( int $target, int $waitingInSeconds = Node::blockInterval * 3 ): int + { + $start = $this->getHeight(); + $prev = $start; + + if( $waitingInSeconds < 1 ) + $waitingInSeconds = 1; + + $pollingIntervalInMillis = 100; + $pollingIntervalInMicros = $pollingIntervalInMillis * 1000; + $waitingInMillis = $waitingInSeconds * 1000; + + $current = $start; + for( $spentMillis = 0; $spentMillis < $waitingInMillis; $spentMillis += $pollingIntervalInMillis ) + { + if( $current >= $target ) + return $current; + else if( $current > $prev ) + { + $prev = $current; + $spentMillis = 0; + } + + usleep( $pollingIntervalInMicros ); + $current = $this->getHeight(); + } + + throw new Exception( __FUNCTION__ . ' could not wait for height `' . $target . '` in ' . $waitingInSeconds . ' seconds', ExceptionCode::TIMEOUT ); + } + + function waitBlocks( int $blocksCount, int $waitingInSeconds = Node::blockInterval * 3 ): int + { + return $this->waitForHeight( $this->getHeight() + $blocksCount, $waitingInSeconds ); + } +} \ No newline at end of file diff --git a/src/Account/Address.php b/src/Account/Address.php new file mode 100644 index 0000000..b1522a8 --- /dev/null +++ b/src/Account/Address.php @@ -0,0 +1,72 @@ +address = Base58String::fromString( $encoded ); + return $address; + } + + static function fromBytes( string $bytes ): Address + { + if( strlen( $bytes ) !== Address::BYTE_LENGTH ) + throw new Exception( __FUNCTION__ . ' bad address length: ' . strlen( $bytes ), ExceptionCode::BAD_ADDRESS ); + $address = new Address; + $address->address = Base58String::fromBytes( $bytes ); + return $address; + } + + static function fromPublicKey( PublicKey $publicKey, ChainId $chainId = null ): Address + { + $address = new Address; + $wk = new \deemru\WavesKit( ( isset( $chainId ) ? $chainId : WavesConfig::chainId() )->asString() ); + $wk->setPublicKey( $publicKey->bytes(), true ); + $address->address = Base58String::fromBytes( $wk->getAddress( true ) ); + return $address; + } + + function chainId(): ChainId + { + return ChainId::fromString( $this->bytes()[1] ); + } + + function bytes(): string + { + $bytes = $this->address->bytes(); + if( strlen( $bytes ) !== Address::BYTE_LENGTH ) + throw new Exception( __FUNCTION__ . ' bad address length: ' . strlen( $bytes ), ExceptionCode::BAD_ADDRESS ); + return $bytes; + } + + function encoded(): string + { + return $this->address->encoded(); + } + + function toString(): string + { + return $this->encoded(); + } + + function publicKeyHash(): string + { + return substr( $this->bytes(), 2, 20 ); + } +} diff --git a/src/Account/PrivateKey.php b/src/Account/PrivateKey.php new file mode 100644 index 0000000..e4384ca --- /dev/null +++ b/src/Account/PrivateKey.php @@ -0,0 +1,53 @@ +key = Base58String::fromBytes( ( new \deemru\WavesKit )->getPrivateKey( true, $seed, pack( 'N', $nonce ) ) ); + return $privateKey; + } + + static function fromBytes( string $key ): PrivateKey + { + $privateKey = new PrivateKey; + $privateKey->key = Base58String::fromBytes( $key ); + return $privateKey; + } + + static function fromString( string $key ): PrivateKey + { + $privateKey = new PrivateKey; + $privateKey->key = Base58String::fromString( $key ); + return $privateKey; + } + + function publicKey(): PublicKey + { + if( !isset( $this->publicKey ) ) + $this->publicKey = PublicKey::fromPrivateKey( $this ); + return $this->publicKey; + } + + function bytes(): string + { + return $this->key->bytes(); + } + + function toString(): string + { + return $this->key->toString(); + } +} diff --git a/src/Account/PublicKey.php b/src/Account/PublicKey.php new file mode 100644 index 0000000..4716754 --- /dev/null +++ b/src/Account/PublicKey.php @@ -0,0 +1,62 @@ +key = Base58String::fromBytes( $key ); + return $publicKey; + } + + static function fromString( string $key ): PublicKey + { + $publicKey = new PublicKey; + $publicKey->key = Base58String::fromString( $key ); + return $publicKey; + } + + static function fromPrivateKey( PrivateKey $key ): PublicKey + { + $publicKey = new PublicKey; + $wk = new \deemru\WavesKit; + $wk->setPrivateKey( $key->bytes(), true ); + $publicKey->key = Base58String::fromBytes( $wk->getPublicKey( true ) ); + return $publicKey; + } + + function address( ChainId $chainId = null ): Address + { + if( !isset( $this->address ) ) + $this->address = Address::fromPublicKey( $this, $chainId ); + return $this->address; + } + + function attachAddress( Address $address ): void + { + $this->address = $address; + } + + function bytes(): string + { + return $this->key->bytes(); + } + + function toString(): string + { + return $this->key->toString(); + } +} diff --git a/src/Common/Base58String.php b/src/Common/Base58String.php new file mode 100644 index 0000000..d9bce6a --- /dev/null +++ b/src/Common/Base58String.php @@ -0,0 +1,54 @@ +bytes = ''; + $base58String->encoded = ''; + return $base58String; + } + + static function fromString( string $encoded ): Base58String + { + $base58String = new Base58String; + $base58String->encoded = $encoded; + return $base58String; + } + + static function fromBytes( string $bytes ): Base58String + { + $base58String = new Base58String; + $base58String->bytes = $bytes; + return $base58String; + } + + function bytes(): string + { + if( !isset( $this->bytes ) ) + $this->bytes = Functions::base58Decode( $this->encoded ); + return $this->bytes; + } + + function encoded(): string + { + if( !isset( $this->encoded ) ) + $this->encoded = Functions::base58Encode( $this->bytes ); + return $this->encoded; + } + + function toString(): string + { + return $this->encoded(); + } +} diff --git a/src/Common/Base64String.php b/src/Common/Base64String.php new file mode 100644 index 0000000..e9cef13 --- /dev/null +++ b/src/Common/Base64String.php @@ -0,0 +1,77 @@ +bytes = ''; + $base64String->encoded = ''; + return $base64String; + } + + static function fromString( string $encoded ): Base64String + { + if( substr( $encoded, 0, 7 ) === Base64String::PROLOG ) + $encoded = substr( $encoded, 7 ); + + $base64String = new Base64String; + $base64String->encoded = $encoded; + return $base64String; + } + + static function fromBytes( string $bytes ): Base64String + { + $base64String = new Base64String; + $base64String->bytes = $bytes; + return $base64String; + } + + function bytes(): string + { + if( !isset( $this->bytes ) ) + { + $this->bytes = base64_decode( $this->encoded ); + if( !is_string( $this->bytes ) ) + throw new Exception( __FUNCTION__ . ' failed to decode string: ' . $this->encoded, ExceptionCode::BASE64_DECODE ); + } + return $this->bytes; + } + + function encoded(): string + { + if( !isset( $this->encoded ) ) + $this->encoded = base64_encode( $this->bytes ); + return $this->encoded; + } + + function encodedWithPrefix(): string + { + return Base64String::PROLOG . $this->encoded(); + } + + function toString(): string + { + return $this->encodedWithPrefix(); + } + + /** + * @return string|null + */ + function toJsonValue() + { + if( $this->bytes() === '' ) + return null; + return $this->encodedWithPrefix(); + } +} diff --git a/src/Common/ExceptionCode.php b/src/Common/ExceptionCode.php new file mode 100644 index 0000000..4107b22 --- /dev/null +++ b/src/Common/ExceptionCode.php @@ -0,0 +1,26 @@ + + */ + private array $data; + + /** + * Json constructor + * + * @param array $data + */ + private function __construct( array $data = [] ) + { + $this->data = $data; + } + + /** + * Json function constructor + * + * @param array $data + * @return Json + */ + static function as( array $data ): Json + { + return new Json( $data ); + } + + static function emptyJson(): Json + { + return new Json; + } + + /** + * Gets native underlying data array + * + * @return array + */ + function data(): array + { + return $this->data; + } + + function toString(): string + { + $string = json_encode( $this->data ); + if( $string === false ) + throw new Exception( __FUNCTION__ . ' failed to encode internal array `' . serialize( $this->data) . '`', ExceptionCode::JSON_ENCODE ); + return $string; + } + + /** + * Gets Value by key + * + * @param mixed $key + * @return Value + */ + function get( $key ): Value + { + if( !isset( $this->data[$key] ) ) + throw new Exception( __FUNCTION__ . ' failed to find key `' . $key . '`', ExceptionCode::KEY_MISSING ); + return Value::as( $this->data[$key] ); + } + + /** + * Gets Value by key or returns fallback value + * + * @param mixed $key + * @param mixed $value + * @return Value + */ + function getOr( $key, $value ): Value + { + return $this->exists( $key ) ? $this->get( $key ) : Value::as( $value ); + } + + /** + * Checks key exists + * + * @param mixed $key + * @return bool + */ + function exists( $key ): bool + { + return isset( $this->data[$key] ); + } + + /** + * Puts value by key + * + * @param mixed $key + * @param mixed $value + * @return Json + */ + function put( $key, $value ): Json + { + $this->data[$key] = $value; + return $this; + } + + /** + * Gets a BlockHeaders value + * + * @return BlockHeaders + */ + function asBlockHeaders(): BlockHeaders + { + return new BlockHeaders( $this ); + } + + /** + * Gets a Balance value + * + * @return Balance + */ + function asBalance(): Balance + { + return new Balance( $this ); + } + + function asHistoryBalance(): HistoryBalance + { + return new HistoryBalance( $this ); + } + + /** + * Gets a AssetBalance value + * + * @return AssetBalance + */ + function asAssetBalance(): AssetBalance + { + return new AssetBalance( $this ); + } + + function asAssetDetails(): AssetDetails + { + return new AssetDetails( $this ); + } + + function asAssetDistribution(): AssetDistribution + { + return new AssetDistribution( $this ); + } + + /** + * Gets a BalanceDetails value + * + * @return BalanceDetails + */ + function asBalanceDetails(): BalanceDetails + { + return new BalanceDetails( $this ); + } + + function asBlockchainRewards(): BlockchainRewards + { + return new BlockchainRewards( $this ); + } + + function asBlock(): Block + { + return new Block( $this ); + } + + /** + * Gets a DataEntry value + * + * @return DataEntry + */ + function asDataEntry(): DataEntry + { + return new DataEntry( $this ); + } + + function asLeaseInfo(): LeaseInfo + { + return new LeaseInfo( $this ); + } + + function asScriptMeta(): ScriptMeta + { + return new ScriptMeta( $this ); + } + + function asScriptInfo(): ScriptInfo + { + return new ScriptInfo( $this ); + } + + function asScriptDetails(): ScriptDetails + { + return new ScriptDetails( $this ); + } + + function asTransactionInfo(): TransactionInfo + { + return new TransactionInfo( $this ); + } + + function asTransactionWithStatus(): TransactionWithStatus + { + return new TransactionWithStatus( $this ); + } + + function asTransactionStatus(): TransactionStatus + { + return new TransactionStatus( $this ); + } + + function asTransaction(): Transaction + { + return new Transaction( $this ); + } + + function asValidation(): Validation + { + return new Validation( $this ); + } + + function asVotes(): Votes + { + return new Votes( $this ); + } + + /** + * Gets an array of BlockHeaders value + * + * @return array + */ + function asArrayBlockHeaders(): array + { + $array = []; + foreach( $this->data as $headers ) + $array[] = Value::as( $headers )->asJson()->asBlockHeaders(); + return $array; + } + + /** + * Gets an array of Block value + * + * @return array + */ + function asArrayBlock(): array + { + $array = []; + foreach( $this->data as $headers ) + $array[] = Value::as( $headers )->asJson()->asBlock(); + return $array; + } + + /** + * Gets an array of LeaseInfo value + * + * @return array + */ + function asArrayLeaseInfo(): array + { + $array = []; + foreach( $this->data as $info ) + $array[] = Value::as( $info )->asJson()->asLeaseInfo(); + return $array; + } + + /** + * Gets an array value + * + * @return array + */ + function asArrayAddress(): array + { + $array = []; + foreach( $this->data as $address ) + $array[] = Address::fromString( Value::as( $address )->asString() ); + return $array; + } + + /** + * Gets an array value + * + * @return array + */ + function asArrayAlias(): array + { + $array = []; + foreach( $this->data as $alias ) + $array[] = Alias::fromFullAlias( Value::as( $alias )->asString() ); + return $array; + } + + /** + * Gets an array value + * + * @return array + */ + function asArrayBalance(): array + { + $array = []; + foreach( $this->data as $balance ) + $array[] = Value::as( $balance )->asJson()->asBalance(); + return $array; + } + + /** + * Gets an array value + * + * @return array + */ + function asArrayHistoryBalance(): array + { + $array = []; + foreach( $this->data as $balance ) + $array[] = Value::as( $balance )->asJson()->asHistoryBalance(); + return $array; + } + + /** + * Gets an array value + * + * @return array + */ + function asArrayAssetBalance(): array + { + $array = []; + foreach( $this->data as $assetBalance ) + $array[] = Value::as( $assetBalance )->asJson()->asAssetBalance(); + return $array; + } + + /** + * Gets an array value + * + * @return array + */ + function asArrayAssetDetails(): array + { + $array = []; + foreach( $this->data as $assetDetails ) + $array[] = Value::as( $assetDetails )->asJson()->asAssetDetails(); + return $array; + } + + /** + * Gets an array value + * + * @return array + */ + function asArrayDataEntry(): array + { + $array = []; + foreach( $this->data as $data ) + $array[] = Value::as( $data )->asJson()->asDataEntry(); + return $array; + } + + /** + * Gets an array value + * + * @return array + */ + function asArrayTransactionWithStatus(): array + { + $array = []; + foreach( $this->data as $tx ) + $array[] = Value::as( $tx )->asJson()->asTransactionWithStatus(); + return $array; + } + + /** + * Gets an array value + * + * @return array + */ + function asArrayTransactionInfo(): array + { + $array = []; + foreach( $this->data as $tx ) + $array[] = Value::as( $tx )->asJson()->asTransactionInfo(); + return $array; + } + + /** + * @return array + */ + function asArrayTransactionStatus(): array + { + $array = []; + foreach( $this->data as $tx ) + $array[] = Value::as( $tx )->asJson()->asTransactionStatus(); + return $array; + } + + /** + * @return array + */ + function asArrayTransaction(): array + { + $array = []; + foreach( $this->data as $tx ) + $array[] = Value::as( $tx )->asJson()->asTransaction(); + return $array; + } +} diff --git a/src/Common/JsonBase.php b/src/Common/JsonBase.php new file mode 100644 index 0000000..6651af9 --- /dev/null +++ b/src/Common/JsonBase.php @@ -0,0 +1,25 @@ +json = $json; + } + + function toString(): string + { + return $this->json->toString(); + } + + function json(): Json + { + return $this->json; + } +} diff --git a/src/Common/Value.php b/src/Common/Value.php new file mode 100644 index 0000000..8d4a37a --- /dev/null +++ b/src/Common/Value.php @@ -0,0 +1,258 @@ +value = $value; + } + + /** + * Value function constructor + * + * @param mixed $value + * @return Value + */ + static function as( $value ): Value + { + return new Value( $value ); + } + + /** + * Gets an boolean value + * + * @return bool + */ + function asBoolean(): bool + { + if( !is_bool( $this->value ) ) + throw new Exception( __FUNCTION__ . ' failed to detect boolean at `' . json_encode( $this->value ) . '`', ExceptionCode::BOOL_EXPECTED ); + return $this->value; + } + + /** + * Gets an integer value + * + * @return int + */ + function asInt(): int + { + if( !is_int( $this->value ) ) + { + if( is_string( $this->value ) ) + { + $intval = intval( $this->value ); + if( strval( $intval ) === $this->value ) + return $intval; + } + throw new Exception( __FUNCTION__ . ' failed to detect integer at `' . json_encode( $this->value ) . '`', ExceptionCode::INT_EXPECTED ); + } + return $this->value; + } + + /** + * Gets a string value + * + * @return string + */ + function asString(): string + { + if( !is_string( $this->value ) ) + throw new Exception( __FUNCTION__ . ' failed to detect string at `' . json_encode( $this->value ) . '`', ExceptionCode::STRING_EXPECTED ); + return $this->value; + } + + function asBase64String(): Base64String + { + if( !is_string( $this->value ) ) + throw new Exception( __FUNCTION__ . ' failed to detect string at `' . json_encode( $this->value ) . '`', ExceptionCode::STRING_EXPECTED ); + return Base64String::fromString( $this->value ); + } + + function asChainId(): ChainId + { + if( is_int( $this->value ) ) + return ChainId::fromInt( $this->value ); + return ChainId::fromString( $this->asString() ); + } + + /** + * Gets a base64 decoded string value + * + * @return string + */ + function asBase64Decoded(): string + { + if( !is_string( $this->value ) ) + throw new Exception( __FUNCTION__ . ' failed to detect string at `' . json_encode( $this->value ) . '`', ExceptionCode::STRING_EXPECTED ); + if( substr( $this->value, 0, 7 ) !== 'base64:' ) + throw new Exception( __FUNCTION__ . ' failed to detect base64 `' . $this->value . '`', ExceptionCode::BASE64_DECODE ); + $decoded = base64_decode( substr( $this->value, 7 ) ); + if( !is_string( $decoded ) ) + throw new Exception( __FUNCTION__ . ' failed to decode base64 `' . substr( $this->value, 7 ) . '`', ExceptionCode::BASE64_DECODE ); + return $decoded; + } + + function asBase58String(): Base58String + { + return Base58String::fromString( $this->asString() ); + } + + /** + * Gets a Json value + * + * @return Json + */ + function asJson(): Json + { + if( !is_array( $this->value ) ) + throw new Exception( __FUNCTION__ . ' failed to detect Json at `' . json_encode( $this->value ) . '`', ExceptionCode::ARRAY_EXPECTED ); + return Json::as( $this->value ); + } + + /** + * Gets an array value + * + * @return array + */ + function asArray(): array + { + if( !is_array( $this->value ) ) + throw new Exception( __FUNCTION__ . ' failed to detect array at `' . json_encode( $this->value ) . '`', ExceptionCode::ARRAY_EXPECTED ); + return $this->value; + } + + /** + * Gets an array of integers value + * + * @return array + */ + function asArrayInt(): array + { + if( !is_array( $this->value ) ) + throw new Exception( __FUNCTION__ . ' failed to detect array at `' . json_encode( $this->value ) . '`', ExceptionCode::ARRAY_EXPECTED ); + $ints = []; + foreach( $this->value as $value ) + $ints[] = Value::as( $value )->asInt(); + return $ints; + } + + /** + * @return array + */ + function asArrayString(): array + { + if( !is_array( $this->value ) ) + throw new Exception( __FUNCTION__ . ' failed to detect array at `' . json_encode( $this->value ) . '`', ExceptionCode::ARRAY_EXPECTED ); + $strings = []; + foreach( $this->value as $value ) + $strings[] = Value::as( $value )->asString(); + return $strings; + } + + /** + * Gets an array of string to integer map + * + * @return array + */ + function asMapStringInt(): array + { + if( !is_array( $this->value ) ) + throw new Exception( __FUNCTION__ . ' failed to detect array at `' . json_encode( $this->value ) . '`', ExceptionCode::ARRAY_EXPECTED ); + $ints = []; + foreach( $this->value as $key => $value ) + $ints[Value::as( $key )->asString()] = Value::as( $value )->asInt(); + return $ints; + } + + function asArgMeta(): ArgMeta + { + return new ArgMeta( $this->asJson() ); + } + + /** + * Gets an Address value + * + * @return Address + */ + function asAddress(): Address + { + return Address::fromString( $this->asString() ); + } + + /** + * @return Recipient + */ + function asRecipient(): Recipient + { + return Recipient::fromAddressOrAlias( $this->asString() ); + } + + function asPublicKey(): PublicKey + { + return PublicKey::fromString( $this->asString() ); + } + + /** + * Gets an AssetId value + * + * @return AssetId + */ + function asAssetId(): AssetId + { + return isset( $this->value ) ? AssetId::fromString( $this->asString() ) : AssetId::WAVES(); + } + + /** + * Gets an Id value + * + * @return Id + */ + function asId(): Id + { + return Id::fromString( $this->asString() ); + } + + function asApplicationStatus(): int + { + switch( $this->asString() ) + { + case ApplicationStatus::SUCCEEDED_S: return ApplicationStatus::SUCCEEDED; + case ApplicationStatus::SCRIPT_EXECUTION_FAILED_S: return ApplicationStatus::SCRIPT_EXECUTION_FAILED; + default: return ApplicationStatus::UNKNOWN; + } + } + + function asStatus(): int + { + switch( $this->asString() ) + { + case Status::CONFIRMED_S: return Status::CONFIRMED; + case Status::UNCONFIRMED_S: return Status::UNCONFIRMED; + case Status::NOT_FOUND_S: return Status::NOT_FOUND; + default: return Status::UNKNOWN; + } + } +} diff --git a/src/Model/Alias.php b/src/Model/Alias.php new file mode 100644 index 0000000..a13b597 --- /dev/null +++ b/src/Model/Alias.php @@ -0,0 +1,73 @@ +name = $alias; + $this->fullAlias = Alias::PREFIX . $chainId->asString() . ':' . $alias; + } + + static function fromString( string $alias, ChainId $chainId = null ): Alias + { + return new Alias( $alias, $chainId ); + } + + static function fromFullAlias( string $fullAlias ): Alias + { + if( strlen( $fullAlias ) >= 12 ) + { + $prefix = substr( $fullAlias, 0, strlen( Alias::PREFIX ) ); + if( $prefix === Alias::PREFIX && $fullAlias[7] === ':' ) + { + $chainId = ChainId::fromString( $fullAlias[6] ); + $alias = substr( $fullAlias, 8 ); + return new Alias( $alias, $chainId ); + } + } + + throw new Exception( __FUNCTION__ . ' bad alias name = `' . serialize( $fullAlias ) . '`', ExceptionCode::BAD_ALIAS ); + } + + static function isValid( string $alias, ChainId $chainId = null ): bool + { + return $alias === (new Alias( $alias, $chainId ))->name(); + } + + function chainId(): ChainId + { + return ChainId::fromString( $this->fullAlias[6] ); + } + + function name(): string + { + return $this->name; + } + + function toString(): string + { + return $this->fullAlias; + } +} diff --git a/src/Model/ApplicationStatus.php b/src/Model/ApplicationStatus.php new file mode 100644 index 0000000..2b98333 --- /dev/null +++ b/src/Model/ApplicationStatus.php @@ -0,0 +1,13 @@ +json->get( 'name' )->asString(); } + function type(): string { return $this->json->get( 'type' )->asString(); } +} diff --git a/src/Model/AssetBalance.php b/src/Model/AssetBalance.php new file mode 100644 index 0000000..67e7392 --- /dev/null +++ b/src/Model/AssetBalance.php @@ -0,0 +1,17 @@ +json->get( 'assetId' )->asAssetId(); } + function balance(): int { return $this->json->get( 'balance' )->asInt(); } + function isReissuable(): bool { return $this->json->get( 'reissuable' )->asBoolean(); } + function quantity(): int { return $this->json->get( 'quantity' )->asInt(); } + function minSponsoredAssetFee(): int { return $this->json->getOr( 'minSponsoredAssetFee', 0 )->asInt(); } + function sponsorBalance(): int { return $this->json->getOr( 'sponsorBalance', 0 )->asInt(); } + function issueTransaction(): Transaction { return $this->json->get( 'issueTransaction' )->asJson()->asTransaction(); } +} diff --git a/src/Model/AssetDetails.php b/src/Model/AssetDetails.php new file mode 100644 index 0000000..e3fcdf2 --- /dev/null +++ b/src/Model/AssetDetails.php @@ -0,0 +1,25 @@ +json->get( 'assetId' )->asAssetId(); } + function issueHeight(): int { return $this->json->get( 'issueHeight' )->asInt(); } + function issueTimestamp(): int { return $this->json->get( 'issueTimestamp' )->asInt(); } + function issuer(): Address { return $this->json->get( 'issuer' )->asAddress(); } + function issuerPublicKey(): PublicKey { return $this->json->get( 'issuerPublicKey' )->asPublicKey(); } + function name(): string { return $this->json->get( 'name' )->asString(); } + function description(): string { return $this->json->get( 'description' )->asString(); } + function decimals(): int { return $this->json->get( 'decimals' )->asInt(); } + function isReissuable(): bool { return $this->json->get( 'reissuable' )->asBoolean(); } + function quantity(): int { return $this->json->get( 'quantity' )->asInt(); } + function isScripted(): bool { return $this->json->get( 'scripted' )->asBoolean(); } + function minSponsoredAssetFee(): int { return $this->json->getOr( 'minSponsoredAssetFee', 0 )->asInt(); } + function originTransactionId(): Id { return $this->json->get( 'originTransactionId' )->asId(); } + function scriptDetails(): ScriptDetails { return $this->json->getOr( 'scriptDetails', ScriptDetails::EMPTY )->asJson()->asScriptDetails(); } +} diff --git a/src/Model/AssetDistribution.php b/src/Model/AssetDistribution.php new file mode 100644 index 0000000..c971469 --- /dev/null +++ b/src/Model/AssetDistribution.php @@ -0,0 +1,15 @@ + + */ + function items(): array { return $this->json->get( 'items' )->asMapStringInt(); } + function lastItem(): string { return $this->json->get( 'lastItem' )->asString(); } + function hasNext(): bool { return $this->json->getOr( 'hasNext', false )->asBoolean(); } +} diff --git a/src/Model/AssetId.php b/src/Model/AssetId.php new file mode 100644 index 0000000..e087449 --- /dev/null +++ b/src/Model/AssetId.php @@ -0,0 +1,77 @@ +assetId = Base58String::fromString( $encoded ); + return $assetId; + } + + static function fromBytes( string $bytes ): AssetId + { + if( $bytes === '' ) + return AssetId::WAVES(); + + if( strlen( $bytes ) !== AssetId::BYTE_LENGTH ) + throw new Exception( __FUNCTION__ . ' bad asset length: ' . strlen( $bytes ), ExceptionCode::BAD_ASSET ); + $assetId = new AssetId; + $assetId->assetId = Base58String::fromBytes( $bytes ); + return $assetId; + } + + function isWaves(): bool + { + return !isset( $this->assetId ); + } + + function bytes(): string + { + if( $this->isWaves() ) + return ''; + $bytes = $this->assetId->bytes(); + if( strlen( $bytes ) !== AssetId::BYTE_LENGTH ) + throw new Exception( __FUNCTION__ . ' bad asset length: ' . strlen( $bytes ), ExceptionCode::BAD_ASSET ); + return $bytes; + } + + function encoded(): string + { + return $this->isWaves() ? AssetId::WAVES_STRING : $this->assetId->encoded(); + } + + function toString(): string + { + return $this->encoded(); + } + + /** + * @return string|null + */ + function toJsonValue() + { + return $this->isWaves() ? null : $this->encoded(); + } +} diff --git a/src/Model/Balance.php b/src/Model/Balance.php new file mode 100644 index 0000000..d2d7b63 --- /dev/null +++ b/src/Model/Balance.php @@ -0,0 +1,11 @@ +json->get( 'id' )->asString(); } + function getBalance(): int { return $this->json->get( 'balance' )->asInt(); } +} diff --git a/src/Model/BalanceDetails.php b/src/Model/BalanceDetails.php new file mode 100644 index 0000000..21e4ef5 --- /dev/null +++ b/src/Model/BalanceDetails.php @@ -0,0 +1,14 @@ +json->get( 'address' )->asString(); } + function available(): int { return $this->json->get( 'available' )->asInt(); } + function regular(): int { return $this->json->get( 'regular' )->asInt(); } + function generating(): int { return $this->json->get( 'generating' )->asInt(); } + function effective(): int { return $this->json->get( 'effective' )->asInt(); } +} diff --git a/src/Model/Block.php b/src/Model/Block.php new file mode 100644 index 0000000..9cf7bc0 --- /dev/null +++ b/src/Model/Block.php @@ -0,0 +1,12 @@ + + */ + function transactions(): array { return $this->json->get( 'transactions' )->asJson()->asArrayTransactionWithStatus(); } + function fee(): int { return $this->json->get( 'fee' )->asInt(); } +} diff --git a/src/Model/BlockHeaders.php b/src/Model/BlockHeaders.php new file mode 100644 index 0000000..9384581 --- /dev/null +++ b/src/Model/BlockHeaders.php @@ -0,0 +1,30 @@ + + */ + function features(): array { return $this->json->get( 'features' )->asArrayInt(); } + function version(): int { return $this->json->get( 'version' )->asInt(); } + function timestamp(): int { return $this->json->get( 'timestamp' )->asInt(); } + function reference(): string { return $this->json->get( 'reference' )->asString(); } + function baseTarget(): int { return $this->json->get( 'nxt-consensus' )->asJson()->get( 'base-target' )->asInt(); } + function generationSignature(): string { return $this->json->get( 'nxt-consensus' )->asJson()->get( 'generation-signature' )->asString(); } + function transactionsRoot(): string { return $this->json->get( 'transactionsRoot' )->asString(); } + function id(): Id { return $this->json->get( 'id' )->asId(); } + function desiredReward(): int { return $this->json->get( 'desiredReward' )->asInt(); } + function generator(): Address { return $this->json->get( 'generator' )->asAddress(); } + function signature(): string { return $this->json->get( 'signature' )->asString(); } + function size(): int { return $this->json->get( 'blocksize' )->asInt(); } + function transactionsCount(): int { return $this->json->get( 'transactionCount' )->asInt(); } + function height(): int { return $this->json->get( 'height' )->asInt(); } + function totalFee(): int { return $this->json->get( 'totalFee' )->asInt(); } + function reward(): int { return $this->json->get( 'reward' )->asInt(); } + function vrf(): string { return $this->json->get( 'VRF' )->asString(); } +} diff --git a/src/Model/BlockchainRewards.php b/src/Model/BlockchainRewards.php new file mode 100644 index 0000000..66ae7a8 --- /dev/null +++ b/src/Model/BlockchainRewards.php @@ -0,0 +1,19 @@ +json->get( 'height' )->asInt(); } + function currentReward(): int { return $this->json->get( 'currentReward' )->asInt(); } + function totalWavesAmount(): int { return $this->json->get( 'totalWavesAmount' )->asInt(); } + function minIncrement(): int { return $this->json->get( 'minIncrement' )->asInt(); } + function term(): int { return $this->json->get( 'term' )->asInt(); } + function nextCheck(): int { return $this->json->get( 'nextCheck' )->asInt(); } + function votingIntervalStart(): int { return $this->json->get( 'votingIntervalStart' )->asInt(); } + function votingInterval(): int { return $this->json->get( 'votingInterval' )->asInt(); } + function votingThreshold(): int { return $this->json->get( 'votingThreshold' )->asInt(); } + function votes(): Votes { return $this->json->get( 'votes' )->asJson()->asVotes(); } +} diff --git a/src/Model/ChainId.php b/src/Model/ChainId.php new file mode 100644 index 0000000..1d6a994 --- /dev/null +++ b/src/Model/ChainId.php @@ -0,0 +1,78 @@ + 255 ) + throw new Exception( __FUNCTION__ . ' bad chainId value: ' . $int, ExceptionCode::BAD_CHAINID ); + $chainId = new ChainId; + $chainId->chainId = chr( $int ); + return $chainId; + } + + static function fromString( string $string ): ChainId + { + if( strlen( $string ) !== 1 ) + throw new Exception( __FUNCTION__ . ' bad chainId value: ' . strlen( $string ), ExceptionCode::BAD_CHAINID ); + $chainId = new ChainId; + $chainId->chainId = $string; + return $chainId; + } + + static function MAINNET(): ChainId + { + static $chainId; + if( !isset( $chainId ) ) + $chainId = ChainId::fromString( ChainId::MAINNET ); + return $chainId; + } + + static function TESTNET(): ChainId + { + static $chainId; + if( !isset( $chainId ) ) + $chainId = ChainId::fromString( ChainId::TESTNET ); + return $chainId; + } + + static function STAGENET(): ChainId + { + static $chainId; + if( !isset( $chainId ) ) + $chainId = ChainId::fromString( ChainId::STAGENET ); + return $chainId; + } + + static function PRIVATE(): ChainId + { + static $chainId; + if( !isset( $chainId ) ) + $chainId = ChainId::fromString( ChainId::PRIVATE ); + return $chainId; + } + + function asInt(): int + { + return ord( $this->chainId ); + } + + function asString(): string + { + return $this->chainId; + } +} diff --git a/src/Model/DataEntry.php b/src/Model/DataEntry.php new file mode 100644 index 0000000..ea0c55f --- /dev/null +++ b/src/Model/DataEntry.php @@ -0,0 +1,140 @@ + $key, 'type' => null ]; + else + if( !isset( $value ) ) + throw new Exception( __FUNCTION__ . ' value expected but not set', ExceptionCode::UNEXPECTED ); + else + if( $type === EntryType::BINARY ) + $json = [ 'key' => $key, 'type' => 'binary', 'value' => Base64String::fromBytes( Value::as( $value )->asString() )->toString() ]; + else + $json = [ 'key' => $key, 'type' => DataEntry::typeToString( $type ), 'value' => $value ]; + return new DataEntry( Value::as( $json )->asJson() ); + } + + static function binary( string $key, string $value ): DataEntry + { + return DataEntry::build( $key, EntryType::BINARY, $value ); + } + + static function string( string $key, string $value ): DataEntry + { + return DataEntry::build( $key, EntryType::STRING, $value ); + } + + static function int( string $key, int $value ): DataEntry + { + return DataEntry::build( $key, EntryType::INTEGER, $value ); + } + + static function boolean( string $key, bool $value ): DataEntry + { + return DataEntry::build( $key, EntryType::BOOLEAN, $value ); + } + + static function delete( string $key ): DataEntry + { + return DataEntry::build( $key, EntryType::DELETE ); + } + + static function stringToType( string $stringType ): int + { + switch( $stringType ) + { + case 'binary': return EntryType::BINARY; + case 'boolean': return EntryType::BOOLEAN; + case 'integer': return EntryType::INTEGER; + case 'string': return EntryType::STRING; + default: throw new Exception( __FUNCTION__ . ' failed to detect type `' . serialize( $stringType ) . '`', ExceptionCode::UNKNOWN_TYPE ); + } + } + + static function typeToString( int $type ): string + { + switch( $type ) + { + case EntryType::BINARY: return 'binary'; + case EntryType::BOOLEAN: return 'boolean'; + case EntryType::INTEGER: return 'integer'; + case EntryType::STRING: return 'string'; + default: throw new Exception( __FUNCTION__ . ' failed to detect type `' . serialize( $type ) . '`', ExceptionCode::UNKNOWN_TYPE ); + } + } + + function key(): string { return $this->json->get( 'key' )->asString(); } + + function type(): int + { + if( !$this->json->exists( 'type' ) ) + return EntryType::DELETE; + return $this->stringToType( $this->json->get( 'type' )->asString() ); + } + + /** + * Returns value of native type + * + * @return bool|int|string|null + */ + function value() + { + switch( $this->type() ) + { + case EntryType::BINARY: return $this->json->get( 'value' )->asBase64Decoded(); + case EntryType::BOOLEAN: return $this->json->get( 'value' )->asBoolean(); + case EntryType::INTEGER: return $this->json->get( 'value' )->asInt(); + case EntryType::STRING: return $this->json->get( 'value' )->asString(); + case EntryType::DELETE: return null; + default: throw new Exception( __FUNCTION__ . ' failed to detect type `' . serialize( $this->type() ) . '`', ExceptionCode::UNKNOWN_TYPE ); // @codeCoverageIgnore + } + } + + function stringValue(): string + { + return Value::as( $this->value() )->asString(); + } + + function intValue(): int + { + return Value::as( $this->value() )->asInt(); + } + + function booleanValue(): bool + { + return Value::as( $this->value() )->asBoolean(); + } + + function toProtobuf(): \Waves\Protobuf\DataTransactionData\DataEntry + { + $pb_DataEntry = new \Waves\Protobuf\DataTransactionData\DataEntry; + $pb_DataEntry->setKey( $this->key() ); + switch( $this->type() ) + { + case EntryType::BINARY: $pb_DataEntry->setBinaryValue( $this->json->get( 'value' )->asBase64Decoded() ); break; + case EntryType::BOOLEAN: $pb_DataEntry->setBoolValue( $this->json->get( 'value' )->asBoolean() ); break; + case EntryType::INTEGER: $pb_DataEntry->setIntValue( $this->json->get( 'value' )->asInt() ); break; + case EntryType::STRING: $pb_DataEntry->setStringValue( $this->json->get( 'value' )->asString() ); break; + case EntryType::DELETE: break; + default: throw new Exception( __FUNCTION__ . ' failed to detect type `' . serialize( $this->type() ) . '`', ExceptionCode::UNKNOWN_TYPE ); // @codeCoverageIgnore + } + return $pb_DataEntry; + } +} diff --git a/src/Model/EntryType.php b/src/Model/EntryType.php new file mode 100644 index 0000000..4320ee2 --- /dev/null +++ b/src/Model/EntryType.php @@ -0,0 +1,12 @@ +json->get( 'height' )->asInt(); } + function balance(): int { return $this->json->get( 'balance' )->asInt(); } +} diff --git a/src/Model/Id.php b/src/Model/Id.php new file mode 100644 index 0000000..4caa091 --- /dev/null +++ b/src/Model/Id.php @@ -0,0 +1,50 @@ +id = Base58String::fromString( $encoded ); + return $id; + } + + static function fromBytes( string $bytes ): Id + { + if( strlen( $bytes ) !== Id::BYTE_LENGTH ) + throw new Exception( __FUNCTION__ . ' bad id length: ' . strlen( $bytes ), ExceptionCode::BAD_ASSET ); + $id = new Id; + $id->id = Base58String::fromBytes( $bytes ); + return $id; + } + + function bytes(): string + { + $bytes = $this->id->bytes(); + if( strlen( $bytes ) !== Id::BYTE_LENGTH ) + throw new Exception( __FUNCTION__ . ' bad id length: ' . strlen( $bytes ), ExceptionCode::BAD_ASSET ); + return $bytes; + } + + function encoded(): string + { + return $this->id->encoded(); + } + + function toString(): string + { + return $this->encoded(); + } +} diff --git a/src/Model/LeaseInfo.php b/src/Model/LeaseInfo.php new file mode 100644 index 0000000..62a839a --- /dev/null +++ b/src/Model/LeaseInfo.php @@ -0,0 +1,29 @@ +json->get( 'id' )->asId(); } + function originTransactionId(): Id { return $this->json->get( 'originTransactionId' )->asId(); } + function sender(): Address { return $this->json->get( 'sender' )->asAddress(); } + function recipient(): Recipient { return $this->json->get( 'recipient' )->asRecipient(); } + function amount(): int { return $this->json->get( 'amount' )->asInt(); } + function height(): int { return $this->json->get( 'height' )->asInt(); } + function status(): int + { + $status = $this->json->getOr( 'status', LeaseStatus::UNKNOWN_S )->asString(); + switch( $status ) + { + case LeaseStatus::ACTIVE_S: return LeaseStatus::ACTIVE; + case LeaseStatus::CANCELED_S: return LeaseStatus::CANCELED; + default: return LeaseStatus::UNKNOWN; + } + } + function cancelHeight(): int { return $this->json->get( 'cancelHeight' )->asInt(); } + function cancelTransactionId(): Id { return $this->json->get( 'cancelTransactionId' )->asId(); } +} diff --git a/src/Model/LeaseStatus.php b/src/Model/LeaseStatus.php new file mode 100644 index 0000000..3a94df2 --- /dev/null +++ b/src/Model/LeaseStatus.php @@ -0,0 +1,13 @@ + '', 'scriptComplexity' => 0 ]; + + function script(): Base64String { return $this->json->get( 'script' )->asBase64String(); } + function complexity(): int { return $this->json->get( 'scriptComplexity' )->asInt(); } +} diff --git a/src/Model/ScriptInfo.php b/src/Model/ScriptInfo.php new file mode 100644 index 0000000..2fd14ad --- /dev/null +++ b/src/Model/ScriptInfo.php @@ -0,0 +1,20 @@ +json->get( 'script' )->asBase64String(); } + function complexity(): int { return $this->json->get( 'complexity' )->asInt(); } + function verifierComplexity(): int { return $this->json->get( 'verifierComplexity' )->asInt(); } + function extraFee(): int { return $this->json->get( 'extraFee' )->asInt(); } + /** + * Gets a map of callable functions with their complexities + * + * @return array + */ + function callableComplexities(): array { return $this->json->get( 'callableComplexities' )->asMapStringInt(); } +} diff --git a/src/Model/ScriptMeta.php b/src/Model/ScriptMeta.php new file mode 100644 index 0000000..8224489 --- /dev/null +++ b/src/Model/ScriptMeta.php @@ -0,0 +1,32 @@ +json->get( 'version' )->asInt(); } + /** + * Gets a map of callable functions with their arguments as ArgMeta + * + * @return array> + */ + function callableFunctions(): array + { + $map = []; + $arrayFuncs = $this->json->get( 'callableFuncTypes' )->asArray(); + foreach( $arrayFuncs as $key => $value ) + { + $function = Value::as( $key )->asString(); + $args = []; + $arrayArgs = Value::as( $value )->asArray(); + foreach( $arrayArgs as $arg ) + $args[] = Value::as( $arg )->asArgMeta(); + $map[$function] = $args; + } + + return $map; + } +} diff --git a/src/Model/Status.php b/src/Model/Status.php new file mode 100644 index 0000000..09ceaed --- /dev/null +++ b/src/Model/Status.php @@ -0,0 +1,15 @@ +json->get( 'height' )->asInt(); } +} diff --git a/src/Model/TransactionStatus.php b/src/Model/TransactionStatus.php new file mode 100644 index 0000000..867d031 --- /dev/null +++ b/src/Model/TransactionStatus.php @@ -0,0 +1,14 @@ +json->get( 'id' )->asId(); } + function status(): int { return $this->json->get( 'status' )->asStatus(); } + function applicationStatus(): int { return $this->json->get( 'applicationStatus' )->asApplicationStatus(); } + function height(): int { return $this->json->getOr( 'height', 0 )->asInt(); } + function confirmations(): int { return $this->json->getOr( 'confirmations', 0 )->asInt(); } +} diff --git a/src/Model/TransactionWithStatus.php b/src/Model/TransactionWithStatus.php new file mode 100644 index 0000000..ca29c3a --- /dev/null +++ b/src/Model/TransactionWithStatus.php @@ -0,0 +1,13 @@ +json->getOr( 'applicationStatus', ApplicationStatus::SUCCEEDED_S )->asApplicationStatus(); + } +} diff --git a/src/Model/Validation.php b/src/Model/Validation.php new file mode 100644 index 0000000..dbf69f2 --- /dev/null +++ b/src/Model/Validation.php @@ -0,0 +1,12 @@ +json->get( 'valid' )->asBoolean(); } + function validationTime(): int { return $this->json->get( 'validationTime' )->asInt(); } + function error(): string { return $this->json->get( 'error' )->asString(); } +} diff --git a/src/Model/Votes.php b/src/Model/Votes.php new file mode 100644 index 0000000..d348278 --- /dev/null +++ b/src/Model/Votes.php @@ -0,0 +1,11 @@ +json->get( 'increase' )->asInt(); } + function decrease(): int { return $this->json->get( 'decrease' )->asInt(); } +} diff --git a/src/Model/WavesConfig.php b/src/Model/WavesConfig.php new file mode 100644 index 0000000..e1db042 --- /dev/null +++ b/src/Model/WavesConfig.php @@ -0,0 +1,17 @@ +amount = $amount; + $this->assetId = $assetId ?? AssetId::WAVES(); + } + + static function of( int $amount, AssetId $assetId = null ): Amount + { + return new Amount( $amount, $assetId ); + } + + static function fromJson( Json $json, string $amountKey = 'amount', string $assetIdKey = ' assetId' ): Amount + { + return Amount::of( $json->get( $amountKey )->asInt(), $json->getOr( $assetIdKey, AssetId::WAVES_STRING )->asAssetId() ); + } + + function value(): int + { + return $this->amount; + } + + function assetId(): AssetId + { + return $this->assetId; + } + + function toString(): string + { + return serialize( $this ); + } + + function toProtobuf(): \Waves\Protobuf\Amount + { + $pb_Amount = new \Waves\Protobuf\Amount; + $pb_Amount->setAmount( $this->value() ); + if( !$this->assetId()->isWaves() ) + $pb_Amount->setAssetId( $this->assetId()->bytes() ); + return $pb_Amount; + } +} diff --git a/src/Transactions/BurnTransaction.php b/src/Transactions/BurnTransaction.php new file mode 100644 index 0000000..4c95415 --- /dev/null +++ b/src/Transactions/BurnTransaction.php @@ -0,0 +1,166 @@ +setBase( $sender, CurrentTransaction::TYPE, CurrentTransaction::LATEST_VERSION, CurrentTransaction::MIN_FEE ); + + // BURN TRANSACTION + { + $tx->setAmount( $amount ); + } + + return $tx; + } + + function getUnsigned(): CurrentTransaction + { + // VERSION + if( $this->version() !== CurrentTransaction::LATEST_VERSION ) + throw new Exception( __FUNCTION__ . ' unexpected version = ' . $this->version(), ExceptionCode::UNEXPECTED ); + + // BASE + $pb_Transaction = $this->getProtobufTransactionBase(); + + // BURN TRANSACTION + { + $pb_TransactionData = new \Waves\Protobuf\BurnTransactionData; + // AMOUNT + { + $pb_TransactionData->setAssetAmount( $this->amount()->toProtobuf() ); + } + } + + // BURN TRANSACTION + $this->setBodyBytes( $pb_Transaction->setBurn( $pb_TransactionData )->serializeToString() ); + return $this; + } + + function amount(): Amount + { + if( !isset( $this->amount ) ) + $this->amount = Amount::fromJson( $this->json, 'quantity' ); + return $this->amount; + } + + function setAmount( Amount $amount ): CurrentTransaction + { + $this->amount = $amount; + $this->json->put( 'quantity', $amount->value() ); + $this->json->put( 'assetId', $amount->assetId()->toJsonValue() ); + return $this; + } + + // COMMON + + function __construct( Json $json = null ) + { + parent::__construct( $json ); + } + + function addProof( PrivateKey $privateKey, int $index = null ): CurrentTransaction + { + $proof = (new \deemru\WavesKit)->sign( $this->bodyBytes(), $privateKey->bytes() ); + if( $proof === false ) + throw new Exception( __FUNCTION__ . ' unexpected sign() error', ExceptionCode::UNEXPECTED ); + $proof = Base58String::fromBytes( $proof )->encoded(); + + $proofs = $this->proofs(); + if( !isset( $index ) ) + $proofs[] = $proof; + else + $proofs[$index] = $proof; + return $this->setProofs( $proofs ); + } + + /** + * @return CurrentTransaction + */ + function setType( int $type ) + { + parent::setType( $type ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setSender( PublicKey $sender ) + { + parent::setSender( $sender ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setVersion( int $version ) + { + parent::setVersion( $version ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setFee( Amount $fee ) + { + parent::setFee( $fee ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setChainId( ChainId $chainId = null ) + { + parent::setChainId( $chainId ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setTimestamp( int $timestamp = null ) + { + parent::setTimestamp( $timestamp ); + return $this; + } + + /** + * @param array $proofs + * @return CurrentTransaction + */ + function setProofs( array $proofs = null ) + { + parent::setProofs( $proofs ); + return $this; + } + + function bodyBytes(): string + { + if( !isset( $this->bodyBytes ) ) + $this->getUnsigned(); + return parent::bodyBytes(); + } +} diff --git a/src/Transactions/CreateAliasTransaction.php b/src/Transactions/CreateAliasTransaction.php new file mode 100644 index 0000000..fde39ab --- /dev/null +++ b/src/Transactions/CreateAliasTransaction.php @@ -0,0 +1,166 @@ +setBase( $sender, CurrentTransaction::TYPE, CurrentTransaction::LATEST_VERSION, CurrentTransaction::MIN_FEE ); + + // ALIAS TRANSACTION + { + $tx->setAlias( $alias ); + } + + return $tx; + } + + function getUnsigned(): CurrentTransaction + { + // VERSION + if( $this->version() !== CurrentTransaction::LATEST_VERSION ) + throw new Exception( __FUNCTION__ . ' unexpected version = ' . $this->version(), ExceptionCode::UNEXPECTED ); + + // BASE + $pb_Transaction = $this->getProtobufTransactionBase(); + + // ALIAS TRANSACTION + { + $pb_TransactionData = new \Waves\Protobuf\CreateAliasTransactionData; + // ID + { + $pb_TransactionData->setAlias( $this->alias()->name() ); + } + } + + // ALIAS TRANSACTION + $this->setBodyBytes( $pb_Transaction->setCreateAlias( $pb_TransactionData )->serializeToString() ); + return $this; + } + + function alias(): Alias + { + if( !isset( $this->alias ) ) + $this->alias = Alias::fromString( $this->json->get( 'alias' )->asString() ); + return $this->alias; + } + + function setAlias( Alias $alias ): CurrentTransaction + { + $this->alias = $alias; + $this->json->put( 'alias', $alias->name() ); + return $this; + } + + // COMMON + + function __construct( Json $json = null ) + { + parent::__construct( $json ); + } + + function addProof( PrivateKey $privateKey, int $index = null ): CurrentTransaction + { + $proof = (new \deemru\WavesKit)->sign( $this->bodyBytes(), $privateKey->bytes() ); + if( $proof === false ) + throw new Exception( __FUNCTION__ . ' unexpected sign() error', ExceptionCode::UNEXPECTED ); + $proof = Base58String::fromBytes( $proof )->encoded(); + + $proofs = $this->proofs(); + if( !isset( $index ) ) + $proofs[] = $proof; + else + $proofs[$index] = $proof; + return $this->setProofs( $proofs ); + } + + /** + * @return CurrentTransaction + */ + function setType( int $type ) + { + parent::setType( $type ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setSender( PublicKey $sender ) + { + parent::setSender( $sender ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setVersion( int $version ) + { + parent::setVersion( $version ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setFee( Amount $fee ) + { + parent::setFee( $fee ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setChainId( ChainId $chainId = null ) + { + parent::setChainId( $chainId ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setTimestamp( int $timestamp = null ) + { + parent::setTimestamp( $timestamp ); + return $this; + } + + /** + * @param array $proofs + * @return CurrentTransaction + */ + function setProofs( array $proofs = null ) + { + parent::setProofs( $proofs ); + return $this; + } + + function bodyBytes(): string + { + if( !isset( $this->bodyBytes ) ) + $this->getUnsigned(); + return parent::bodyBytes(); + } +} diff --git a/src/Transactions/DataTransaction.php b/src/Transactions/DataTransaction.php new file mode 100644 index 0000000..6d11e4e --- /dev/null +++ b/src/Transactions/DataTransaction.php @@ -0,0 +1,196 @@ + + */ + private array $data; + + /** + * @param PublicKey $sender + * @param array $data + * @return CurrentTransaction + */ + static function build( PublicKey $sender, array $data ): CurrentTransaction + { + $tx = new CurrentTransaction; + $tx->setBase( $sender, CurrentTransaction::TYPE, CurrentTransaction::LATEST_VERSION, CurrentTransaction::MIN_FEE ); + + // DATA TRANSACTION + { + $tx->setData( $data ); + } + + // ADDITIONAL FEE CALCULATION + $tx->setFee( Amount::of( CurrentTransaction::calculateFee( strlen( $tx->bodyBytes() ) ) ) ); + + return $tx; + } + + static function calculateFee( int $bodyBytesLen ): int + { + return 100_000 * ( 1 + intdiv( $bodyBytesLen - 1, 1024 ) ); + } + + function getUnsigned(): CurrentTransaction + { + // VERSION + if( $this->version() !== CurrentTransaction::LATEST_VERSION ) + throw new Exception( __FUNCTION__ . ' unexpected version = ' . $this->version(), ExceptionCode::UNEXPECTED ); + + // BASE + $pb_Transaction = $this->getProtobufTransactionBase(); + + // DATA TRANSACTION + { + $pb_TransactionData = new \Waves\Protobuf\DataTransactionData; + // DATA + { + $pb_Data = []; + foreach( $this->data() as $dataEntry ) + $pb_Data[] = $dataEntry->toProtobuf(); + $pb_TransactionData->setData( $pb_Data ); + } + } + + // DATA TRANSACTION + $this->setBodyBytes( $pb_Transaction->setDataTransaction( $pb_TransactionData )->serializeToString() ); + return $this; + } + + /** + * @return array + */ + function data(): array + { + if( !isset( $this->data ) ) + $this->data = $this->json->get( 'data' )->asJson()->asArrayDataEntry(); + return $this->data; + } + + /** + * @param array $data + * @return CurrentTransaction + */ + function setData( array $data ): CurrentTransaction + { + $this->data = $data; + + $data = []; + foreach( $this->data as $dataEntry ) + $data[] = $dataEntry->json()->data(); + $this->json->put( 'data', $data ); + return $this; + } + + // COMMON + + function __construct( Json $json = null ) + { + parent::__construct( $json ); + } + + function addProof( PrivateKey $privateKey, int $index = null ): CurrentTransaction + { + $proof = (new \deemru\WavesKit)->sign( $this->bodyBytes(), $privateKey->bytes() ); + if( $proof === false ) + throw new Exception( __FUNCTION__ . ' unexpected sign() error', ExceptionCode::UNEXPECTED ); + $proof = Base58String::fromBytes( $proof )->encoded(); + + $proofs = $this->proofs(); + if( !isset( $index ) ) + $proofs[] = $proof; + else + $proofs[$index] = $proof; + return $this->setProofs( $proofs ); + } + + /** + * @return CurrentTransaction + */ + function setType( int $type ) + { + parent::setType( $type ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setSender( PublicKey $sender ) + { + parent::setSender( $sender ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setVersion( int $version ) + { + parent::setVersion( $version ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setFee( Amount $fee ) + { + parent::setFee( $fee ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setChainId( ChainId $chainId = null ) + { + parent::setChainId( $chainId ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setTimestamp( int $timestamp = null ) + { + parent::setTimestamp( $timestamp ); + return $this; + } + + /** + * @param array $proofs + * @return CurrentTransaction + */ + function setProofs( array $proofs = null ) + { + parent::setProofs( $proofs ); + return $this; + } + + function bodyBytes(): string + { + if( !isset( $this->bodyBytes ) ) + $this->getUnsigned(); + return parent::bodyBytes(); + } +} diff --git a/src/Transactions/Invocation/Arg.php b/src/Transactions/Invocation/Arg.php new file mode 100644 index 0000000..7888d8c --- /dev/null +++ b/src/Transactions/Invocation/Arg.php @@ -0,0 +1,127 @@ +type = $type; + $arg->value = $value; + return $arg; + } + + static function fromJson( Json $json ): Arg + { + $type = Arg::stringToType( $json->get( 'type' )->asString() ); + if( $type === Arg::LIST ) + { + $args = []; + foreach( $json->get( 'value' )->asArray() as $arg ) + $args[] = Arg::fromJson( Value::as( $arg )->asJson() ); + $value = Value::as( $args ); + } + else + if( $type === Arg::BINARY ) + { + $value = Value::as( $json->get( 'value' )->asBase64Decoded() ); + } + else + { + $value = $json->get( 'value' ); + } + + return Arg::as( $type, $value ); + } + + static function stringToType( string $stringType ): int + { + switch( $stringType ) + { + case 'binary': return Arg::BINARY; + case 'boolean': return Arg::BOOLEAN; + case 'integer': return Arg::INTEGER; + case 'string': return Arg::STRING; + case 'list': return Arg::LIST; + default: throw new Exception( __FUNCTION__ . ' failed to detect type `' . serialize( $stringType ) . '`', ExceptionCode::UNKNOWN_TYPE ); + } + } + + static function typeToString( int $type ): string + { + switch( $type ) + { + case Arg::BINARY: return 'binary'; + case Arg::BOOLEAN: return 'boolean'; + case Arg::INTEGER: return 'integer'; + case Arg::STRING: return 'string'; + case Arg::LIST: return 'list'; + default: throw new Exception( __FUNCTION__ . ' failed to detect type `' . serialize( $type ) . '`', ExceptionCode::UNKNOWN_TYPE ); + } + } + + function type(): int + { + return $this->type; + } + + function typeAsString(): string + { + return Arg::typeToString( $this->type() ); + } + + function value(): Value + { + return $this->value; + } + + /** + * @return mixed + */ + private function valueAsJson() + { + switch( $this->type() ) + { + case Arg::BINARY: return Base64String::fromBytes( $this->value()->asString() )->encodedWithPrefix(); + case Arg::BOOLEAN: return $this->value()->asBoolean(); + case Arg::INTEGER: return $this->value()->asInt(); + case Arg::STRING: return $this->value()->asString(); + case Arg::LIST: + { + $values = []; + foreach( $this->value()->asArray() as $arg ) + { + if( !( $arg instanceof Arg ) ) + throw new Exception( __FUNCTION__ . ' failed to detect Arg class', ExceptionCode::UNEXPECTED ); + $values[] = $arg->toJsonValue(); + } + return $values; + } + default: throw new Exception( __FUNCTION__ . ' failed to detect type `' . serialize( $this->type() ) . '`', ExceptionCode::UNKNOWN_TYPE ); + } + } + + /** + * @return array + */ + function toJsonValue(): array + { + return [ 'type' => $this->typeAsString(), 'value' => $this->valueAsJson() ]; + } +} diff --git a/src/Transactions/Invocation/FunctionCall.php b/src/Transactions/Invocation/FunctionCall.php new file mode 100644 index 0000000..ea5fe6c --- /dev/null +++ b/src/Transactions/Invocation/FunctionCall.php @@ -0,0 +1,128 @@ + + */ + private array $args; + + /** + * @param string|null $name + * @param array|null $args + * @return FunctionCall + */ + static function as( string $name = null, array $args = null ): FunctionCall + { + $func = new FunctionCall; + $func->name = $name ?? FunctionCall::DEFAULT_NAME; + $func->args = $args ?? []; + return $func; + } + + static function fromJson( Json $json ): FunctionCall + { + $name = $json->getOr( 'function', FunctionCall::DEFAULT_NAME )->asString(); + $args = []; + foreach( $json->get( 'args' )->asArray() as $arg ) + $args[] = Arg::fromJson( Value::as( $arg )->asJson() ); + return FunctionCall::as( $name, $args ); + } + + function name(): string + { + return $this->name; + } + + function isDefault(): bool + { + return $this->name == FunctionCall::DEFAULT_NAME; + } + + /** + * @return array + */ + function args(): array + { + return $this->args; + } + + /** + * @return array + */ + function toJsonValue(): array + { + $args = []; + foreach( $this->args() as $arg ) + $args[] = $arg->toJsonValue(); + return + [ + 'function' => $this->name(), + 'args' => $args, + ]; + } + + /** + * @param array $args + * @return string + */ + static function argsBodyBytes( array $args ): string + { + $bytes = pack( 'N', count( $args ) ); + foreach( $args as $arg ) + { + if( !( $arg instanceof Arg ) ) + throw new Exception( __FUNCTION__ . ' failed to detect Arg class', ExceptionCode::UNEXPECTED ); + $value = $arg->value(); + switch( $arg->type() ) + { + case Arg::INTEGER: + $bytes .= chr( 0 ) . pack( 'J', $value->asInt() ); + break; + + case Arg::BINARY: + $value = $value->asString(); + $bytes .= chr( 1 ) . pack( 'N', strlen( $value ) ) . $value; + break; + + case Arg::STRING: + $value = $value->asString(); + $bytes .= chr( 2 ) . pack( 'N', strlen( $value ) ) . $value; + break; + + case Arg::BOOLEAN: + $bytes .= chr( $value->asBoolean() ? 6 : 7 ); + break; + + case Arg::LIST: + $bytes .= chr( 11 ) . FunctionCall::argsBodyBytes( $value->asArray() ); + break; + + default: + throw new Exception( __FUNCTION__ . ' failed to detect type `' . serialize( $arg->type() ) . '`', ExceptionCode::UNKNOWN_TYPE ); + } + } + return $bytes; + } + + function toBodyBytes(): string + { + if( $this->isDefault() ) + return chr( 0 ); + + $bytes = chr( 1 ) . chr( 9 ). chr( 1 ); + $bytes .= pack( 'N', strlen( $this->name() ) ) . $this->name(); + $bytes .= FunctionCall::argsBodyBytes( $this->args() ); + return $bytes; + } +} diff --git a/src/Transactions/InvokeScriptTransaction.php b/src/Transactions/InvokeScriptTransaction.php new file mode 100644 index 0000000..3b35e0b --- /dev/null +++ b/src/Transactions/InvokeScriptTransaction.php @@ -0,0 +1,238 @@ + + */ + private array $payments; + + /** + * @param PublicKey $sender + * @param Recipient $dApp + * @param FunctionCall|null $function + * @param array|null $payments + * @return CurrentTransaction + */ + static function build( PublicKey $sender, Recipient $dApp, FunctionCall $function = null, array $payments = null ): CurrentTransaction + { + $tx = new CurrentTransaction; + $tx->setBase( $sender, CurrentTransaction::TYPE, CurrentTransaction::LATEST_VERSION, CurrentTransaction::MIN_FEE ); + + // INVOKE TRANSACTION + { + $tx->setDApp( $dApp ); + $tx->setFunction( $function ); + $tx->setPayments( $payments ); + } + + return $tx; + } + + function getUnsigned(): CurrentTransaction + { + // VERSION + if( $this->version() !== CurrentTransaction::LATEST_VERSION ) + throw new Exception( __FUNCTION__ . ' unexpected version = ' . $this->version(), ExceptionCode::UNEXPECTED ); + + // BASE + $pb_Transaction = $this->getProtobufTransactionBase(); + + // INVOKE TRANSACTION + { + + $pb_TransactionData = new \Waves\Protobuf\InvokeScriptTransactionData; + // DAPP + { + $pb_TransactionData->setDApp( $this->dApp()->toProtobuf() ); + } + // FUNCTION + { + $pb_TransactionData->setFunctionCall( $this->function()->toBodyBytes() ); + } + // PAYMENTS + { + $pb_Payments = []; + foreach( $this->payments() as $payment ) + $pb_Payments[] = $payment->toProtobuf(); + $pb_TransactionData->setPayments( $pb_Payments ); + } + } + + // INVOKE TRANSACTION + $this->setBodyBytes( $pb_Transaction->setInvokeScript( $pb_TransactionData )->serializeToString() ); + return $this; + } + + function dApp(): Recipient + { + if( !isset( $this->dApp ) ) + $this->dApp = $this->json->get( 'dApp' )->asRecipient(); + return $this->dApp; + } + + function setDApp( Recipient $dApp ): CurrentTransaction + { + $this->dApp = $dApp; + $this->json->put( 'dApp', $dApp->toString() ); + return $this; + } + + function function(): FunctionCall + { + if( !isset( $this->function ) ) + $this->function = FunctionCall::fromJson( $this->json->get( 'call' )->asJson() ); + return $this->function; + } + + function setFunction( FunctionCall $function = null ): CurrentTransaction + { + $function = $function ?? FunctionCall::as(); + $this->function = $function; + $this->json->put( 'call', $function->toJsonValue() ); + return $this; + } + + /** + * @return array + */ + function payments(): array + { + if( !isset( $this->payments ) ) + { + $payments = []; + foreach( $this->json->get( 'payment' )->asArray() as $value ) + $payments[] = Amount::fromJson( Value::as( $value )->asJson() ); + $this->payments = $payments; + } + return $this->payments; + } + + /** + * @param array|null $payments + * @return CurrentTransaction + */ + function setPayments( array $payments = null ): CurrentTransaction + { + $this->payments = $payments ?? []; + + $payments = []; + foreach( $this->payments as $payment ) + $payments[] = [ 'amount' => $payment->value(), 'assetId' => $payment->assetId()->toJsonValue() ]; + $this->json->put( 'payment', $payments ); + return $this; + } + + // COMMON + + function __construct( Json $json = null ) + { + parent::__construct( $json ); + } + + function addProof( PrivateKey $privateKey, int $index = null ): CurrentTransaction + { + $proof = (new \deemru\WavesKit)->sign( $this->bodyBytes(), $privateKey->bytes() ); + if( $proof === false ) + throw new Exception( __FUNCTION__ . ' unexpected sign() error', ExceptionCode::UNEXPECTED ); + $proof = Base58String::fromBytes( $proof )->encoded(); + + $proofs = $this->proofs(); + if( !isset( $index ) ) + $proofs[] = $proof; + else + $proofs[$index] = $proof; + return $this->setProofs( $proofs ); + } + + /** + * @return CurrentTransaction + */ + function setType( int $type ) + { + parent::setType( $type ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setSender( PublicKey $sender ) + { + parent::setSender( $sender ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setVersion( int $version ) + { + parent::setVersion( $version ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setFee( Amount $fee ) + { + parent::setFee( $fee ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setChainId( ChainId $chainId = null ) + { + parent::setChainId( $chainId ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setTimestamp( int $timestamp = null ) + { + parent::setTimestamp( $timestamp ); + return $this; + } + + /** + * @param array $proofs + * @return CurrentTransaction + */ + function setProofs( array $proofs = null ) + { + parent::setProofs( $proofs ); + return $this; + } + + function bodyBytes(): string + { + if( !isset( $this->bodyBytes ) ) + $this->getUnsigned(); + return parent::bodyBytes(); + } +} diff --git a/src/Transactions/IssueTransaction.php b/src/Transactions/IssueTransaction.php new file mode 100644 index 0000000..aa9aaf5 --- /dev/null +++ b/src/Transactions/IssueTransaction.php @@ -0,0 +1,271 @@ +setBase( $sender, CurrentTransaction::TYPE, CurrentTransaction::LATEST_VERSION, $minFee ); + + // ISSUE TRANSACTION + { + $tx->setName( $name ); + $tx->setDescription( $description ); + $tx->setQuantity( $quantity ); + $tx->setDecimals( $decimals ); + $tx->setIsReissuable( $isReissuable ); + $tx->setScript( $script ); + } + + return $tx; + } + + function getUnsigned(): CurrentTransaction + { + // VERSION + if( $this->version() !== CurrentTransaction::LATEST_VERSION ) + throw new Exception( __FUNCTION__ . ' unexpected version = ' . $this->version(), ExceptionCode::UNEXPECTED ); + + // BASE + $pb_Transaction = $this->getProtobufTransactionBase(); + + // ISSUE TRANSACTION + { + $pb_TransactionData = new \Waves\Protobuf\IssueTransactionData; + // NAME + { + $pb_TransactionData->setName( $this->name() ); + } + // DESCRIPTION + { + $pb_TransactionData->setDescription( $this->description() ); + } + // QUANTITY + { + $pb_TransactionData->setAmount( $this->quantity() ); + } + // DECIMALS + { + $pb_TransactionData->setDecimals( $this->decimals() ); + } + // REISSUABLE + { + $pb_TransactionData->setReissuable( $this->isReissuable() ); + } + // SCRIPT + { + $pb_TransactionData->setScript( $this->script()->bytes() ); + } + } + + // ISSUE TRANSACTION + $this->setBodyBytes( $pb_Transaction->setIssue( $pb_TransactionData )->serializeToString() ); + return $this; + } + + function name(): string + { + if( !isset( $this->name ) ) + $this->name = $this->json->get( 'name' )->asString(); + return $this->name; + } + + function setName( string $name ): CurrentTransaction + { + $this->name = $name; + $this->json->put( 'name', $name ); + return $this; + } + + function description(): string + { + if( !isset( $this->description ) ) + $this->description = $this->json->get( 'description' )->asString(); + return $this->description; + } + + function setDescription( string $description ): CurrentTransaction + { + $this->description = $description; + $this->json->put( 'description', $description ); + return $this; + } + + function quantity(): int + { + if( !isset( $this->quantity ) ) + $this->quantity = $this->json->get( 'quantity' )->asInt(); + return $this->quantity; + } + + function setQuantity( int $quantity ): CurrentTransaction + { + $this->quantity = $quantity; + $this->json->put( 'quantity', $quantity ); + return $this; + } + + function decimals(): int + { + if( !isset( $this->decimals ) ) + $this->decimals = $this->json->get( 'decimals' )->asInt(); + return $this->decimals; + } + + function setDecimals( int $decimals ): CurrentTransaction + { + $this->decimals = $decimals; + $this->json->put( 'decimals', $decimals ); + return $this; + } + + function isReissuable(): bool + { + if( !isset( $this->isReissuable ) ) + $this->isReissuable = $this->json->get( 'reissuable' )->asBoolean(); + return $this->isReissuable; + } + + function setIsReissuable( bool $isReissuable ): CurrentTransaction + { + $this->isReissuable = $isReissuable; + $this->json->put( 'reissuable', $isReissuable ); + return $this; + } + + function script(): Base64String + { + if( !isset( $this->script ) ) + $this->script = $this->json->exists( 'script' ) ? $this->json->get( 'script' )->asBase64String() : Base64String::emptyString(); + return $this->script; + } + + function setScript( Base64String $script = null ): CurrentTransaction + { + $script = $script ?? Base64String::emptyString(); + $this->script = $script; + $this->json->put( 'script', $script->toJsonValue() ); + return $this; + } + + // COMMON + + function __construct( Json $json = null ) + { + parent::__construct( $json ); + } + + function addProof( PrivateKey $privateKey, int $index = null ): CurrentTransaction + { + $proof = (new \deemru\WavesKit)->sign( $this->bodyBytes(), $privateKey->bytes() ); + if( $proof === false ) + throw new Exception( __FUNCTION__ . ' unexpected sign() error', ExceptionCode::UNEXPECTED ); + $proof = Base58String::fromBytes( $proof )->encoded(); + + $proofs = $this->proofs(); + if( !isset( $index ) ) + $proofs[] = $proof; + else + $proofs[$index] = $proof; + return $this->setProofs( $proofs ); + } + + /** + * @return CurrentTransaction + */ + function setType( int $type ) + { + parent::setType( $type ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setSender( PublicKey $sender ) + { + parent::setSender( $sender ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setVersion( int $version ) + { + parent::setVersion( $version ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setFee( Amount $fee ) + { + parent::setFee( $fee ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setChainId( ChainId $chainId = null ) + { + parent::setChainId( $chainId ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setTimestamp( int $timestamp = null ) + { + parent::setTimestamp( $timestamp ); + return $this; + } + + /** + * @param array $proofs + * @return CurrentTransaction + */ + function setProofs( array $proofs = null ) + { + parent::setProofs( $proofs ); + return $this; + } + + function bodyBytes(): string + { + if( !isset( $this->bodyBytes ) ) + $this->getUnsigned(); + return parent::bodyBytes(); + } +} diff --git a/src/Transactions/LeaseCancelTransaction.php b/src/Transactions/LeaseCancelTransaction.php new file mode 100644 index 0000000..6e38e0d --- /dev/null +++ b/src/Transactions/LeaseCancelTransaction.php @@ -0,0 +1,166 @@ +setBase( $sender, CurrentTransaction::TYPE, CurrentTransaction::LATEST_VERSION, CurrentTransaction::MIN_FEE ); + + // LEASE_CANCEL TRANSACTION + { + $tx->setLeaseId( $leaseId ); + } + + return $tx; + } + + function getUnsigned(): CurrentTransaction + { + // VERSION + if( $this->version() !== CurrentTransaction::LATEST_VERSION ) + throw new Exception( __FUNCTION__ . ' unexpected version = ' . $this->version(), ExceptionCode::UNEXPECTED ); + + // BASE + $pb_Transaction = $this->getProtobufTransactionBase(); + + // LEASE_CANCEL TRANSACTION + { + $pb_TransactionData = new \Waves\Protobuf\LeaseCancelTransactionData; + // ID + { + $pb_TransactionData->setLeaseId( $this->leaseId()->bytes() ); + } + } + + // LEASE_CANCEL TRANSACTION + $this->setBodyBytes( $pb_Transaction->setLeaseCancel( $pb_TransactionData )->serializeToString() ); + return $this; + } + + function leaseId(): Id + { + if( !isset( $this->leaseId ) ) + $this->leaseId = $this->json->get( 'leaseId' )->asId(); + return $this->leaseId; + } + + function setLeaseId( Id $leaseId ): CurrentTransaction + { + $this->leaseId = $leaseId; + $this->json->put( 'leaseId', $leaseId->toString() ); + return $this; + } + + // COMMON + + function __construct( Json $json = null ) + { + parent::__construct( $json ); + } + + function addProof( PrivateKey $privateKey, int $index = null ): CurrentTransaction + { + $proof = (new \deemru\WavesKit)->sign( $this->bodyBytes(), $privateKey->bytes() ); + if( $proof === false ) + throw new Exception( __FUNCTION__ . ' unexpected sign() error', ExceptionCode::UNEXPECTED ); + $proof = Base58String::fromBytes( $proof )->encoded(); + + $proofs = $this->proofs(); + if( !isset( $index ) ) + $proofs[] = $proof; + else + $proofs[$index] = $proof; + return $this->setProofs( $proofs ); + } + + /** + * @return CurrentTransaction + */ + function setType( int $type ) + { + parent::setType( $type ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setSender( PublicKey $sender ) + { + parent::setSender( $sender ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setVersion( int $version ) + { + parent::setVersion( $version ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setFee( Amount $fee ) + { + parent::setFee( $fee ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setChainId( ChainId $chainId = null ) + { + parent::setChainId( $chainId ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setTimestamp( int $timestamp = null ) + { + parent::setTimestamp( $timestamp ); + return $this; + } + + /** + * @param array $proofs + * @return CurrentTransaction + */ + function setProofs( array $proofs = null ) + { + parent::setProofs( $proofs ); + return $this; + } + + function bodyBytes(): string + { + if( !isset( $this->bodyBytes ) ) + $this->getUnsigned(); + return parent::bodyBytes(); + } +} diff --git a/src/Transactions/LeaseTransaction.php b/src/Transactions/LeaseTransaction.php new file mode 100644 index 0000000..a88f18b --- /dev/null +++ b/src/Transactions/LeaseTransaction.php @@ -0,0 +1,185 @@ +setBase( $sender, CurrentTransaction::TYPE, CurrentTransaction::LATEST_VERSION, CurrentTransaction::MIN_FEE ); + + // LEASE TRANSACTION + { + $tx->setRecipient( $recipient ); + $tx->setAmount( $amount ); + } + + return $tx; + } + + function getUnsigned(): CurrentTransaction + { + // VERSION + if( $this->version() !== CurrentTransaction::LATEST_VERSION ) + throw new Exception( __FUNCTION__ . ' unexpected version = ' . $this->version(), ExceptionCode::UNEXPECTED ); + + // BASE + $pb_Transaction = $this->getProtobufTransactionBase(); + + // LEASE TRANSACTION + { + $pb_TransactionData = new \Waves\Protobuf\LeaseTransactionData; + // RECIPIENT + { + $pb_TransactionData->setRecipient( $this->recipient()->toProtobuf() ); + } + // AMOUNT + { + $pb_TransactionData->setAmount( $this->amount() ); + } + } + + // LEASE TRANSACTION + $this->setBodyBytes( $pb_Transaction->setLease( $pb_TransactionData )->serializeToString() ); + return $this; + } + + function recipient(): Recipient + { + if( !isset( $this->recipient ) ) + $this->recipient = $this->json->get( 'recipient' )->asRecipient(); + return $this->recipient; + } + + function setRecipient( Recipient $recipient ): CurrentTransaction + { + $this->recipient = $recipient; + $this->json->put( 'recipient', $recipient->toString() ); + return $this; + } + + function amount(): int + { + if( !isset( $this->amount ) ) + $this->amount = $this->json->get( 'amount' )->asInt(); + return $this->amount; + } + + function setAmount( int $amount ): CurrentTransaction + { + $this->amount = $amount; + $this->json->put( 'amount', $amount ); + return $this; + } + + // COMMON + + function __construct( Json $json = null ) + { + parent::__construct( $json ); + } + + function addProof( PrivateKey $privateKey, int $index = null ): CurrentTransaction + { + $proof = (new \deemru\WavesKit)->sign( $this->bodyBytes(), $privateKey->bytes() ); + if( $proof === false ) + throw new Exception( __FUNCTION__ . ' unexpected sign() error', ExceptionCode::UNEXPECTED ); + $proof = Base58String::fromBytes( $proof )->encoded(); + + $proofs = $this->proofs(); + if( !isset( $index ) ) + $proofs[] = $proof; + else + $proofs[$index] = $proof; + return $this->setProofs( $proofs ); + } + + /** + * @return CurrentTransaction + */ + function setType( int $type ) + { + parent::setType( $type ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setSender( PublicKey $sender ) + { + parent::setSender( $sender ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setVersion( int $version ) + { + parent::setVersion( $version ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setFee( Amount $fee ) + { + parent::setFee( $fee ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setChainId( ChainId $chainId = null ) + { + parent::setChainId( $chainId ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setTimestamp( int $timestamp = null ) + { + parent::setTimestamp( $timestamp ); + return $this; + } + + /** + * @param array $proofs + * @return CurrentTransaction + */ + function setProofs( array $proofs = null ) + { + parent::setProofs( $proofs ); + return $this; + } + + function bodyBytes(): string + { + if( !isset( $this->bodyBytes ) ) + $this->getUnsigned(); + return parent::bodyBytes(); + } +} diff --git a/src/Transactions/Mass/Transfer.php b/src/Transactions/Mass/Transfer.php new file mode 100644 index 0000000..3a1625b --- /dev/null +++ b/src/Transactions/Mass/Transfer.php @@ -0,0 +1,27 @@ +recipient = $recipient; + $this->amount = $amount; + } + + function recipient(): Recipient + { + return $this->recipient; + } + + function amount(): int + { + return $this->amount; + } +} diff --git a/src/Transactions/MassTransferTransaction.php b/src/Transactions/MassTransferTransaction.php new file mode 100644 index 0000000..8716a8f --- /dev/null +++ b/src/Transactions/MassTransferTransaction.php @@ -0,0 +1,254 @@ + + */ + private array $transfers; + private AssetId $assetId; + private Base58String $attachment; + + /** + * @param PublicKey $sender + * @param AssetId $assetId + * @param array $transfers + * @param Base58String $attachment + * @return CurrentTransaction + */ + static function build( PublicKey $sender, AssetId $assetId, array $transfers, Base58String $attachment = null ): CurrentTransaction + { + $tx = new CurrentTransaction; + $tx->setBase( $sender, CurrentTransaction::TYPE, CurrentTransaction::LATEST_VERSION, CurrentTransaction::calculateFee( count( $transfers ) ) ); + + // MASS_TRANSFER TRANSACTION + { + $tx->setAssetId( $assetId ); + $tx->setTransfers( $transfers ); + $tx->setAttachment( $attachment ); + } + + return $tx; + } + + static function calculateFee( int $transfersCount ): int + { + return 100_000 + ( $transfersCount + ( $transfersCount & 1 ) ) * 50_000; + } + + function getUnsigned(): CurrentTransaction + { + // VERSION + if( $this->version() !== CurrentTransaction::LATEST_VERSION ) + throw new Exception( __FUNCTION__ . ' unexpected version = ' . $this->version(), ExceptionCode::UNEXPECTED ); + + // BASE + $pb_Transaction = $this->getProtobufTransactionBase(); + + // MASS_TRANSFER TRANSACTION + { + $pb_TransactionData = new \Waves\Protobuf\MassTransferTransactionData; + // TRANSFERS + { + $pb_Transfers = []; + foreach( $this->transfers() as $transfer ) + { + $pb_Transfer = new \Waves\Protobuf\MassTransferTransactionData\Transfer; + $pb_Transfer->setRecipient( $transfer->recipient()->toProtobuf() ); + $pb_Transfer->setAmount( $transfer->amount() ); + $pb_Transfers[] = $pb_Transfer; + } + + $pb_TransactionData->setTransfers( $pb_Transfers ); + } + // ASSET + { + $pb_TransactionData->setAssetId( $this->assetId()->bytes() ); + } + // ATTACHMENT + { + $pb_TransactionData->setAttachment( $this->attachment()->bytes() ); + } + } + + // MASS_TRANSFER TRANSACTION + $this->setBodyBytes( $pb_Transaction->setMassTransfer( $pb_TransactionData )->serializeToString() ); + return $this; + } + + function assetId(): AssetId + { + if( !isset( $this->assetId ) ) + $this->assetId = $this->json->get( 'assetId' )->asAssetId(); + return $this->assetId; + } + + function setAssetId( AssetId $assetId ): CurrentTransaction + { + $this->assetId = $assetId; + $this->json->put( 'assetId', $assetId->toJsonValue() ); + return $this; + } + + /** + * @return array + */ + function transfers(): array + { + if( !isset( $this->transfers ) ) + { + $transfers = []; + foreach( $this->json->get( 'amount' )->asArray() as $value ) + { + $json = Value::as( $value )->asJson(); + $recipient = $json->get( 'recipient' )->asRecipient(); + $amount = $json->get( 'amount' )->asInt(); + $transfers[] = new Transfer( $recipient, $amount ); + } + $this->transfers = $transfers; + } + return $this->transfers; + } + + /** + * @param array $transfers + * @return CurrentTransaction + */ + function setTransfers( array $transfers ): CurrentTransaction + { + $this->transfers = $transfers; + + $transfers = []; + foreach( $this->transfers as $transfer ) + $transfers[] = [ 'recipient' => $transfer->recipient()->toString(), 'amount' => $transfer->amount() ]; + $this->json->put( 'transfers', $transfers ); + return $this; + } + + function attachment(): Base58String + { + if( !isset( $this->attachment ) ) + $this->attachment = $this->json->get( 'attachment' )->asBase58String(); + return $this->attachment; + } + + function setAttachment( Base58String $attachment = null ): CurrentTransaction + { + $attachment = $attachment ?? Base58String::emptyString(); + $this->attachment = $attachment; + $this->json->put( 'attachment', $attachment->toString() ); + return $this; + } + + // COMMON + + function __construct( Json $json = null ) + { + parent::__construct( $json ); + } + + function addProof( PrivateKey $privateKey, int $index = null ): CurrentTransaction + { + $proof = (new \deemru\WavesKit)->sign( $this->bodyBytes(), $privateKey->bytes() ); + if( $proof === false ) + throw new Exception( __FUNCTION__ . ' unexpected sign() error', ExceptionCode::UNEXPECTED ); + $proof = Base58String::fromBytes( $proof )->encoded(); + + $proofs = $this->proofs(); + if( !isset( $index ) ) + $proofs[] = $proof; + else + $proofs[$index] = $proof; + return $this->setProofs( $proofs ); + } + + /** + * @return CurrentTransaction + */ + function setType( int $type ) + { + parent::setType( $type ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setSender( PublicKey $sender ) + { + parent::setSender( $sender ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setVersion( int $version ) + { + parent::setVersion( $version ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setFee( Amount $fee ) + { + parent::setFee( $fee ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setChainId( ChainId $chainId = null ) + { + parent::setChainId( $chainId ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setTimestamp( int $timestamp = null ) + { + parent::setTimestamp( $timestamp ); + return $this; + } + + /** + * @param array $proofs + * @return CurrentTransaction + */ + function setProofs( array $proofs = null ) + { + parent::setProofs( $proofs ); + return $this; + } + + function bodyBytes(): string + { + if( !isset( $this->bodyBytes ) ) + $this->getUnsigned(); + return parent::bodyBytes(); + } +} diff --git a/src/Transactions/Recipient.php b/src/Transactions/Recipient.php new file mode 100644 index 0000000..d137ab4 --- /dev/null +++ b/src/Transactions/Recipient.php @@ -0,0 +1,76 @@ +address = $address; + return $recipient; + } + + static function fromAlias( Alias $alias ): Recipient + { + $recipient = new Recipient; + $recipient->alias = $alias; + return $recipient; + } + + static function fromAddressOrAlias( string $addressOrAlias ): Recipient + { + if( strlen( $addressOrAlias ) === Address::STRING_LENGTH ) + return Recipient::fromAddress( Address::fromString( $addressOrAlias ) ); + try + { + return Recipient::fromAlias( Alias::fromFullAlias( $addressOrAlias ) ); + } + catch( Exception $e ) + { + return Recipient::fromAlias( Alias::fromString( $addressOrAlias ) ); + } + } + + function isAlias(): bool + { + return isset( $this->alias ); + } + + function toString(): string + { + if( $this->isAlias() ) + return $this->alias->toString(); + return $this->address->toString(); + } + + function address(): Address + { + return $this->address; + } + + function alias(): Alias + { + return $this->alias; + } + + function toProtobuf(): \Waves\Protobuf\Recipient + { + $pb_Recipient = new \Waves\Protobuf\Recipient; + if( $this->isAlias() ) + $pb_Recipient->setAlias( $this->alias()->name() ); + else + $pb_Recipient->setPublicKeyHash( $this->address()->publicKeyHash() ); + return $pb_Recipient; + } +} diff --git a/src/Transactions/ReissueTransaction.php b/src/Transactions/ReissueTransaction.php new file mode 100644 index 0000000..5b381f2 --- /dev/null +++ b/src/Transactions/ReissueTransaction.php @@ -0,0 +1,186 @@ +setBase( $sender, CurrentTransaction::TYPE, CurrentTransaction::LATEST_VERSION, CurrentTransaction::MIN_FEE ); + + // REISSUE TRANSACTION + { + $tx->setAmount( $amount ); + $tx->setIsReissuable( $isReissuable ); + } + + return $tx; + } + + function getUnsigned(): CurrentTransaction + { + // VERSION + if( $this->version() !== CurrentTransaction::LATEST_VERSION ) + throw new Exception( __FUNCTION__ . ' unexpected version = ' . $this->version(), ExceptionCode::UNEXPECTED ); + + // BASE + $pb_Transaction = $this->getProtobufTransactionBase(); + + // REISSUE TRANSACTION + { + $pb_TransactionData = new \Waves\Protobuf\ReissueTransactionData; + // AMOUNT + { + $pb_TransactionData->setAssetAmount( $this->amount()->toProtobuf() ); + } + // REISSUABLE + { + $pb_TransactionData->setReissuable( $this->isReissuable() ); + } + } + + // REISSUE TRANSACTION + $this->setBodyBytes( $pb_Transaction->setReissue( $pb_TransactionData )->serializeToString() ); + return $this; + } + + function amount(): Amount + { + if( !isset( $this->amount ) ) + $this->amount = Amount::fromJson( $this->json, 'quantity' ); + return $this->amount; + } + + function setAmount( Amount $amount ): CurrentTransaction + { + $this->amount = $amount; + $this->json->put( 'quantity', $amount->value() ); + $this->json->put( 'assetId', $amount->assetId()->toJsonValue() ); + return $this; + } + + function isReissuable(): bool + { + if( !isset( $this->isReissuable ) ) + $this->isReissuable = $this->json->get( 'reissuable' )->asBoolean(); + return $this->isReissuable; + } + + function setIsReissuable( bool $isReissuable ): CurrentTransaction + { + $this->isReissuable = $isReissuable; + $this->json->put( 'reissuable', $isReissuable ); + return $this; + } + + // COMMON + + function __construct( Json $json = null ) + { + parent::__construct( $json ); + } + + function addProof( PrivateKey $privateKey, int $index = null ): CurrentTransaction + { + $proof = (new \deemru\WavesKit)->sign( $this->bodyBytes(), $privateKey->bytes() ); + if( $proof === false ) + throw new Exception( __FUNCTION__ . ' unexpected sign() error', ExceptionCode::UNEXPECTED ); + $proof = Base58String::fromBytes( $proof )->encoded(); + + $proofs = $this->proofs(); + if( !isset( $index ) ) + $proofs[] = $proof; + else + $proofs[$index] = $proof; + return $this->setProofs( $proofs ); + } + + /** + * @return CurrentTransaction + */ + function setType( int $type ) + { + parent::setType( $type ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setSender( PublicKey $sender ) + { + parent::setSender( $sender ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setVersion( int $version ) + { + parent::setVersion( $version ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setFee( Amount $fee ) + { + parent::setFee( $fee ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setChainId( ChainId $chainId = null ) + { + parent::setChainId( $chainId ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setTimestamp( int $timestamp = null ) + { + parent::setTimestamp( $timestamp ); + return $this; + } + + /** + * @param array $proofs + * @return CurrentTransaction + */ + function setProofs( array $proofs = null ) + { + parent::setProofs( $proofs ); + return $this; + } + + function bodyBytes(): string + { + if( !isset( $this->bodyBytes ) ) + $this->getUnsigned(); + return parent::bodyBytes(); + } +} diff --git a/src/Transactions/SetAssetScriptTransaction.php b/src/Transactions/SetAssetScriptTransaction.php new file mode 100644 index 0000000..8cbc245 --- /dev/null +++ b/src/Transactions/SetAssetScriptTransaction.php @@ -0,0 +1,188 @@ +setBase( $sender, CurrentTransaction::TYPE, CurrentTransaction::LATEST_VERSION, CurrentTransaction::MIN_FEE ); + + // SET_ASSET_SCRIPT TRANSACTION + { + $tx->setAssetId( $assetId ); + $tx->setScript( $script ); + } + + return $tx; + } + + function getUnsigned(): CurrentTransaction + { + // VERSION + if( $this->version() !== CurrentTransaction::LATEST_VERSION ) + throw new Exception( __FUNCTION__ . ' unexpected version = ' . $this->version(), ExceptionCode::UNEXPECTED ); + + // BASE + $pb_Transaction = $this->getProtobufTransactionBase(); + + // SET_ASSET_SCRIPT TRANSACTION + { + $pb_TransactionData = new \Waves\Protobuf\SetAssetScriptTransactionData; + // ASSET + { + $pb_TransactionData->setAssetId( $this->assetId()->bytes() ); + } + // SCRIPT + { + $pb_TransactionData->setScript( $this->script()->bytes() ); + } + } + + // SET_ASSET_SCRIPT TRANSACTION + $this->setBodyBytes( $pb_Transaction->setSetAssetScript( $pb_TransactionData )->serializeToString() ); + return $this; + } + + function assetId(): AssetId + { + if( !isset( $this->assetId ) ) + $this->assetId = $this->json->get( 'assetId' )->asAssetId(); + return $this->assetId; + } + + function setAssetId( AssetId $assetId ): CurrentTransaction + { + $this->assetId = $assetId; + $this->json->put( 'assetId', $assetId->toJsonValue() ); + return $this; + } + + function script(): Base64String + { + if( !isset( $this->script ) ) + $this->script = $this->json->exists( 'script' ) ? $this->json->get( 'script' )->asBase64String() : Base64String::emptyString(); + return $this->script; + } + + function setScript( Base64String $script = null ): CurrentTransaction + { + $script = $script ?? Base64String::emptyString(); + $this->script = $script; + $this->json->put( 'script', $script->toJsonValue() ); + return $this; + } + + // COMMON + + function __construct( Json $json = null ) + { + parent::__construct( $json ); + } + + function addProof( PrivateKey $privateKey, int $index = null ): CurrentTransaction + { + $proof = (new \deemru\WavesKit)->sign( $this->bodyBytes(), $privateKey->bytes() ); + if( $proof === false ) + throw new Exception( __FUNCTION__ . ' unexpected sign() error', ExceptionCode::UNEXPECTED ); + $proof = Base58String::fromBytes( $proof )->encoded(); + + $proofs = $this->proofs(); + if( !isset( $index ) ) + $proofs[] = $proof; + else + $proofs[$index] = $proof; + return $this->setProofs( $proofs ); + } + + /** + * @return CurrentTransaction + */ + function setType( int $type ) + { + parent::setType( $type ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setSender( PublicKey $sender ) + { + parent::setSender( $sender ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setVersion( int $version ) + { + parent::setVersion( $version ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setFee( Amount $fee ) + { + parent::setFee( $fee ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setChainId( ChainId $chainId = null ) + { + parent::setChainId( $chainId ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setTimestamp( int $timestamp = null ) + { + parent::setTimestamp( $timestamp ); + return $this; + } + + /** + * @param array $proofs + * @return CurrentTransaction + */ + function setProofs( array $proofs = null ) + { + parent::setProofs( $proofs ); + return $this; + } + + function bodyBytes(): string + { + if( !isset( $this->bodyBytes ) ) + $this->getUnsigned(); + return parent::bodyBytes(); + } +} diff --git a/src/Transactions/SetScriptTransaction.php b/src/Transactions/SetScriptTransaction.php new file mode 100644 index 0000000..814479b --- /dev/null +++ b/src/Transactions/SetScriptTransaction.php @@ -0,0 +1,175 @@ +setBase( $sender, CurrentTransaction::TYPE, CurrentTransaction::LATEST_VERSION, CurrentTransaction::MIN_FEE ); + + // SET_SCRIPT TRANSACTION + { + $tx->setScript( $script ); + } + + // ADDITIONAL FEE CALCULATION + $tx->setFee( Amount::of( CurrentTransaction::calculateFee( strlen( $tx->bodyBytes() ) ) ) ); + + return $tx; + } + + static function calculateFee( int $bodyBytesLen ): int + { + return 100_000 * ( 1 + intdiv( $bodyBytesLen - 1, 1024 ) ); + } + + function getUnsigned(): CurrentTransaction + { + // VERSION + if( $this->version() !== CurrentTransaction::LATEST_VERSION ) + throw new Exception( __FUNCTION__ . ' unexpected version = ' . $this->version(), ExceptionCode::UNEXPECTED ); + + // BASE + $pb_Transaction = $this->getProtobufTransactionBase(); + + // SET_SCRIPT TRANSACTION + { + $pb_TransactionData = new \Waves\Protobuf\SetScriptTransactionData; + // SCRIPT + { + $pb_TransactionData->setScript( $this->script()->bytes() ); + } + } + + // SET_SCRIPT TRANSACTION + $this->setBodyBytes( $pb_Transaction->setSetScript( $pb_TransactionData )->serializeToString() ); + return $this; + } + + function script(): Base64String + { + if( !isset( $this->script ) ) + $this->script = $this->json->exists( 'script' ) ? $this->json->get( 'script' )->asBase64String() : Base64String::emptyString(); + return $this->script; + } + + function setScript( Base64String $script = null ): CurrentTransaction + { + $script = $script ?? Base64String::emptyString(); + $this->script = $script; + $this->json->put( 'script', $script->toJsonValue() ); + return $this; + } + + // COMMON + + function __construct( Json $json = null ) + { + parent::__construct( $json ); + } + + function addProof( PrivateKey $privateKey, int $index = null ): CurrentTransaction + { + $proof = (new \deemru\WavesKit)->sign( $this->bodyBytes(), $privateKey->bytes() ); + if( $proof === false ) + throw new Exception( __FUNCTION__ . ' unexpected sign() error', ExceptionCode::UNEXPECTED ); + $proof = Base58String::fromBytes( $proof )->encoded(); + + $proofs = $this->proofs(); + if( !isset( $index ) ) + $proofs[] = $proof; + else + $proofs[$index] = $proof; + return $this->setProofs( $proofs ); + } + + /** + * @return CurrentTransaction + */ + function setType( int $type ) + { + parent::setType( $type ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setSender( PublicKey $sender ) + { + parent::setSender( $sender ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setVersion( int $version ) + { + parent::setVersion( $version ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setFee( Amount $fee ) + { + parent::setFee( $fee ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setChainId( ChainId $chainId = null ) + { + parent::setChainId( $chainId ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setTimestamp( int $timestamp = null ) + { + parent::setTimestamp( $timestamp ); + return $this; + } + + /** + * @param array $proofs + * @return CurrentTransaction + */ + function setProofs( array $proofs = null ) + { + parent::setProofs( $proofs ); + return $this; + } + + function bodyBytes(): string + { + if( !isset( $this->bodyBytes ) ) + $this->getUnsigned(); + return parent::bodyBytes(); + } +} diff --git a/src/Transactions/SponsorFeeTransaction.php b/src/Transactions/SponsorFeeTransaction.php new file mode 100644 index 0000000..8f3d768 --- /dev/null +++ b/src/Transactions/SponsorFeeTransaction.php @@ -0,0 +1,182 @@ +setBase( $sender, CurrentTransaction::TYPE, CurrentTransaction::LATEST_VERSION, CurrentTransaction::MIN_FEE ); + + // SPONSORSHIP TRANSACTION + { + $tx->setAssetId( $assetId ); + $tx->setMinSponsoredFee( $minSponsoredFee ); + } + + return $tx; + } + + function getUnsigned(): CurrentTransaction + { + // VERSION + if( $this->version() !== CurrentTransaction::LATEST_VERSION ) + throw new Exception( __FUNCTION__ . ' unexpected version = ' . $this->version(), ExceptionCode::UNEXPECTED ); + + // BASE + $pb_Transaction = $this->getProtobufTransactionBase(); + + // SPONSORSHIP TRANSACTION + { + $pb_TransactionData = new \Waves\Protobuf\SponsorFeeTransactionData; + // MINFEE + { + $pb_TransactionData->setMinFee( Amount::of( $this->minSponsoredFee(), $this->assetId() )->toProtobuf() ); + } + } + + // SPONSORSHIP TRANSACTION + $this->setBodyBytes( $pb_Transaction->setSponsorFee( $pb_TransactionData )->serializeToString() ); + return $this; + } + + function assetId(): AssetId + { + if( !isset( $this->assetId ) ) + $this->assetId = $this->json->get( 'assetId' )->asAssetId(); + return $this->assetId; + } + + function setAssetId( AssetId $assetId ): CurrentTransaction + { + $this->assetId = $assetId; + $this->json->put( 'assetId', $assetId->toJsonValue() ); + return $this; + } + + function minSponsoredFee(): int + { + if( !isset( $this->minSponsoredFee ) ) + $this->minSponsoredFee = $this->json->get( 'minSponsoredAssetFee' )->asInt(); + return $this->minSponsoredFee; + } + + function setMinSponsoredFee( int $minSponsoredFee ): CurrentTransaction + { + $this->minSponsoredFee = $minSponsoredFee; + $this->json->put( 'minSponsoredAssetFee', $minSponsoredFee ); + return $this; + } + + // COMMON + + function __construct( Json $json = null ) + { + parent::__construct( $json ); + } + + function addProof( PrivateKey $privateKey, int $index = null ): CurrentTransaction + { + $proof = (new \deemru\WavesKit)->sign( $this->bodyBytes(), $privateKey->bytes() ); + if( $proof === false ) + throw new Exception( __FUNCTION__ . ' unexpected sign() error', ExceptionCode::UNEXPECTED ); + $proof = Base58String::fromBytes( $proof )->encoded(); + + $proofs = $this->proofs(); + if( !isset( $index ) ) + $proofs[] = $proof; + else + $proofs[$index] = $proof; + return $this->setProofs( $proofs ); + } + + /** + * @return CurrentTransaction + */ + function setType( int $type ) + { + parent::setType( $type ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setSender( PublicKey $sender ) + { + parent::setSender( $sender ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setVersion( int $version ) + { + parent::setVersion( $version ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setFee( Amount $fee ) + { + parent::setFee( $fee ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setChainId( ChainId $chainId = null ) + { + parent::setChainId( $chainId ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setTimestamp( int $timestamp = null ) + { + parent::setTimestamp( $timestamp ); + return $this; + } + + /** + * @param array $proofs + * @return CurrentTransaction + */ + function setProofs( array $proofs = null ) + { + parent::setProofs( $proofs ); + return $this; + } + + function bodyBytes(): string + { + if( !isset( $this->bodyBytes ) ) + $this->getUnsigned(); + return parent::bodyBytes(); + } +} diff --git a/src/Transactions/Transaction.php b/src/Transactions/Transaction.php new file mode 100644 index 0000000..8970acb --- /dev/null +++ b/src/Transactions/Transaction.php @@ -0,0 +1,51 @@ +type ) ) + $this->type = $this->json->get( 'type' )->asInt(); + return $this->type; + } + + /** + * @return mixed + */ + function setType( int $type ) + { + $this->type = $type; + $this->json->put( 'type', $type ); + return $this; + } + + protected function setBase( PublicKey $sender, int $type, int $version, int $minFee ): void + { + $this->setSender( $sender ); + $this->setType( $type ); + $this->setVersion( $version ); + $this->setFee( Amount::of( $minFee ) ); + + $this->setChainId(); + $this->setTimestamp(); + $this->setProofs(); + } + + function getProtobufTransactionBase(): \Waves\Protobuf\Transaction + { + $pb_Transaction = new \Waves\Protobuf\Transaction(); + $pb_Transaction->setSenderPublicKey( $this->sender()->bytes() ); + $pb_Transaction->setVersion( $this->version() ); + $pb_Transaction->setFee( $this->fee()->toProtobuf() ); + $pb_Transaction->setChainId( $this->chainId()->asInt() ); + $pb_Transaction->setTimestamp( $this->timestamp() ); + + return $pb_Transaction; + } +} diff --git a/src/Transactions/TransactionOrOrder.php b/src/Transactions/TransactionOrOrder.php new file mode 100644 index 0000000..da629e4 --- /dev/null +++ b/src/Transactions/TransactionOrOrder.php @@ -0,0 +1,178 @@ + + */ + private array $proofs; + private string $bodyBytes; + + function id(): Id + { + if( !isset( $this->id ) ) + { + if( !$this->json()->exists( 'id' ) && isset( $this->bodyBytes ) ) + $this->setId( Functions::calculateTransactionId( $this->bodyBytes ) ); + else + $this->id = $this->json->get( 'id' )->asId(); + } + return $this->id; + } + + function setId( Id $id ): void + { + $this->id = $id; + $this->json->put( 'id', $id->toString() ); + } + + function version(): int + { + if( !isset( $this->version ) ) + $this->version = $this->json->get( 'version' )->asInt(); + return $this->version; + } + + /** + * @return mixed + */ + function setVersion( int $version ) + { + $this->version = $version; + $this->json->put( 'version', $version ); + return $this; + } + + function chainId(): ChainId + { + if( !isset( $this->chainId ) ) + { + if( $this->json->exists( 'chainId' ) ) + $this->chainId = $this->json->get( 'chainId' )->asChainId(); + else if( $this->json->exists( 'sender' ) ) + $this->chainId = $this->json->get( 'sender' )->asAddress()->chainId(); + else + $this->chainId = WavesConfig::chainId(); + } + return $this->chainId; + } + + /** + * @return mixed + */ + function setChainId( ChainId $chainId = null ) + { + if( !isset( $chainId ) ) + $chainId = WavesConfig::chainId(); + $this->chainId = $chainId; + $this->json->put( 'chainId', $chainId->asInt() ); + return $this; + } + + function sender(): PublicKey + { + if( !isset( $this->sender ) ) + { + $this->sender = $this->json->get( 'senderPublicKey' )->asPublicKey(); + if( $this->json->exists( 'sender' ) ) + $this->sender->attachAddress( $this->json->get( 'sender' )->asAddress() ); + } + return $this->sender; + } + + /** + * @return mixed + */ + function setSender( PublicKey $sender ) + { + $this->sender = $sender; + $this->json->put( 'senderPublicKey', $sender->toString() ); + $this->json->put( 'sender', $sender->address()->toString() ); + return $this; + } + + function timestamp(): int + { + if( !isset( $this->timestamp ) ) + $this->timestamp = $this->json->get( 'timestamp' )->asInt(); + return $this->timestamp; + } + + /** + * @return mixed + */ + function setTimestamp( int $timestamp = null ) + { + if( !isset( $timestamp ) ) + $timestamp = intval( microtime( true ) * 1000 ); + $this->timestamp = $timestamp; + $this->json->put( 'timestamp', $timestamp ); + return $this; + } + + function fee(): Amount + { + if( !isset( $this->fee ) ) + $this->fee = Amount::fromJson( $this->json, 'fee', 'feeAssetId' ); + return $this->fee; + } + + /** + * @return mixed + */ + function setFee( Amount $fee ) + { + $this->fee = $fee; + $this->json->put( 'fee', $fee->value() ); + $this->json->put( 'feeAssetId', $fee->assetId()->toJsonValue() ); + return $this; + } + + /** + * @return array + */ + function proofs(): array + { + if( !isset( $this->proofs ) ) + $this->proofs = $this->json->getOr( 'proofs', [] )->asArrayString(); + return $this->proofs; + } + + /** + * @param array $proofs + * @return mixed + */ + function setProofs( array $proofs = null ) + { + if( !isset( $proofs ) ) + $proofs = []; + $this->proofs = $proofs; + $this->json->put( 'proofs', $proofs ); + return $this; + } + + function bodyBytes(): string + { + return $this->bodyBytes; + } + + protected function setBodyBytes( string $bodyBytes ): void + { + $this->bodyBytes = $bodyBytes; + } +} diff --git a/src/Transactions/TransferTransaction.php b/src/Transactions/TransferTransaction.php new file mode 100644 index 0000000..dfbfea1 --- /dev/null +++ b/src/Transactions/TransferTransaction.php @@ -0,0 +1,207 @@ +setBase( $sender, CurrentTransaction::TYPE, CurrentTransaction::LATEST_VERSION, CurrentTransaction::MIN_FEE ); + + // TRANSFER TRANSACTION + { + $tx->setRecipient( $recipient ); + $tx->setAmount( $amount ); + $tx->setAttachment( $attachment ); + } + + return $tx; + } + + function getUnsigned(): CurrentTransaction + { + // VERSION + if( $this->version() !== CurrentTransaction::LATEST_VERSION ) + throw new Exception( __FUNCTION__ . ' unexpected version = ' . $this->version(), ExceptionCode::UNEXPECTED ); + + // BASE + $pb_Transaction = $this->getProtobufTransactionBase(); + + // TRANSFER TRANSACTION + { + $pb_TransactionData = new \Waves\Protobuf\TransferTransactionData; + // RECIPIENT + { + $pb_TransactionData->setRecipient( $this->recipient()->toProtobuf() ); + } + // AMOUNT + { + $pb_TransactionData->setAmount( $this->amount()->toProtobuf() ); + } + // ATTACHMENT + { + $pb_TransactionData->setAttachment( $this->attachment()->bytes() ); + } + } + + // TRANSFER TRANSACTION + $this->setBodyBytes( $pb_Transaction->setTransfer( $pb_TransactionData )->serializeToString() ); + return $this; + } + + function recipient(): Recipient + { + if( !isset( $this->recipient ) ) + $this->recipient = $this->json->get( 'recipient' )->asRecipient(); + return $this->recipient; + } + + function setRecipient( Recipient $recipient ): CurrentTransaction + { + $this->recipient = $recipient; + $this->json->put( 'recipient', $recipient->toString() ); + return $this; + } + + function amount(): Amount + { + if( !isset( $this->amount ) ) + $this->amount = Amount::fromJson( $this->json ); + return $this->amount; + } + + function setAmount( Amount $amount ): CurrentTransaction + { + $this->amount = $amount; + $this->json->put( 'amount', $amount->value() ); + $this->json->put( 'assetId', $amount->assetId()->toJsonValue() ); + return $this; + } + + function attachment(): Base58String + { + if( !isset( $this->attachment ) ) + $this->attachment = $this->json->get( 'attachment' )->asBase58String(); + return $this->attachment; + } + + function setAttachment( Base58String $attachment = null ): CurrentTransaction + { + $attachment = $attachment ?? Base58String::emptyString(); + $this->attachment = $attachment; + $this->json->put( 'attachment', $attachment->toString() ); + return $this; + } + + // COMMON + + function __construct( Json $json = null ) + { + parent::__construct( $json ); + } + + function addProof( PrivateKey $privateKey, int $index = null ): CurrentTransaction + { + $proof = (new \deemru\WavesKit)->sign( $this->bodyBytes(), $privateKey->bytes() ); + if( $proof === false ) + throw new Exception( __FUNCTION__ . ' unexpected sign() error', ExceptionCode::UNEXPECTED ); + $proof = Base58String::fromBytes( $proof )->encoded(); + + $proofs = $this->proofs(); + if( !isset( $index ) ) + $proofs[] = $proof; + else + $proofs[$index] = $proof; + return $this->setProofs( $proofs ); + } + + /** + * @return CurrentTransaction + */ + function setType( int $type ) + { + parent::setType( $type ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setSender( PublicKey $sender ) + { + parent::setSender( $sender ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setVersion( int $version ) + { + parent::setVersion( $version ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setFee( Amount $fee ) + { + parent::setFee( $fee ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setChainId( ChainId $chainId = null ) + { + parent::setChainId( $chainId ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setTimestamp( int $timestamp = null ) + { + parent::setTimestamp( $timestamp ); + return $this; + } + + /** + * @param array $proofs + * @return CurrentTransaction + */ + function setProofs( array $proofs = null ) + { + parent::setProofs( $proofs ); + return $this; + } + + function bodyBytes(): string + { + if( !isset( $this->bodyBytes ) ) + $this->getUnsigned(); + return parent::bodyBytes(); + } +} diff --git a/src/Transactions/UpdateAssetInfoTransaction.php b/src/Transactions/UpdateAssetInfoTransaction.php new file mode 100644 index 0000000..d659900 --- /dev/null +++ b/src/Transactions/UpdateAssetInfoTransaction.php @@ -0,0 +1,206 @@ +setBase( $sender, CurrentTransaction::TYPE, CurrentTransaction::LATEST_VERSION, CurrentTransaction::MIN_FEE ); + + // RENAME TRANSACTION + { + $tx->setAssetId( $assetId ); + $tx->setName( $name ); + $tx->setDescription( $description ); + } + + return $tx; + } + + function getUnsigned(): CurrentTransaction + { + // VERSION + if( $this->version() !== CurrentTransaction::LATEST_VERSION ) + throw new Exception( __FUNCTION__ . ' unexpected version = ' . $this->version(), ExceptionCode::UNEXPECTED ); + + // BASE + $pb_Transaction = $this->getProtobufTransactionBase(); + + // RENAME TRANSACTION + { + $pb_TransactionData = new \Waves\Protobuf\UpdateAssetInfoTransactionData; + // ASSET + { + $pb_TransactionData->setAssetId( $this->assetId()->bytes() ); + } + // NAME + { + $pb_TransactionData->setName( $this->name() ); + } + // DESCRIPTION + { + $pb_TransactionData->setDescription( $this->description() ); + } + } + + // RENAME TRANSACTION + $this->setBodyBytes( $pb_Transaction->setUpdateAssetInfo( $pb_TransactionData )->serializeToString() ); + return $this; + } + + function assetId(): AssetId + { + if( !isset( $this->assetId ) ) + $this->assetId = $this->json->get( 'assetId' )->asAssetId(); + return $this->assetId; + } + + function setAssetId( AssetId $assetId ): CurrentTransaction + { + $this->assetId = $assetId; + $this->json->put( 'assetId', $assetId->toJsonValue() ); + return $this; + } + + function name(): string + { + if( !isset( $this->name ) ) + $this->name = $this->json->get( 'name' )->asString(); + return $this->name; + } + + function setName( string $name ): CurrentTransaction + { + $this->name = $name; + $this->json->put( 'name', $name ); + return $this; + } + + function description(): string + { + if( !isset( $this->description ) ) + $this->description = $this->json->get( 'description' )->asString(); + return $this->description; + } + + function setDescription( string $description ): CurrentTransaction + { + $this->description = $description; + $this->json->put( 'description', $description ); + return $this; + } + + // COMMON + + function __construct( Json $json = null ) + { + parent::__construct( $json ); + } + + function addProof( PrivateKey $privateKey, int $index = null ): CurrentTransaction + { + $proof = (new \deemru\WavesKit)->sign( $this->bodyBytes(), $privateKey->bytes() ); + if( $proof === false ) + throw new Exception( __FUNCTION__ . ' unexpected sign() error', ExceptionCode::UNEXPECTED ); + $proof = Base58String::fromBytes( $proof )->encoded(); + + $proofs = $this->proofs(); + if( !isset( $index ) ) + $proofs[] = $proof; + else + $proofs[$index] = $proof; + return $this->setProofs( $proofs ); + } + + /** + * @return CurrentTransaction + */ + function setType( int $type ) + { + parent::setType( $type ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setSender( PublicKey $sender ) + { + parent::setSender( $sender ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setVersion( int $version ) + { + parent::setVersion( $version ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setFee( Amount $fee ) + { + parent::setFee( $fee ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setChainId( ChainId $chainId = null ) + { + parent::setChainId( $chainId ); + return $this; + } + + /** + * @return CurrentTransaction + */ + function setTimestamp( int $timestamp = null ) + { + parent::setTimestamp( $timestamp ); + return $this; + } + + /** + * @param array $proofs + * @return CurrentTransaction + */ + function setProofs( array $proofs = null ) + { + parent::setProofs( $proofs ); + return $this; + } + + function bodyBytes(): string + { + if( !isset( $this->bodyBytes ) ) + $this->getUnsigned(); + return parent::bodyBytes(); + } +} diff --git a/src/Util/Functions.php b/src/Util/Functions.php new file mode 100644 index 0000000..640c72e --- /dev/null +++ b/src/Util/Functions.php @@ -0,0 +1,44 @@ +decode( $string ); + if( $decoded === false ) + throw new Exception( __FUNCTION__ . ' failed to decode string: ' . $string, ExceptionCode::BASE58_DECODE ); + return $decoded; + } + + /** + * Encodes binary data to base58 string + * + * @param string $bytes + * @return string + */ + static function base58Encode( string $bytes ): string + { + $encoded = \deemru\ABCode::base58()->encode( $bytes ); + if( $encoded === false ) + // Unreachable for binary encodings + throw new Exception( __FUNCTION__ . ' failed to encode bytes: ' . bin2hex( $bytes ), ExceptionCode::BASE58_ENCODE ); // @codeCoverageIgnore + return $encoded; + } + + static function calculateTransactionId( string $bodyBytes ): Id + { + return Id::fromBytes( (new \deemru\WavesKit)->blake2b256( $bodyBytes ) ); + } +} diff --git a/tests/NodeTest.php b/tests/NodeTest.php new file mode 100644 index 0000000..0e6f250 --- /dev/null +++ b/tests/NodeTest.php @@ -0,0 +1,430 @@ +fail( 'Failed to catch exception with code:' . $code ); + } + catch( Exception $e ) + { + $this->assertEquals( $code, $e->getCode(), $e->getMessage() ); + } + } + + function testNode(): void + { + $nodeW = Node::MAINNET(); + $nodeT = Node::TESTNET(); + $nodeS = Node::STAGENET(); + + $version = $nodeS->getVersion(); + $nodeS->waitBlocks( 0 ); + + $ethAsset = $nodeS->ethToWavesAsset( '0x7a087b3384447a48393eda243e630b07db443597' ); + $this->assertEquals( '9DNEvLFSSnSSaNCb5WEYMz64hsadDjx1THZw3z2hiyJe', $ethAsset ); + + $someScript = file_get_contents( 'https://raw.githubusercontent.com/waves-exchange/neutrino-defo-contract/df334ea97952692983d1038a4818626ee01bfea6/factory.ride' ); + if( $someScript === false ) + { + $this->assertNotEquals( $someScript, false ); + return; + } + + $scriptInfo = $nodeW->compileScript( $someScript ); + $script1 = $scriptInfo->script(); + $scriptInfo = $nodeW->compileScript( $someScript, true ); + $script2 = $scriptInfo->script(); + $this->assertNotEquals( $script1, $script2 ); + $this->assertLessThan( strlen( $script1->bytes() ), strlen( $script2->bytes() ) ); + + $address = Address::fromString( '3P5dg6PtSAQmdH1qCGKJWu7bkzRG27mny5i' ); + $historyBalances = $nodeW->getBalanceHistory( $address ); + foreach( $historyBalances as $historyBalance ) + { + $historyBalance->height(); + $historyBalance->balance(); + } + + $txs = $nodeW->getTransactionsByAddress( $address, 2 ); + foreach( $txs as $tx ) + { + $status = $nodeW->getTransactionStatus( $tx->id() ); + $status->status(); + $status->confirmations(); + $this->assertSame( $status->id()->toString(), $tx->id()->toString() ); + $this->assertSame( $status->applicationStatus(), $tx->applicationStatus() ); + $this->assertSame( $status->height(), $tx->height() ); + } + + if( isset( $txs[1] ) ) + { + $tx = $txs[1]; + $txs2 = $nodeW->getTransactionsByAddress( $address, 2, $tx->id() ); + foreach( $txs2 as $tx2 ) + $this->assertNotEquals( $tx->id()->toString(), $tx2->id()->toString() ); + + $statuses = $nodeW->getTransactionsStatus( [ $tx->id() ] ); + foreach( $statuses as $status ) + $this->assertSame( $status->id()->toString(), $tx->id()->toString() ); + } + + $doUtx = 0; + for( ; $doUtx; ) + { + $utxSize = $nodeW->getUtxSize(); + if( $utxSize > 0 ) + { + $txs = $nodeW->getUnconfirmedTransactions(); + if( isset( $txs[0] ) ) + { + $tx = $txs[0]; + try + { + $txUnconfirmed = $nodeW->getUnconfirmedTransaction( $tx->id() ); + $this->assertSame( $tx->id()->toString(), $txUnconfirmed->id()->toString() ); + } + catch( Exception $e ) + { + $this->assertEquals( ExceptionCode::FETCH_URI, $e->getCode(), $e->getMessage() ); + } + } + + break; + } + + sleep( 1 ); + } + + + $leases = $nodeW->getActiveLeases( $address ); + foreach( $leases as $lease ) + { + $lease->amount(); + $lease->height(); + $lease->id(); + $lease->originTransactionId(); + $lease->recipient(); + $lease->sender(); + if( $lease->status() == LeaseStatus::CANCELED ) + { + $lease->cancelHeight(); + $lease->cancelTransactionId(); + } + } + + if( isset( $lease ) ) + { + $this->assertSame( $lease->toString(), $nodeW->getLeaseInfo( $lease->id() )->toString() ); + $this->assertSame( $lease->toString(), $nodeW->getLeasesInfo( [ $lease->id() ] )[0]->toString() ); + } + + $leaseId = Id::fromString( '45uZvPeDva4CyXXTsTkh7fhzqTJCe2eqnz1HFt4aYNdZ' ); + $lease = $nodeW->getLeaseInfo( $leaseId ); + if( $lease->status() == LeaseStatus::CANCELED ) + { + $lease->cancelHeight(); + $lease->cancelTransactionId(); + $transactionInfo = $nodeW->getTransactionInfo( $lease->cancelTransactionId() ); + $this->assertSame( $lease->cancelHeight(), $transactionInfo->height() ); + } + + $heightT = $nodeT->getHeight(); + $heightW = $nodeW->getHeight(); + + $block = $nodeW->getGenesisBlock(); + $block = $nodeW->getLastBlock(); + $blocks = $nodeW->getBlocksGeneratedBy( $block->generator(), $heightW - 10, $heightW ); + + $blocks = $nodeW->getBlocks( $heightW - 4, $heightW ); + $this->assertSame( 5, count( $blocks ) ); + foreach( $blocks as $block ) + foreach( $block->transactions() as $tx ) + { + $tx->applicationStatus(); + $tx->chainId(); + $tx->fee(); + $tx->id(); + $tx->proofs(); + $tx->sender(); + $tx->timestamp(); + $tx->type(); + $tx->version(); + } + + $block1 = $nodeT->getBlockByHeight( 2126170 ); + $block2 = $nodeT->getBlockById( $block1->id() ); + $this->assertSame( $block1->toString(), $block2->toString() ); + $block1->fee(); + $txs = $block1->transactions(); + foreach( $txs as $tx ) + { + $tx->applicationStatus(); + $tx->chainId(); + $tx->fee(); + $tx->id(); + $tx->proofs(); + $tx->sender(); + $tx->timestamp(); + $tx->type(); + $tx->version(); + + if( !isset( $validation ) ) + { + $validation = $nodeT->validateTransaction( $tx ); + $validation->isValid(); + $validation->validationTime(); + $validation->error(); + } + } + + $blockchainRewards1 = $nodeT->getBlockchainRewards(); + $blockchainRewards2 = $nodeT->getBlockchainRewards( 1600000 ); + $this->assertNotEquals( $blockchainRewards1->toString(), $blockchainRewards2->toString() ); + $blockchainRewards1->currentReward(); + $blockchainRewards1->height(); + $blockchainRewards1->minIncrement(); + $blockchainRewards1->nextCheck(); + $blockchainRewards1->term(); + $blockchainRewards1->totalWavesAmount(); + $blockchainRewards1->votes()->increase(); + $blockchainRewards1->votes()->decrease(); + $blockchainRewards1->votingInterval(); + $blockchainRewards1->votingIntervalStart(); + $blockchainRewards1->votingThreshold(); + + $addressT = Address::fromString( '3N4q2D5bh5sAL3b4PighYyKw2WshKCiFD4F' ); + $nfts1 = $nodeT->getNft( $addressT, 10 ); + $nfts2 = $nodeT->getNft( $addressT, 10, $nfts1[0]->assetId() ); + $this->assertSame( $nfts1[1]->toString(), $nfts2[0]->toString() ); + + $addressT = Address::fromString( '3N9WtaPoD1tMrDZRG26wA142Byd35tLhnLU' ); + $assetId = AssetId::WAVES(); + + $assetIds = []; + $balances = $nodeT->getAssetsBalance( $addressT ); + $max = 10; + foreach( $balances as $balance ) + { + $assetId = $balance->assetId(); + $assetIds[] = $assetId; + $addressBalance = $nodeT->getAssetBalance( $addressT, $assetId ); + $this->assertSame( $addressBalance, $balance->balance() ); + $balance->isReissuable(); + $balance->quantity(); + $balance->minSponsoredAssetFee(); + $balance->sponsorBalance(); + $balance->issueTransaction(); + + $distribution = $nodeT->getAssetDistribution( $assetId, $nodeT->getHeight() - 10, 10 ); + if( $distribution->hasNext() ) + $distribution = $nodeT->getAssetDistribution( $assetId, $nodeT->getHeight() - 10, 10, $distribution->lastItem() ); + + $details = $nodeT->getAssetDetails( $assetId ); + $this->assertSame( $assetId->bytes(), $details->assetId()->bytes() ); + $details->decimals(); + $details->description(); + $details->isReissuable(); + $details->isScripted(); + $details->issueHeight(); + $details->issuer(); + $details->issuerPublicKey(); + $details->issueTimestamp(); + $details->minSponsoredAssetFee(); + $details->name(); + $details->originTransactionId(); + $details->quantity(); + $scriptDetails = $details->scriptDetails(); + $scriptDetails->complexity(); + $scriptDetails->script(); + if( --$max === 0 ) + break; + } + + if( isset( $details ) ) + { + $assetsDetails = $nodeT->getAssetsDetails( $assetIds ); + foreach( $assetsDetails as $assetDetails ) + if( $assetDetails->assetId()->toString() === $details->assetId()->toString() ) + $this->assertSame( $assetDetails->toString(), $details->toString() ); + } + + $aliases = $nodeT->getAliasesByAddress( $addressT ); + foreach( $aliases as $alias ) + { + $alias->name(); + $alias->toString(); + $this->assertSame( $addressT->encoded(), $nodeT->getAddressByAlias( $alias )->encoded() ); + } + + $addressT = Address::fromString( '3N7uoMNjqNt1jf9q9f9BSr7ASk1QtzJABEY' ); + + $scriptInfo = $nodeT->getScriptInfo( $addressT ); + $scriptInfo->script(); + $scriptInfo->complexity(); + $scriptInfo->extraFee(); + $scriptInfo->verifierComplexity(); + $mapComplexities = $scriptInfo->callableComplexities(); + + $scriptMeta = $nodeT->getScriptMeta( $addressT ); + $version = $scriptMeta->metaVersion(); + $funcs = $scriptMeta->callableFunctions(); + foreach( $funcs as $func => $args ) + foreach( $args as $arg ) + { + $arg->name(); + $arg->type(); + } + + $addressT = Address::fromString( '3NAV8CuN5Zn6TT1gChFM2wXRtdhUBDUtCVt' ); + + $dataEntries1 = $nodeT->getData( $addressT, 'key_\d' ); + $dataEntries2 = $nodeT->getData( $addressT ); + + $this->assertLessThan( count( $dataEntries2 ), count( $dataEntries1 ) ); + + $keys = []; + foreach( $dataEntries1 as $dataEntry ) + $keys[] = $dataEntry->key(); + + $dataEntries2 = $nodeT->getDataByKeys( $addressT, $keys ); + $this->assertSame( count( $dataEntries1 ), count( $dataEntries2 ) ); + + $n = count( $dataEntries1 ); + for( $i = 0; $i < $n; ++$i ) + $this->assertSame( $dataEntries1[$i]->value(), $dataEntries2[$i]->value() ); + + $this->assertSame( ChainId::MAINNET, $nodeW->chainId()->asString() ); + $this->assertSame( ChainId::TESTNET, $nodeT->chainId()->asString() ); + $this->assertSame( ChainId::STAGENET, $nodeS->chainId()->asString() ); + + $this->assertSame( $nodeW->uri(), Node::MAINNET ); + $this->assertSame( $nodeT->uri(), Node::TESTNET ); + $this->assertSame( $nodeS->uri(), Node::STAGENET ); + + $heightW = $nodeW->getHeight(); + $heightT = $nodeT->getHeight(); + $heightS = $nodeS->getHeight(); + + $this->assertLessThan( $heightW, $heightT ); + $this->assertLessThan( $heightT, $heightS ); + $this->assertLessThan( $heightS, 1 ); + + $addresses = $nodeW->getAddresses(); + + $address1 = $addresses[0]; + $address2 = $nodeW->getAddressesByIndexes( 0, 1 )[0]; + + $this->assertSame( $address1->encoded(), Functions::base58Encode( $address2->bytes() ) ); + + $balance1 = $nodeW->getBalance( $address1 ); + $balance2 = $nodeW->getBalance( $address2, 0 ); + + $this->assertSame( $balance1, $balance2 ); + + $balances = $nodeW->getBalances( $addresses ); + + $balance1 = $balances[0]; + $balance2 = $nodeW->getBalances( $addresses, $heightW )[0]; + + $this->assertSame( $balance1->getAddress(), $balance2->getAddress() ); + $this->assertSame( $balance1->getBalance(), $balance2->getBalance() ); + + $balanceDetails = $nodeW->getBalanceDetails( $address1 ); + + $this->assertSame( $balanceDetails->address(), $address1->toString() ); + $this->assertSame( $balanceDetails->available(), $balance1->getBalance() ); + $balanceDetails->effective(); + $balanceDetails->generating(); + $balanceDetails->regular(); + + $headers = $nodeW->getLastBlockHeaders(); + $headers = $nodeW->getBlockHeadersByHeight( $headers->height() - 10 ); + + $headers->baseTarget(); + $headers->desiredReward(); + $headers->features(); + $headers->generationSignature(); + $headers->generator(); + $headers->height(); + $headers->id(); + $headers->reference(); + $headers->reward(); + $headers->signature(); + $headers->size(); + $headers->timestamp(); + $headers->totalFee(); + $headers->transactionsCount(); + $headers->transactionsRoot(); + $headers->version(); + $headers->vrf(); + + $height1 = $nodeW->getBlockHeightById( $headers->id()->encoded() ); + $height2 = $nodeW->getBlockHeightByTimestamp( $headers->timestamp() ); + + $this->assertSame( $headers->height(), $height1 ); + $this->assertSame( $headers->height(), $height2 ); + + $headers1 = $nodeW->getBlockHeadersByHeight( $headers->height() ); + $headers2 = $nodeW->getBlockHeadersById( $headers->id()->encoded() ); + $headers3 = $nodeW->getBlocksHeaders( $headers->height() - 1, $headers->height() )[1]; + $this->assertSame( $headers->toString(), $headers1->toString() ); + $this->assertSame( $headers->toString(), $headers2->toString() ); + $this->assertSame( $headers->toString(), $headers3->toString() ); + + $delay = $nodeW->getBlocksDelay( $nodeW->getBlockHeadersByHeight( $headers->height() - 200 )->id()->encoded(), 100 ); + $this->assertLessThan( 70 * 1000, $delay ); + $this->assertLessThan( $delay, 50 * 1000 ); + } + + function testMoreCoverage(): void + { + $node1 = new Node( Node::MAINNET ); + $node2 = new Node( Node::MAINNET, ChainId::MAINNET() ); + $node3 = new Node( str_replace( 'https', 'http', Node::MAINNET ) ); + $this->assertSame( $node1->chainId()->asString(), $node2->chainId()->asString() ); + $this->assertSame( $node2->chainId()->asString(), $node3->chainId()->asString() ); + } + + function testExceptions(): void + { + $node = new Node( Node::MAINNET ); + $json = $node->get( '/blocks/headers/last' ); + + $this->catchExceptionOrFail( ExceptionCode::FETCH_URI, function() use ( $node ){ $node->get( '/test' ); } ); + $this->catchExceptionOrFail( ExceptionCode::JSON_DECODE, function() use ( $node ){ $node->get( '/api-docs/favicon-16x16.png' ); } ); + $this->catchExceptionOrFail( ExceptionCode::KEY_MISSING, function() use ( $json ){ $json->get( 'x' ); } ); + $this->catchExceptionOrFail( ExceptionCode::INT_EXPECTED, function() use ( $json ){ $json->get( 'signature' )->asInt(); } ); + $this->catchExceptionOrFail( ExceptionCode::STRING_EXPECTED, function() use ( $json ){ $json->get( 'height' )->asString(); } ); + $this->catchExceptionOrFail( ExceptionCode::ARRAY_EXPECTED, function() use ( $json ){ $json->get( 'height' )->asArrayInt(); } ); + + $this->catchExceptionOrFail( ExceptionCode::BAD_ALIAS, function(){ Recipient::fromAddressOrAlias( '123' ); } ); + $this->catchExceptionOrFail( ExceptionCode::BASE58_DECODE, function(){ Functions::base58Decode( 'ill' ); } ); + } +} + +if( !defined( 'PHPUNIT_RUNNING' ) ) +{ + $test = new NodeTest; + $test->testNode(); + $test->testMoreCoverage(); + $test->testExceptions(); +} diff --git a/tests/TransactionsTest.php b/tests/TransactionsTest.php new file mode 100644 index 0000000..5534270 --- /dev/null +++ b/tests/TransactionsTest.php @@ -0,0 +1,1032 @@ +chainId ) ) + return; + + if( !defined( 'WAVES_NODE' ) || !defined( 'WAVES_FAUCET' ) ) + throw new Exception( 'Missing WAVES_NODE and WAVES_FAUCET definitions', ExceptionCode::UNEXPECTED ); + + $WAVES_NODE = constant( 'WAVES_NODE' ); + $WAVES_FAUCET = constant( 'WAVES_FAUCET' ); + + if( !is_string( $WAVES_NODE ) || !is_string( $WAVES_FAUCET ) ) + throw new Exception( '$WAVES_NODE and $WAVES_FAUCET should be strings', ExceptionCode::UNEXPECTED ); + + $account = getenv( 'WAVES_ACCOUNT' ); + if( is_string( $account ) ) + $account = PrivateKey::fromString( $account ); + else + { + $account = PrivateKey::fromBytes( random_bytes( 32 ) ); + putenv( 'WAVES_ACCOUNT=' . $account->toString() ); + } + + $node = new Node( $WAVES_NODE ); + $chainId = $node->chainId(); + + WavesConfig::chainId( $chainId ); + $faucet = PrivateKey::fromSeed( $WAVES_FAUCET ); + + $publicKey = PublicKey::fromPrivateKey( $account ); + $address = Address::fromPublicKey( $publicKey ); + + $this->assertSame( $account->publicKey()->toString(), $publicKey->toString() ); + $this->assertSame( $account->publicKey()->address()->toString(), $address->toString() ); + + $this->node = $node; + $this->faucet = $faucet; + $this->account = $account; + $this->chainId = $chainId; + + $this->prepareRoot(); + $this->prepareFunds(); + $this->prepareSponsor(); + $this->prepareToken(); + } + + private function prepareRoot(): void + { + $node = $this->node; + $faucet = $this->faucet; + + $address = $this->fetchOr( function(){ return $this->node->getAddressByAlias( Alias::fromString( 'root' ) ); }, false ); + if( $address === false ) + { + $node->waitForTransaction( + $node->broadcast( + CreateAliasTransaction::build( $faucet->publicKey(), Alias::fromString( 'root' ) )->addProof( $faucet ) + )->id() + ); + } + + $scriptCode = file_get_contents( __DIR__ . '/retransmit.ride' ); + if( $scriptCode === false ) + throw new Exception( 'Missing `retransmit.ride` file', ExceptionCode::UNEXPECTED ); + $scriptCompiled = $node->compileScript( $scriptCode ); + + $script = $this->fetchOr( function(){ return $this->node->getScriptInfo( $this->faucet->publicKey()->address() ); }, false ); + if( !( $script instanceof ScriptInfo ) || $script->script()->bytes() !== $scriptCompiled->script()->bytes() ) + { + $node->waitForTransaction( + $node->broadcast( + SetScriptTransaction::build( $faucet->publicKey(), $scriptCompiled->script() )->addProof( $faucet ) + )->id() + ); + } + + $assets = $node->getAssetsBalance( $faucet->publicKey()->address() ); + foreach( $assets as $asset ) + { + $amount = Amount::of( $asset->balance(), $asset->assetId() ); + $this->fetchOr( function() use ( $node, $faucet, $amount ){ $node->broadcast( BurnTransaction::build( $faucet->publicKey(), $amount )->addProof( $faucet ) ); }, false ); + } + } + + private function prepareFunds(): void + { + $node = $this->node; + $address = $this->account->publicKey()->address(); + $faucet = $this->faucet; + + $balance = $this->node->getBalance( $address ); + if( $balance < self::WAVES_FOR_TEST ) + $node->waitForTransaction( + $node->broadcast( + TransferTransaction::build( $faucet->publicKey(), Recipient::fromAddress( $address ), Amount::of( self::WAVES_FOR_TEST * 2 ) )->addProof( $faucet ) + )->id() + ); + + $this->assertGreaterThan( self::WAVES_FOR_TEST, $this->node->getBalance( $address ) ); + } + + /** + * @param callable $block + * @param mixed $default + * @return mixed + */ + private function fetchOr( callable $block, $default ) + { + try + { + return $block(); + } + catch( Exception $e ) + { + if( $e->getCode() & ExceptionCode::BASE ) + return $default; + throw $e; + } + } + + private function prepareSponsor(): void + { + $sponsorId = $this->fetchOr( function(){ return $this->node->getDataByKey( $this->account->publicKey()->address(), self::SPONSOR_ID )->stringValue(); }, false ); + if( is_string( $sponsorId ) ) + $this->sponsorId = AssetId::fromString( $sponsorId ); + else + { + $node = $this->node; + $account = $this->account; + $sender = $account->publicKey(); + + $tx = $node->waitForTransaction( $node->broadcast( IssueTransaction::build( $sender, 'SPONSOR', '', 1000, 0, false )->addProof( $account ) )->id() ); + $this->sponsorId = AssetId::fromString( $tx->id()->toString() ); + $tx = $node->waitForTransaction( $node->broadcast( SponsorFeeTransaction::build( $sender, $this->sponsorId, 1 )->addProof( $account ) )->id() ); + + $node->waitForTransaction( $node->broadcast( DataTransaction::build( $sender, [ DataEntry::string( self::SPONSOR_ID, $this->sponsorId->toString() ) ] )->addProof( $account ) )->id() ); + } + } + + private function prepareToken(): void + { + $tokenId = $this->fetchOr( function(){ return $this->node->getDataByKey( $this->account->publicKey()->address(), self::TOKEN_ID )->stringValue(); }, false ); + if( is_string( $tokenId ) ) + $this->tokenId = AssetId::fromString( $tokenId ); + else + { + $node = $this->node; + $account = $this->account; + $sender = $account->publicKey(); + + $tx = $node->waitForTransaction( $node->broadcast( IssueTransaction::build( $sender, 'TOKEN', '', 1000000, 6, true )->addProof( $account ) )->id() ); + $this->tokenId = AssetId::fromString( $tx->id()->toString() ); + + $node->waitForTransaction( $node->broadcast( DataTransaction::build( $sender, [ DataEntry::string( self::TOKEN_ID, $this->tokenId->toString() ) ] )->addProof( $account ) )->id() ); + } + } + + function testAlias(): void + { + $this->prepare(); + $chainId = $this->chainId; + $node = $this->node; + $account = $this->account; + $sender = $account->publicKey(); + + $tx = CreateAliasTransaction::build( + $sender, + Alias::fromString( 'name-' . mt_rand( 10000000000, 99999999999 ) ) + ); + + $tx->bodyBytes(); + + $id = $tx->id(); + $tx->version(); + $tx->chainId(); + $tx->sender(); + $tx->timestamp(); + $tx->fee(); + $tx->proofs(); + + $tx->alias(); + + $tx1 = $node->waitForTransaction( $node->broadcast( $tx->addProof( $account ) )->id() ); + + $this->assertSame( $id->toString(), $tx1->id()->toString() ); + $this->assertSame( $tx1->applicationStatus(), ApplicationStatus::SUCCEEDED ); + + $tx2 = $node->waitForTransaction( + $node->broadcast( + (new CreateAliasTransaction) + ->setAlias( Alias::fromString( 'name-' . mt_rand( 10000000000, 99999999999 ) ) ) + + ->setSender( $sender ) + ->setType( CreateAliasTransaction::TYPE ) + ->setVersion( CreateAliasTransaction::LATEST_VERSION ) + ->setFee( Amount::of( CreateAliasTransaction::MIN_FEE ) ) + ->setChainId( $chainId ) + ->setTimestamp() + + ->addProof( $account ) + )->id() + ); + + $this->assertNotSame( $tx1->id(), $tx2->id() ); + $this->assertSame( $tx2->applicationStatus(), ApplicationStatus::SUCCEEDED ); + } + + function testLeaseAndLeaseCancel(): void + { + $this->prepare(); + $chainId = $this->chainId; + $node = $this->node; + $account = $this->account; + $sender = $account->publicKey(); + + $recipient = Recipient::fromAddressOrAlias( 'root' ); + + // LEASE + + $tx = LeaseTransaction::build( + $sender, + $recipient, + 10_000_000 + ); + + $tx->bodyBytes(); + + $id = $tx->id(); + $tx->version(); + $tx->chainId(); + $tx->sender(); + $tx->timestamp(); + $tx->fee(); + $tx->proofs(); + + $tx->recipient(); + $tx->amount(); + + $tx1 = $node->waitForTransaction( $node->broadcast( $tx->addProof( $account ) )->id() ); + + $this->assertSame( $id->toString(), $tx1->id()->toString() ); + $this->assertSame( $tx1->applicationStatus(), ApplicationStatus::SUCCEEDED ); + + $tx2 = $node->waitForTransaction( + $node->broadcast( + (new LeaseTransaction) + ->setRecipient( Recipient::fromAddress( $node->getAddressByAlias( $recipient->alias() ) ) ) + ->setAmount( 20_000_000 ) + + ->setSender( $sender ) + ->setType( LeaseTransaction::TYPE ) + ->setVersion( LeaseTransaction::LATEST_VERSION ) + ->setFee( Amount::of( LeaseTransaction::MIN_FEE ) ) + ->setChainId( $chainId ) + ->setTimestamp() + + ->addProof( $account ) + )->id() + ); + + $this->assertNotSame( $tx1->id(), $tx2->id() ); + $this->assertSame( $tx2->applicationStatus(), ApplicationStatus::SUCCEEDED ); + + // LEASE_CANCEL + + $tx = LeaseCancelTransaction::build( + $sender, + $tx1->id() + ); + + $tx->bodyBytes(); + + $id = $tx->id(); + $tx->version(); + $tx->chainId(); + $tx->sender(); + $tx->timestamp(); + $tx->fee(); + $tx->proofs(); + + $tx->leaseId(); + + $tx1 = $node->waitForTransaction( $node->broadcast( $tx->addProof( $account ) )->id() ); + + $this->assertSame( $id->toString(), $tx1->id()->toString() ); + $this->assertSame( $tx1->applicationStatus(), ApplicationStatus::SUCCEEDED ); + + $tx2 = $node->waitForTransaction( + $node->broadcast( + (new LeaseCancelTransaction) + ->setLeaseId( $tx2->id() ) + + ->setSender( $sender ) + ->setType( LeaseCancelTransaction::TYPE ) + ->setVersion( LeaseCancelTransaction::LATEST_VERSION ) + ->setFee( Amount::of( LeaseCancelTransaction::MIN_FEE ) ) + ->setChainId( $chainId ) + ->setTimestamp() + + ->addProof( $account ) + )->id() + ); + + $this->assertNotSame( $tx1->id(), $tx2->id() ); + $this->assertSame( $tx2->applicationStatus(), ApplicationStatus::SUCCEEDED ); + } + + function testSetScript(): void + { + $this->prepare(); + $chainId = $this->chainId; + $node = $this->node; + $account = $this->account; + $sender = $account->publicKey(); + + $script = Base64String::fromString( 'AAIFAAAAAAAAAAcIAhIDCgEfAAAAAAAAAAEAAAABaQEAAAAEY2FsbAAAAAEAAAAEbGlzdAUAAAADbmlsAAAAAQAAAAJ0eAEAAAAGdmVyaWZ5AAAAAAkACcgAAAADCAUAAAACdHgAAAAJYm9keUJ5dGVzCQABkQAAAAIIBQAAAAJ0eAAAAAZwcm9vZnMAAAAAAAAAAAAIBQAAAAJ0eAAAAA9zZW5kZXJQdWJsaWNLZXmQFHRt' ); + + $tx = SetScriptTransaction::build( + $sender, + $script + ); + + $tx->bodyBytes(); + + $id = $tx->id(); + $tx->version(); + $tx->chainId(); + $tx->sender(); + $tx->timestamp(); + $tx->fee(); + $tx->proofs(); + + $tx->script(); + + $tx1 = $node->waitForTransaction( $node->broadcast( $tx->addProof( $account ) )->id() ); + + $this->assertSame( $id->toString(), $tx1->id()->toString() ); + $this->assertSame( $tx1->applicationStatus(), ApplicationStatus::SUCCEEDED ); + + $tx2 = $node->waitForTransaction( + $node->broadcast( + (new SetScriptTransaction) + ->setScript() // remove script + + ->setSender( $sender ) + ->setType( SetScriptTransaction::TYPE ) + ->setVersion( SetScriptTransaction::LATEST_VERSION ) + ->setFee( Amount::of( SetScriptTransaction::MIN_FEE ) ) + ->setChainId( $chainId ) + ->setTimestamp() + + ->addProof( $account ) + )->id() + ); + + $this->assertNotSame( $tx1->id(), $tx2->id() ); + $this->assertSame( $tx2->applicationStatus(), ApplicationStatus::SUCCEEDED ); + } + + function testSetAssetScript(): void + { + $this->prepare(); + $chainId = $this->chainId; + $node = $this->node; + $account = $this->account; + $sender = $account->publicKey(); + + $script = Base64String::fromString( 'BQbtKNoM' ); + + $tx = $node->waitForTransaction( $node->broadcast( IssueTransaction::build( $sender, 'SCRIPTED', '', 1, 0, false, $script )->addProof( $account ) )->id() ); + $scriptedId = AssetId::fromString( $tx->id()->toString() ); + + $tx = SetAssetScriptTransaction::build( + $sender, + $scriptedId, + $script + ); + + $tx->bodyBytes(); + + $id = $tx->id(); + $tx->version(); + $tx->chainId(); + $tx->sender(); + $tx->timestamp(); + $tx->fee(); + $tx->proofs(); + + $tx->assetId(); + $tx->script(); + + $tx1 = $node->waitForTransaction( $node->broadcast( $tx->addProof( $account ) )->id() ); + + $this->assertSame( $id->toString(), $tx1->id()->toString() ); + $this->assertSame( $tx1->applicationStatus(), ApplicationStatus::SUCCEEDED ); + + $tx2 = $node->waitForTransaction( + $node->broadcast( + (new SetAssetScriptTransaction) + ->setAssetId( $scriptedId ) + ->setScript( $script ) + + ->setSender( $sender ) + ->setType( SetAssetScriptTransaction::TYPE ) + ->setVersion( SetAssetScriptTransaction::LATEST_VERSION ) + ->setFee( Amount::of( SetAssetScriptTransaction::MIN_FEE ) ) + ->setChainId( $chainId ) + ->setTimestamp() + + ->addProof( $account ) + )->id() + ); + + $this->assertNotSame( $tx1->id(), $tx2->id() ); + $this->assertSame( $tx2->applicationStatus(), ApplicationStatus::SUCCEEDED ); + } + + function testReissue(): void + { + $this->prepare(); + $chainId = $this->chainId; + $node = $this->node; + $account = $this->account; + $sender = $account->publicKey(); + + $tx = ReissueTransaction::build( + $sender, + Amount::of( 1000_000_000, $this->tokenId ), + true + ); + + $tx->bodyBytes(); + + $id = $tx->id(); + $tx->version(); + $tx->chainId(); + $tx->sender(); + $tx->timestamp(); + $tx->fee(); + $tx->proofs(); + + $tx->amount(); + $tx->isReissuable(); + + $tx1 = $node->waitForTransaction( $node->broadcast( $tx->addProof( $account ) )->id() ); + + $this->assertSame( $id->toString(), $tx1->id()->toString() ); + $this->assertSame( $tx1->applicationStatus(), ApplicationStatus::SUCCEEDED ); + + $tx2 = $node->waitForTransaction( + $node->broadcast( + (new ReissueTransaction) + ->setAmount( Amount::of( 2000_000_000, $this->tokenId ) ) + ->setIsReissuable( true ) + + ->setSender( $sender ) + ->setType( ReissueTransaction::TYPE ) + ->setVersion( ReissueTransaction::LATEST_VERSION ) + ->setFee( Amount::of( ReissueTransaction::MIN_FEE ) ) + ->setChainId( $chainId ) + ->setTimestamp() + + ->addProof( $account ) + )->id() + ); + + $this->assertNotSame( $tx1->id(), $tx2->id() ); + $this->assertSame( $tx2->applicationStatus(), ApplicationStatus::SUCCEEDED ); + } + + function testBurn(): void + { + $this->prepare(); + $chainId = $this->chainId; + $node = $this->node; + $account = $this->account; + $sender = $account->publicKey(); + + $tx = BurnTransaction::build( + $sender, + Amount::of( 100_000_000, $this->tokenId ) + ); + + $tx->bodyBytes(); + + $id = $tx->id(); + $tx->version(); + $tx->chainId(); + $tx->sender(); + $tx->timestamp(); + $tx->fee(); + $tx->proofs(); + + $tx->amount(); + + $tx1 = $node->waitForTransaction( $node->broadcast( $tx->addProof( $account ) )->id() ); + + $this->assertSame( $id->toString(), $tx1->id()->toString() ); + $this->assertSame( $tx1->applicationStatus(), ApplicationStatus::SUCCEEDED ); + + $tx2 = $node->waitForTransaction( + $node->broadcast( + (new BurnTransaction) + ->setAmount( Amount::of( 10_000_000, $this->tokenId ) ) + + ->setSender( $sender ) + ->setType( BurnTransaction::TYPE ) + ->setVersion( BurnTransaction::LATEST_VERSION ) + ->setFee( Amount::of( BurnTransaction::MIN_FEE ) ) + ->setChainId( $chainId ) + ->setTimestamp() + + ->addProof( $account ) + )->id() + ); + + $this->assertNotSame( $tx1->id(), $tx2->id() ); + $this->assertSame( $tx2->applicationStatus(), ApplicationStatus::SUCCEEDED ); + } + + function testIssue(): void + { + $this->prepare(); + $chainId = $this->chainId; + $node = $this->node; + $account = $this->account; + $sender = $account->publicKey(); + + $tx = IssueTransaction::build( + $sender, + 'NFT-' . mt_rand( 100000, 999999 ), + 'test description', + 1, + 0, + false + ); + + $tx->bodyBytes(); + + $id = $tx->id(); + $tx->version(); + $tx->chainId(); + $tx->sender(); + $tx->timestamp(); + $tx->fee(); + $tx->proofs(); + + $tx->name(); + $tx->description(); + $tx->quantity(); + $tx->decimals(); + $tx->isReissuable(); + $tx->script(); + + $tx1 = $node->waitForTransaction( $node->broadcast( $tx->addProof( $account ) )->id() ); + + $this->assertSame( $id->toString(), $tx1->id()->toString() ); + $this->assertSame( $tx1->applicationStatus(), ApplicationStatus::SUCCEEDED ); + + $tx2 = $node->waitForTransaction( + $node->broadcast( + (new IssueTransaction) + ->setName( 'NFT-' . mt_rand( 100000, 999999 ) ) + ->setDescription( 'test description' ) + ->setQuantity( 1 ) + ->setDecimals( 0 ) + ->setIsReissuable( false ) + + ->setSender( $sender ) + ->setType( IssueTransaction::TYPE ) + ->setVersion( IssueTransaction::LATEST_VERSION ) + ->setFee( Amount::of( IssueTransaction::NFT_MIN_FEE ) ) + ->setChainId( $chainId ) + ->setTimestamp() + + ->addProof( $account ) + )->id() + ); + + $this->assertNotSame( $tx1->id(), $tx2->id() ); + $this->assertSame( $tx2->applicationStatus(), ApplicationStatus::SUCCEEDED ); + } + + function testRename(): void + { + $this->prepare(); + $chainId = $this->chainId; + $node = $this->node; + $account = $this->account; + $sender = $account->publicKey(); + + $tokenId = $this->tokenId; + + $assetInfo = $node->getAssetDetails( $tokenId ); + $node->waitForHeight( $assetInfo->issueHeight() + 2 ); + + $tx = UpdateAssetInfoTransaction::build( + $sender, + $tokenId, + 'TOKEN-RENAMED', + 'renamed description' + ); + + $tx->bodyBytes(); + + $id = $tx->id(); + $tx->version(); + $tx->chainId(); + $tx->sender(); + $tx->timestamp(); + $tx->fee(); + $tx->proofs(); + + $tx->assetId(); + $tx->name(); + $tx->description(); + + $tx1 = $node->waitForTransaction( $node->broadcast( $tx->addProof( $account ) )->id() ); + + $this->assertSame( $id->toString(), $tx1->id()->toString() ); + $this->assertSame( $tx1->applicationStatus(), ApplicationStatus::SUCCEEDED ); + + $node->waitBlocks( 2 ); + + $tx2 = $node->waitForTransaction( + $node->broadcast( + (new UpdateAssetInfoTransaction) + ->setAssetId( $this->sponsorId ) + ->setName( 'SPONSOR-RENAMED' ) + ->setDescription( 'renamed description' ) + + ->setSender( $sender ) + ->setType( UpdateAssetInfoTransaction::TYPE ) + ->setVersion( UpdateAssetInfoTransaction::LATEST_VERSION ) + ->setFee( Amount::of( UpdateAssetInfoTransaction::MIN_FEE ) ) + ->setChainId( $chainId ) + ->setTimestamp() + + ->addProof( $account ) + )->id() + ); + + $this->assertNotSame( $tx1->id(), $tx2->id() ); + $this->assertSame( $tx2->applicationStatus(), ApplicationStatus::SUCCEEDED ); + } + + function testSponsorship(): void + { + $this->prepare(); + $chainId = $this->chainId; + $node = $this->node; + $account = $this->account; + $sender = $account->publicKey(); + + $sponsorId = $this->sponsorId; + + $tx = SponsorFeeTransaction::build( + $sender, + $sponsorId, + 1 + ); + + $tx->bodyBytes(); + + $id = $tx->id(); + $tx->version(); + $tx->chainId(); + $tx->sender(); + $tx->timestamp(); + $tx->fee(); + $tx->proofs(); + + $tx->assetId(); + $tx->minSponsoredFee(); + + $tx1 = $node->broadcast( $tx->addProof( $account ) ); + $node->waitForTransactions( [ $tx1->id() ] ); + + $tx1 = $node->waitForTransaction( $tx1->id() ); + + $this->assertSame( $id->toString(), $tx1->id()->toString() ); + $this->assertSame( $tx1->applicationStatus(), ApplicationStatus::SUCCEEDED ); + + $tx2 = $node->waitForTransaction( + $node->broadcast( + (new SponsorFeeTransaction()) + ->setAssetId( $sponsorId ) + ->setMinSponsoredFee( 1 ) + + ->setSender( $sender ) + ->setType( SponsorFeeTransaction::TYPE ) + ->setVersion( SponsorFeeTransaction::LATEST_VERSION ) + ->setFee( Amount::of( SponsorFeeTransaction::MIN_FEE ) ) + ->setChainId( $chainId ) + ->setTimestamp() + + ->addProof( $account ) + )->id() + ); + + $this->assertNotSame( $tx1->id(), $tx2->id() ); + $this->assertSame( $tx2->applicationStatus(), ApplicationStatus::SUCCEEDED ); + } + + function testData(): void + { + $this->prepare(); + $chainId = $this->chainId; + $node = $this->node; + $account = $this->account; + $sender = $account->publicKey(); + + $data = []; + $data[] = DataEntry::build( 'key_string', EntryType::STRING, '123' ); + $data[] = DataEntry::build( 'key_binary', EntryType::BINARY, '123' ); + $data[] = DataEntry::build( 'key_boolean', EntryType::BOOLEAN, true ); + $data[] = DataEntry::build( 'key_integer', EntryType::INTEGER, 123 ); + $data[] = DataEntry::build( 'key_delete', EntryType::DELETE ); + + $tx = DataTransaction::build( + $sender, + $data + ); + + $tx->bodyBytes(); + + $id = $tx->id(); + $tx->version(); + $tx->chainId(); + $tx->sender(); + $tx->timestamp(); + $tx->fee(); + $tx->proofs(); + + $tx->data(); + + $tx1 = $node->waitForTransaction( $node->broadcast( $tx->addProof( $account ) )->id() ); + + $this->assertSame( $id->toString(), $tx1->id()->toString() ); + $this->assertSame( $tx1->applicationStatus(), ApplicationStatus::SUCCEEDED ); + + $tx2 = $node->waitForTransaction( + $node->broadcast( + (new DataTransaction) + ->setData( $data ) + + ->setSender( $sender ) + ->setType( DataTransaction::TYPE ) + ->setVersion( DataTransaction::LATEST_VERSION ) + ->setFee( Amount::of( DataTransaction::MIN_FEE ) ) + ->setChainId( $chainId ) + ->setTimestamp() + + ->addProof( $account ) + )->id() + ); + + $this->assertNotSame( $tx1->id(), $tx2->id() ); + $this->assertSame( $tx2->applicationStatus(), ApplicationStatus::SUCCEEDED ); + } + + function testMassTransfer(): void + { + $this->prepare(); + $chainId = $this->chainId; + $node = $this->node; + $account = $this->account; + $sender = $account->publicKey(); + + $transfers = []; + $transfers[] = new Transfer( Recipient::fromAlias( Alias::fromString( 'root' ) ), 1 ); + $transfers[] = new Transfer( Recipient::fromAddress( $sender->address() ), 2 ); + + $attachment = Base58String::fromBytes( 'root' ); + + $tx = MassTransferTransaction::build( + $sender, + $this->tokenId, + $transfers, + $attachment, + ); + + $tx->bodyBytes(); + + $id = $tx->id(); + $tx->version(); + $tx->chainId(); + $tx->sender(); + $tx->timestamp(); + $tx->fee(); + $tx->proofs(); + + $tx->assetId(); + $tx->transfers(); + $tx->attachment(); + + $tx1 = $node->waitForTransaction( $node->broadcast( $tx->addProof( $account ) )->id() ); + + $this->assertSame( $id->toString(), $tx1->id()->toString() ); + $this->assertSame( $tx1->applicationStatus(), ApplicationStatus::SUCCEEDED ); + + $tx2 = $node->waitForTransaction( + $node->broadcast( + (new MassTransferTransaction) + ->setAssetId( AssetId::WAVES() ) + ->setTransfers( $transfers ) + ->setAttachment( $attachment ) + + ->setSender( $sender ) + ->setType( MassTransferTransaction::TYPE ) + ->setVersion( MassTransferTransaction::LATEST_VERSION ) + ->setFee( Amount::of( MassTransferTransaction::calculateFee( count( $transfers ) ) ) ) + ->setChainId( $chainId ) + ->setTimestamp() + + ->addProof( $account ) + )->id() + ); + + $this->assertNotSame( $tx1->id(), $tx2->id() ); + $this->assertSame( $tx2->applicationStatus(), ApplicationStatus::SUCCEEDED ); + } + + function testTransfer(): void + { + $this->prepare(); + $chainId = $this->chainId; + $node = $this->node; + $account = $this->account; + $sender = $account->publicKey(); + + $recipient = Recipient::fromAlias( Alias::fromString( 'root' ) ); + $amount = new Amount( 1, AssetId::WAVES() ); + $attachment = Base58String::fromBytes( 'root' ); + + $tx = TransferTransaction::build( + $sender, + $recipient, + $amount, + )->setFee( Amount::of( 1, $this->sponsorId ) ); + + $tx->bodyBytes(); + + $id = $tx->id(); + $tx->version(); + $tx->chainId(); + $tx->sender(); + $tx->timestamp(); + $tx->fee(); + $tx->proofs(); + + $tx->recipient(); + $tx->amount(); + $tx->attachment(); + + $tx1 = $node->waitForTransaction( $node->broadcast( $tx->addProof( $account ) )->id() ); + + $this->assertSame( $id->toString(), $tx1->id()->toString() ); + $this->assertSame( $tx1->applicationStatus(), ApplicationStatus::SUCCEEDED ); + + $tx2 = $node->waitForTransaction( + $node->broadcast( + (new TransferTransaction) + ->setRecipient( Recipient::fromAddress( $node->getAddressByAlias( $recipient->alias() ) ) ) + ->setAmount( Amount::of( 1000, $this->tokenId ) ) + ->setAttachment( $attachment ) + + ->setSender( $sender ) + ->setType( TransferTransaction::TYPE ) + ->setVersion( TransferTransaction::LATEST_VERSION ) + ->setFee( Amount::of( TransferTransaction::MIN_FEE ) ) + ->setChainId( $chainId ) + ->setTimestamp() + + ->addProof( $account ) + )->id() + ); + + $this->assertNotSame( $tx1->id(), $tx2->id() ); + $this->assertSame( $tx2->applicationStatus(), ApplicationStatus::SUCCEEDED ); + } + + function testInvoke(): void + { + $this->prepare(); + $chainId = $this->chainId; + $node = $this->node; + $account = $this->account; + $sender = $account->publicKey(); + + $dApp = Recipient::fromAddressOrAlias( 'root' ); + $args = []; + $args[] = Arg::as( Arg::STRING, Value::as( $sender->address()->toString() ) ); + $args[] = Arg::as( Arg::INTEGER, Value::as( 1000 ) ); + $args[] = Arg::as( Arg::BINARY, Value::as( '' ) ); + $args[] = Arg::as( Arg::BOOLEAN, Value::as( true ) ); + $list = []; + $list[] = Arg::as( Arg::STRING, Value::as( '0' ) ); + $list[] = Arg::as( Arg::STRING, Value::as( '1' ) ); + $list[] = Arg::as( Arg::STRING, Value::as( '2' ) ); + $args[] = Arg::as( Arg::LIST, Value::as( $list ) ); + $function = FunctionCall::as( 'retransmit', $args ); + $payments = []; + $payments[] = Amount::of( 1000 ); + + $tx = InvokeScriptTransaction::build( + $sender, + $dApp, + $function, + $payments + )->setFee( Amount::of( 5, $this->sponsorId ) );; + + $tx->bodyBytes(); + + $id = $tx->id(); + $tx->version(); + $tx->chainId(); + $tx->sender(); + $tx->timestamp(); + $tx->fee(); + $tx->proofs(); + + $tx->dApp(); + $tx->function(); + $tx->payments(); + + $tx1 = $node->waitForTransaction( $node->broadcast( $tx->addProof( $account ) )->id() ); + + $this->assertSame( $id->toString(), $tx1->id()->toString() ); + $this->assertSame( $tx1->applicationStatus(), ApplicationStatus::SUCCEEDED ); + + $txFromJson = new InvokeScriptTransaction( $tx1->json() ); + $this->assertSame( $tx->dApp()->toString(), $txFromJson->dApp()->toString() ); + $this->assertSame( serialize( $tx->payments() ), serialize( $txFromJson->payments() ) ); + $this->assertSame( serialize( $tx->function() ), serialize( $txFromJson->function() ) ); + + $tx2 = $node->waitForTransaction( + $node->broadcast( + (new InvokeScriptTransaction) + ->setDApp( $dApp ) + ->setFunction( $function ) + ->setPayments( $payments ) + + ->setSender( $sender ) + ->setType( InvokeScriptTransaction::TYPE ) + ->setVersion( InvokeScriptTransaction::LATEST_VERSION ) + ->setFee( Amount::of( InvokeScriptTransaction::MIN_FEE ) ) + ->setChainId( $chainId ) + ->setTimestamp() + + ->addProof( $account ) + )->id() + ); + + $this->assertNotSame( $tx1->id(), $tx2->id() ); + $this->assertSame( $tx2->applicationStatus(), ApplicationStatus::SUCCEEDED ); + } +} + +if( !defined( 'PHPUNIT_RUNNING' ) ) +{ + $test = new TransactionsTest; + $test->testInvoke(); + $test->testSponsorship(); + $test->testRename(); + $test->testSetScript(); + $test->testData(); + $test->testMassTransfer(); + $test->testTransfer(); + $test->testAlias(); + $test->testLeaseAndLeaseCancel(); + $test->testIssue(); + $test->testReissue(); + $test->testBurn(); + $test->testSetAssetScript(); +} diff --git a/tests/common.php b/tests/common.php new file mode 100644 index 0000000..c810398 --- /dev/null +++ b/tests/common.php @@ -0,0 +1,32 @@ + $value ) + if( is_string( $key ) ) + define( $key, $value ); +} + +prepare(); diff --git a/tests/retransmit.ride b/tests/retransmit.ride new file mode 100644 index 0000000..30d8485 --- /dev/null +++ b/tests/retransmit.ride @@ -0,0 +1,12 @@ +{-# STDLIB_VERSION 6 #-} +{-# CONTENT_TYPE DAPP #-} +{-# SCRIPT_TYPE ACCOUNT #-} + +@Callable(i) +func retransmit( address: String, amount: Int, asset: ByteVector, bool: Boolean, list: List[String] ) = +[ + StringEntry( "LIST_0", list[0] ), + StringEntry( "LIST_1", list[1] ), + StringEntry( "LIST_2", list[2] ), + ScriptTransfer( addressFromStringValue( address ), amount, if( asset == base58'' ) then unit else asset ) +]