diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..aac03199 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,14 @@ + +# Path-based git attributes +# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html + +# Ignore all test and documentation with "export-ignore". +/.github export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/build export-ignore +/tests export-ignore +/.editorconfig export-ignore +/.php-cs-fixer.php export-ignore +/phpstan.neon.dist export-ignore +/phpunit.xml.dist export-ignore \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..501e1b30 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# This file is used to define a set of default reviewers. +* @v16Studios @leonardocustodio @enjinabner diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..df1b3cf6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: composer + directory: / + schedule: + interval: daily + open-pull-requests-limit: 10 + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 10 \ No newline at end of file diff --git a/.github/graphql-inspector.yaml b/.github/graphql-inspector.yaml new file mode 100644 index 00000000..4f351677 --- /dev/null +++ b/.github/graphql-inspector.yaml @@ -0,0 +1,9 @@ +diff: + annotations: true + failOnBreaking: true + experimental_merge: true + approveLabel: breaking-change + summaryLimit: 150 + +branch: master +schema: "schema/schema.gql" diff --git a/.github/workflows/code_analysis.yml b/.github/workflows/code_analysis.yml new file mode 100644 index 00000000..040312ea --- /dev/null +++ b/.github/workflows/code_analysis.yml @@ -0,0 +1,40 @@ +name: Code Analysis and Linting + +on: + pull_request: + paths-ignore: + - '**.md' + push: + paths-ignore: + - '**.md' + +jobs: + tests: + runs-on: ubuntu-latest + name: Code Analysis + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.2 + tools: composer:v2 + coverage: none + + - name: Checkout friendsofphp php-cs-fixer code + uses: actions/checkout@v3 + with: + repository: 'friendsofphp/php-cs-fixer' + ref: 'v3.15.1' + path: '.php-cs-fixer' + + - name: Install dependencies + run: | + cd .php-cs-fixer/ + composer install + cd .. + - name: Run + run: ./.php-cs-fixer/php-cs-fixer fix --dry-run --diff --show-progress=none --verbose \ No newline at end of file diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml new file mode 100644 index 00000000..a420a1a5 --- /dev/null +++ b/.github/workflows/run_tests.yml @@ -0,0 +1,78 @@ +name: Run Tests + +on: + pull_request: + paths-ignore: + - '**.md' + push: + paths-ignore: + - '**.md' + +jobs: + test: + runs-on: ubuntu-latest + services: + mysql: + image: mysql:8 + env: + MYSQL_DATABASE: platform + MYSQL_ROOT_PASSWORD: password + ports: + - 33306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + redis: + image: redis:7 + ports: + - 6379:6379 + options: --entrypoint redis-server + strategy: + fail-fast: true + matrix: + php: [8.1, 8.2] + + name: PHP ${{ matrix.php }} + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, gd, gmp, intl, json, mysql, readline, sodium, bcmath + tools: composer:v2 + coverage: xdebug + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup GO + uses: actions/setup-go@v4 + with: + go-version: '^1.19' + + - name: Setup problem matchers + run: | + echo "::add-matcher::${{ runner.tool_cache }}/php.json" + echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - name: Install dependencies + run: | + composer install --no-interaction --no-progress + composer build-sr25519 + composer dumpautoload + + - name: Execute tests + run: | + XDEBUG_MODE=coverage ./vendor/bin/phpunit --colors=always --coverage-clover coverage.xml + env: + DB_HOST: 127.0.0.1:${{ job.services.mysql.ports[3306] }} + DB_DATABASE: platform + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + dry_run: github.event.pull_request.draft == true + token: ${{ secrets.CODECOV_TOKEN }} + files: coverage.xml + verbose: true diff --git a/.github/workflows/security_checker.yml b/.github/workflows/security_checker.yml new file mode 100644 index 00000000..5db0f1f0 --- /dev/null +++ b/.github/workflows/security_checker.yml @@ -0,0 +1,41 @@ +name: Dependencies Security Checker + +on: + pull_request: + paths-ignore: + - '**.md' + push: + paths-ignore: + - '**.md' + +jobs: + security-checker: + runs-on: ubuntu-latest + name: Sensiolabs Security Checker + + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + ssh-key: ${{ secrets.SSH_PHP_COMMONS }} + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.1 + tools: composer:v2 + coverage: none + + - name: Checkout sensiolabs security-checker code + uses: actions/checkout@v3 + with: + repository: 'sensiolabs/security-checker' + path: 'security-checker' + + - name: Install dependencies + run: | + cd security-checker/ + composer install + + - name: Run + run: php security-checker security:check ../composer.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..7224e5f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +/.idea +/.vscode +/temp +/node_modules +/vendor +/build +/tests/content/* +.env +.*.cache +coverage.xml +composer.lock diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 00000000..cb660f83 --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,165 @@ +notPath('vendor') + ->notPath('build') + ->in(__DIR__) + ->name('*.php') + ->ignoreDotFiles(true) + ->ignoreVCS(true); + +return (new PhpCsFixer\Config()) + ->setFinder($finder) + ->setRiskyAllowed(true) + ->setLineEnding("\n") + ->setRules([ + '@PSR1' => true, + '@PSR2' => true, + '@PSR12' => true, + 'align_multiline_comment' => true, + 'array_indentation' => true, + 'backtick_to_shell_exec' => true, + 'blank_line_after_opening_tag' => true, + 'blank_line_before_statement' => true, + 'cast_spaces' => true, + 'combine_consecutive_issets' => true, + 'combine_consecutive_unsets' => true, + 'combine_nested_dirname' => true, + 'comment_to_phpdoc' => true, + 'compact_nullable_typehint' => true, + 'declare_equal_normalize' => true, + 'dir_constant' => true, + 'explicit_indirect_variable' => true, + 'explicit_string_variable' => true, + 'fully_qualified_strict_types' => true, + 'function_to_constant' => true, + 'function_typehint_space' => true, + 'single_line_comment_style' => [ + 'comment_types' => ['hash'], + ], + 'heredoc_to_nowdoc' => true, + 'implode_call' => true, + 'include' => true, + 'linebreak_after_opening_tag' => true, + 'list_syntax' => ['syntax' => 'short'], + 'logical_operators' => true, + 'lowercase_cast' => true, + 'constant_case' => ['case' => 'lower'], + 'lowercase_static_reference' => true, + 'magic_constant_casing' => true, + 'magic_method_casing' => true, + 'method_chaining_indentation' => true, + 'modernize_types_casting' => true, + 'multiline_comment_opening_closing' => true, + 'multiline_whitespace_before_semicolons' => true, + 'native_function_casing' => true, + 'no_alias_functions' => true, + 'no_alternative_syntax' => true, + 'no_binary_string' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_empty_phpdoc' => true, + 'no_empty_statement' => true, + 'no_leading_import_slash' => true, + 'no_leading_namespace_whitespace' => true, + 'no_mixed_echo_print' => true, + 'no_multiline_whitespace_around_double_arrow' => true, + 'no_null_property_initialization' => true, + 'no_php4_constructor' => true, + 'no_short_bool_cast' => true, + 'echo_tag_syntax' => ['format' => 'long'], + 'no_singleline_whitespace_before_semicolons' => true, + 'no_spaces_around_offset' => true, + 'no_trailing_comma_in_singleline' => true, + 'no_unneeded_control_parentheses' => true, + 'no_unneeded_curly_braces' => true, + 'no_unneeded_final_method' => true, + 'no_unreachable_default_argument_value' => true, + 'no_unused_imports' => true, + 'no_useless_else' => true, + 'no_useless_return' => true, + 'no_whitespace_before_comma_in_array' => true, + 'no_whitespace_in_blank_line' => true, + 'non_printable_character' => true, + 'normalize_index_brace' => true, + 'object_operator_without_whitespace' => true, + 'ordered_class_elements' => true, + 'ordered_imports' => true, + 'ordered_interfaces' => true, + 'php_unit_construct' => true, + 'php_unit_dedicate_assert' => true, + 'php_unit_dedicate_assert_internal_type' => true, + 'php_unit_expectation' => true, + 'php_unit_method_casing' => ['case' => 'snake_case'], + 'phpdoc_indent' => true, + 'phpdoc_no_access' => true, + 'phpdoc_no_alias_tag' => true, + 'phpdoc_no_package' => true, + 'phpdoc_order' => true, + 'phpdoc_scalar' => true, + 'phpdoc_trim' => true, + 'phpdoc_types' => true, + 'phpdoc_var_without_name' => true, + 'phpdoc_summary' => true, + 'phpdoc_trim_consecutive_blank_line_separation' => true, + 'pow_to_exponentiation' => true, + 'psr_autoloading' => true, + 'random_api_migration' => true, + 'return_type_declaration' => [ + 'space_before' => 'none', + ], + 'self_accessor' => true, + 'semicolon_after_instruction' => true, + 'short_scalar_cast' => true, + 'simple_to_complex_string_variable' => true, + 'simplified_null_return' => true, + 'single_blank_line_before_namespace' => true, + 'single_quote' => true, + 'space_after_semicolon' => true, + 'standardize_increment' => true, + 'standardize_not_equals' => true, + 'string_line_ending' => true, + 'ternary_operator_spaces' => true, + 'ternary_to_null_coalescing' => true, + 'trailing_comma_in_multiline' => [ + 'elements' => ['arrays'], + ], + 'trim_array_spaces' => true, + 'unary_operator_spaces' => true, + 'whitespace_after_comma_in_array' => true, + 'native_function_type_declaration_casing' => true, + 'no_closing_tag' => true, + 'no_spaces_inside_parenthesis' => true, + 'no_spaces_after_function_name' => true, + 'nullable_type_declaration_for_default_null_value' => [ + 'use_nullable_type_declaration' => true, + ], + 'single_line_after_imports' => true, + 'single_import_per_statement' => true, + 'switch_case_semicolon_to_colon' => true, + 'switch_case_space' => true, + 'binary_operator_spaces' => [ + 'default' => 'single_space', + 'operators' => ['=>' => null], + ], + 'array_syntax' => ['syntax' => 'short'], + 'class_attributes_separation' => [ + 'elements' => ['method' => 'one'], + ], + 'concat_space' => [ + 'spacing' => 'one', + ], + 'general_phpdoc_annotation_remove' => [ + 'annotations' => ['author'], + ], + 'increment_style' => [ + 'style' => 'post', + ], + 'no_extra_blank_lines' => [ + 'tokens' => [ + 'break', + 'continue', + 'throw', + 'use', + ], + ], + ]); diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..dfaaa0a9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +All notable changes to `platform-core` will be documented in this file. + +## 1.0.0 - 202X-XX-XX + +- initial release diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..2bffa039 --- /dev/null +++ b/LICENSE @@ -0,0 +1,67 @@ +GNU LESSER GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. + +0. Additional Definitions. + +As used herein, “this License” refers to version 3 of the GNU Lesser General Public License, and the “GNU GPL” refers to version 3 of the GNU General Public License. + +“The Library” refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. + +An “Application” is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. + +A “Combined Work” is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the “Linked Version”. + +The “Minimal Corresponding Source” for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. + +The “Corresponding Application Code” for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. + +1. Exception to Section 3 of the GNU GPL. + +You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. + +2. Conveying Modified Versions. + +If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: + +a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or +b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. +3. Object Code Incorporating Material from Library Header Files. + +The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer +lines in length), you do both of the following: + +a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. +b) Accompany the object code with a copy of the GNU GPL and this license document. +4. Combined Works. + +You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: + +a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. +b) Accompany the Combined Work with a copy of the GNU GPL and this license document. +c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. +d) Do one of the following: +0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for +conveying Corresponding Source. +1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user’s computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. +e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked +Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) +5. Combined Libraries. + +You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: + +a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. +b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. +6. Revised Versions of the GNU Lesser General Public License. + +The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software +Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. + +If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy’s public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. diff --git a/README.md b/README.md new file mode 100644 index 00000000..281d9a8c --- /dev/null +++ b/README.md @@ -0,0 +1,109 @@ +# Enjin Platform + +The core package for the Enjin Platform. + +[![License: LGPL 3.0](https://img.shields.io/badge/license-LGPL_3.0-purple)](https://opensource.org/license/lgpl-3-0/) +[![codecov](https://codecov.io/gh/enjin/platform-core/branch/master/graph/badge.svg)](https://codecov.io/gh/enjin/platform-core) +[![Tests](https://github.com/enjin/platform-core/workflows/run_tests/badge.svg)](https://github.com/enjin/platform-core/actions?query=workflow%3Arun_tests) + + +Enjin Platform is the most powerful and advanced open-source framework for building NFT Platforms. + +## Requirements + +Please make sure you have Go installed in your machine. You can check it by typing: +```bash +go version +# go version go1.18.1 linux/amd64 +``` + +If you don't have it, you can find instructions on how to install it in [here](https://go.dev/learn/). + +## Installation + +You should add to your composer.json: + +```json +{ + "require": { + "enjin/platform-core": "dev-master" + }, + "repositories": [ + { + "type": "vcs", + "url": "git@github.com:enjin/platform-core.git" + } + ] +} +``` + +You can then run `composer install` to have it installed in your laravel application. +After that you will need to build one dependency by typing: + +```bash +cd vendor/gmajor/sr25519-bindings/go && go build -buildmode=c-shared -o sr25519.so . && mv sr25519.so ../src/Crypto/sr25519.so +``` + +This package will load its migrations automatically, execute them by running: + +```bash +php artisan migrate +``` + +You can publish the config file with: + +```bash +php artisan vendor:publish --tag="platform-core-config" +``` + + +## Usage + +First, you should sync your platform with a snapshot of Efinity state: +```bash +php artisan platform:sync +``` + +After that you need to start fetching the blocks from the blockchain: +```bash +php artisan platform:ingest +``` + +Then you should start the processor to update your local database: +```bash +php artisan queue:work + +# Or, if you're using Laravel Horizon +php artisan horizon +``` + +Finally, you may start the development server to access the API by running: +```bash +php artisan serve +``` + +You will find the GraphiQL playground on: +``` +http://localhost:8000/graphiql +``` + +## Changelog + +Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. + +## Contributing + +Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. + +## Security Vulnerabilities + +Please review [our security policy](../../security/policy) on how to report security vulnerabilities. + +## Credits + +- [Enjin](https://github.com/enjin) +- [All Contributors](../../contributors) + +## License + +The LGPL 3.0 License. Please see [License File](LICENSE.md) for more information. diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..b173ed96 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,26 @@ +ignore: + - "**/Ethereum" + - "**/Ethereum/**" + - "**/Ethereum.php" + +github_checks: + annotations: true + +coverage: + range: 80..100 + round: down + precision: 2 + + status: + project: + default: + enabled: no + target: 80% + threshold: 0.5% + if_not_found: success + if_ci_failed: error + patch: + default: + enabled: no + target: 80% + threshold: 10% \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 00000000..159eaee3 --- /dev/null +++ b/composer.json @@ -0,0 +1,109 @@ +{ + "name": "enjin/platform-core", + "description": "The core package for the Enjin Platform.", + "keywords": [ + "enjin", + "platform", + "enjin platform", + "enjin platform package", + "laravel" + ], + "homepage": "https://github.com/enjin/platform-core", + "license": "LGPL-3.0-only", + "authors": [ + { + "name": "Enjin", + "email": "support@enjin.io" + } + ], + "require": { + "php": "^8.1|^8.2", + "ext-bcmath": "*", + "ext-ffi": "*", + "ext-gmp": "*", + "ext-json": "*", + "ext-redis": "*", + "ext-sodium": "*", + "amphp/amp": "^3.0", + "amphp/http": "2.x-dev#b9f04ee", + "amphp/http-client": "v5.x-dev#149cc0f", + "amphp/parallel": "^2.1.0", + "amphp/socket": "^2.0.0", + "amphp/websocket": "v2.x-dev#1bdf01a", + "amphp/websocket-client": "v2.x-dev#89f6a4a", + "composer/semver": "^3.0", + "doctrine/dbal": "^3.0", + "enjin/php-blockchain-tools": "^1.0", + "fakerphp/faker": "^1.0", + "gmajor/sr25519-bindings": "dev-main", + "gmajor/substrate-codec-php": "dev-master", + "guzzlehttp/guzzle": "7.6.1", + "illuminate/contracts": "^10.0", + "kevinrob/guzzle-cache-middleware": "^4.0", + "mll-lab/laravel-graphiql": "^3.0", + "phrity/websocket": "^1.0", + "rebing/graphql-laravel": "^9.0.0-rc1", + "revolt/event-loop": "^1.0", + "spatie/laravel-package-tools": "^1.0", + "spatie/laravel-ray": "^1.0", + "staudenmeir/eloquent-eager-limit": "^1.0", + "stechstudio/backoff": "^1.0", + "tuupola/base58": "^2.0" + }, + "require-dev": { + "dms/phpunit-arraysubset-asserts": "dev-master", + "friendsofphp/php-cs-fixer": "^3.0", + "nunomaduro/collision": "^7.0", + "nunomaduro/larastan": "^2.0", + "orchestra/testbench": "^8.0", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/php-code-coverage": "^10.0", + "phpunit/phpunit": "^10.0", + "roave/security-advisories": "dev-latest" + }, + "repositories": [], + "autoload": { + "psr-4": { + "Enjin\\Platform\\": "src", + "Enjin\\Platform\\Database\\Factories\\": "database/factories" + } + }, + "autoload-dev": { + "psr-4": { + "Enjin\\Platform\\Tests\\": "tests" + }, + "classmap": [ + "src/GraphQL", + "src/Services" + ] + }, + "scripts": { + "build-sr25519": "cd vendor/gmajor/sr25519-bindings/go && go build -buildmode=c-shared -o sr25519.so . && mv sr25519.so ../src/Crypto/sr25519.so", + "analyse": "vendor/bin/phpstan analyse", + "test": "vendor/bin/phpunit", + "test-coverage": "vendor/bin/phpunit --coverage-html ../../temp/coverage", + "post-autoload-dump": [ + "@php ./vendor/bin/testbench package:discover --ansi" + ] + }, + "config": { + "sort-packages": true, + "allow-plugins": { + "phpstan/extension-installer": true + } + }, + "extra": { + "laravel": { + "providers": [ + "Enjin\\Platform\\CoreServiceProvider" + ], + "aliases": { + "Package": "Enjin\\Platform\\Facades\\Package" + } + } + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/config/enjin-platform.php b/config/enjin-platform.php new file mode 100644 index 00000000..a2af5497 --- /dev/null +++ b/config/enjin-platform.php @@ -0,0 +1,215 @@ + env('AUTH_DRIVER'), + + /* + |-------------------------------------------------------------------------- + | Authentication Methods + |-------------------------------------------------------------------------- + | + | These are the supported authentication drivers + | + */ + 'auth_drivers' => [ + 'basic_token' => [ + 'driver' => 'basic_token', + 'token' => env('BASIC_AUTH_TOKEN'), + ], + ], + + /* + |-------------------------------------------------------------------------- + | Uses decoder container + |-------------------------------------------------------------------------- + | + | If you wish to use a decoder container set the host here. + | + */ + 'decoder_container' => env('DECODER_CONTAINER', '127.0.0.1:8090'), + + /* + |-------------------------------------------------------------------------- + | Deep links + |-------------------------------------------------------------------------- + | + | Here you can change the deep links used throughout the platform. + | + */ + 'deep_links' => [ + 'proof' => env('PROOF_DEEPLINK', 'https://deeplink.wallet.enjin.io/proof/'), + ], + + /* + |-------------------------------------------------------------------------- + | Token ID Encoder + |-------------------------------------------------------------------------- + | + | This defines the default encoder to use to encode your token ID + | + */ + 'token_id_encoder' => env('TOKEN_ID_ENCODER', 'hash'), + + /* + |-------------------------------------------------------------------------- + | Token ID Encoders + |-------------------------------------------------------------------------- + | + | These are the different encoders supported base from the best practices + | https://platform.docs.enjin.io/getting-started-with-the-platform-api/tokenid-structure-best-practices + | + */ + 'token_id_encoders' => [ + 'hash' => [ + 'driver' => 'hash', + 'algo' => 'blake2', + ], + ], + + /* + |-------------------------------------------------------------------------- + | The blockchain networks + |-------------------------------------------------------------------------- + | + | These are the list of networks that platform is currently supporting. + | You may configure the network setting for each network. + | + */ + 'chains' => [ + 'supported' => [ + 'substrate' => [ + 'enjin' => [ + 'chain-id' => 0, + 'network-id' => 2000, + 'testnet' => false, + 'platform-id' => env('SUBSTRATE_ENJIN_PLATFORM_ID', 0), + 'node' => env('SUBSTRATE_ENJIN_RPC', 'wss://rpc.matrix.blockchain.enjin.io'), + 'ss58-prefix' => env('SUBSTRATE_ENJIN_SS58_PREFIX', 12120), + ], + 'canary' => [ + 'chain-id' => 0, + 'network-id' => 2010, + 'testnet' => true, + 'platform-id' => env('SUBSTRATE_CANARY_PLATFORM_ID', 0), + 'node' => env('SUBSTRATE_CANARY_RPC', 'wss://rpc.matrix.canary.enjin.io'), + 'ss58-prefix' => env('SUBSTRATE_CANARY_SS58_PREFIX', 9030), + ], + 'polkadot' => [ + 'chain-id' => 0, + 'network-id' => 101, + 'testnet' => false, + 'platform-id' => env('EFINITY_POLKADOT_PLATFORM_ID', 0), + 'node' => env('EFINITY_POLKADOT_RPC', 'wss://rpc.efinity.io:443'), + 'ss58-prefix' => env('EFINITY_POLKADOT_SS58_PREFIX', 1110), + ], + 'rococo' => [ + 'chain-id' => 0, + 'network-id' => 102, + 'testnet' => true, + 'platform-id' => env('EFINITY_ROCOCO_PLATFORM_ID', 0), + 'node' => env('EFINITY_ROCOCO_RPC', 'wss://rpc.rococo.efinity.io:443'), + 'ss58-prefix' => env('EFINITY_ROCOCO_SS58_PREFIX', 195), + ], + 'westend' => [ + 'chain-id' => 0, + 'network-id' => 103, + 'testnet' => true, + 'platform-id' => env('EFINITY_WESTEND_PLATFORM_ID', 0), + 'node' => env('EFINITY_WESTEND_RPC', 'http://localhost:2021'), + 'ss58-prefix' => env('EFINITY_WESTEND_SS58_PREFIX', 1110), + ], + 'local' => [ + 'chain-id' => 0, + 'network-id' => 104, + 'testnet' => true, + 'platform-id' => env('EFINITY_LOCAL_PLATFORM_ID', 0), + 'node' => env('EFINITY_LOCAL_RPC', 'ws://localhost:10010'), + 'ss58-prefix' => env('EFINITY_LOCAL_SS58_PREFIX', 195), + ], + 'developer' => [ + 'chain-id' => 0, + 'network-id' => 105, + 'testnet' => true, + 'platform-id' => env('EFINITY_DEVELOPER_PLATFORM_ID', 0), + 'node' => env('EFINITY_DEVELOPER_RPC', 'wss://rpc.parachain.efinitystaging.com:443'), + 'ss58-prefix' => env('EFINITY_DEVELOPER_SS58_PREFIX', 195), + ], + ], + ], + + 'selected' => env('CHAIN', 'substrate'), + + 'network' => env('NETWORK', 'developer'), + + 'daemon-account' => env('DAEMON_ACCOUNT') ?: '0x0000000000000000000000000000000000000000000000000000000000000000', + ], + + /* + |-------------------------------------------------------------------------- + | The pagination limit + |-------------------------------------------------------------------------- + | + | Here you may set the default pagination limit for the APIs + | + */ + 'pagination' => [ + 'limit' => env('DEFAULT_PAGINATION_LIMIT', 15), + ], + + /* + |-------------------------------------------------------------------------- + | The indexing IDs + |-------------------------------------------------------------------------- + | + | Here you may set the collection chain IDs for the indexer. + | + */ + 'indexing' => [ + 'filters' => [ + 'collections' => array_filter(explode(',', env('INDEX_COLLECTIONS', ''))), + ], + ], + + /* + |-------------------------------------------------------------------------- + | The flag to cache event + |-------------------------------------------------------------------------- + | + | When true, events are cached + | + */ + 'cache_events' => env('PLATFORM_CACHE_EVENTS', true), + + /* + |-------------------------------------------------------------------------- + | The websocket channel name + |-------------------------------------------------------------------------- + | + | Here you may configure the name of the websocket channel + | + */ + 'platform_channel' => env('PLATFORM_CHANNEL', 'platform'), + + /* + |-------------------------------------------------------------------------- + | The ingest sync wait timeout + |-------------------------------------------------------------------------- + | + | Here you may set how long the ingest command to wait for the sync to finish + | + */ + 'sync_max_wait_timeout' => env('SYNC_MAX_WAIT_TIMEOUT', 3600), + +]; diff --git a/config/graphql.php b/config/graphql.php new file mode 100644 index 00000000..cbb9dbcd --- /dev/null +++ b/config/graphql.php @@ -0,0 +1,221 @@ + [ + // The prefix for routes; do NOT use a leading slash! + 'prefix' => 'graphql', + + // The controller/method to use in GraphQL request. + // Also supported array syntax: `[\Rebing\GraphQL\GraphQLController::class, 'query']` + 'controller' => \Enjin\Platform\Http\Controllers\GraphQLController::class . '@query', + + // Any middleware for the graphql route group + // This middleware will apply to all schemas + 'middleware' => [ + \Illuminate\Session\Middleware\StartSession::class, + \Enjin\Platform\Middlewares\Authenticated::class, + ], + + // Additional route group attributes + // + // Example: + // + // 'group_attributes' => ['guard' => 'api'] + // + 'group_attributes' => [], + ], + + // The name of the default schema + // Used when the route group is directly accessed + 'default_schema' => 'primary', + + 'batching' => [ + // Whether to support GraphQL batching or not. + // See e.g. https://www.apollographql.com/blog/batching-client-graphql-queries-a685f5bcd41b/ + // for pro and con + 'enable' => true, + ], + + // The schemas for query and/or mutation. It expects an array of schemas to provide + // both the 'query' fields and the 'mutation' fields. + // + // You can also provide a middleware that will only apply to the given schema + // + // Example: + // + // 'schemas' => [ + // 'default' => [ + // 'controller' => MyController::class . '@method', + // 'query' => [ + // App\GraphQL\Queries\UsersQuery::class, + // ], + // 'mutation' => [ + // + // ] + // ], + // 'user' => [ + // 'query' => [ + // App\GraphQL\Queries\ProfileQuery::class, + // ], + // 'mutation' => [ + // + // ], + // 'middleware' => ['auth'], + // ], + // 'user/me' => [ + // 'query' => [ + // App\GraphQL\Queries\MyProfileQuery::class, + // ], + // 'mutation' => [ + // + // ], + // 'middleware' => ['auth'], + // ], + // ] + // + 'schemas' => [ + 'primary' => [ + 'query' => [ + // ExampleQuery::class, + ], + 'mutation' => [ + // ExampleMutation::class, + ], + // The types only available in this schema + 'types' => [ + // ExampleType::class, + ], + + // Laravel HTTP middleware + 'middleware' => null, + + // Which HTTP methods to support; must be given in UPPERCASE! + 'method' => ['POST'], + + // An array of middlewares, overrides the global ones + 'execution_middleware' => null, + ], + ], + + // The global types available to all schemas. + // You can then access it from the facade like this: GraphQL::type('user') + // + // Example: + // + // 'types' => [ + // App\GraphQL\Types\UserType::class + // ] + // + 'types' => [ + // ExampleType::class, + // ExampleRelationType::class, + \Rebing\GraphQL\Support\UploadType::class, + ], + + // This callable will be passed the Error object for each errors GraphQL catch. + // The method should return an array representing the error. + // Typically: + // [ + // 'message' => '', + // 'locations' => [] + // ] + 'error_formatter' => [\Rebing\GraphQL\GraphQL::class, 'formatError'], + + /* + * Custom Error Handling + * + * Expected handler signature is: function (array $errors, callable $formatter): array + * + * The default handler will pass exceptions to laravel Error Handling mechanism + */ + 'errors_handler' => [\Rebing\GraphQL\GraphQL::class, 'handleErrors'], + + /* + * Options to limit the query complexity and depth. See the doc + * @ https://webonyx.github.io/graphql-php/security + * for details. Disabled by default. + */ + 'security' => [ + 'query_max_complexity' => null, + 'query_max_depth' => null, + 'disable_introspection' => false, + ], + + /* + * You can define your own pagination type. + * Reference \Rebing\GraphQL\Support\PaginationType::class + */ + 'pagination_type' => \Enjin\Platform\GraphQL\Types\Pagination\ConnectionType::class, + + /* + * You can define your own simple pagination type. + * Reference \Rebing\GraphQL\Support\SimplePaginationType::class + */ + 'simple_pagination_type' => \Rebing\GraphQL\Support\SimplePaginationType::class, + + /* + * Overrides the default field resolver + * See http://webonyx.github.io/graphql-php/data-fetching/#default-field-resolver + * + * Example: + * + * ```php + * 'defaultFieldResolver' => function ($root, $args, $context, $info) { + * }, + * ``` + * or + * ```php + * 'defaultFieldResolver' => [SomeKlass::class, 'someMethod'], + * ``` + */ + 'defaultFieldResolver' => null, + + /* + * Any headers that will be added to the response returned by the default controller + */ + 'headers' => [], + + /* + * Any JSON encoding options when returning a response from the default controller + * See http://php.net/manual/function.json-encode.php for the full list of options + */ + 'json_encoding_options' => 0, + + /* + * Automatic Persisted Queries (APQ) + * See https://www.apollographql.com/docs/apollo-server/performance/apq/ + * + * Note 1: this requires the `AutomaticPersistedQueriesMiddleware` being enabled + * + * Note 2: even if APQ is disabled per configuration and, according to the "APQ specs" (see above), + * to return a correct response in case it's not enabled, the middleware needs to be active. + * Of course if you know you do not have a need for APQ, feel free to remove the middleware completely. + */ + 'apq' => [ + // Enable/Disable APQ - See https://www.apollographql.com/docs/apollo-server/performance/apq/#disabling-apq + 'enable' => env('GRAPHQL_APQ_ENABLE', false), + + // The cache driver used for APQ + 'cache_driver' => env('GRAPHQL_APQ_CACHE_DRIVER', config('cache.default')), + + // The cache prefix + 'cache_prefix' => config('cache.prefix') . ':graphql.apq', + + // The cache ttl in seconds - See https://www.apollographql.com/docs/apollo-server/performance/apq/#adjusting-cache-time-to-live-ttl + 'cache_ttl' => 300, + ], + + /* + * Execution middlewares + */ + 'execution_middleware' => [ + \Rebing\GraphQL\Support\ExecutionMiddleware\ValidateOperationParamsMiddleware::class, + // AutomaticPersistedQueriesMiddleware listed even if APQ is disabled, see the docs for the `'apq'` configuration + \Rebing\GraphQL\Support\ExecutionMiddleware\AutomaticPersistedQueriesMiddleware::class, + \Rebing\GraphQL\Support\ExecutionMiddleware\AddAuthUserContextValueMiddleware::class, + // \Rebing\GraphQL\Support\ExecutionMiddleware\UnusedVariablesMiddleware::class, + \Enjin\Platform\Middlewares\UniqueFieldNamesArray::class, + ], +]; diff --git a/database/factories/AttributeFactory.php b/database/factories/AttributeFactory.php new file mode 100644 index 00000000..a61a531a --- /dev/null +++ b/database/factories/AttributeFactory.php @@ -0,0 +1,33 @@ + + */ + public function definition() + { + return [ + 'collection_id' => Collection::factory(), + 'token_id' => Token::factory(), + 'key' => fake()->unique()->word(), + 'value' => fake()->text(), + ]; + } +} diff --git a/database/factories/BlockFactory.php b/database/factories/BlockFactory.php new file mode 100644 index 00000000..e57d5340 --- /dev/null +++ b/database/factories/BlockFactory.php @@ -0,0 +1,29 @@ + + */ + public function definition() + { + return [ + 'number' => random_int(1, 1000), + 'hash' => $this->faker->unique()->public_key(), + ]; + } +} diff --git a/database/factories/CollectionAccountApprovalFactory.php b/database/factories/CollectionAccountApprovalFactory.php new file mode 100644 index 00000000..1bca6785 --- /dev/null +++ b/database/factories/CollectionAccountApprovalFactory.php @@ -0,0 +1,32 @@ + + */ + public function definition() + { + return [ + 'collection_account_id' => CollectionAccount::factory(), + 'wallet_id' => Wallet::factory(), + 'expiration' => fake()->numberBetween(1), + ]; + } +} diff --git a/database/factories/CollectionAccountFactory.php b/database/factories/CollectionAccountFactory.php new file mode 100644 index 00000000..4eaeb7e0 --- /dev/null +++ b/database/factories/CollectionAccountFactory.php @@ -0,0 +1,33 @@ + + */ + public function definition() + { + return [ + 'collection_id' => Collection::factory(), + 'wallet_id' => Wallet::factory(), + 'is_frozen' => false, + 'account_count' => 0, + ]; + } +} diff --git a/database/factories/CollectionFactory.php b/database/factories/CollectionFactory.php new file mode 100644 index 00000000..463e6ddc --- /dev/null +++ b/database/factories/CollectionFactory.php @@ -0,0 +1,38 @@ + + */ + public function definition() + { + return [ + 'collection_chain_id' => (string) fake()->unique()->numberBetween(2000), + 'owner_wallet_id' => Wallet::factory(), + 'max_token_count' => fake()->numberBetween(1), + 'max_token_supply' => (string) fake()->numberBetween(1), + 'force_single_mint' => fake()->boolean(), + 'is_frozen' => false, + 'token_count' => '0', + 'attribute_count' => '0', + 'total_deposit' => '0', + 'network' => 'developer', + ]; + } +} diff --git a/database/factories/CollectionRoyaltyCurrencyFactory.php b/database/factories/CollectionRoyaltyCurrencyFactory.php new file mode 100644 index 00000000..982b466b --- /dev/null +++ b/database/factories/CollectionRoyaltyCurrencyFactory.php @@ -0,0 +1,32 @@ + + */ + public function definition() + { + return [ + 'collection_id' => Collection::factory(), + 'currency_collection_chain_id' => Collection::factory()->create()->collection_chain_id, + 'currency_token_chain_id' => Token::factory()->create()->token_chain_id, + ]; + } +} diff --git a/database/factories/EventFactory.php b/database/factories/EventFactory.php new file mode 100644 index 00000000..6f425ffe --- /dev/null +++ b/database/factories/EventFactory.php @@ -0,0 +1,39 @@ + + */ + public function definition() + { + return [ + 'transaction_id' => Transaction::factory(), + 'phase' => 0, + 'look_up' => '2800', + 'module_id' => 'MultiTokens', + 'event_id' => 'CollectionCreated', + 'params' => sprintf( + '[{"type":"U128","value":"%s"},{"type":"sp_core:crypto:AccountId32","value":"%s"}]', + $this->faker->unique()->randomNumber(), + HexConverter::unPrefix(config('enjin-platform.chains.daemon-account') ?? $this->faker->unique()->public_key()) + ), + ]; + } +} diff --git a/database/factories/TokenAccountApprovalFactory.php b/database/factories/TokenAccountApprovalFactory.php new file mode 100644 index 00000000..6f50e33a --- /dev/null +++ b/database/factories/TokenAccountApprovalFactory.php @@ -0,0 +1,33 @@ + + */ + public function definition() + { + return [ + 'token_account_id' => TokenAccount::factory(), + 'wallet_id' => Wallet::factory(), + 'amount' => (string) fake()->numberBetween(1), + 'expiration' => fake()->numberBetween(1), + ]; + } +} diff --git a/database/factories/TokenAccountFactory.php b/database/factories/TokenAccountFactory.php new file mode 100644 index 00000000..1cfdba8c --- /dev/null +++ b/database/factories/TokenAccountFactory.php @@ -0,0 +1,38 @@ + + */ + public function definition() + { + return [ + 'wallet_id' => Wallet::factory(), + 'collection_id' => $collectionFactory = Collection::factory()->create(), + 'token_id' => Token::factory([ + 'collection_id' => $collectionFactory, + ]), + 'balance' => (string) fake()->numberBetween(1), + 'reserved_balance' => '0', + 'is_frozen' => false, + ]; + } +} diff --git a/database/factories/TokenAccountNamedReserveFactory.php b/database/factories/TokenAccountNamedReserveFactory.php new file mode 100644 index 00000000..75fe599a --- /dev/null +++ b/database/factories/TokenAccountNamedReserveFactory.php @@ -0,0 +1,32 @@ + + */ + public function definition() + { + return [ + 'token_account_id' => TokenAccount::factory(), + 'pallet' => fake()->randomElement(PalletIdentifier::caseNamesAsArray()), + 'amount' => (string) fake()->numberBetween(1), + ]; + } +} diff --git a/database/factories/TokenFactory.php b/database/factories/TokenFactory.php new file mode 100644 index 00000000..416f0533 --- /dev/null +++ b/database/factories/TokenFactory.php @@ -0,0 +1,39 @@ + + */ + public function definition() + { + return [ + 'collection_id' => Collection::factory(), + 'token_chain_id' => (string) fake()->unique()->numberBetween(), + 'supply' => (string) $supply = fake()->numberBetween(1), + 'cap' => TokenMintCapType::INFINITE->name, + 'cap_supply' => null, + 'is_frozen' => false, + 'unit_price' => (string) $unitPrice = fake()->numberBetween(1 / $supply * 10 ** 17), + 'mint_deposit' => (string) ($unitPrice * $supply), + 'minimum_balance' => '1', + 'attribute_count' => '0', + ]; + } +} diff --git a/database/factories/TransactionFactory.php b/database/factories/TransactionFactory.php new file mode 100644 index 00000000..a4650dc4 --- /dev/null +++ b/database/factories/TransactionFactory.php @@ -0,0 +1,39 @@ + + */ + public function definition() + { + return [ + 'transaction_chain_id' => fake()->unique()->numerify('#####-#'), + 'wallet_public_key' => config('enjin-platform.chains.daemon-account') ?? $this->faker->unique()->public_key(), + 'transaction_chain_hash' => HexConverter::prefix(fake()->unique()->sha256()), + 'encoded_data' => HexConverter::prefix(fake()->unique()->sha256()), + 'state' => TransactionState::PENDING->name, + 'result' => null, + 'method' => fake()->randomElement((new TransactionMethodEnum())->getAttributes()['values']), + 'idempotency_key' => fake()->uuid(), + 'signed_at_block' => fake()->numberBetween(), + ]; + } +} diff --git a/database/factories/VerificationFactory.php b/database/factories/VerificationFactory.php new file mode 100644 index 00000000..d4fd596b --- /dev/null +++ b/database/factories/VerificationFactory.php @@ -0,0 +1,33 @@ + + */ + public function definition() + { + $verification = VerificationService::generate(); + + return [ + 'verification_id' => $verification['verification_id'], + 'code' => $verification['code'], + 'public_key' => null, + ]; + } +} diff --git a/database/factories/WalletFactory.php b/database/factories/WalletFactory.php new file mode 100644 index 00000000..73bdc4cf --- /dev/null +++ b/database/factories/WalletFactory.php @@ -0,0 +1,38 @@ + + */ + public function definition() + { + // For some reason the observer is not called when running PHPUnit + // This makes sure the cache is cleaned when a new account is created on tests + Cache::forget(PlatformCache::MANAGED_ACCOUNTS->key()); + + return [ + 'public_key' => $this->faker->unique()->public_key(), + 'external_id' => fake()->unique()->uuid(), + 'managed' => fake()->boolean(), + 'verification_id' => fake()->unique()->uuid(), + 'network' => 'developer', + ]; + } +} diff --git a/database/migrations/2022_04_09_120404_create_wallets_table.php b/database/migrations/2022_04_09_120404_create_wallets_table.php new file mode 100644 index 00000000..1ad9a8fe --- /dev/null +++ b/database/migrations/2022_04_09_120404_create_wallets_table.php @@ -0,0 +1,34 @@ +id(); + $table->string('external_id')->unique()->nullable(); + $table->string('verification_id')->unique()->nullable(); + $table->string('public_key')->unique()->nullable(); + $table->boolean('managed')->default(false)->index(); + $table->string('network'); + $table->string('linking_code', 9)->unique()->nullable(); + $table->timestamps(); + + $table->index(['managed', 'public_key']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::drop('wallets'); + } +}; diff --git a/database/migrations/2022_04_09_120405_create_collections_table.php b/database/migrations/2022_04_09_120405_create_collections_table.php new file mode 100644 index 00000000..e56d38ca --- /dev/null +++ b/database/migrations/2022_04_09_120405_create_collections_table.php @@ -0,0 +1,47 @@ +id(); + $table->string('collection_chain_id')->index(); + $table->foreignId('owner_wallet_id') + ->index() + ->constrained('wallets') + ->cascadeOnUpdate() + ->cascadeOnDelete(); + $table->string('max_token_count')->nullable(); + $table->string('max_token_supply')->nullable(); + $table->boolean('force_single_mint')->default(false); + $table->foreignId('royalty_wallet_id') + ->nullable() + ->index() + ->constrained('wallets') + ->cascadeOnUpdate() + ->cascadeOnDelete(); + $table->float('royalty_percentage')->nullable(); + $table->boolean('is_frozen')->default(false); + $table->string('token_count')->default('0'); + $table->string('attribute_count')->default('0'); + $table->string('total_deposit')->default('0'); + $table->string('network'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::drop('collections'); + } +}; diff --git a/database/migrations/2022_04_09_120406_create_collection_accounts_table.php b/database/migrations/2022_04_09_120406_create_collection_accounts_table.php new file mode 100644 index 00000000..43d4547a --- /dev/null +++ b/database/migrations/2022_04_09_120406_create_collection_accounts_table.php @@ -0,0 +1,40 @@ +id(); + $table->foreignId('wallet_id') + ->index() + ->constrained() + ->cascadeOnUpdate() + ->cascadeOnDelete(); + $table->foreignId('collection_id') + ->index() + ->constrained() + ->cascadeOnUpdate() + ->cascadeOnDelete(); + $table->boolean('is_frozen')->default(false); + $table->string('account_count')->default('0'); + $table->timestamps(); + + $table->index(['collection_id', 'wallet_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::drop('collection_accounts'); + } +}; diff --git a/database/migrations/2022_04_09_120407_create_tokens_table.php b/database/migrations/2022_04_09_120407_create_tokens_table.php new file mode 100644 index 00000000..bbd0dcbd --- /dev/null +++ b/database/migrations/2022_04_09_120407_create_tokens_table.php @@ -0,0 +1,51 @@ +id(); + $table->foreignId('collection_id') + ->index() + ->constrained() + ->cascadeOnUpdate() + ->cascadeOnDelete(); + $table->string('token_chain_id')->index(); + $table->string('supply')->default('1'); + $table->string('cap'); + $table->string('cap_supply')->nullable(); + $table->foreignId('royalty_wallet_id') + ->nullable() + ->index() + ->constrained('wallets') + ->cascadeOnUpdate() + ->cascadeOnDelete(); + $table->float('royalty_percentage')->nullable(); + $table->boolean('is_currency')->default(false); + $table->boolean('listing_forbidden')->default(false); + $table->boolean('is_frozen')->default(false); + $table->string('minimum_balance')->default('1'); + $table->string('unit_price')->default('0'); + $table->string('mint_deposit')->default('0'); + $table->integer('attribute_count')->default(0); + $table->timestamps(); + + $table->index(['collection_id', 'token_chain_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::drop('tokens'); + } +}; diff --git a/database/migrations/2022_04_09_120408_create_token_accounts_table.php b/database/migrations/2022_04_09_120408_create_token_accounts_table.php new file mode 100644 index 00000000..a874ac84 --- /dev/null +++ b/database/migrations/2022_04_09_120408_create_token_accounts_table.php @@ -0,0 +1,47 @@ +id(); + $table->foreignId('wallet_id') + ->index() + ->constrained() + ->cascadeOnUpdate() + ->cascadeOnDelete(); + $table->foreignId('collection_id') + ->index() + ->constrained() + ->cascadeOnUpdate() + ->cascadeOnDelete(); + $table->foreignId('token_id') + ->index() + ->constrained() + ->cascadeOnUpdate() + ->cascadeOnDelete(); + $table->string('balance')->default('1'); + $table->string('reserved_balance')->default('0'); + $table->boolean('is_frozen')->default(false); + $table->timestamps(); + + $table->index(['wallet_id', 'collection_id']); + $table->index(['wallet_id', 'collection_id', 'token_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::drop('token_accounts'); + } +}; diff --git a/database/migrations/2022_04_09_120409_create_attributes_table.php b/database/migrations/2022_04_09_120409_create_attributes_table.php new file mode 100644 index 00000000..6686bfeb --- /dev/null +++ b/database/migrations/2022_04_09_120409_create_attributes_table.php @@ -0,0 +1,42 @@ +id(); + $table->foreignId('collection_id') + ->index() + ->constrained() + ->cascadeOnUpdate() + ->cascadeOnDelete(); + $table->foreignId('token_id') + ->nullable() + ->index() + ->constrained() + ->cascadeOnUpdate() + ->cascadeOnDelete(); + $table->string('key'); + $table->text('value'); + $table->timestamps(); + + $table->index(['collection_id', 'token_id']); + $table->index(['collection_id', 'token_id', 'key']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::drop('attributes'); + } +}; diff --git a/database/migrations/2022_04_09_120410_create_blocks_table.php b/database/migrations/2022_04_09_120410_create_blocks_table.php new file mode 100644 index 00000000..284e7127 --- /dev/null +++ b/database/migrations/2022_04_09_120410_create_blocks_table.php @@ -0,0 +1,34 @@ +id(); + $table->integer('number')->unique(); + $table->string('hash')->nullable(); + $table->boolean('synced')->index()->default(false); + $table->boolean('failed')->default(false); + $table->longText('exception')->nullable(); + $table->boolean('retried')->default(false); + $table->longText('events')->nullable(); + $table->longText('extrinsics')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::drop('blocks'); + } +}; diff --git a/database/migrations/2022_04_09_120411_create_transactions_table.php b/database/migrations/2022_04_09_120411_create_transactions_table.php new file mode 100644 index 00000000..ba164a48 --- /dev/null +++ b/database/migrations/2022_04_09_120411_create_transactions_table.php @@ -0,0 +1,37 @@ +id(); + $table->string('idempotency_key', 255)->nullable()->unique(); + $table->string('transaction_chain_id')->index()->nullable(); + $table->string('transaction_chain_hash')->nullable(); + $table->char('wallet_public_key', 70)->index(); + $table->string('method'); + $table->char('state', 15)->index()->default('PENDING'); + $table->string('result')->nullable(); + $table->text('encoded_data')->nullable(); + $table->timestamps(); + + $table->index(['state', 'wallet_public_key']); + $table->index(['transaction_chain_hash', 'wallet_public_key']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::drop('transactions'); + } +}; diff --git a/database/migrations/2022_05_26_101200_create_verifications_table.php b/database/migrations/2022_05_26_101200_create_verifications_table.php new file mode 100644 index 00000000..bca1272f --- /dev/null +++ b/database/migrations/2022_05_26_101200_create_verifications_table.php @@ -0,0 +1,29 @@ +id(); + $table->string('verification_id')->unique(); + $table->string('code'); + $table->string('public_key')->nullable()->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::drop('verifications'); + } +}; diff --git a/database/migrations/2022_06_19_103015_create_token_account_approvals_table.php b/database/migrations/2022_06_19_103015_create_token_account_approvals_table.php new file mode 100644 index 00000000..29e99bf3 --- /dev/null +++ b/database/migrations/2022_06_19_103015_create_token_account_approvals_table.php @@ -0,0 +1,38 @@ +id(); + $table->foreignId('token_account_id') + ->index() + ->constrained() + ->cascadeOnUpdate() + ->cascadeOnDelete(); + $table->foreignId('wallet_id') + ->index() + ->constrained() + ->cascadeOnUpdate() + ->cascadeOnDelete(); + $table->string('amount')->default('0'); + $table->unsignedInteger('expiration')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::drop('token_account_approvals'); + } +}; diff --git a/database/migrations/2022_06_19_144041_create_token_account_named_reserves_table.php b/database/migrations/2022_06_19_144041_create_token_account_named_reserves_table.php new file mode 100644 index 00000000..ff1cfba2 --- /dev/null +++ b/database/migrations/2022_06_19_144041_create_token_account_named_reserves_table.php @@ -0,0 +1,33 @@ +id(); + $table->foreignId('token_account_id') + ->index() + ->constrained() + ->cascadeOnUpdate() + ->cascadeOnDelete(); + $table->string('pallet'); + $table->string('amount')->default('0'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::drop('token_account_named_reserves'); + } +}; diff --git a/database/migrations/2022_06_19_152115_create_collection_account_approvals_table.php b/database/migrations/2022_06_19_152115_create_collection_account_approvals_table.php new file mode 100644 index 00000000..e6840349 --- /dev/null +++ b/database/migrations/2022_06_19_152115_create_collection_account_approvals_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('collection_account_id') + ->index() + ->constrained() + ->cascadeOnUpdate() + ->cascadeOnDelete(); + $table->foreignId('wallet_id') + ->index() + ->constrained() + ->cascadeOnUpdate() + ->cascadeOnDelete(); + $table->unsignedInteger('expiration')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::drop('collection_account_approvals'); + } +}; diff --git a/database/migrations/2022_09_14_144041_create_collection_royalty_currencies_table.php b/database/migrations/2022_09_14_144041_create_collection_royalty_currencies_table.php new file mode 100644 index 00000000..7aea7642 --- /dev/null +++ b/database/migrations/2022_09_14_144041_create_collection_royalty_currencies_table.php @@ -0,0 +1,33 @@ +id(); + $table->foreignId('collection_id') + ->index() + ->constrained() + ->cascadeOnUpdate() + ->cascadeOnDelete(); + $table->string('currency_collection_chain_id')->index(); + $table->string('currency_token_chain_id')->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::drop('collection_royalty_currencies'); + } +}; diff --git a/database/migrations/2022_10_22_131819_create_events_table.php b/database/migrations/2022_10_22_131819_create_events_table.php new file mode 100644 index 00000000..65071bf0 --- /dev/null +++ b/database/migrations/2022_10_22_131819_create_events_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('transaction_id') + ->index() + ->constrained() + ->cascadeOnUpdate() + ->cascadeOnDelete(); + $table->integer('phase'); + $table->string('look_up'); + $table->string('module_id')->index(); + $table->string('event_id')->index(); + $table->text('params')->nullable(); + + $table->index(['module_id', 'event_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::drop('events'); + } +}; diff --git a/database/migrations/2023_02_03_135022_create_pending_events_table.php b/database/migrations/2023_02_03_135022_create_pending_events_table.php new file mode 100644 index 00000000..cac0dbb5 --- /dev/null +++ b/database/migrations/2023_02_03_135022_create_pending_events_table.php @@ -0,0 +1,30 @@ +id(); + $table->uuid()->unique(); + $table->string('name')->index(); + $table->timestamp('sent')->index(); + $table->json('channels'); + $table->json('data'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::drop('pending_events'); + } +}; diff --git a/database/migrations/2023_03_20_204012_add_signed_at_block.php b/database/migrations/2023_03_20_204012_add_signed_at_block.php new file mode 100644 index 00000000..42eec204 --- /dev/null +++ b/database/migrations/2023_03_20_204012_add_signed_at_block.php @@ -0,0 +1,27 @@ +unsignedInteger('signed_at_block')->nullable()->after('encoded_data'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('transactions', function (Blueprint $table) { + $table->dropColumn('signed_at_block'); + }); + } +}; diff --git a/database/migrations/2023_05_23_034339_remove_link_code_from_wallets_table.php b/database/migrations/2023_05_23_034339_remove_link_code_from_wallets_table.php new file mode 100644 index 00000000..8ac57b2d --- /dev/null +++ b/database/migrations/2023_05_23_034339_remove_link_code_from_wallets_table.php @@ -0,0 +1,27 @@ +dropColumn('linking_code'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('wallets', function (Blueprint $table) { + $table->string('linking_code', 9)->unique()->nullable(); + }); + } +}; diff --git a/lang/en/args.php b/lang/en/args.php new file mode 100644 index 00000000..4da3efa6 --- /dev/null +++ b/lang/en/args.php @@ -0,0 +1,7 @@ + 'The token ID.', + 'idempotencyKey' => 'The idempotency key to set. It is recommended to use a UUID for this.', + 'tokenId' => 'The token ID to set. This must be unique for this collection.', +]; diff --git a/lang/en/commands.php b/lang/en/commands.php new file mode 100644 index 00000000..32c7b307 --- /dev/null +++ b/lang/en/commands.php @@ -0,0 +1,26 @@ + 'Initialize Platform and sync it with a snapshot of Substrate state.', + 'sync.truncating' => 'Truncating the database before syncing', + 'sync.decoding' => 'Decoding, parsing and saving chain data to the database', + 'sync.fetching' => 'Starting to fetch storage values from the RPC node', + 'sync.syncing' => 'Syncing chain state at block #:blockNumber', + 'sync.overview' => '==================== Sync Overview ====================', + 'sync.header' => '==================== Enjin Platform Syncer ====================', + 'sync.total_time' => 'Synced chain state in: :sec seconds', + 'ingest.description' => 'Ingests the blockchain information into Platform database for processing.', + 'transactions.description' => 'Updates Platform transactions with the blockchain data.', + 'transactions.header' => '==================== Enjin Platform Syncer ====================', + 'transactions.specify_start' => 'Please specify the block number to start from with: --from=', + 'transactions.syncing' => 'Syncing transactions from block #:fromBlock to #:toBlock', + 'transactions.start_lower_than_end' => 'The block number to start from must be lower or equal than the block number to end at.', + 'transactions.overview' => '==================== Sync Overview ====================', + 'transactions.total_time' => 'Synced extrinsics in: :sec seconds', + 'transactions.fetching' => 'Starting to fetch storage values from the RPC node', + 'transactions.decoding' => 'Decoding, parsing and saving updates to the database', + 'transactions.total_blocks' => 'Blocks: :blocks', + 'transactions.total_extrinsics' => 'Extrinsics: :extrinsics', + 'clear_cache.description' => 'Clears the Enjin Platform cache.', + 'clear_cache.finished' => 'Enjin Platform cache cleared.', +]; diff --git a/lang/en/connection_input.php b/lang/en/connection_input.php new file mode 100644 index 00000000..e6b8f6f2 --- /dev/null +++ b/lang/en/connection_input.php @@ -0,0 +1,6 @@ + 'The cursor to fetch.', + 'args.first' => 'The number of results to return per page.', +]; diff --git a/lang/en/directive.php b/lang/en/directive.php new file mode 100644 index 00000000..542fdc6c --- /dev/null +++ b/lang/en/directive.php @@ -0,0 +1,5 @@ + "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", +]; diff --git a/lang/en/enum.php b/lang/en/enum.php new file mode 100644 index 00000000..cbe1bf03 --- /dev/null +++ b/lang/en/enum.php @@ -0,0 +1,14 @@ + 'The type of encryption algorithm used to sign messages.', + 'event_type.description' => 'The event types related to blockchain token transactions.', + 'freezable_type.description' => 'The freezable objects supported on-chain.', + 'pallet_identifier.description' => 'The on-chain pallet identifier.', + 'token_market_behavior_type.description' => 'The market behaviour types a token supports.', + 'token_mint_cap_type.description' => 'The mint cap. The token will be classified as fungible if the cap is set to either infinite, or supply with a value higher than one.', + 'token_type.description' => 'The token type, fungible or non-fungible.', + 'transaction_method.description' => 'The currently supported transactions.', + 'transaction_result.description' => 'The result status of a transaction.', + 'transaction_state.description' => "The states in a transaction's lifecycle.", +]; diff --git a/lang/en/enumvalue.php b/lang/en/enumvalue.php new file mode 100644 index 00000000..b78972de --- /dev/null +++ b/lang/en/enumvalue.php @@ -0,0 +1,5 @@ + 'One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.', +]; diff --git a/lang/en/error.php b/lang/en/error.php new file mode 100644 index 00000000..7893e877 --- /dev/null +++ b/lang/en/error.php @@ -0,0 +1,50 @@ + 'The account has already been taken.', + 'auth.auth_not_defined' => 'The auth is not defined.', + 'auth.basic_token.token_not_defined' => 'The basic token is not defined in your .env', + 'auth.driver_not_supported' => 'Driver [:driver] is not supported.', + 'cannot_represent_integer_range' => 'Cannot represent following value as integer range: :value', + 'cannot_represent_integer_ranges_array' => 'Cannot represent following value as integer ranges array: :value', + 'cannot_represent_object' => 'Cannot represent following value as object: ', + 'cannot_represent_uint256' => 'Cannot represent following value as uint256: :value', + 'cannot_set_create_and_mint_params_with_same_recipient' => 'Cannot set create params and mint params for the same recipient.', + 'cannot_set_simple_and_operator_params_for_same_recipient' => 'Cannot set simple params and operator params for the same recipient.', + 'invalid_json' => 'Invalid json format.', + 'middleware.single_arg_only' => 'Please supply just one field.', + 'middleware.single_filter_only.only_one_filter' => 'Only one of these filter(s) can be used: :filterOptions', + 'middleware.single_filter_only.only_used_alone' => 'The filter(s) ":filterOptions" can only be used alone. You cannot combine them with other filters.', + 'not_valid_integer_range' => 'Not a valid integer range.', + 'not_valid_integer_ranges_array' => 'Not a valid integer ranges array.', + 'not_valid_object' => 'Not a valid object.', + 'not_valid_uint256' => 'Not a valid uint256.', + 'serialization.method_does_not_exist' => "Method ':method' does not exist.", + 'set_either_create_or_mint_param_for_recipient' => 'You need to set either create params or mint params for every recipient.', + 'set_either_simple_and_operator_params_for_recipient' => 'You need to set either simple params or operator params for every recipient.', + 'supply_cap_must_be_greater_than_initial' => 'Supply CAP amount must be greater than or equal to initial supply.', + 'supply_cap_must_be_set' => 'Supply CAP amount must be set when using Supply CAP.', + 'there_can_only_one_input_name' => 'There can be only one input field named ":name".', + 'token_id_encoder.encoder_config_not_defined' => 'The encoder config is not defined.', + 'token_id_encoder.encoder_not_supported' => 'Encoder [:driverClass] is not supported.', + 'token_id_encoder.invalid_data' => 'Invalid data supplied to encoder.', + 'token_id_encoder.token_id_encoder_not_defined_in_env' => 'The token_id_encoder is not defined in the .env', + 'token_id_encoder.hash.algo_not_defined_in_config' => 'The hash algo is not defined in the config.', + 'token_id_encoder.hash.algo_not_supported' => 'Algo :algo is not supported.', + 'token_int_too_large' => 'This tokenId cannot be used as the int value it converts to is larger than a 128bit uint.', + 'token_not_found' => 'Token not found.', + 'transaction_not_found' => 'Transaction not found.', + 'unable_to_load_metadata' => 'Unable to load the metadata files.', + 'unauthorized_header' => 'Unauthorized. Please provide a valid Authorization header.', + 'verification.invalid_signature' => 'The signature provided is not valid.', + 'verification.unable_to_generate_verification_id' => 'Unable to generate a verification id.', + 'verification.verification_not_found' => 'Verification not found.', + 'wallet_is_immutable' => 'The wallet account is immutable once set.', + 'skip_validation_field_not_found' => 'When using HasSkippableRules trait, you must provide a skipValidation field.', + 'attribute_count_empty' => 'The attribute count for this collection or token is empty.', + 'failed_to_truncate' => 'Failed to truncate the tables...', + 'exception_in_sync' => 'We got an exception in the sync process:', + 'failed_to_get_current_block' => 'Failed to get the current block...', + 'line_and_file' => 'Line :line in :file', + 'unable_to_process' => "Sorry, we're unable to process your request at this time. Please try again later.", +]; diff --git a/lang/en/event_param.php b/lang/en/event_param.php new file mode 100644 index 00000000..92aa2951 --- /dev/null +++ b/lang/en/event_param.php @@ -0,0 +1,5 @@ + 'An event param.', +]; diff --git a/lang/en/field.php b/lang/en/field.php new file mode 100644 index 00000000..cd3378c4 --- /dev/null +++ b/lang/en/field.php @@ -0,0 +1,5 @@ + 'Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.', +]; diff --git a/lang/en/input_type.php b/lang/en/input_type.php new file mode 100644 index 00000000..9dc233af --- /dev/null +++ b/lang/en/input_type.php @@ -0,0 +1,52 @@ + 'The params to burn a token.', + 'burn_params.field.removeTokenStorage' => 'If true, the token storage will be removed if no tokens are left. Defaults to False.', + 'collection_mutation.description' => 'The params that can be mutated for a collection.', + 'collection_mutation.field.owner' => 'The new owner account of the collection.', + 'collection_mutation.field.royalty' => 'The new royalty of the collection.', + 'create_token_params.description' => 'The params to create a token.', + 'create_token_params.field.cap' => 'The token cap (if required). A cap of 1 will create this token as a Single Mint type to produce an NFT.', + 'create_token_params.field.initialSupply' => 'The initial supply of tokens to mint to the specified recipient. Must not exceed the token cap if set.', + 'create_token_params.field.listingForbidden' => 'If the token can be listed in the marketplace.', + 'create_token_params.field.attributes' => 'Set initial attributes for this token.', + 'create_token_params.field.unitPrice' => 'The price of each token. The price cannot be zero and unitPrice * totalSupply must be greater than the token account deposit.', + 'encodeable_token_id.description' => 'The params to encode the token ID.', + 'encode_token_id.field.data' => 'The data to encode into a token ID. Check the docs for the different encoder payload requirements.', + 'encode_token_id.field.type' => 'The encoding strategy to use to encode the token ID. Defaults to HASH.', + 'market_policy.description' => 'The marketplace policy for a collection.', + 'market_policy.field.royalty' => 'The royalty set to this marketplace policy.', + 'mint_token_params.description' => 'The params to mint a token.', + 'mint_token_params.field.unitPrice' => 'Leave as null if you want to keep the same unitPrice. You can also put a value if you want to change the unitPrice. Please note you can only increase it and a deposit to the difference of every token previously minted will also be needed.', + 'mint_policy.description' => 'The mint policy for a new collection.', + 'mint_policy.field.forceSingleMint' => 'Set whether the tokens in this collection will be minted as SingleMint types. This would indicate the tokens in this collection are NFTs.', + 'mint_recipient.description' => 'The recipient account for a mint.', + 'mint_recipient.field.account' => 'The recipient account of the token.', + 'multi_token_id.description' => 'The unique identifier for a token. Composed using a collection ID and a token ID.', + 'multi_token_id.field.collectionId' => 'The collection id of a multi token.', + 'multi_token_id.field.tokenId' => 'The token ID of a multi token.', + 'mutation_royalty.description' => 'The royalty for a new collection or token.', + 'mutation_royalty.field.beneficiary' => 'The account that will receive the royalty.', + 'mutation_royalty.field.isCurrency' => 'If the token is a currency.', + 'mutation_royalty.field.percentage' => 'The amount of royalty the beneficiary receives in percentage.', + 'operator_transfer_params.description' => "The params to make an operator transfer. Operator transfers are transfers that you make using tokens from somebody else's wallet as the source. To make this type of transfer the source wallet owner must approve you for transferring their tokens.", + 'operator_transfer_params.field.source' => 'The source account of the token.', + 'simple_transfer_params.description' => 'The params to make a simple transfer.', + 'token_data.description' => 'Data for a token on the Ethereum network.', + 'token_id_encoder.erc1155.description' => 'Creates an integer representation from an ERC1155 style token input.', + 'token_id_encoder.erc1155.token_id.description' => 'A 16 character hex formatted ERC1155 style token id, e.g. 0x1080000000000123.', + 'token_id_encoder.erc1155.index.description' => 'A 64bit integer index. This will be converted to hex and concatenated with the tokenId to make the final unique NFT id. Defaults to 0 is not supplied.', + 'token_id_encoder.hash.description' => 'Hashes an arbitrary object into an integer.', + 'token_id_encoder.integer.description' => 'A 128bit unsigned integer, the native format for Substrate.', + 'token_id_encoder.string_id.description' => 'Converts a string into a hex value, then converts that to an integer. This encoding is reversible.', + 'token_market_behavior.description' => 'The market behavior for a token.', + 'token_mint_cap.description' => 'The token mint cap type and value.', + 'token_mint_cap.field.amount' => 'The cap amount when using the SUPPLY type.', + 'token_mint_cap.field.type' => 'The type of mint cap for this token. A SINGLE_MINT type means a token can only be minted once, and cannot be re-minted once burned. A SUPPLY type allows you to set a limit on the total number of circulating tokens that can be minted, this type allows for burned tokens to be re-minted even if the supply amount is 1.', + 'token_mutation.description' => 'The params that can be mutated for a token.', + 'token_mutation.field.behavior' => 'Set if the token has royalty or is a currency. If null, the behavior will not be changed.', + 'token_mutation.field.listingForbidden' => 'Set if the token can be listed on the marketplace. If null, the listingForbidden property will not be changed.', + 'transfer_recipient.description' => 'The recipient account for a transfer.', + 'erc1155_encoder.description' => 'ERC1155 Style Token ID.', +]; diff --git a/lang/en/inputvalue.php b/lang/en/inputvalue.php new file mode 100644 index 00000000..9e80c09c --- /dev/null +++ b/lang/en/inputvalue.php @@ -0,0 +1,5 @@ + 'Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.', +]; diff --git a/lang/en/mutation.php b/lang/en/mutation.php new file mode 100644 index 00000000..c439b11b --- /dev/null +++ b/lang/en/mutation.php @@ -0,0 +1,94 @@ + 'Skip all validation rules, use with caution. Defaults to false.', + 'acknowledge_events.args.uuids' => 'The event UUIDs to acknowledge.', + 'acknowledge_events.description' => 'Use this mutation to acknowledge cached events and remove them from the cache.', + 'approve_collection.args.collectionId' => 'The collection that will be approved.', + 'approve_collection.args.operator' => 'The account that will be approved to operate the collection.', + 'approve_collection.description' => 'Approve another account to transfer any tokens from a collection account. You can also specify a block number where this approval will expire.', + 'approve_token.args.amount' => 'The amount of tokens it will be approved to operate.', + 'approve_token.args.collectionId' => 'The collection that the token that will be approved belongs to.', + 'approve_token.args.currentAmount' => 'The current amount of tokens the operator has.', + 'approve_token.args.expiration' => 'The block number where the approval will expire. Leave it as null for no expiration.', + 'approve_token.args.operator' => 'The account that will be approved to operate the token.', + 'approve_token.args.tokenId' => 'The token ID that will be approved.', + 'approve_token.description' => 'Approve another account to make transfers from a token account. You can also specify a block number where this approval will expire and the amount of tokens this account will be able to transfer.', + 'batch_mint.args.collectionId' => 'The collection ID that you be minting the tokens to.', + 'batch_mint.args.continueOnFailure' => 'If set to true this option will skip data that would cause the whole batch to fail. Defaults to false.', + 'batch_mint.description' => 'Use this method to batch together several mints into one transaction. You can mix and match Create Token and Mint Token params, as well as use the continueOnFailure flag to skip mints which fail on chain so they can be fixed later.', + 'batch_set_attribute.args.amount' => 'The amount to transfer.', + 'batch_set_attribute.args.collectionId' => 'The collection ID that you be adding attributes to.', + 'batch_set_attribute.args.keepAlive' => 'If true, the transaction will fail if the balance drops below the minimum requirement. Defaults to False.', + 'batch_set_attribute.args.continueOnFailure' => 'Whether to make the possible extrinsics if one of them fails. Defaults to false.', + 'batch_set_attribute.args.key' => 'The attribute key.', + 'batch_set_attribute.args.recipient' => 'The recipient account who is going to receive the transfer.', + 'batch_set_attribute.args.value' => 'The attribute value.', + 'batch_set_attribute.description' => 'Use this to set multiple attributes on a collection or token in one transaction. Setting the continueOnFailure flag to true will allow all valid attributes to be set while skipping invalid attributes so they can be fixed and attempted again in another transaction.', + 'batch_transfer.args.collectionId' => 'The collection ID that you be transferring the tokens from.', + 'batch_transfer.args.signingAccount' => 'The signing wallet for this transaction. Defaults to wallet daemon.', + 'batch_transfer.description' => 'Use this method to transfer multiple tokens in one transaction. You can include up to 250 different transfers per batch. Set the continueOnFailure to true to allow all valid transfers to complete while skipping transfers which would fali so they can be fixed and attempted again in another transaction.', + 'batch_transfer.args.continueOnFailure' => 'Whether to make the possible extrinsics if one of them fails. Defaults to false.', + 'burn.args.collectionId' => 'The collection ID to create this token in.', + 'burn.args.params' => 'The params required to burn a token.', + 'burn.description' => 'Deletes a collection and get its reserved value back. You can only destroy a collection after all tokens have been burned.', + 'create_collection.args.attributes' => 'Set initial attributes for this collection.', + 'create_collection.args.explicitRoyaltyCurrencies' => 'Set the explicit royalty currencies for tokens in this collection.', + 'create_collection.args.marketPolicy' => 'The marketplace policy for a collection.', + 'create_collection.args.mintPolicy' => 'Set the mint policy for tokens in this collection.', + 'create_collection.description' => 'Creates a new on-chain collection. The new collection ID will be returned in the transaction events after being finalized on-chain.', + 'create_token.args.recipient' => 'The recipient account of the tokens for the initial mint.', + 'create_token.description' => 'Creates a new token in a collection. The new token will be automatically transferred to the specified recipient account.', + 'create_wallet.args.externalId' => 'The external ID set for this wallet.', + 'create_wallet.description' => 'Store a new unverified wallet record using an external ID.', + 'freeze.args.collectionAccount' => 'The collection account to freeze.', + 'freeze.args.collectionId' => 'The collection ID to freeze.', + 'freeze.args.freezeType' => 'The type of freezing to do.', + 'freeze.args.tokenAccount' => 'The token account to freeze.', + 'freeze.args.tokenId' => 'The token ID to freeze.', + 'freeze.description' => 'Freezes a collection, token, collection account or token account. Tokens cannot be transferred or burned if they are frozen. Freezing a collection or collection account will freeze all the tokens in it.', + 'link_wallet.description' => 'Note: This workflow and mutation are placeholder, please use the VerifyAccount flow to associate a wallet account to this platform.', + 'mark_and_list_pending_transactions.args.accounts' => 'The accounts to filter the transactions.', + 'mark_and_list_pending_transactions.description' => 'Get a list of new pending transactions and mark them as processing.', + 'mint_token.args.collectionId' => 'The collection ID to mint from.', + 'mint_token.args.recipient' => 'The recipient account of the tokens being minted.', + 'mint_token.description' => 'Mint more of an existing token. This only applies to tokens which have a supply cap greater than 1.', + 'mutate_collection.args.collectionId' => 'The collection that will be mutated.', + 'mutate_collection.args.mutation' => 'The params that will be mutated.', + 'mutate_collection.args.tokenId' => 'The token that will be mutated.', + 'mutate_collection.description' => 'Changes collection default values.', + 'mutate_token.description' => 'Changes token default values.', + 'operator_transfer_token.description' => "Transfer tokens as the operator of someone else's wallet. Operator transfers are transfers that you make using tokens from somebody else's wallet as the source. To make this type of transfer the source wallet owner must approve you for transferring their tokens.", + 'remove_collection.description' => 'Remove an attribute from the specified collection.', + 'remove_token_attribute.description' => 'Remove an attribute from the specified token.', + 'set_collection_attribute.description' => 'Set an attribute on a collection.', + 'set_token_attribute.description' => 'Set an attribute on a token.', + 'set_wallet_account.description' => 'Set the account on a wallet model.', + 'simple_transfer_token.description' => 'Transfers a single token to a recipient account.', + 'thaw.args.collectionId' => 'The collection ID to thaw.', + 'thaw.args.freezeType' => 'The type of thawing to do.', + 'thaw.args.tokenAccount' => 'The token account to thaw.', + 'thaw.args.tokenId' => 'The token ID to thaw.', + 'thaw.description' => 'Thaw a previously frozen collection or token.', + 'transfer_all_balance.description' => 'Transfers all balances of an account to another. You can pass a keepAlive argument if you want to keep at least the existential deposit.', + 'transfer_balance.description' => 'Transfers a balance from one account to another. You can pass the keepAlive argument if you want to check if the account will be left with at least the existential deposit.', + 'unapprove_collection.args.collectionId' => 'The collection that approval will be removed from.', + 'unapprove_collection.args.operator' => 'The account that collection approval will be removed from.', + 'unapprove_collection.description' => 'Removes the approval of any specific account to make transfers from a collection account.', + 'unapprove_token.args.collectionId' => 'The collection that the token belongs to.', + 'unapprove_token.args.operator' => 'The account that token approval will be removed from.', + 'unapprove_token.args.tokenId' => 'The token that approval will be removed from.', + 'unapprove_token.description' => 'Removes the approval of any specific account to make transfers from a token account.', + 'update_external_id.description' => 'Change the external ID on a wallet model.', + 'update_transaction.args.state' => 'The new state of the transaction.', + 'update_transaction.args.transactionHash' => 'The on chain transaction hash.', + 'update_transaction.args.transactionId' => 'The on chain transaction id.', + 'update_transaction.args.signedAtBlock' => 'The block number the transaction was signed at.', + 'update_transaction.description' => 'Update a transaction with a new state, transaction ID and transaction hash. Please note that the transaction ID and transaction hash are immutable once set.', + 'update_transaction.error.hash_and_id_are_immutable' => 'The transaction id and hash are immutable once set.', + 'update_wallet_external_id.cannot_update_id_on_managed_wallet' => 'Cannot update the external id on a managed wallet.', + 'verify_account.description' => 'The wallet calls this mutation to prove the ownership of the user account.', + 'remove_all_attributes.description' => 'Removes all attributes from the given collection ID and token ID.', + 'remove_all_attributes.args.attributeCount' => 'This is an advanced feature and is used to calculate the weight of the on-chain extrinsic. Putting a value in that isn\'t equal to the on-chain attribute count will lead to the transaction failing. When empty, the attribute count will be auto calculated from data stored in the local database.', + 'retry_transaction.description' => "Retries transactions that have failed or otherwise not been included on-chain after some time. Use with caution and ensure the transactions really aren't yet on-chain (or likely to be) to make sure they are not accidentally included twice.", +]; diff --git a/lang/en/query.php b/lang/en/query.php new file mode 100644 index 00000000..f690a9d6 --- /dev/null +++ b/lang/en/query.php @@ -0,0 +1,51 @@ + 'The wallet account that you want to check if it is verified.', + 'get_account_verified.args.verificationId' => 'The verification ID that you want to check if it was verified.', + 'get_account_verified.description' => 'Get the verification status of an account.', + 'get_blocks.args.hashes' => 'The blockchain transaction hashes to filter to.', + 'get_blocks.args.number' => 'The blockchain transaction IDs to filter to.', + 'get_collection.args.collectionId' => 'The on-chain collection ID to get.', + 'get_collection.description' => 'Get a collection by its collection ID.', + 'get_collections.args.collectionIds' => 'The on-chain collection IDs to filter to.', + 'get_collections.description' => 'Get an array of collections optionally filtered by collection IDs.', + 'get_pending_events.args.acknowledgeEvents' => 'Automatically acknowledge all returned events (defaults to false).', + 'get_pending_events.description' => 'Get a list of events that were broadcast but not yet acknowledged.', + 'get_pending_wallets.description' => 'Get an array of wallet accounts which have yet to be verified.', + 'get_token.args.collectionId' => 'The token collection ID.', + 'get_token.args.tokenId' => 'The specific token ID to get.', + 'get_token.description' => 'Get a token from a collection using its token ID.', + 'get_tokens.args.collectionId' => 'The Collection to return tokens from.', + 'get_tokens.args.tokenIds' => 'Filter to specific token IDs or omit to return all.', + 'get_tokens.description' => 'Get an array of tokens from a collection, optionally filtered by token IDs.', + 'get_transaction.args.id' => 'The internal ID of the transaction.', + 'get_transaction.args.idempotencyKey' => 'The idempotency keys to filter to.', + 'get_transaction.args.transactionHash' => 'The blockchain transaction hash.', + 'get_transaction.args.transactionId' => 'The blockchain transaction id.', + 'get_transaction.description' => 'Get a transaction using its database ID, on-chain transaction ID or transaction hash.', + 'get_transactions.args.ids' => 'The internal ID of the transaction.', + 'get_transactions.args.idempotencyKeys' => 'The idempotency keys to filter to.', + 'get_transactions.args.transactionHashes' => 'The blockchain transaction hash.', + 'get_transactions.args.transactionIds' => 'The blockchain transaction id.', + 'get_transactions.args.signedAtBlocks' => 'The block numbers that the transactions were signed at.', + 'get_transactions.args.accounts' => 'The wallet accounts to filter to.', + 'get_transactions.args.methods' => 'The transaction method types to filter to.', + 'get_transactions.args.results' => 'Filter transactions to the specified results.', + 'get_transactions.args.states' => 'Filter transactions to the specified states.', + 'get_transactions.description' => 'Get an array of transactions optionally filtered by transaction IDs, transaction hashes, methods, states, results or accounts.', + 'get_wallet.args.account' => 'The wallet account on the blockchain.', + 'get_wallet.args.externalId' => 'The external ID for this wallet.', + 'get_wallet.args.id' => 'The internal ID of this wallet.', + 'get_wallet.args.newExternalId' => 'The new external ID to set for this wallet.', + 'get_wallet.args.verificationId' => 'The verification ID of this wallet.', + 'get_wallet.description' => 'Get a wallet using either its database ID, external ID, verification ID or account address.', + 'get_wallets.description' => 'Get wallets using either its database ID, external ID, verification ID or account address.', + 'request_account.args.callback' => 'This is the callback URL that the wallet should send the verification to.', + 'request_account.description' => 'This query generates a QR code that the user can scan to give us their wallet account.', + 'verify_message.args.cryptoSignatureType' => 'The signature crypto type. This field is optional and it will use sr25519 by default.', + 'verify_message.args.message' => 'The message that the user signed.', + 'verify_message.args.publicKey' => 'The public key of the user.', + 'verify_message.args.signature' => 'The signed message.', + 'verify_message.description' => 'Verifies a message was signed with the public key provided.', +]; diff --git a/lang/en/scalar.php b/lang/en/scalar.php new file mode 100644 index 00000000..bae3008a --- /dev/null +++ b/lang/en/scalar.php @@ -0,0 +1,9 @@ + 'The `Boolean` scalar type represents `true` or `false`.', + 'float.description' => 'The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point).', + 'id.description' => 'The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID.', + 'int.description' => 'The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.', + 'string.description' => 'The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.', +]; diff --git a/lang/en/schema.php b/lang/en/schema.php new file mode 100644 index 00000000..97be761d --- /dev/null +++ b/lang/en/schema.php @@ -0,0 +1,45 @@ + 'A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.', + 'directives.deprecated' => 'Marks an element of a GraphQL schema as no longer supported.', + 'directives.deprecated.reason' => 'Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax (as specified by [CommonMark](https://commonmark.org/).', + 'directives.include' => 'Directs the executor to include this field or fragment only when the `if` argument is true.', + 'directives.include.args.if' => 'Included when true.', + 'directives.skip' => 'Directs the executor to skip this field or fragment when the `if` argument is true.', + 'directives.skip.args.if' => 'Skipped when true.', + 'inputvalue.field.defaultValue' => 'A GraphQL-formatted string representing the default value for this input value.', + 'types.directiveLocation.ARGUMENT_DEFINITION' => 'Location adjacent to an argument definition.', + 'types.directiveLocation.ENUM' => 'Location adjacent to an enum definition.', + 'types.directiveLocation.ENUM_VALUE' => 'Location adjacent to an enum value definition.', + 'types.directiveLocation.FIELD' => 'Location adjacent to a field.', + 'types.directiveLocation.FIELD_DEFINITION' => 'Location adjacent to a field definition.', + 'types.directiveLocation.FRAGMENT_DEFINITION' => 'Location adjacent to a fragment definition.', + 'types.directiveLocation.FRAGMENT_SPREAD' => 'Location adjacent to a fragment spread.', + 'types.directiveLocation.INLINE_FRAGMENT' => 'Location adjacent to an inline fragment.', + 'types.directiveLocation.INPUT_FIELD_DEFINITION' => 'Location adjacent to an input object field definition.', + 'types.directiveLocation.INPUT_OBJECT' => 'Location adjacent to an input object type definition.', + 'types.directiveLocation.INTERFACE' => 'Location adjacent to an interface definition.', + 'types.directiveLocation.LIST' => 'Indicates this type is a list. `ofType` is a valid field.', + 'types.directiveLocation.MUTATION' => 'Location adjacent to a mutation operation.', + 'types.directiveLocation.OBJECT' => 'Location adjacent to an object type definition.', + 'types.directiveLocation.QUERY' => 'Location adjacent to a query operation.', + 'types.directiveLocation.SCALAR' => 'Location adjacent to a scalar definition.', + 'types.directiveLocation.SCHEMA' => 'Location adjacent to a schema definition.', + 'types.directiveLocation.SUBSCRIPTION' => 'Location adjacent to a subscription operation.', + 'types.directiveLocation.UNION' => 'Location adjacent to a union definition.', + 'types.directiveLocation.VARIABLE_DEFINITION' => 'Location adjacent to a variable definition.', + 'types.enumValues.ENUM' => 'Indicates this type is an enum. `enumValues` is a valid field.', + 'types.enumValues.INPUT_OBJECT' => 'Indicates this type is an input object. `inputFields` is a valid field.', + 'types.enumValues.INTERFACE' => 'Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields.', + 'types.enumValues.LIST' => 'Indicates this type is a list. `ofType` is a valid field.', + 'types.enumValues.NON_NULL' => 'Indicates this type is a non-null. `ofType` is a valid field.', + 'types.enumValues.OBJECT' => 'Indicates this type is an object. `fields` and `interfaces` are valid fields.', + 'types.enumValues.SCALAR' => 'Indicates this type is a scalar.', + 'types.enumValues.UNION' => 'Indicates this type is a union. `possibleTypes` is a valid field.', + 'types.field.directives' => 'A list of all directives supported by this server.', + 'types.field.mutationType' => 'If this server supports mutation, the type that mutation operations will be rooted at.', + 'types.field.queryType' => 'The type that query operations will be rooted at.', + 'types.field.subscriptionType' => 'If this server support subscription, the type that subscription operations will be rooted at.', + 'types.field.types' => 'A list of all types supported by this server.', +]; diff --git a/lang/en/ss58_address.php b/lang/en/ss58_address.php new file mode 100644 index 00000000..6c46ae7c --- /dev/null +++ b/lang/en/ss58_address.php @@ -0,0 +1,11 @@ + 'Cannot decode :address: :message', + 'error.format_out_of_range' => 'SS58 format out of range.', + 'error.invalid_decoded_address_checksum' => 'Invalid decoded address checksum.', + 'error.invalid_empty_address' => 'Invalid empty address passed.', + 'error.invalid_uint8array' => 'Invalid Uint8Array.', + 'error.unexpected_format' => 'Expected ss58Format :ss58Format, received :ss58Decoded.', + 'error.valid_key_expected' => 'Expected a valid key to convert, with length :length.', +]; diff --git a/lang/en/traits.php b/lang/en/traits.php new file mode 100644 index 00000000..c0ada2ff --- /dev/null +++ b/lang/en/traits.php @@ -0,0 +1,10 @@ + ':class: : Unable to find attribute tokenId #:tokenId, collectionId #:collectionId, key #:key.', + 'query_data_or_fail.unable_to_find_collection' => ':class: Unable to find collection collectionChainId #:collectionChainId.', + 'query_data_or_fail.unable_to_find_collection_account' => ':class: Unable to find collection account walletId #:walletId, collectionId #:collectionId.', + 'query_data_or_fail.unable_to_find_token' => ':class: Unable to find token tokenChainId #:tokenChainId, collectionId #:collectionId.', + 'query_data_or_fail.unable_to_find_token_account' => ':class: Unable to find token account walletId #:walletId, collectionId #:collectionId, tokenId #:tokenId.', + 'query_data_or_fail.unable_to_find_wallet_account' => ':class: Unable to find wallet account #:publicKey.', +]; diff --git a/lang/en/type.php b/lang/en/type.php new file mode 100644 index 00000000..1f4d4412 --- /dev/null +++ b/lang/en/type.php @@ -0,0 +1,132 @@ + 'A substrate account.', + 'account.field.address' => 'The account address.', + 'account.field.publicKey' => 'The account public key.', + 'account_request.description' => 'A request to verify an account.', + 'account_request.field.qrCode' => 'The QR code a user can scan in the wallet app to verify their account.', + 'account_request.field.verificationId' => 'This is a verification ID generated to get the account from.', + 'account_verified.description' => 'The verification status of an account.', + 'account_verified.field.account' => 'The account that was verified.', + 'account_verified.field.verified' => 'If the user account has already been verified.', + 'attribute.description' => 'An on-chain key/value pair.', + 'attribute_input.description' => 'The attribute for a collection or token.', + 'balances.description' => 'The balance properties for a wallet account.', + 'big_int.description' => 'A type that represents unsigned integers up to 256 bits. The value must be a PHP numeric (int or string) and must not use scientific notation.', + 'block.description' => 'An blockchain block.', + 'block.field.exception' => 'The exception that happened when processing the block.', + 'block.field.failed' => 'If the block failed to be processed.', + 'block.field.hash' => 'The on-chain block hash.', + 'block.field.id' => 'The internal ID of the block.', + 'block.field.number' => 'The on-chain block number.', + 'block.field.synced' => 'If the block was already synced.', + 'collection.description' => 'A collection groups together tokens and sets the policies that apply to them.', + 'collection_account.description' => "A collection account groups together a wallet's token accounts for a given collection and controls options such as freezing and approvals for all tokens in them.", + 'collection_account.field.accountCount' => 'The number of token accounts attached to this collection account.', + 'collection_account.field.approvals' => 'A list of approvals for this account.', + 'collection_account.field.collection' => 'The collection this collection account belongs to.', + 'collection_account.field.isFrozen' => 'Specifies if this collection account is frozen.', + 'collection_account.field.namedReserves' => 'The named reserves for this account.', + 'collection_account.field.wallet' => 'The wallet which owns this collection account.', + 'collection_account_approval.description' => 'The wallets that have been approved to use this collection account.', + 'collection_account_approval.field.account' => 'The token account this approval belongs to.', + 'collection_account_approval.field.expiration' => 'The expiration block the wallet will lose the approval.', + 'collection_account_approval.field.wallet' => 'The wallet that has been approved.', + 'collection_type.field.accounts' => 'The accounts for this collection.', + 'collection_type.field.attributes' => 'The attributes for this collection.', + 'collection_type.field.collectionId' => 'The ID assigned to this collection.', + 'collection_type.field.forceSingleMint' => 'Whether the tokens in this collection will be minted as SingleMint types. This would indicate the tokens in this collection are NFTs.', + 'collection_type.field.frozen' => 'Whether this collection is frozen.', + 'collection_type.field.maxTokenCount' => 'The maximum number of tokens that can be issued for this collection.', + 'collection_type.field.maxTokenSupply' => 'The maximum amount of each token in this collection that can be minted.', + 'collection_type.field.network' => 'The network this collection belongs to.', + 'collection_type.field.owner' => 'The wallet which can mint tokens from this collection.', + 'collection_type.field.royalty' => 'Specifies if this token has a royalty policy.', + 'collection_type.field.tokens' => 'The tokens minted from this collection.', + 'description' => "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", + 'edge.field.node' => 'List of items on the current cursor.', + 'event.description' => 'A blockchain event.', + 'event.field.eventId' => 'The event ID.', + 'event.field.lookUp' => 'The method look up.', + 'event.field.moduleId' => 'The pallet module.', + 'event.field.params' => 'The params from this event.', + 'event.field.phase' => 'The phase of block execution it happened.', + 'event_param.field.type' => 'The value type of the param.', + 'event_param.field.value' => 'The value of the param.', + 'integer_range.description' => "A string value that can be used to represent a range of integer numbers. Use a double full stop to supply a range between 2 integers. For example an integer range that looks like this: \n\n\"3..8\"\n\nWill be automatically expanded to:\n\n[3, 4, 5, 6, 7, 8]", + 'integer_ranges_array.description' => "An array that can be used to represent ranges of integer numbers. Use a double full stop to supply a range between 2 integers in the array. For example an integer ranges array that looks like this: \n\n[\"1\", \"3..8\", \"11\", \"15..18\"]\n\nWill be automatically expanded to:\n\n[1, 3, 4, 5, 6, 7, 8, 11, 15, 16, 17, 18]", + 'json.description' => 'A type that represents json data.', + 'page_info.field.endCursor' => 'The next cursor.', + 'page_info.field.hasNextPage' => 'Determines if cursor has more pages after the current page.', + 'page_info.field.hasPreviousPage' => 'Determines if cursor has more pages before the current page.', + 'page_info.field.startCursor' => 'The previous cursor.', + 'pending_event.description' => 'A websocket event pending to be acknowledge.', + 'pending_event.field.id' => 'The internal ID of the event.', + 'pending_event.field.uuid' => 'The UUID of the event.', + 'pending_event.field.name' => 'The name of the event.', + 'pending_event.field.sent' => 'The timestamp when the event was sent.', + 'pending_event.field.channels' => 'The channels the event was sent to.', + 'pending_event.field.data' => 'The data of the event.', + 'royalty.description' => 'Royalty settings.', + 'token.description' => 'A token on the blockchain. Tokens have settings specific to them and can also have their own attributes which can be used to override the parent collection attributes.', + 'token.field.accounts' => 'The token accounts that hold this token.', + 'token.field.attributeCount' => 'The number of attributes set on this token.', + 'token.field.attributes' => 'The token attributes.', + 'token.field.cap' => 'The maximum quantity available for this token.', + 'token.field.collection' => 'The collection this token belongs to.', + 'token.field.isFrozen' => 'Specifies if this token is frozen, disallowing transfers.', + 'token.field.minimumBalance' => 'The minimum required balance of this token for all accounts.', + 'token.field.mintDeposit' => 'The amount of currency reserved from the issuer for minting.', + 'token.field.name' => 'The name of the new token.', + 'token.field.isCurrency' => 'Shows if this token is a currency. Being a currency makes the token fungible automatically.', + 'token.field.nonFungible' => 'Shows if this token considered non-fungible (i.e. there is only one available and therefore truly unique).', + 'token.field.royalty' => 'Returns the token royalty if set, or null if not.', + 'token.field.supply' => 'The current supply of this token.', + 'token.field.tokenId' => 'The token chain ID which is a 128bit unsigned integer number.', + 'token.field.unitPrice' => 'The price of each token in EFI.', + 'token_account.description' => "A token account stores a wallet's balance of a specific token in a collection.", + 'token_account.field.balance' => 'The balance of the token this account holds.', + 'token_account.field.collection' => 'The collection this token account belongs to.', + 'token_account.field.isFrozen' => 'Specifies if this token account is frozen, disallowing transfers.', + 'token_account.field.reservedBalance' => 'The reserved value for this account.', + 'token_account.field.token' => 'The token for this account.', + 'token_account.field.wallet' => 'The wallet which owns this token account.', + 'token_account_approval.args.amount' => 'The amount the wallet has been approved.', + 'token_account_approval.description' => 'The wallets that have been approved to use this token account.', + 'token_account_named_reserve.args.amount' => 'The amount in the wallet that has been reserved.', + 'token_account_named_reserve.args.pallet' => 'The pallet that has created this reserve.', + 'token_account_named_reserve.description' => 'The pallet that has reserved some tokens and the amount.', + 'transaction.description' => 'An blockchain transaction.', + 'transaction.eth.description' => 'An Ethereum transaction.', + 'transaction.eth.field.transactionId' => 'The transaction hash.', + 'transaction.field.encodedData' => 'The encoded transaction data.', + 'transaction.field.events' => 'The events generated by this transaction.', + 'transaction.field.idempotencyKey' => 'The idempotency key set for this transaction.', + 'transaction.field.method' => 'The on-chain method used.', + 'transaction.field.result' => 'The transaction result.', + 'transaction.field.state' => 'The transaction state.', + 'transaction.field.transactionHash' => 'The on-chain transaction hash.', + 'transaction.field.transactionId' => 'The on-chain transaction ID.', + 'transaction.field.createdAt' => 'The date and time the transaction was created.', + 'transaction.field.updatedAt' => 'The date and time the transaction was last updated.', + 'transaction.field.signedAtBlock' => 'The block number the transaction was signed at.', + 'transaction.field.wallet' => 'The wallet used for signing this transaction.', + 'wallet.address.' => 'The wallet address.', + 'wallet.description' => 'A blockchain wallet.', + 'wallet.field.account' => 'The wallet account.', + 'wallet.field.balances' => "The EFI balance of the account. The balances will be null if the wallet doesn't exist on the blockchain.", + 'wallet.field.collectionAccountApprovals' => 'The collection account approvals this wallet has.', + 'wallet.field.collectionAccounts' => 'The collection accounts this wallet has.', + 'wallet.field.collectionIds' => 'The collection to return.', + 'wallet.field.externalId' => 'The external ID associated with the wallet.', + 'wallet.field.id' => 'The internal ID of the wallet.', + 'wallet.field.managed' => 'Whether this is a managed wallet.', + 'wallet.field.network' => 'The blockchain network this wallet belongs to.', + 'wallet.field.nonce' => "The nonce of the account. A nonce will be null if the wallet doesn't exist on the blockchain.", + 'wallet.field.ownedCollections' => 'The collections this wallet owns.', + 'wallet.field.tokenAccountApprovals' => 'The token account approvals this wallet has.', + 'wallet.field.tokenAccounts' => 'The token accounts this wallet owns. Token accounts store the balances of tokens.', + 'wallet.field.transactions' => 'The transactions performed by this wallet.', + 'wallet_link.field.code' => 'The code a user can input into the wallet app to link their account on the platform.', +]; diff --git a/lang/en/typekind.php b/lang/en/typekind.php new file mode 100644 index 00000000..287cf82b --- /dev/null +++ b/lang/en/typekind.php @@ -0,0 +1,5 @@ + 'An enum describing what kind of type a given `__Type` is.', +]; diff --git a/lang/en/validation.php b/lang/en/validation.php new file mode 100644 index 00000000..c08bb771 --- /dev/null +++ b/lang/en/validation.php @@ -0,0 +1,38 @@ + 'Could not find a collection account for :account at collection :collectionId.', + 'account_exists_in_token' => 'Could not find a token account for :account at collection :collectionId and token :tokenId.', + 'account_exists_in_wallet' => 'Could not find the :attribute specified.', + 'approval_exists_in_collection' => 'Could not find an approval for :operator at collection :collectionId.', + 'approval_exists_in_token' => 'Could not find an approval for :operator at collection :collectionId and token :tokenId.', + 'attribute_exists_in_collection' => 'The key does not exist in the specified collection.', + 'check_token_count' => 'The overall token count :total have exceeded the maximum cap of :maxToken tokens.', + 'daemon_prohibited' => 'The :attribute cannot be set to the daemon account.', + 'distinct_attribute' => 'The :attribute must be an array of distinct attributes keys.', + 'distinct_multi_asset' => 'The :attribute must be an array of distinct multi assets.', + 'future_block' => 'The :attribute must be at least :block.', + 'is_collection_owner' => 'The :attribute provided is not owned by you.', + 'is_collection_owner_or_approved' => 'The :attribute provided is not owned by you and you are not currently approved to use it.', + 'is_managed_wallet' => 'The :attribute is not a wallet managed by this platform.', + 'key_doesnt_exit_in_token' => 'The key does not exist in the specified token.', + 'max_big_int' => 'The :attribute is too large, the maximum value it can be is :max.', + 'max_token_balance' => 'The :attribute is invalid, the amount provided is bigger than the token account balance.', + 'min_big_int' => 'The :attribute is too small, the minimum value it can be is :min.', + 'min_token_deposit' => 'The :attribute is too small, the min token deposit is 0.01 EFI thus initialSupply * unitPrice must be greater than 10^16.', + 'mutation.behavior.isCurrency.accepted' => "The isCurrency parameter only accepts true. If you don't want it to be a currency, don't pass it.", + 'no_tokens_in_collection' => 'The :attribute must not have any existing tokens.', + 'token_doesnt_exist_in_collection' => 'The :attribute already exists in the specified collection.', + 'token_encode_doesnt_exist_in_collection' => 'The :attribute already exists in the specified collection.', + 'token_encode_exist_in_collection' => 'The :attribute does not exist in the specified collection.', + 'token_encode_exists' => "The :attribute doesn't exist.", + 'token_exists_in_collection' => 'The :attribute does not exist in the specified collection.', + 'valid_hex' => 'The :attribute has an invalid hex string.', + 'valid_royalty_percentage' => 'The :attribute valid for a royalty is in the range of 0.1% to 50% and a maximum of 7 decimal places.', + 'valid_substrate_account' => 'The :attribute is not a valid substrate account.', + 'valid_substrate_address' => 'The :attribute is not a valid substrate address.', + 'valid_substrate_transaction_id' => 'The :attribute has a not valid substrate transaction ID.', + 'valid_verification_id' => 'The verification ID is not valid.', + 'numeric' => 'The :attribute must be numeric.', + 'collection_has_tokens' => "The collection doesn't have any tokens.", +]; diff --git a/lang/ja/args.php b/lang/ja/args.php new file mode 100644 index 00000000..a4be8a48 --- /dev/null +++ b/lang/ja/args.php @@ -0,0 +1,8 @@ + 'トークンID。', + 'encodeTokenId' => 'トークンIDアダプター。', + 'idempotencyKey' => '設定するidempotency key。これにはUUIDの使用が推奨されます。', + 'tokenId' => '設定するトークンID。これはこのコレクションに一意である必要があります。', +]; diff --git a/lang/ja/commands.php b/lang/ja/commands.php new file mode 100644 index 00000000..efe5cb18 --- /dev/null +++ b/lang/ja/commands.php @@ -0,0 +1,6 @@ + 'OpenPlatformを初期化し、Substrateの状態のスナップショットと同期します。', + 'ingest.description' => 'ブロックチェーン情報をOpenPlatformデータベースに取り込んで処理します。', +]; diff --git a/lang/ja/connection_input.php b/lang/ja/connection_input.php new file mode 100644 index 00000000..2d66542c --- /dev/null +++ b/lang/ja/connection_input.php @@ -0,0 +1,6 @@ + '取得するカーソル。', + 'args.first' => '1ページごとに返す結果数。', +]; diff --git a/lang/ja/directive.php b/lang/ja/directive.php new file mode 100644 index 00000000..bf556670 --- /dev/null +++ b/lang/ja/directive.php @@ -0,0 +1,5 @@ + "ディレクティブを使用して、GraphQLドキュメントに代替ランタイム実行と型検証の動作を記述することができます。\n\n一部のケースでは、条件付きでフィールドを含めたりスキップしたりするなど、フィールド引数では不十分な方法でGraphQLの実行動作を変更するオプションを提供する必要があります。ディレクティブは、Executorへの追加情報を記述することで、これを提供します。", +]; diff --git a/lang/ja/enum.php b/lang/ja/enum.php new file mode 100644 index 00000000..bed4d966 --- /dev/null +++ b/lang/ja/enum.php @@ -0,0 +1,14 @@ + 'メッセージに署名するために使用される暗号アルゴリズムのタイプ。', + 'event_type.description' => 'ブロックチェーントークンのトランザクションに関連するイベントタイプ。', + 'freezable_type.description' => 'オンチェーンでサポートされている凍結可能なオブジェクト。', + 'pallet_identifier.description' => 'オンチェーンパレット識別子。', + 'token_market_behavior_type.description' => 'トークンがサポートする市場行動タイプ。', + 'token_mint_cap_type.description' => 'サポートされているトークンの発行タイプ。', + 'token_type.description' => '代替可能または非代替可能のトークンタイプ。', + 'transaction_method.description' => '現在サポートされているトランザクション。', + 'transaction_result.description' => 'トランザクションの結果ステータス。', + 'transaction_state.description' => 'トランザクションのライフサイクルにおける状態。', +]; diff --git a/lang/ja/enumvalue.php b/lang/ja/enumvalue.php new file mode 100644 index 00000000..89bb95d0 --- /dev/null +++ b/lang/ja/enumvalue.php @@ -0,0 +1,5 @@ + '特定の列挙の 1 つの可能値。列挙値は一意の値であり、文字列や数値のプレースホルダーではありません。ただし、JSONレスポンスでは、列挙値は文字列として返されます。', +]; diff --git a/lang/ja/error.php b/lang/ja/error.php new file mode 100644 index 00000000..696f5ed3 --- /dev/null +++ b/lang/ja/error.php @@ -0,0 +1,39 @@ + 'アカウントはすでに使用されています。', + 'auth.auth_not_defined' => '認証が定義されていません。', + 'auth.basic_token.token_not_defined' => '基本トークンが.envに定義されていません', + 'auth.driver_not_supported' => 'ドライバー [:driver] はサポートされていません。', + 'cannot_represent_object' => '次の値をオブジェクトとして表現できません:', + 'cannot_represent_uint256' => '次の値をuint256として表現できません::value', + 'cannot_set_create_and_mint_params_with_same_recipient' => '同じ受取人にcreateパラメーターとmintパラメーターを設定できません。', + 'cannot_set_simple_and_operator_params_for_same_recipient' => '同じ受取人にsimpleパラメーターとoperatorパラメーターを設定できません。', + 'invalid_json' => '無効なJSON形式です。', + 'middleware.single_arg_only' => 'フィルターするフィールドを1つ選択してください。', + 'middleware.single_filter_only.only_one_filter' => 'これらのフィルターの1つのみが使用されます::filterOptions', + 'middleware.single_filter_only.only_used_alone' => 'フィルター":filterOptions"は単独でしか使用できません。他のフィルターと組み合わせることはできません。', + 'not_valid_object' => '有効なオブジェクトではありません。', + 'not_valid_uint256' => '有効なuint256ではありません。', + 'serialization.method_does_not_exist' => "メソッド':method'は存在しません。", + 'set_either_create_or_mint_param_for_recipient' => '受取人ごとに、createパラメーターかmintパラメーターを設定する必要があります。', + 'set_either_simple_and_operator_params_for_recipient' => '受取人ごとに、simpleパラメーターかoperatorパラメーターを設定する必要があります。', + 'supply_cap_must_be_greater_than_initial' => '供給時価総額の金額は、初期供給以上である必要があります。', + 'supply_cap_must_be_set' => '供給時価総額を使用する場合、供給時価総額の金額が設定されている必要があります。', + 'there_can_only_one_input_name' => ':nameという入力フィールドは1つしか存在できません。', + 'token_id_encoder.encoder_config_not_defined' => 'エンコーダーの構成が定義されていません。', + 'token_id_encoder.encoder_not_supported' => 'エンコーダー [:driverClass] はサポートされていません。', + 'token_id_encoder.token_id_encoder_not_defined_in_env' => 'token_id_encoderが.envに定義されていません', + 'token_id_encoder.hash.algo_not_defined_in_config' => 'ハッシュアルゴリズムは構成に定義されていません。', + 'token_id_encoder.hash.algo_not_supported' => ':algoアルゴリズムはサポートされていません。', + 'token_id_encoder.string_id.requires_key_named_string' => 'StringIdエンコーダーには、stringという名前のキーを持つデータペイロードが必要です。', + 'token_int_too_large' => 'このtokenIdは、128bitユニットより大きく変換されるint値として使用できません。', + 'token_not_found' => 'トークンが見つかりません。', + 'transaction_not_found' => 'トランザクションが見つかりません。', + 'unable_to_load_metadata' => 'メタデータファイルを読み込めません。', + 'unauthorized_header' => '未承認です。有効な承認ヘッダーを提供してください。', + 'verification.invalid_signature' => '提供された署名は有効ではありません。', + 'verification.unable_to_generate_verification_id' => '確認IDを生成できません。', + 'verification.verification_not_found' => '確認が見つかりません。', + 'wallet_is_immutable' => 'ウォレットアカウントは一度設定されると変更できません。', +]; diff --git a/lang/ja/event_param.php b/lang/ja/event_param.php new file mode 100644 index 00000000..9f25d81f --- /dev/null +++ b/lang/ja/event_param.php @@ -0,0 +1,5 @@ + 'eventパラメーター。', +]; diff --git a/lang/ja/field.php b/lang/ja/field.php new file mode 100644 index 00000000..66048f54 --- /dev/null +++ b/lang/ja/field.php @@ -0,0 +1,5 @@ + 'オブジェクト型とインターフェース型はフィールドのリストで記述されます。このリストには、名前、該当する場合は引数のリスト、および戻り値の型が含まれます。', +]; diff --git a/lang/ja/input_type.php b/lang/ja/input_type.php new file mode 100644 index 00000000..ad0e1763 --- /dev/null +++ b/lang/ja/input_type.php @@ -0,0 +1,44 @@ + 'トークンをバーンするパラメーター。', + 'burn_params.field.removeTokenStorage' => 'trueである場合、トークンが残っていなければ、トークンストレージが削除されます。デフォルトは false です。', + 'collection_mutation.description' => 'コレクション用にミューテーション可能なパラメーター。', + 'collection_mutation.field.owner' => 'コレクションの新しいオーナーアカウント。', + 'collection_mutation.field.royalty' => 'コレクションの新しいロイヤルティ。', + 'create_token_params.description' => 'トークンを作成するパラメーター。', + 'create_token_params.field.cap' => 'トークンの上限(必須の場合)。上限1は、NFTを生成するSingleMintタイプとしてこのトークンを作成します。', + 'create_token_params.field.initialSupply' => '特定の受取人に対して発行するトークンの初期供給。設定されている場合はトークンの上限を超えることはできません。', + 'create_token_params.field.listingForbidden' => 'トークンをマーケットプレイスで販売できるかどうか。', + 'create_token_params.field.unitPrice' => '各トークンの価格。価格はゼロにできません。また「unitPrice × totalSupply」はトークンアカウントの実存残高より大きい必要があります。', + 'encode_token_id.description' => 'トークンIDをエンコードするパラメーター。', + 'encode_token_id.field.data' => 'トークンIDにエンコードするデータ。様々なエンコーダーのペイロード要件については、ドキュメントをご覧ください。', + 'encode_token_id.field.type' => 'トークンIDをエンコードするために使用するエンコーディング手法。デフォルトはHASHです。', + 'market_policy.description' => 'コレクションのマーケットプレイスポリシー。', + 'market_policy.field.royalty' => 'このマーケットプレイスポリシーに設定されたロイヤルティ。', + 'mint_token_params.description' => 'トークンを発行するパラメーター。', + 'mint_token_params.field.unitPrice' => '同じunitPriceを維持する場合はnullのままにします。unitPriceを変更する場合は、値を指定することもできます。増加しかできないこと、また、過去に発行されたトークンごとに差額を入金する必要もあることに注意してください。', + 'mint_policy.description' => '新しいコレクションの発行ポリシー。', + 'mint_policy.field.forceSingleMint' => 'このコレクションのトークンがSingleMintタイプとして発行されるかどうかを設定します。これにより、このコレクションのトークンがNFTであることが示されます。', + 'mint_recipient.description' => '発行の受取人アカウント。', + 'mint_recipient.field.account' => 'トークンの受取人アカウント。', + 'multi_token_id.description' => 'トークンの一意の識別子。コレクションIDとトークンIDで構成されます。', + 'multi_token_id.field.collectionId' => 'マルチトークンのコレクションID。', + 'multi_token_id.field.tokenId' => 'マルチトークンのトークンID。', + 'mutation_royalty.description' => '新しいコレクションまたはトークンのロイヤルティ。', + 'mutation_royalty.field.beneficiary' => 'ロイヤルティを受け取るアカウント。', + 'mutation_royalty.field.isCurrency' => 'トークンが通貨であるかどうか。', + 'mutation_royalty.field.percentage' => 'パーセント率で示す、受取人が受け取るロイヤルティの額。', + 'operator_transfer_params.description' => 'オペレーター転送を行うパラメーター。オペレーター転送とは、他の誰かのウォレットのトークンをソースとして使用する転送です。この種の転送を行うには、ソースウォレットのオーナーがトークンの転送を承認する必要があります。', + 'operator_transfer_params.field.source' => 'トークンのソースアカウント。', + 'simple_transfer_params.description' => '簡易転送を行うパラメーター。', + 'token_data.description' => 'イーサリアムネットワークにあるトークンのデータ。', + 'token_market_behavior.description' => 'トークンの市場行動。', + 'token_mint_cap.description' => 'トークン発行上限のタイプと値。', + 'token_mint_cap.field.amount' => 'SUPPYタイプを使用する場合の上限金額。', + 'token_mint_cap.field.type' => 'このトークンの発行上限のタイプ。SINGLE_MINTタイプの場合、トークンは一度しか発行されず、バーンされると再発行できません。SUPPLYタイプの場合、発行できる循環トークンの合計数に上限を設定できます。このタイプでは、供給量が1であっても、バーンされたトークンの再発行が可能です。', + 'token_mutation.description' => 'トークン用にミューテーション可能なパラメーター。', + 'token_mutation.field.behavior' => 'トークンにロイヤルティがあるかトークンが通貨であるかを設定します。nullの場合、行動は変更されません。', + 'token_mutation.field.listingForbidden' => 'トークンをマーケットプレイスで販売できるかどうかを設定します。nullの場合、listingForbiddenプロパティは変更されません。', + 'transfer_recipient.description' => '転送の受取人アカウント', +]; diff --git a/lang/ja/inputvalue.php b/lang/ja/inputvalue.php new file mode 100644 index 00000000..b9b699f4 --- /dev/null +++ b/lang/ja/inputvalue.php @@ -0,0 +1,5 @@ + 'フィールドまたはディレクティブに指定された引数とInputObjectの入力フィールドは、型とオプションでデフォルト値を記述する入力値として表現されます。', +]; diff --git a/lang/ja/mutation.php b/lang/ja/mutation.php new file mode 100644 index 00000000..3c444e1d --- /dev/null +++ b/lang/ja/mutation.php @@ -0,0 +1,85 @@ + '確認するイベントUUID。', + 'acknowledge_events.description' => 'このミューテーションを使って、キャッシュ済みのイベントを確認してキャッシュから削除します。', + 'approve_collection.args.collectionId' => '承認されるコレクション。', + 'approve_collection.args.operator' => 'コレクションの運営を承認されるアカウント。', + 'approve_collection.description' => '別のアカウントがコレクションアカウントのトークンを転送することを承認します。また、この承認が期限切れとなるブロック番号を指定することもできます。', + 'approve_token.args.amount' => '運営を承認されるトークンの数量。', + 'approve_token.args.collectionId' => '承認されるトークンが属するコレクション。', + 'approve_token.args.currentAmount' => 'オペレーターが所持するトークンの現在の数量。', + 'approve_token.args.expiration' => '承認が期限切れとなるブロック番号。無期限の場合はnullのままにします。', + 'approve_token.args.operator' => 'トークンの運営を承認されるアカウント。', + 'approve_token.args.tokenId' => '承認されるトークンID。', + 'approve_token.description' => '別のアカウントがトークンアカウントから転送を行うことを承認します。また、この承認が期限切れとなるブロック番号とこのアカウントが転送できるトークンの数量を指定することもできます。', + 'batch_mint.args.continueOnFailure' => 'trueに設定した場合、バッチ全体を失敗させるデータが省略されます。デフォルトはfalseです。', + 'batch_mint.description' => 'このメソッドは、複数の発行を1回のトランザクションで一括して行うために使用します。Create TokeとMint Tokenパラメーターを混合させたり、チェーンで失敗する発行を後で修正できるように、continueOnFailureフラグを使って省略したりできます。', + 'batch_set_attribute.args.amount' => '転送する金額。', + 'batch_set_attribute.args.collectionId' => 'コレクションID。', + 'batch_set_attribute.args.keepAlive' => 'trueの場合、残高が最低要件を下回ると、トランザクションは失敗します。デフォルトはfalseです。', + 'batch_set_attribute.args.key' => '属性キー。', + 'batch_set_attribute.args.recipient' => '転送を受け取る受取人のアカウント。', + 'batch_set_attribute.args.value' => '属性値。', + 'batch_set_attribute.description' => 'これは、1回のトランザクションで、コレクションまたはトークンに複数の属性を設定するために使用します。continueOnFailureフラグをtrueに設定すると、すべての有効な属性を設定して、無効な属性を省略することができます。これらは、修正されてから別のトランザクションでもう一度試行されます。', + 'batch_transfer.args.signingAccount' => 'このトランザクションで署名するウォレット。デフォルトはウォレットデーモンです。', + 'batch_transfer.description' => 'このメソッドは、1回のトランザクションで複数のトークンを転送するために使用します。バッチあたり最大250回の異なる転送を含めることができます。continueOnFailureをtrueに設定すると、すべての有効な転送を完了させて、失敗する転送を省略することができます。これらは、修正されてから別のトランザクアクションでもう一度試行されます。', + 'burn.args.collectionId' => 'このトークンを作成するコレクションID。', + 'burn.args.params' => 'トークンをバーンするために必要なパラメーター。', + 'burn.description' => 'コレクションを削除し、予約された値を戻します。コレクションはすべてのトークンがバーンされてからのみ、破棄することができます。', + 'create_collection.args.attributes' => 'このコレクションの初期属性を設定します。', + 'create_collection.args.explicitRoyaltyCurrencies' => 'このコレクションのトークンの明示的なロイヤルティ通貨を設定します。', + 'create_collection.args.marketPolicy' => 'トークンIDをエンコードするために使用するエンコーディング手法。', + 'create_collection.args.mintPolicy' => 'このコレクションのトークンの発行ポリシーを設定します。', + 'create_collection.description' => '新しいオンチェーンコレクションを作成します。新しいコレクションIDは、オンチェーンで処理された後に、トランザクションイベントで返されます。', + 'create_token.args.recipient' => '初期発行のトークンの受取人アカウント。', + 'create_token.description' => 'コレクションに新しいトークンを作成します。新しいトークンは、指定された受取人アカウントに自動的に転送されます。', + 'create_wallet.args.externalId' => 'このウォレットに設定された外部ID。', + 'create_wallet.description' => '外部IDを使用して、認証されていない新しいウォレットレコードを保存します。', + 'freeze.args.collectionAccount' => '凍結するコレクションアカウント。', + 'freeze.args.collectionId' => '凍結するコレクションID。', + 'freeze.args.freezeType' => '実行する凍結のタイプ。', + 'freeze.args.tokenAccount' => '凍結するトークンアカウント。', + 'freeze.args.tokenId' => '凍結するトークンID。', + 'freeze.description' => 'コレクション、トークン、コレクションアカウント、またはトークンアカウントを凍結します。トークンが凍結している場合、これらを転送またはバーンできません。コレクションまたはコレクションアカウントを凍結すると、その中のすべてのトークンが凍結されます。', + 'link_wallet.description' => '注意:このワークフローとミューテーションはプレースホルダーです。VerifyAccountフローを使用して、ウォレットアカウントをこのプラットフォームに関連付けてください。', + 'mark_and_list_pending_transactions.args.accounts' => 'トランザクションをフィルターするアカウント。', + 'mark_and_list_pending_transactions.description' => '新しい保留中のトランザクションのリストを取得し、それらを処理中としてマークします。', + 'mint_token.args.collectionId' => '発行元のコレクションID。', + 'mint_token.args.recipient' => '発行されているトークンの受取人アカウント。', + 'mint_token.description' => '既存のトークンをさらに発行します。これは、供給上限が1を上回るトークンにのみ適用されます。', + 'mutate_collection.args.collectionId' => 'ミューテーションされるコレクション。', + 'mutate_collection.args.mutation' => 'ミューテーションされるパラメーター。', + 'mutate_collection.args.tokenId' => 'ミューテーションされるトークン。', + 'mutate_collection.description' => 'コレクションのデフォルト値を変更します。', + 'mutate_token.description' => 'トークンのデフォルト値を変更します。', + 'operator_transfer_token.description' => '他の誰かのウォレットのオペレーターとしてトークンを転送します。オペレーター転送は他の誰かのウォレットのトークンをソースとして使用する転送です。この種の転送を行うには、ソースウォレットのオーナーがトークンの転送を承認する必要があります。', + 'remove_collection.description' => '指定されたコレクションから属性を削除します。', + 'remove_token_attribute.description' => '指定されたトークンから属性を削除します。', + 'set_collection_attribute.description' => 'コレクションに属性を設定します。', + 'set_token_attribute.description' => 'トークンに属性を設定します。', + 'set_wallet_account.description' => 'ウォレットモデルにアカウントを設定します。', + 'simple_transfer_token.description' => '単一のトークンを受取人アカウントに転送します。', + 'thaw.args.collectionId' => '解凍するコレクションID。', + 'thaw.args.freezeType' => '実行する解凍のタイプ。', + 'thaw.args.tokenAccount' => '解凍するトークンアカウント。', + 'thaw.args.tokenId' => '解凍するトークンID。', + 'thaw.description' => '以前に凍結されたコレクションまたはトークンを解凍します。', + 'transfer_all_balance.description' => 'アカウントから別のアカウントにすべての残高を転送します。少なくとも既存のデポジットを維持する場合は、keepAlive引数を渡すことができます。', + 'transfer_balance.description' => 'アカウントから別のアカウントに残高を転送します。アカウントに少なくとも既存のデポジットが残されることを確認する場合は、keepAlive引数を渡すことができます。', + 'unapprove_collection.args.collectionId' => '承認が削除されるコレクション。', + 'unapprove_collection.args.operator' => 'コレクション承認が削除されるアカウント。', + 'unapprove_collection.description' => 'コレクションアカウントから転送を行うために、特定のアカウントの承認を削除します。', + 'unapprove_token.args.collectionId' => 'トークンが属するコレクション。', + 'unapprove_token.args.operator' => 'トークン承認が削除されるアカウント。', + 'unapprove_token.args.tokenId' => '承認が削除されるトークン。', + 'unapprove_token.description' => 'トークンアカウントから転送を行うために、特定のアカウントの承認を削除します。', + 'update_external_id.description' => 'ウォレットモデルの外部IDを変更します。', + 'update_transaction.args.state' => '転送の新しい状態。', + 'update_transaction.args.transactionHash' => 'オンチェーントランザクションのハッシュ。', + 'update_transaction.args.transactionId' => 'オンチェーントランザクションid', + 'update_transaction.description' => '新しい状態、トランザクションID、およびトランザクションハッシュでトランザクションを更新します。トランザクションIDとトランザクションハッシュは、一度設定されるとイミュータブルになることに注意してください。', + 'update_transaction.error.hash_and_id_are_immutable' => 'トランザクションのidとハッシュは、一度設定されるとイミュータブルになります。', + 'update_wallet_external_id.cannot_update_id_on_managed_wallet' => 'マネージドウォレットの外部idを更新できません。', + 'verify_account.description' => 'ウォレットはこのミューテーションを呼び出して、ユーザーアカウントのオーナーシップを証明します。', +]; diff --git a/lang/ja/query.php b/lang/ja/query.php new file mode 100644 index 00000000..ed936950 --- /dev/null +++ b/lang/ja/query.php @@ -0,0 +1,45 @@ + '認証済みかどうかをチェックするウォレットアカウント。', + 'get_account_verified.args.verificationId' => '認証済みかどうかを確認する認証ID。', + 'get_account_verified.description' => 'アカウントの認証ステータスを取得します。', + 'get_blocks.args.hashes' => 'フィルターするブロックチェーントランザクションのハッシュ。', + 'get_blocks.args.number' => 'フィルターするブロックチェーントランザクションID。', + 'get_collection.args.collectionId' => '取得するオンチェーンコレクションID。', + 'get_collection.description' => 'コレクションIDでコレクションを取得します。', + 'get_collections.args.collectionIds' => 'フィルターするオンチェーンコレクションID。', + 'get_collections.description' => 'オプションとしてコレクションIDでフィルターされるコレクションの配列を取得します。', + 'get_pending_events.args.acknowledgeEvents' => '返されるすべてのイベントを自動的に確認します(デフォルトはfalse)。', + 'get_pending_events.description' => '配信済みで未確認のイベントのリストを取得します。', + 'get_pending_wallets.description' => '認証が必要なウォレットアカウントの配列を取得します。', + 'get_token.args.collectionId' => 'トークンコレクションID。', + 'get_token.args.tokenId' => '取得する特定のトークンID。', + 'get_token.description' => 'トークンIDを使用して、コレクションのトークンを取得します。', + 'get_tokens.args.collectionId' => 'トークンを返すコレクション。', + 'get_tokens.args.tokenIds' => '特定のトークンIDをフィルターするか、省略してすべてを返します。', + 'get_tokens.description' => 'コレクションからトークンの配列を取得します。オプションで、トークンIDでフィルターできます。', + 'get_transaction.args.id' => 'トランザクションの内部ID。', + 'get_transaction.args.idempotencyKey' => 'フィルターするidempotency key。', + 'get_transaction.args.transactionHash' => 'ブロックチェーンのトランザクションのハッシュ。', + 'get_transaction.args.transactionId' => 'ブロックチェーンのトランザクションID。', + 'get_transaction.description' => 'データベースID、オンチェーントランザクションID、またはトランザクションハッシュを使ってトランザクションを取得します。', + 'get_transactions.args.accounts' => 'フィルターするウォレットアカウント。', + 'get_transactions.args.methods' => 'フィルターするトランザクションメソッドのタイプ。', + 'get_transactions.args.results' => '特定の結果のトランザクションをフィルターします。', + 'get_transactions.args.states' => '特定の状態のトランザクションをフィルターします。', + 'get_transactions.description' => 'オプションでトランザクションID、トランザクションハッシュ、メソッド、状態、結果、またはアカウントでフィルターされたトランザクションの配列を取得します。', + 'get_wallet.args.account' => 'ブロックチェーンのウォレットアカウント。', + 'get_wallet.args.externalId' => 'このウォレットの外部ID。', + 'get_wallet.args.id' => 'このウォレットの内部ID。', + 'get_wallet.args.newExternalId' => 'このウォレットに設定する新しい外部ID。', + 'get_wallet.args.verificationId' => 'このウォレットの認証ID。', + 'get_wallet.description' => 'データベースID、外部ID、認証ID、またはアカウントアドレスのいずれかでウォレットを取得します。', + 'request_account.args.callback' => 'これは、ウォレットが認証を送信するコールバックURLです。', + 'request_account.description' => 'このクエリは、ユーザーがスキャンしてウォレットアカウントを提供できるQRコードを生成します。', + 'verify_message.args.cryptoSignatureType' => '署名の暗号タイプ。このフィールドはオプションであり、デフォルトでsr25519が使用されます。', + 'verify_message.args.message' => 'ユーザーが署名したメッセージ。', + 'verify_message.args.publicKey' => 'ユーザーの公開鍵。', + 'verify_message.args.signature' => '署名済みのメッセージ。', + 'verify_message.description' => '指定された公開鍵でメッセージが署名されていることを確認します。', +]; diff --git a/lang/ja/scalar.php b/lang/ja/scalar.php new file mode 100644 index 00000000..ffb7fd99 --- /dev/null +++ b/lang/ja/scalar.php @@ -0,0 +1,9 @@ + '`Boolean`スカラー型は`true`または`false`です。', + 'float.description' => '`Float`スカラー型は、[IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point)で指定されるとおり、符号付き倍精度小数点数値です。', + 'id.description' => '`ID`スカラー型は一意の識別子で、通常オブジェクトを再取得するため、またはキャッシュのキーとして使用されます。JSONレスポンスの場合、ID型は文字列として出現しますが、人間が読み取れることは意図されていません。入力型として期待されている場合、文字列(`"4"`など)または整数(`4`など)の入力値はIDとして受け入れられます。', + 'int.description' => '`Int`スカラー型は小数値を含まない符号付き整数値です。Intは-(2^31)から2^31 - 1の値です。', + 'string.description' => '`String`スカラー型は、UTF-8文字シーケンスとして表現されるテキストデータです。String型は、GraphQLによって人間が判読できる自由形式テキストを表現するために最もよく使用されます。', +]; diff --git a/lang/ja/schema.php b/lang/ja/schema.php new file mode 100644 index 00000000..974dfe25 --- /dev/null +++ b/lang/ja/schema.php @@ -0,0 +1,45 @@ + 'GraphQLのスキーマは、GraphQLサーバーの機能を定義します。サーバーで利用可能なすべての型とディレクトリ、そしてクエリ、ミューテーション、およびサブスクリプション操作のエントリーポイントを公開します。', + 'directives.deprecated' => 'GraphQLスキーマの要素をサポート対象外としてマークします。', + 'directives.deprecated.reason' => 'この要素が廃止になった理由を説明し、通常、サポートされている類似データにアクセスする方法についての提案が含まれます。([CommonMark](https://commonmark.org/)で指定されているとおり)Markdown構文を使ってフォーマットされます。', + 'directives.include' => '`if`引数がtrueである場合にのみ、executorにこのフィールドまたはフラグメントを含めるように指示します。', + 'directives.include.args.if' => 'trueの場合、含められます。', + 'directives.skip' => '`if`引数がtrueである場合に、executorにこのフィールドまたはフラグメントを省略するように指示します。', + 'directives.skip.args.if' => 'trueの場合、省略されます。', + 'inputvalue.field.defaultValue' => 'この入力値のデフォルト値を表現する、GraphQLでフォーマットされた文字列。', + 'types.directiveLocation.ARGUMENT_DEFINITION' => '引数の定義に隣接する場所。', + 'types.directiveLocation.ENUM' => '列挙型の定義に隣接する場所。', + 'types.directiveLocation.ENUM_VALUE' => '列挙値の定義に隣接する場所。', + 'types.directiveLocation.FIELD' => 'フィールドに隣接する場所。', + 'types.directiveLocation.FIELD_DEFINITION' => 'フィールドの定義に隣接する場所。', + 'types.directiveLocation.FRAGMENT_DEFINITION' => 'フラグメントの定義に隣接する場所。', + 'types.directiveLocation.FRAGMENT_SPREAD' => 'フラグメントスプレッドに隣接する場所。', + 'types.directiveLocation.INLINE_FRAGMENT' => 'インラインフラグメントに隣接する場所。', + 'types.directiveLocation.INPUT_FIELD_DEFINITION' => '入力オブジェクトフィールドの定義に隣接する場所。', + 'types.directiveLocation.INPUT_OBJECT' => '入力オブジェクト型の定義に隣接する場所。', + 'types.directiveLocation.INTERFACE' => 'インターフェースの定義に隣接する場所。', + 'types.directiveLocation.LIST' => 'これがリスト型であることを示します。`ofType`は有効なフィールドです。', + 'types.directiveLocation.MUTATION' => 'ミューテーション操作に隣接する場所。', + 'types.directiveLocation.OBJECT' => 'オブジェクト型の定義に隣接する場所。', + 'types.directiveLocation.QUERY' => 'クエリ操作に隣接する場所。', + 'types.directiveLocation.SCALAR' => 'スカラーの定義に隣接する場所。', + 'types.directiveLocation.SCHEMA' => 'スキーマの定義に隣接する場所。', + 'types.directiveLocation.SUBSCRIPTION' => 'サブスクリプション操作に隣接する場所。', + 'types.directiveLocation.UNION' => 'ユニオンの定義に隣接する場所。', + 'types.directiveLocation.VARIABLE_DEFINITION' => '変数の定義に隣接する場所。', + 'types.enumValues.ENUM' => 'これが列挙型であることを示します。`enumValues`は有効なフィールドです。', + 'types.enumValues.INPUT_OBJECT' => 'これが入力オブジェクト型であることを示します。`inputFields`は有効なフィールドです。', + 'types.enumValues.INTERFACE' => 'これがインターフェース型であることを示します。`fields`、`interfaces`、および`possibleTypes`は有効なフィールドです。', + 'types.enumValues.LIST' => 'これがリスト型であることを示します。`ofType`は有効なフィールドです。', + 'types.enumValues.NON_NULL' => 'これがNon-Null型であることを示します。`ofType`は有効なフィールドです。', + 'types.enumValues.OBJECT' => 'これがオブジェクト型であることを示します。`fields`および`interfaces`は有効なフィールドです。', + 'types.enumValues.SCALAR' => 'これがスカラー型であることを示します。', + 'types.enumValues.UNION' => 'これがユニオン型であることを示します。`possibleTypes`は有効なフィールドです。', + 'types.field.directives' => 'このサーバーでサポートされているすべてのディレクティブのリスト。', + 'types.field.mutationType' => 'このサーバーがミューテーションをサポートしている場合に、ミューテーション操作のルートとなる型。', + 'types.field.queryType' => 'クエリ操作のルートとなる型。', + 'types.field.subscriptionType' => 'このサーバーがサブスクリプションをサポートしている場合、サブスクリプション操作がルートとなる型。', + 'types.field.types' => 'このサーバーでサポートされているすべての型のリスト。', +]; diff --git a/lang/ja/ss58_address.php b/lang/ja/ss58_address.php new file mode 100644 index 00000000..bf06430f --- /dev/null +++ b/lang/ja/ss58_address.php @@ -0,0 +1,11 @@ + ':address: :messageをデコードできません', + 'error.format_out_of_range' => 'SS58フォーマットは範囲外です。', + 'error.invalid_decoded_address_checksum' => 'デコードされたアドレスのチェックサムが無効です。', + 'error.invalid_empty_address' => '無効な空のアドレスが渡されました。', + 'error.invalid_uint8array' => '無効なUint8Arrayです。', + 'error.unexpected_format' => 'ss58Format :ss58Formatが必要ですが、:ss58Decodedを受信しました。', + 'error.valid_key_expected' => '長さ:lengthの、変換する有効なキーが必要です。', +]; diff --git a/lang/ja/traits.php b/lang/ja/traits.php new file mode 100644 index 00000000..a8a94920 --- /dev/null +++ b/lang/ja/traits.php @@ -0,0 +1,10 @@ + ':class::属性tokenId #:tokenId、collectionId #:collectionId、key #:keyが見つかりません。', + 'query_data_or_fail.unable_to_find_collection' => ':class: コレクションcollectionChainId #:collectionChainIdが見つかりません。', + 'query_data_or_fail.unable_to_find_collection_account' => ':class: コレクションアカウントwalletId #:walletId、collectionId #:collectionIdが見つかりません。', + 'query_data_or_fail.unable_to_find_token' => ':class: トークンtokenChainId #:tokenChainId, collectionId #:collectionIdが見つかりません。', + 'query_data_or_fail.unable_to_find_token_account' => ':class: トークンアカウントwalletId #:walletId、collectionId #:collectionId、tokenId #:tokenIdが見つかりません。', + 'query_data_or_fail.unable_to_find_wallet_account' => ':class: ウォレットアカウント#:publicKeyが見つかりません。', +]; diff --git a/lang/ja/type.php b/lang/ja/type.php new file mode 100644 index 00000000..9b0fb45e --- /dev/null +++ b/lang/ja/type.php @@ -0,0 +1,119 @@ + 'Substrateアカウント。', + 'account.field.address' => 'アカウントのアドレス。', + 'account.field.publicKey' => 'アカウントの公開鍵。', + 'account_request.description' => 'アカウントを認証するためのリクエスト。', + 'account_request.field.qrCode' => 'ウォレットアプリでスキャンしてアカウントを認証するためのQRコード。', + 'account_request.field.verificationId' => 'これは、アカウントを取得するために生成された認証IDです。', + 'account_verified.description' => 'アカウントの認証ステータス。', + 'account_verified.field.account' => '認証されたアカウント。', + 'account_verified.field.verified' => 'ユーザーアカウントの認証が既に完了しているかどうか。', + 'attribute.description' => 'オンチェーンkey/valueペア。', + 'attribute_input.description' => 'コレクションまたはトークンの属性。', + 'balances.description' => 'ウォレットアカウントの残高プロパティ。', + 'big_int.description' => '256ビットまでの符号なし整数を表す型。値はPHP数値(intまたはstring)であり、科学表記法を使用してはいけません。', + 'block.description' => 'ブロックチェーンのブロック。', + 'block.field.exception' => 'ブロックを処理中に発生した例外。', + 'block.field.failed' => 'ブロックの処理に失敗したかどうか。', + 'block.field.hash' => 'オンチェーンブロックのハッシュ。', + 'block.field.id' => 'ブロックの内部ID。', + 'block.field.number' => 'オンチェーンブロック番号。', + 'block.field.synced' => 'ブロックが同期済であるかどうか。', + 'collection.description' => 'コレクションは、トークンを1つにグループ化し、それに適用するポリシーを設定します。', + 'collection_account.description' => 'コレクションアカウントは、特定のコレクションに対してウォレットのトークンアカウントを1つにグループ化し、それに含まれるすべてのトークンの凍結や承認などのオプションを制御します。', + 'collection_account.field.accountCount' => 'このコレクションアカウントに関連付けられたトークンアカウントの数。', + 'collection_account.field.approvals' => 'このアカウントの承認のリスト。', + 'collection_account.field.collection' => 'このコレクションアカウントが属するコレクション。', + 'collection_account.field.isFrozen' => 'このコレクションアカウントが凍結されているかどうかを指定します。', + 'collection_account.field.namedReserves' => 'このアカウントの名前付き準備金。', + 'collection_account.field.wallet' => 'このコレクションアカウントを所有するウォレット。', + 'collection_account_approval.description' => 'このコレクションアカウントの使用が承認されているウォレット。', + 'collection_account_approval.field.account' => 'この承認が属するトークンアカウント。', + 'collection_account_approval.field.expiration' => 'ウォレットが承認を失う有効期限ブロック', + 'collection_account_approval.field.wallet' => '承認されているウォレット。', + 'collection_type.field.accounts' => 'このコレクションのアカウント。', + 'collection_type.field.attributes' => 'このコレクションの属性。', + 'collection_type.field.collectionId' => 'このコレクションに割り当てられたID。', + 'collection_type.field.forceSingleMint' => 'このコレクションのトークンがSingleMintタイプとして発行されるかどうか。これは、このコレクションのトークンがNFTであることを示します。', + 'collection_type.field.frozen' => 'このコレクションが凍結しているかどうか。', + 'collection_type.field.maxTokenCount' => 'このコレクションに対して発行可能なトークンの最大数。', + 'collection_type.field.maxTokenSupply' => '発行可能なこのコレクションの各トークンの最大数。', + 'collection_type.field.network' => 'このコレクションが属するネットワーク。', + 'collection_type.field.owner' => 'このコレクションからトークンを発行できるウォレット。', + 'collection_type.field.royalty' => 'このトークンに既にロイヤルティーポリシーがあるかどうかを指定します。', + 'collection_type.field.tokens' => 'このコレクションから発行されたトークン。', + 'description' => 'GraphQLスキーマの基本単位は型です。GraphQLには、`__TypeKind`列挙で表現される様々な型が多数あります。型の種類に応じて、その型に関する情報が特定のフィールドに記述されます。スカラー型には名前と説明以外の情報がありませんが、列挙型には値が含まれます。オブジェクト型とインターフェース型はそれらが記述するフィールドを提供します。抽象型、ユニオン型、インターフェース型は、ランタイム時に可能なオブジェクト型を提供します。リスト型とNon-Null型は、その他の型で構成されます。', + 'edge.field.node' => '現在のカーソルにある項目のリスト。', + 'event.description' => 'ブロックチェーンのイベント。', + 'event.field.eventId' => 'イベントID。', + 'event.field.lookUp' => 'メソッドのルックアップ。', + 'event.field.moduleId' => 'パレットモジュール。', + 'event.field.params' => 'このイベントのパラメーター。', + 'event.field.phase' => '発生したブロック実行のフェーズ。', + 'event_param.field.type' => 'パラメーターの値の型。', + 'event_param.field.value' => 'パラメーターの値。', + 'json.description' => 'JSONデータを表す型。', + 'page_info.field.endCursor' => '次のカーソル。', + 'page_info.field.hasNextPage' => 'カーソルに、現在のページ以降のページがあるかを判定します。', + 'page_info.field.hasPreviousPage' => 'カーソルに現在のページより前のページがあるかどうかを判定します。', + 'page_info.field.startCursor' => '前のカーソル。', + 'royalty.description' => 'ロイヤルティの設定。', + 'token.description' => 'ブロックチェーンのトークン。トークンにはそれぞれに特有の設定があり、親コレクション属性をオーバーライドするために使用できる独自の属性もあります。', + 'token.field.accounts' => 'このトークンを保有するトークンアカウント。', + 'token.field.attributeCount' => 'このトークンに設定された属性の数。', + 'token.field.attributes' => 'トークンの属性。', + 'token.field.cap' => 'このトークンで利用可能な最大量。', + 'token.field.collection' => 'このトークンが属するコレクション。', + 'token.field.isFrozen' => 'このトークンが凍結され、転送が許可されていないかどうかを指定します。', + 'token.field.minimumBalance' => '全アカウントのこのトークンの最低必要残高。', + 'token.field.mintDeposit' => '発行者が発行のために予約している通貨の量。', + 'token.field.name' => '新しいトークンの名前。', + 'token.field.nonFungible' => 'このトークンが代替不可能であると見なされているかどうかを示します(使用できるトークンが1つしかないため、真に一意である)。', + 'token.field.royalty' => '設定されている場合はトークンロイヤルティを返し、設定されていない場合はnullを返します。', + 'token.field.supply' => 'このトークンの現在の供給。', + 'token.field.tokenId' => '128ビット符号なし整数値で表されるトークンチェーンID。', + 'token.field.unitPrice' => 'EFIによる各トークンの価格。', + 'token_account.description' => 'トークンアカウントはウォレットにあるコレクション内の特定のトークンの残高を保存します。', + 'token_account.field.balance' => 'このアカウントが保有するトークンの残高。', + 'token_account.field.collection' => 'このトークンアカウントが属するコレクション。', + 'token_account.field.isFrozen' => 'このトークンアカウントが凍結され、転送が許可されていないかどうかを指定します。', + 'token_account.field.reservedBalance' => 'このアカウントに予約された値。', + 'token_account.field.token' => 'このアカウントのトークン。', + 'token_account.field.wallet' => 'このトークンアカウントを所有するウォレット。', + 'token_account_approval.args.amount' => 'ウォレットが承認されている金額。', + 'token_account_approval.description' => 'このトークンアカウントの使用が承認されているウォレット。', + 'token_account_named_reserve.args.amount' => '予約されているウォレットの金額。', + 'token_account_named_reserve.args.pallet' => 'この予約を作成したパレット。', + 'token_account_named_reserve.description' => 'トークンと金額を予約したパレット。', + 'transaction.description' => 'ブロックチェーンのトランザクション。', + 'transaction.eth.description' => 'Ethereumトランザクション。', + 'transaction.eth.field.transactionId' => 'トランザクションのハッシュ。', + 'transaction.field.encodedData' => 'エンコードされたトランザクションデータ。', + 'transaction.field.events' => 'このトランザクションによって生成されたイベント。', + 'transaction.field.idempotencyKey' => 'このトランザクションに設定されたidempotency key。', + 'transaction.field.method' => '使用されているオンチェーンメソッド。', + 'transaction.field.result' => 'トランザクションの結果。', + 'transaction.field.state' => 'トランザクションの状態。', + 'transaction.field.transactionHash' => 'オンチェーントランザクションのハッシュ。', + 'transaction.field.transactionId' => 'オンチェーントランザクションID。', + 'transaction.field.wallet' => 'このトランザクションの署名に使用されるウォレット。', + 'wallet.address.' => 'ウォレットのアドレス。', + 'wallet.description' => 'ブロックチェーンウォレット。', + 'wallet.field.account' => 'ウォレットアカウント。', + 'wallet.field.balances' => 'アカウントのEFI残高。ブロックチェーンにウォレットが存在しない場合、残高はnullとなります。', + 'wallet.field.collectionAccountApprovals' => 'このウォレットにあるコレクションアカウントの承認。', + 'wallet.field.collectionAccounts' => 'このウォレットにあるコレクションアカウント。', + 'wallet.field.collectionIds' => '返すコレクション。', + 'wallet.field.externalId' => 'ウォレットに関連付けられた外部ID。', + 'wallet.field.id' => 'ウォレットの内部ID。', + 'wallet.field.managed' => 'これがマネージドウォレットであるかどうか。', + 'wallet.field.network' => 'このウォレットが属するブロックチェーンネットワーク。', + 'wallet.field.nonce' => 'アカウントのノンス。ブロックチェーンにウォレットが存在しない場合、ノンスはnullとなります。', + 'wallet.field.ownedCollections' => 'このウォレットが所有するコレクション。', + 'wallet.field.tokenAccountApprovals' => 'このウォレットにあるトークンアカウントの承認。', + 'wallet.field.tokenAccounts' => 'このウォレットが所有するトークンアカウント。トークンアカウントはトークンの残高を保存します。', + 'wallet.field.transactions' => 'このウォレットによって実行されるトランザクション。', + 'wallet_link.field.code' => 'プラットフォームのアカウントにリンクするためにユーザーがウォレットアプリに入力するコード。', +]; diff --git a/lang/ja/typekind.php b/lang/ja/typekind.php new file mode 100644 index 00000000..b8d5da5c --- /dev/null +++ b/lang/ja/typekind.php @@ -0,0 +1,5 @@ + '特定の`__Type`がどの種類の型かを記述する列挙。', +]; diff --git a/lang/ja/validation.php b/lang/ja/validation.php new file mode 100644 index 00000000..98278809 --- /dev/null +++ b/lang/ja/validation.php @@ -0,0 +1,35 @@ + 'コレクション :collectionIdで:accountのコレクションアカウントが見つかりませんでした。', + 'account_exists_in_token' => 'コレクション :collectionIdとトークン :tokenIdで:accountのトークンアカウントが見つかりませんでした。', + 'account_exists_in_wallet' => '指定された:attributeが見つかりませんでした。', + 'approval_exists_in_collection' => 'コレクション :collectionIdで:operatorの承認が見つかりませんでした。', + 'approval_exists_in_token' => 'コレクション :collectionIdとトークン :tokenIdで:operatorの承認が見つかりませんでした。', + 'attribute_exists_in_collection' => 'キーは指定されたコレクションに存在しません。', + 'check_token_count' => 'トークン総量の:totalは:maxTokenトークンの上限を超えました。', + 'distinct_attribute' => ':attributeは異なる属性キーの配列である必要があります。', + 'distinct_multi_asset' => ':attributeは異なるマルチアセットの配列である必要があります。', + 'future_block' => ':attributeは少なくとも:blockである必要があります。', + 'is_collection_owner' => 'あなたは指定された:attributeを所有していません。', + 'is_managed_wallet' => ':attributeはこのプラットフォームが管理するウォレットではありません。', + 'key_doesnt_exit_in_token' => 'キーは指定されたトークンに存在しません。', + 'max_big_int' => ':attributeが大きすぎます。可能な最大値は:maxです。', + 'max_token_balance' => ':attributeは無効です。トークンアカウント残高より大きい金額が指定されています。', + 'min_big_int' => ':attributeが小さすぎます。可能な最低値は:minです。', + 'min_token_deposit' => ':attributeが小さすぎます。最低トークンデポジットは0.01 EFIであるため、「initialSupply × unitPrice」は10^16より大きくなる必要があります。', + 'mutation.behavior.isCurrency.accepted' => 'isCurrencyパラメーターはtrueのみを受け入れます。通貨にしない場合は省略できます。', + 'no_tokens_in_collection' => ':attributeには既存のトークンがあってはいけません。', + 'token_doesnt_exist_in_collection' => ':attributeは指定されたコレクションに既に存在します。', + 'token_encode_doesnt_exist_in_collection' => ':attributeは指定されたコレクションに既に存在します。', + 'token_encode_exist_in_collection' => ':attributeは指定されたコレクションに存在しません。', + 'token_encode_exists' => ':attributeは存在しません。', + 'token_exists_in_collection' => ':attributeは指定されたコレクションに存在しません。', + 'valid_hex' => ':attributeには無効な16進文字列があります。', + 'valid_royalty_percentage' => 'ロイヤルティに有効な:attributeは、0.1%~50%で、小数点以下 7 桁までです。', + 'valid_substrate_account' => ':attributeは有効なSubstrateアカウントではありません。', + 'valid_substrate_address' => ':attributeは有効なSubstrateアドレスではありません。', + 'valid_substrate_transaction_id' => ':attributeには有効なSubstrateトランザクションIDがありません。', + 'valid_verification_id' => '認証IDは有効ではありません。', + 'numeric' => ':attributeは数値である必要があります。', +]; diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 00000000..c9246b2a --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,13 @@ +includes: + - ./vendor/nunomaduro/larastan/extension.neon + +parameters: + level: 5 + paths: + - src + - config + - database + tmpDir: build/phpstan + checkOctaneCompatibility: true + checkModelProperties: true + checkMissingIterableValueType: false \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 00000000..e744dcb4 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,34 @@ + + + + + tests/Unit + + + tests/Feature/GraphQL + + + + + + + + + + + + + + + + + + + + + + + ./src + + + diff --git a/resources/IDEHelp/IDEAutocomplete.php b/resources/IDEHelp/IDEAutocomplete.php new file mode 100644 index 00000000..3cec2d53 --- /dev/null +++ b/resources/IDEHelp/IDEAutocomplete.php @@ -0,0 +1,23 @@ +push( + new CacheMiddleware( + new PrivateCacheStrategy( + new LaravelCacheStorage( + Cache::store('redis') + ) + ) + ), + 'cache' + ); + + return parent::getClient()->setHandler($stack); + } + + /** + * Get the response data. + * + * @throws RequestException + */ + protected function getResponse(Response|PromiseInterface $response): mixed + { + return parent::getResponse($response); + } +} diff --git a/src/Clients/Abstracts/HttpAbstract.php b/src/Clients/Abstracts/HttpAbstract.php new file mode 100644 index 00000000..73a227d6 --- /dev/null +++ b/src/Clients/Abstracts/HttpAbstract.php @@ -0,0 +1,33 @@ +withoutVerifying()->asJson()->acceptJson(); + } + + /** + * Get the response data. + * + * @throws RequestException + */ + protected function getResponse(Response|PromiseInterface $response): mixed + { + return $response instanceof Response ? + $response->throw()->body() : + $response; + } +} diff --git a/src/Clients/Abstracts/JsonHttpAbstract.php b/src/Clients/Abstracts/JsonHttpAbstract.php new file mode 100644 index 00000000..2dcb390f --- /dev/null +++ b/src/Clients/Abstracts/JsonHttpAbstract.php @@ -0,0 +1,33 @@ +asJson() + ->acceptJson(); + } + + /** + * Get the response data. + * + * @throws RequestException + */ + protected function getResponse(Response|PromiseInterface $response): mixed + { + return $response instanceof Response ? + $response->throw()->json() : + $response; + } +} diff --git a/src/Clients/Abstracts/WebsocketAbstract.php b/src/Clients/Abstracts/WebsocketAbstract.php new file mode 100644 index 00000000..5223f3f7 --- /dev/null +++ b/src/Clients/Abstracts/WebsocketAbstract.php @@ -0,0 +1,108 @@ +setHost($host); + } + + /** + * Close the websocket connection. + */ + public function __destruct() + { + $this->close(); + } + + /** + * Send a request to the websocket server. + */ + public function send(string $method, array $params = [], bool $rawResponse = false): array|string|null + { + $response = $this->sendRaw(Util::createJsonRpc($method, $params)); + + return $rawResponse ? $response : $response['result'] ?? null; + } + + /** + * Send a raw request to the websocket server. + */ + public function sendRaw(string $payload): ?array + { + $this->client()->send($payload); + + return JSON::decode($this->client->receive(), true); + } + + /** + * Set the timeout for the websocket connection. + */ + public function setTimeout(int $timeout): self + { + $this->client()->setTimeout($timeout); + + return $this; + } + + /** + * Set the host name. + */ + public function setHost(string $host): self + { + $this->host = $host; + $this->close(); + + return $this; + } + + /** + * Get data from the websocket server. + */ + public function receive(): mixed + { + return $this->client()->receive(); + } + + /** + * Close the websocket connection. + */ + public function close(): void + { + if ($this->client) { + try { + $this->client->close(); + } catch (\Throwable $e) { + } + } + } + + /** + * Get the websocket client instance. + */ + protected function client(): Client + { + if (!$this->client || !$this->client->isConnected()) { + $this->client = app(Client::class, [ + 'uri' => $this->host, + 'options' => ['timeout' => 20], + ]); + } + + return $this->client; + } +} diff --git a/src/Clients/Implementations/AsyncWebsocket.php b/src/Clients/Implementations/AsyncWebsocket.php new file mode 100644 index 00000000..f350347e --- /dev/null +++ b/src/Clients/Implementations/AsyncWebsocket.php @@ -0,0 +1,74 @@ +host = $url ?? config(sprintf('enjin-platform.chains.supported.%s.%s.node', config('enjin-platform.chains.selected'), config('enjin-platform.chains.network'))); + $this->client = $this->connect(); + } + + /** + * Connect to the websocket server. + */ + public function connect(): WebsocketConnection + { + $handshake = (new WebsocketHandshake($this->host))->withHeader('Sec-WebSocket-Protocol', 'dumb-increment-protocol'); + + return connect($handshake); + } + + /** + * Send a request to the websocket server. + */ + public function send(string $method, array $params = [], bool $rawResponse = false): ?array + { + $response = $this->sendRaw(Util::createJsonRpc($method, $params)); + + return $rawResponse ? $response : $response['result'] ?? null; + } + + /** + * Send a raw request to the websocket server. + */ + public function sendRaw(string $payload): ?array + { + $this->client->send($payload); + $received = $this->client->receive(); + if (!$received) { + return null; + } + + return JSON::decode($received->buffer(), true); + } + + /** + * Close the websocket connection. + */ + public function close(): void + { + $this->client->close(); + } +} diff --git a/src/Clients/Implementations/DecoderClient.php b/src/Clients/Implementations/DecoderClient.php new file mode 100644 index 00000000..b1c95882 --- /dev/null +++ b/src/Clients/Implementations/DecoderClient.php @@ -0,0 +1,27 @@ +getClient()->get($url); + + return $this->getResponse($result); + } catch (\Throwable $e) { + return null; + } + } +} diff --git a/src/Clients/Implementations/SubstrateWebsocket.php b/src/Clients/Implementations/SubstrateWebsocket.php new file mode 100644 index 00000000..7270e92a --- /dev/null +++ b/src/Clients/Implementations/SubstrateWebsocket.php @@ -0,0 +1,18 @@ +description = __('enjin-platform::commands.clear_cache.description'); + } + + /** + * Process the command. + */ + public function handle(): int + { + $packageCaches = Package::getClassesThatImplementInterface(PlatformCacheable::class); + + $packageCaches->each( + fn ($packageCache) => $packageCache::clearable()->each( + fn ($cache) => Cache::forget($cache->key()) + ) + ); + + $this->info(__('enjin-platform::commands.clear_cache.finished')); + + return CommandAlias::SUCCESS; + } +} diff --git a/src/Commands/Ingest.php b/src/Commands/Ingest.php new file mode 100644 index 00000000..d3db260e --- /dev/null +++ b/src/Commands/Ingest.php @@ -0,0 +1,62 @@ +description = __('enjin-platform::commands.ingest.description'); + } + + /** + * Process the command. + */ + public function handle(Backoff $backoff, BlockProcessor $processor): int + { + try { + $backoff->setStrategy(new PolynomialStrategy(300)) + ->setMaxAttempts(10) + ->setErrorHandler(function (Throwable|null $e, int $attempt) { + Log::error('We got an exception in the ingest process...'); + if ($e) { + Log::error("On run {$attempt} error in {$e->getFile()}:{$e->getLine()}: {$e->getMessage()}"); + } + }) + ->run(fn () => $processor->ingest()); + } catch (Throwable $e) { + Log::error('We got another exception in the ingest... Restarting the service.'); + Log::error("Error in {$e->getFile()}:{$e->getLine()}: {$e->getMessage()}"); + } + + // We will sleep for three minutes to avoid rate limits + sleep(180); + + return self::FAILURE; + } +} diff --git a/src/Commands/Sync.php b/src/Commands/Sync.php new file mode 100644 index 00000000..750e89b3 --- /dev/null +++ b/src/Commands/Sync.php @@ -0,0 +1,274 @@ +description = __('enjin-platform::commands.sync.description'); + $this->nodeUrl = config(sprintf('enjin-platform.chains.supported.substrate.%s.node', config('enjin-platform.chains.network'))); + $this->start = now(); + } + + /** + * Process the command. + */ + public function handle(Backoff $backoff, SubstrateWebsocket $rpc): int + { + PlatformSyncing::dispatch(); + + $backoff->setStrategy(new PolynomialStrategy(250, 2)) + ->setWaitCap(600000) + ->setErrorHandler(function (Throwable|null $e) { + $this->error(__('enjin-platform::error.exception_in_sync')); + $this->error($e->getMessage()); + $this->error($message = __('enjin-platform::error.line_and_file', ['line' => $e->getLine(), 'file' => $e->getFile()])); + PlatformSyncError::dispatch($message); + }) + ->run(fn () => $this->startSync($rpc)); + + PlatformSynced::dispatch(); + + return CommandAlias::SUCCESS; + } + + /** + * Display the progress bar. + */ + protected function displayMessageAboveBar(string $message): void + { + $this->progressBar->clear(); + $this->info($message); + $this->progressBar->display(); + } + + /** + * Start the sync process. + */ + protected function startSync(SubstrateWebsocket $rpc): void + { + $this->info(__('enjin-platform::commands.sync.header')); + if (!$this->truncateTables()) { + throw new PlatformException(__('enjin-platform::error.failed_to_truncate')); + } + + $block = $this->getCurrentBlock($rpc); + $storages = $this->getStorageAt($block->hash); + $this->parseStorages($block, $storages); + $this->displayOverview($storages); + } + + /** + * Display the overview of the sync. + */ + protected function displayOverview(array $storages): void + { + $this->progressBar->finish(); + $this->newLine(); + + $this->info(__('enjin-platform::commands.sync.overview')); + foreach ($storages as $storage) { + $this->info(sprintf( + '%s: %s', + ucwords(str_replace('_', ' ', strtolower($storage[0]->name))), + $storage[2] + )); + } + $this->info(__('enjin-platform::commands.sync.total_time', ['sec' => now()->diffInMilliseconds($this->start) / 1000])); + $this->info('======================================================='); + } + + /** + * Get the current block. + */ + protected function getCurrentBlock(SubstrateWebsocket $rpc): Block + { + $blockHash = $rpc->send('chain_getBlockHash'); + $blockNumber = Arr::get($rpc->send('chain_getBlock', [$blockHash]), 'block.header.number'); + $rpc->close(); + + if (!$blockHash || !$blockNumber) { + throw new PlatformException(__('enjin-platform::error.failed_to_get_current_block')); + } + + $blockNumber = HexConverter::hexToUInt($blockNumber); + $this->info(__('enjin-platform::commands.sync.syncing', ['blockNumber' => $blockNumber])); + + return Block::create([ + 'number' => $blockNumber, + 'hash' => $blockHash, + 'synced' => false, + ]); + } + + /** + * Get the storage at the given block hash. + */ + protected function getStorageAt(string $blockHash): array + { + $storageKeys = $this->getStorageKeys($blockHash); + + $this->progressBar = $this->output->createProgressBar(count($storageKeys) * 2); + $this->progressBar->setFormat('debug'); + $this->progressBar->start(); + $this->displayMessageAboveBar(__('enjin-platform::commands.sync.fetching')); + + return Future\await( + array_map( + fn ($keyAndHash) => async(function () use ($keyAndHash) { + $storageKey = $keyAndHash[0]; + + $context = (new ProcessContextFactory())->start(__DIR__ . '/contexts/get_storage.php'); + $context->send($keyAndHash); + [$storage, $total] = $context->join(); + + $this->progressBar->advance(); + + return [$storageKey, $storage, $total]; + }), + $storageKeys, + ) + ); + } + + protected function getStorageKeys(string $blockHash): array + { + $storage = [ + StorageKey::COLLECTIONS, + StorageKey::COLLECTION_ACCOUNTS, + StorageKey::TOKENS, + StorageKey::TOKEN_ACCOUNTS, + StorageKey::ATTRIBUTES, + ]; + + if (class_exists($enum = '\Enjin\Platform\FuelTanks\Enums\Substrate\StorageKey')) { + $storage = array_merge($storage, [$enum::TANKS, $enum::ACCOUNTS]); + } + + if (class_exists($enum = '\Enjin\Platform\Marketplace\Enums\Substrate\StorageKey')) { + $storage = array_merge($storage, [$enum::LISTINGS]); + } + + return array_map( + fn ($key) => [$key, $blockHash, $this->nodeUrl], + $storage + ); + } + + /** + * Parse the storages. + */ + protected function parseStorages(Block $block, array $storages): void + { + $this->displayMessageAboveBar(__('enjin-platform::commands.sync.decoding')); + + for ($x = 0; $x < count($storages); $x++) { + [$storageKey, $storageValues] = $storages[$x]; + foreach ($storageValues as $storagePage) { + $facade = $storageKey->parserFacade(); + $facade::{$storageKey->parser()}($storagePage); + } + $this->progressBar->advance(); + } + + $block->synced = true; + $block->save(); + } + + /** + * Truncate the tables. + */ + protected function truncateTables(): bool + { + $this->info(__('enjin-platform::commands.sync.truncating')); + + try { + Schema::disableForeignKeyConstraints(); + array_map( + fn ($table) => DB::table($table)->truncate(), + $this->tablesToTruncate() + ); + } catch (\Exception $e) { + Schema::enableForeignKeyConstraints(); + + return false; + } + Schema::enableForeignKeyConstraints(); + + return true; + } + + protected function tablesToTruncate(): array + { + $tables = Truncate::tables(); + + if (class_exists($truncate = '\Enjin\Platform\FuelTanks\Commands\contexts\Truncate')) { + $tables = array_merge($tables, $truncate::tables()); + } + + if (class_exists($truncate = '\Enjin\Platform\Marketplace\Commands\contexts\Truncate')) { + $tables = array_merge($tables, $truncate::tables()); + } + + return $tables; + } +} diff --git a/src/Commands/Transactions.php b/src/Commands/Transactions.php new file mode 100644 index 00000000..fbb6529e --- /dev/null +++ b/src/Commands/Transactions.php @@ -0,0 +1,148 @@ +description = __('enjin-platform::commands.transactions.description'); + $this->nodeUrl = config(sprintf('enjin-platform.chains.supported.substrate.%s.node', config('enjin-platform.chains.network'))); + $this->start = now(); + } + + public function handle(SubstrateWebsocket $rpc): int + { + $fromBlock = $this->option('from'); + if (!$fromBlock) { + $this->warn(__('enjin-platform::commands.transactions.specify_start')); + + return CommandAlias::FAILURE; + } + + $toBlock = $this->option('to'); + + if (!$toBlock) { + $blockHash = $rpc->send('chain_getBlockHash'); + $blockNumber = Arr::get($rpc->send('chain_getBlock', [$blockHash]), 'block.header.number'); + $rpc->close(); + + if (!$blockHash || !$blockNumber) { + throw new PlatformException(__('enjin-platform::error.failed_to_get_current_block')); + } + + $toBlock = HexConverter::hexToUInt($blockNumber); + } + + if ($toBlock < $fromBlock) { + $this->warn(__('enjin-platform::commands.transactions.start_lower_than_end')); + + return CommandAlias::FAILURE; + } + + $this->updateTransactions($fromBlock, $toBlock); + + return CommandAlias::SUCCESS; + } + + protected function updateTransactions(int $fromBlock, int $toBlock): void + { + $this->info(__('enjin-platform::commands.transactions.header')); + $this->info(__('enjin-platform::commands.transactions.syncing', ['fromBlock' => $fromBlock, 'toBlock' => $toBlock])); + $this->info(__('enjin-platform::commands.transactions.fetching')); + + $rangeEnd = min($fromBlock + self::CONCURRENT_REQUESTS - 1, $toBlock); + $requests = array_map(fn ($from) => [$this->nodeUrl, self::CONCURRENT_REQUESTS, $from, $toBlock], range($fromBlock, $rangeEnd)); + + $totalBlocks = $toBlock - $fromBlock + 1; + $totalProgress = $totalBlocks + count($requests); + $this->progressBar = $this->output->createProgressBar($totalProgress); + $this->progressBar->setFormat('debug'); + $this->progressBar->start(); + + $extrinsics = Future\await(array_map( + fn ($request) => async(function () use ($request) { + $context = (new ProcessContextFactory())->start(__DIR__ . '/contexts/get_extrinsics.php'); + $context->send($request); + $result = $context->join(); + $this->progressBar->advance(); + + return $result; + }), + $requests + )); + + $this->parseTransactions($extrinsics, $totalBlocks); + } + + protected function parseTransactions(array $extrinsics, int $totalBlocks) + { + $this->displayMessageAboveBar(__('enjin-platform::commands.transactions.decoding')); + + $codec = new Codec(); + $totalExtrinsics = 0; + collect($extrinsics)->each(function ($chunk) use ($codec, &$totalExtrinsics) { + foreach ($chunk as $blockNumber => $extrinsics) { + $block = new Block(); + $block->number = $blockNumber; + $block->extrinsics = State::extrinsicsForBlock(['number' => $block->number, 'extrinsics' => json_encode($extrinsics)]) ?? []; + $totalExtrinsics += count($block->extrinsics); + + (new ExtrinsicProcessor($block, $codec))->run(); + $this->progressBar->advance(); + } + }); + + $this->displayOverview($totalBlocks, $totalExtrinsics); + } + + protected function displayOverview(int $totalBlocks, int $totalExtrinsics): void + { + $this->progressBar->finish(); + $this->newLine(); + + $this->info(__('enjin-platform::commands.transactions.overview')); + $this->info(__('enjin-platform::commands.transactions.total_extrinsics', ['extrinsics' => $totalExtrinsics])); + $this->info(__('enjin-platform::commands.transactions.total_blocks', ['blocks' => $totalBlocks])); + $this->info(__('enjin-platform::commands.transactions.total_time', ['sec' => now()->diffInMilliseconds($this->start) / 1000])); + $this->info('======================================================='); + } + + protected function displayMessageAboveBar(string $message): void + { + $this->progressBar->clear(); + $this->info($message); + $this->progressBar->display(); + } +} diff --git a/src/Commands/contexts/Truncate.php b/src/Commands/contexts/Truncate.php new file mode 100644 index 00000000..e2a9f832 --- /dev/null +++ b/src/Commands/contexts/Truncate.php @@ -0,0 +1,22 @@ +receive(); + $nodeUrl = $receivedMessage[0]; + $interval = $receivedMessage[1]; + $current = $receivedMessage[2]; + $to = $receivedMessage[3]; + + $rpc = new AsyncWebsocket($nodeUrl); + $data = []; + + while (true) { + $blockHash = $rpc->send('chain_getBlockHash', [$current]); + $extrinsics = Arr::get($rpc->send('chain_getBlock', [$blockHash]), 'block.extrinsics', []); + $data[$current] = $extrinsics; + + $current += $interval; + if ($current > $to) { + $rpc->close(); + + break; + } + } + + return $data; +}; diff --git a/src/Commands/contexts/get_storage.php b/src/Commands/contexts/get_storage.php new file mode 100644 index 00000000..e37aec26 --- /dev/null +++ b/src/Commands/contexts/get_storage.php @@ -0,0 +1,59 @@ +receive(); + $storageKey = $receivedMessage[0]; + $blockHash = $receivedMessage[1]; + $nodeUrl = $receivedMessage[2]; + + $rpcKey = new AsyncWebsocket($nodeUrl); + $rpcStorage = new AsyncWebsocket($nodeUrl); + + $total = 0; + $storageValues = []; + $asyncQueries = []; + + while (true) { + $keys = $rpcKey->send( + 'state_getKeysPaged', + [ + $storageKey->value, + 1000, + $startKey ?? null, + $blockHash, + ] + ); + + if (empty($keys)) { + break; + } + + $total += count($keys); + $asyncQueries[] = async(function () use ($rpcStorage, $keys, $blockHash, &$storageValues) { + $storage = $rpcStorage->send( + 'state_queryStorageAt', + [ + $keys, + $blockHash, + ] + ); + $storageValues[] = Arr::get($storage, '0.changes'); + }); + + $startKey = Arr::last($keys); + } + + Future\await($asyncQueries); + + $rpcStorage->close(); + $rpcKey->close(); + + return [$storageValues, $total]; +}; diff --git a/src/CoreServiceProvider.php b/src/CoreServiceProvider.php new file mode 100644 index 00000000..863cbe7f --- /dev/null +++ b/src/CoreServiceProvider.php @@ -0,0 +1,136 @@ +name('enjin-platform') + ->hasConfigFile(['enjin-platform', 'graphql']) + ->hasMigration('create_wallets_table') + ->hasMigration('create_collections_table') + ->hasMigration('create_collection_accounts_table') + ->hasMigration('create_tokens_table') + ->hasMigration('create_token_accounts_table') + ->hasMigration('create_attributes_table') + ->hasMigration('create_blocks_table') + ->hasMigration('create_transactions_table') + ->hasMigration('create_verifications_table') + ->hasMigration('create_token_account_approvals_table') + ->hasMigration('create_token_account_named_reserves_table') + ->hasMigration('create_collection_account_approvals_table') + ->hasMigration('create_collection_royalty_currencies_table') + ->hasMigration('create_events_table') + ->hasMigration('create_pending_events_table') + ->hasMigration('add_signed_at_block') + ->hasMigration('remove_linking_code_from_wallets_table') + ->hasRoute('enjin-platform') + ->hasCommand(Sync::class) + ->hasCommand(Ingest::class) + ->hasCommand(Transactions::class) + ->hasCommand(ClearCache::class) + ->hasTranslations(); + } + + /** + * Bootstrap any application services. + */ + public function boot() + { + parent::boot(); + + $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); + $this->loadRoutesFrom(__DIR__ . '/../routes/enjin-platform.php'); + $this->loadTranslationsFrom(__DIR__ . '/../lang', 'enjin-platform'); + + $this->app->register(SerializationServiceProvider::class); + $this->app->register(BlockchainServiceProvider::class); + $this->app->register(WebsocketClientProvider::class); + $this->app->register(GraphQlServiceProvider::class); + $this->app->register(FakerServiceProvider::class); + $this->app->register(AuthServiceProvider::class); + + Event::listen(PlatformSyncing::class, fn () => BlockProcessor::synching()); + Event::listen(PlatformSynced::class, fn () => BlockProcessor::synchingDone()); + Event::listen(PlatformSyncError::class, fn () => BlockProcessor::synchingDone()); + + Builder::macro('cursorPaginateWithTotal', function ($order, $limit, $cache = true) { + if ($cache) { + $totalCount = (int) Cache::remember( + $this->toSql(), + 6, + fn () => Cache::lock('enjin-platform.lock:' . $this->toSql())->get(fn () => $this->count()) + ); + } + + return [ + 'total' => $totalCount ?? $this->count(), + 'items' => $this->orderBy($order)->cursorPaginate($limit), + ]; + }); + + Builder::macro('cursorPaginateWithTotalDesc', function ($order, $limit, $cache = true) { + if ($cache) { + $totalCount = (int) Cache::remember( + $this->toSql(), + 6, + fn () => Cache::lock('enjin-platform.lock:' . $this->toSql())->get(fn () => $this->count()) + ); + } + + return [ + 'total' => $totalCount ?? $this->count(), + 'items' => $this->orderByDesc($order)->cursorPaginate($limit), + ]; + }); + + Collection::macro('recursive', function () { + return $this->whenNotEmpty($recursive = function ($item) use (&$recursive) { + if (is_array($item)) { + return $recursive(new static($item)); + } elseif ($item instanceof Collection) { + $item->transform(static function ($collection, $key) use ($recursive, $item) { + return $item->{$key} = $recursive($collection); + }); + } elseif (is_object($item)) { + foreach ($item as $key => &$val) { + $item->{$key} = $recursive($val); + } + } + + return $item; + }); + }); + } +} diff --git a/src/Enums/CoreRoute.php b/src/Enums/CoreRoute.php new file mode 100644 index 00000000..c5c36f1f --- /dev/null +++ b/src/Enums/CoreRoute.php @@ -0,0 +1,12 @@ +value . ($suffix ? ":{$suffix}" : ''); + } + + public static function clearable(): Collection + { + return collect([ + self::METADATA, + self::CALL_INDEXES, + self::CUSTOM_TYPES, + self::MANAGED_ACCOUNTS, + ]); + } +} diff --git a/src/Enums/Global/TokenType.php b/src/Enums/Global/TokenType.php new file mode 100644 index 00000000..7aa6bd74 --- /dev/null +++ b/src/Enums/Global/TokenType.php @@ -0,0 +1,13 @@ + new CollectionCreated(), + self::COLLECTION_DESTROYED => new CollectionDestroyed(), + self::COLLECTION_ACCOUNT_CREATED => new CollectionAccountCreated(), + self::COLLECTION_ACCOUNT_DESTROYED => new CollectionAccountDestroyed(), + self::TOKEN_CREATED => new TokenCreated(), + self::TOKEN_DESTROYED => new TokenDestroyed(), + self::TOKEN_ACCOUNT_CREATED => new TokenAccountCreated(), + self::TOKEN_ACCOUNT_DESTROYED => new TokenAccountDestroyed(), + self::MINTED => new Minted(), + self::BURNED => new TokenBurned(), + self::FROZEN => new Freeze(), + self::THAWED => new Thawed(), + self::TRANSFERRED => new Transferred(), + self::ATTRIBUTE_SET => new AttributeSet(), + self::ATTRIBUTE_REMOVED => new AttributeRemoved(), + self::APPROVED => new Approved(), + self::UNAPPROVED => new Unapproved(), + self::TOKEN_MUTATED => new TokenMutated(), + self::COLLECTION_MUTATED => new CollectionMutated(), + }; + } +} diff --git a/src/Enums/Substrate/PalletIdentifier.php b/src/Enums/Substrate/PalletIdentifier.php new file mode 100644 index 00000000..85cf3670 --- /dev/null +++ b/src/Enums/Substrate/PalletIdentifier.php @@ -0,0 +1,23 @@ + 'collectionsStorages', + self::COLLECTION_ACCOUNTS => 'collectionsAccountsStorages', + self::TOKENS => 'tokensStorages', + self::TOKEN_ACCOUNTS => 'tokensAccountsStorages', + self::ATTRIBUTES => 'attributesStorages', + default => throw new PlatformException('No parser for this storage key.'), + }; + } + + public function parserFacade(): string + { + return '\Facades\Enjin\Platform\Services\Processor\Substrate\Parser'; + } +} diff --git a/src/Enums/Substrate/SystemEventType.php b/src/Enums/Substrate/SystemEventType.php new file mode 100644 index 00000000..04ee5ee2 --- /dev/null +++ b/src/Enums/Substrate/SystemEventType.php @@ -0,0 +1,20 @@ +model = $transaction; + + $this->broadcastData = [ + 'id' => $transaction->id, + 'method' => $transaction->method, + 'state' => $transaction->state, + 'idempotencyKey' => $transaction->idempotency_key, + ]; + + $this->broadcastChannels = [ + new Channel($transaction->wallet->address), + ]; + } +} diff --git a/src/Events/Global/TransactionUpdated.php b/src/Events/Global/TransactionUpdated.php new file mode 100644 index 00000000..99ea879c --- /dev/null +++ b/src/Events/Global/TransactionUpdated.php @@ -0,0 +1,34 @@ +broadcastData = [ + 'id' => $transaction->id, + 'method' => $transaction->method, + 'state' => $transaction->state, + 'result' => $transaction->result, + 'transactionId' => $transaction->transaction_chain_id, + 'transactionHash' => $transaction->transaction_chain_hash, + 'idempotencyKey' => $transaction->idempotency_key, + ]; + + $this->broadcastChannels = [ + new Channel($transaction->wallet_address), + ]; + } +} diff --git a/src/Events/PlatformBroadcastEvent.php b/src/Events/PlatformBroadcastEvent.php new file mode 100644 index 00000000..912ae97e --- /dev/null +++ b/src/Events/PlatformBroadcastEvent.php @@ -0,0 +1,116 @@ +className = (new \ReflectionClass(static::class))->getShortName(); + $this->uuid = Str::uuid()->toString(); + } + + /** + * Get the name the event should be broadcast on. + */ + public function broadcastAs() + { + $className = Str::kebab($this->className); + + return "platform:{$className}"; + } + + /** + * Get the channels the event should broadcast on. + * + * @return \Illuminate\Broadcasting\Channel|array + */ + public function broadcastOn() + { + return $this->broadcastChannels; + } + + /** + * Get the data that should be sent with the broadcast event. + * + * @return array + */ + public function broadcastWith(): array + { + if (config('enjin-platform.cache_events')) { + $this->cacheEvent(); + } + + $this->broadcastData['uuid'] = $this->uuid; + + return $this->broadcastData; + } + + /** + * Broadcast the event and catch any errors. + */ + public static function safeBroadcast() + { + try { + static::broadcast(...func_get_args()); + } catch (\Throwable $e) { + $class = (new \ReflectionClass(static::class))->getShortName(); + Log::info("{$class} : Event cached but no websocket open to broadcast on. {$e->getMessage()}"); + } + } + + /** + * Store the event in the database. + */ + protected function cacheEvent() + { + $pendingEvent = PendingEvent::create([ + 'uuid' => $this->uuid, + 'name' => $this->broadcastAs(), + 'sent' => now()->toIso8601String(), + 'channels' => collect($this->broadcastChannels)->pluck('name')->toJson(), + 'data' => json_encode($this->broadcastData), + ]); + + PlatformEventCached::dispatch($pendingEvent); + } +} diff --git a/src/Events/PlatformEvent.php b/src/Events/PlatformEvent.php new file mode 100644 index 00000000..59e80679 --- /dev/null +++ b/src/Events/PlatformEvent.php @@ -0,0 +1,32 @@ +className = (new \ReflectionClass(static::class))->getShortName(); + } + + public function getClassName() + { + return $this->className; + } +} diff --git a/src/Events/Substrate/Commands/PlatformBlockIngested.php b/src/Events/Substrate/Commands/PlatformBlockIngested.php new file mode 100644 index 00000000..771ce3ae --- /dev/null +++ b/src/Events/Substrate/Commands/PlatformBlockIngested.php @@ -0,0 +1,9 @@ +broadcastData = [ + 'idempotencyKey' => $transaction?->idempotency_key, + 'collectionId' => $collection, + 'wallet' => $wallet->address, + ]; + + $this->broadcastChannels = [ + new Channel("collection;{$this->broadcastData['collectionId']}"), + new Channel($wallet->address), + new PlatformAppChannel(), + ]; + } +} diff --git a/src/Events/Substrate/MultiTokens/CollectionAccountDestroyed.php b/src/Events/Substrate/MultiTokens/CollectionAccountDestroyed.php new file mode 100644 index 00000000..311a305b --- /dev/null +++ b/src/Events/Substrate/MultiTokens/CollectionAccountDestroyed.php @@ -0,0 +1,32 @@ +broadcastData = [ + 'idempotencyKey' => $transaction?->idempotency_key, + 'collectionId' => $collection->collection_chain_id, + 'wallet' => $wallet->address, + ]; + + $this->broadcastChannels = [ + new Channel("collection;{$this->broadcastData['collectionId']}"), + new Channel($wallet->address), + new PlatformAppChannel(), + ]; + } +} diff --git a/src/Events/Substrate/MultiTokens/CollectionAccountFrozen.php b/src/Events/Substrate/MultiTokens/CollectionAccountFrozen.php new file mode 100644 index 00000000..b64b219c --- /dev/null +++ b/src/Events/Substrate/MultiTokens/CollectionAccountFrozen.php @@ -0,0 +1,31 @@ +broadcastData = [ + 'idempotencyKey' => $transaction?->idempotency_key, + 'collectionId' => $collectionAccount->collection->collection_chain_id, + 'collectionAccount' => $collectionAccount->wallet->address, + ]; + + $this->broadcastChannels = [ + new Channel("collection;{$this->broadcastData['collectionId']}"), + new Channel($this->broadcastData['collectionAccount']), + new PlatformAppChannel(), + ]; + } +} diff --git a/src/Events/Substrate/MultiTokens/CollectionAccountThawed.php b/src/Events/Substrate/MultiTokens/CollectionAccountThawed.php new file mode 100644 index 00000000..4dc62b37 --- /dev/null +++ b/src/Events/Substrate/MultiTokens/CollectionAccountThawed.php @@ -0,0 +1,31 @@ +broadcastData = [ + 'idempotencyKey' => $transaction?->idempotency_key, + 'collectionId' => $collectionAccount->collection->collection_chain_id, + 'collectionAccount' => $collectionAccount->wallet->address, + ]; + + $this->broadcastChannels = [ + new Channel("collection;{$this->broadcastData['collectionId']}"), + new Channel($this->broadcastData['collectionAccount']), + new PlatformAppChannel(), + ]; + } +} diff --git a/src/Events/Substrate/MultiTokens/CollectionApproved.php b/src/Events/Substrate/MultiTokens/CollectionApproved.php new file mode 100644 index 00000000..98252ada --- /dev/null +++ b/src/Events/Substrate/MultiTokens/CollectionApproved.php @@ -0,0 +1,32 @@ +broadcastData = [ + 'idempotencyKey' => $transaction?->idempotency_key, + 'collectionId' => $collectionId, + 'operator' => $operator, + 'expiration' => $expiration, + ]; + + $this->broadcastChannels = [ + new Channel("collection;{$collectionId}"), + new Channel($operator), + new PlatformAppChannel(), + ]; + } +} diff --git a/src/Events/Substrate/MultiTokens/CollectionAttributeRemoved.php b/src/Events/Substrate/MultiTokens/CollectionAttributeRemoved.php new file mode 100644 index 00000000..bba30f4b --- /dev/null +++ b/src/Events/Substrate/MultiTokens/CollectionAttributeRemoved.php @@ -0,0 +1,32 @@ +broadcastData = [ + 'idempotencyKey' => $transaction?->idempotency_key, + 'collectionId' => $collection->collection_chain_id, + 'key' => $attributeKey, + 'value' => $attributeValue, + ]; + + $this->broadcastChannels = [ + new Channel("collection;{$this->broadcastData['collectionId']}"), + new Channel($collection->owner->address), + new PlatformAppChannel(), + ]; + } +} diff --git a/src/Events/Substrate/MultiTokens/CollectionAttributeSet.php b/src/Events/Substrate/MultiTokens/CollectionAttributeSet.php new file mode 100644 index 00000000..33ed813f --- /dev/null +++ b/src/Events/Substrate/MultiTokens/CollectionAttributeSet.php @@ -0,0 +1,32 @@ +broadcastData = [ + 'idempotencyKey' => $transaction?->idempotency_key, + 'collectionId' => $collection->collection_chain_id, + 'key' => $attributeKey, + 'value' => $attributeValue, + ]; + + $this->broadcastChannels = [ + new Channel("collection;{$this->broadcastData['collectionId']}"), + new Channel($collection->owner->address), + new PlatformAppChannel(), + ]; + } +} diff --git a/src/Events/Substrate/MultiTokens/CollectionCreated.php b/src/Events/Substrate/MultiTokens/CollectionCreated.php new file mode 100644 index 00000000..8aca5862 --- /dev/null +++ b/src/Events/Substrate/MultiTokens/CollectionCreated.php @@ -0,0 +1,32 @@ +model = $collection; + + $this->broadcastData = [ + 'idempotencyKey' => $transaction?->idempotency_key, + 'collectionId' => $collection->collection_chain_id, + 'owner' => $collection->owner->address, + ]; + + $this->broadcastChannels = [ + new Channel($collection->owner->address), + new PlatformAppChannel(), + ]; + } +} diff --git a/src/Events/Substrate/MultiTokens/CollectionDestroyed.php b/src/Events/Substrate/MultiTokens/CollectionDestroyed.php new file mode 100644 index 00000000..2c595c17 --- /dev/null +++ b/src/Events/Substrate/MultiTokens/CollectionDestroyed.php @@ -0,0 +1,30 @@ +broadcastData = [ + 'idempotencyKey' => $transaction?->idempotency_key, + 'collectionId' => $collection->collection_chain_id, + ]; + + $this->broadcastChannels = [ + new Channel("collection;{$this->broadcastData['collectionId']}"), + new Channel($collection->owner->address), + new PlatformAppChannel(), + ]; + } +} diff --git a/src/Events/Substrate/MultiTokens/CollectionFrozen.php b/src/Events/Substrate/MultiTokens/CollectionFrozen.php new file mode 100644 index 00000000..95f388ea --- /dev/null +++ b/src/Events/Substrate/MultiTokens/CollectionFrozen.php @@ -0,0 +1,30 @@ +broadcastData = [ + 'idempotencyKey' => $transaction?->idempotency_key, + 'collectionId' => $collection->collection_chain_id, + ]; + + $this->broadcastChannels = [ + new Channel("collection;{$this->broadcastData['collectionId']}"), + new Channel($collection->owner->address), + new PlatformAppChannel(), + ]; + } +} diff --git a/src/Events/Substrate/MultiTokens/CollectionMutated.php b/src/Events/Substrate/MultiTokens/CollectionMutated.php new file mode 100644 index 00000000..0a9ba668 --- /dev/null +++ b/src/Events/Substrate/MultiTokens/CollectionMutated.php @@ -0,0 +1,31 @@ +broadcastData = [ + 'idempotencyKey' => $transaction?->idempotency_key, + 'collectionId' => $collection->collection_chain_id, + 'mutation' => $mutation, + ]; + + $this->broadcastChannels = [ + new Channel("collection;{$this->broadcastData['collectionId']}"), + new PlatformAppChannel(), + new Channel($collection->owner->address), + ]; + } +} diff --git a/src/Events/Substrate/MultiTokens/CollectionThawed.php b/src/Events/Substrate/MultiTokens/CollectionThawed.php new file mode 100644 index 00000000..934a0030 --- /dev/null +++ b/src/Events/Substrate/MultiTokens/CollectionThawed.php @@ -0,0 +1,30 @@ +broadcastData = [ + 'idempotencyKey' => $transaction?->idempotency_key, + 'collectionId' => $collection->collection_chain_id, + ]; + + $this->broadcastChannels = [ + new Channel("collection;{$this->broadcastData['collectionId']}"), + new Channel($collection->owner->address), + new PlatformAppChannel(), + ]; + } +} diff --git a/src/Events/Substrate/MultiTokens/CollectionUnapproved.php b/src/Events/Substrate/MultiTokens/CollectionUnapproved.php new file mode 100644 index 00000000..fa81d3b2 --- /dev/null +++ b/src/Events/Substrate/MultiTokens/CollectionUnapproved.php @@ -0,0 +1,31 @@ +broadcastData = [ + 'idempotencyKey' => $transaction?->idempotency_key, + 'collectionId' => $collectionId, + 'operator' => $operator, + ]; + + $this->broadcastChannels = [ + new Channel("collection;{$collectionId}"), + new Channel($operator), + new PlatformAppChannel(), + ]; + } +} diff --git a/src/Events/Substrate/MultiTokens/TokenAccountCreated.php b/src/Events/Substrate/MultiTokens/TokenAccountCreated.php new file mode 100644 index 00000000..c64a8c2f --- /dev/null +++ b/src/Events/Substrate/MultiTokens/TokenAccountCreated.php @@ -0,0 +1,33 @@ +broadcastData = [ + 'idempotencyKey' => $transaction?->idempotency_key, + 'collectionId' => $collection->collection_chain_id, + 'tokenId' => $token->token_chain_id, + 'wallet' => $wallet->address, + ]; + + $this->broadcastChannels = [ + new Channel("collection;{$this->broadcastData['collectionId']}"), + new Channel("token;{$this->broadcastData['tokenId']}"), + new Channel($wallet->address), + new PlatformAppChannel(), + ]; + } +} diff --git a/src/Events/Substrate/MultiTokens/TokenAccountDestroyed.php b/src/Events/Substrate/MultiTokens/TokenAccountDestroyed.php new file mode 100644 index 00000000..385035eb --- /dev/null +++ b/src/Events/Substrate/MultiTokens/TokenAccountDestroyed.php @@ -0,0 +1,33 @@ +broadcastData = [ + 'idempotencyKey' => $transaction?->idempotency_key, + 'collectionId' => $collection->collection_chain_id, + 'tokenId' => $token->token_chain_id, + 'wallet' => $wallet->address, + ]; + + $this->broadcastChannels = [ + new Channel("collection;{$this->broadcastData['collectionId']}"), + new Channel("token;{$this->broadcastData['tokenId']}"), + new Channel($wallet->address), + new PlatformAppChannel(), + ]; + } +} diff --git a/src/Events/Substrate/MultiTokens/TokenAccountFrozen.php b/src/Events/Substrate/MultiTokens/TokenAccountFrozen.php new file mode 100644 index 00000000..c0f8613e --- /dev/null +++ b/src/Events/Substrate/MultiTokens/TokenAccountFrozen.php @@ -0,0 +1,33 @@ +broadcastData = [ + 'idempotencyKey' => $transaction?->idempotency_key, + 'collectionId' => $tokenAccount->collection->collection_chain_id, + 'tokenId' => $tokenAccount->token->token_chain_id, + 'tokenAccount' => $tokenAccount->wallet->address, + ]; + + $this->broadcastChannels = [ + new Channel("collection;{$this->broadcastData['collectionId']}"), + new Channel($this->broadcastData['tokenAccount']), + new Channel("token;{$this->broadcastData['tokenId']}"), + new PlatformAppChannel(), + ]; + } +} diff --git a/src/Events/Substrate/MultiTokens/TokenAccountThawed.php b/src/Events/Substrate/MultiTokens/TokenAccountThawed.php new file mode 100644 index 00000000..916105c0 --- /dev/null +++ b/src/Events/Substrate/MultiTokens/TokenAccountThawed.php @@ -0,0 +1,33 @@ +broadcastData = [ + 'idempotencyKey' => $transaction?->idempotency_key, + 'collectionId' => $tokenAccount->collection->collection_chain_id, + 'tokenId' => $tokenAccount->token->token_chain_id, + 'tokenAccount' => $tokenAccount->wallet->address, + ]; + + $this->broadcastChannels = [ + new Channel("collection;{$this->broadcastData['collectionId']}"), + new Channel($this->broadcastData['tokenAccount']), + new Channel("token;{$this->broadcastData['tokenId']}"), + new PlatformAppChannel(), + ]; + } +} diff --git a/src/Events/Substrate/MultiTokens/TokenApproved.php b/src/Events/Substrate/MultiTokens/TokenApproved.php new file mode 100644 index 00000000..8ae80239 --- /dev/null +++ b/src/Events/Substrate/MultiTokens/TokenApproved.php @@ -0,0 +1,41 @@ +broadcastData = [ + 'idempotencyKey' => $transaction?->idempotency_key, + 'collectionId' => $collectionId, + 'tokenId' => $tokenId, + 'operator' => $operator, + 'amount' => $amount, + 'expiration' => $expiration, + ]; + + $this->broadcastChannels = [ + new Channel("collection;{$collectionId}"), + new Channel("token;{$this->broadcastData['tokenId']}"), + new Channel($operator), + new PlatformAppChannel(), + ]; + } +} diff --git a/src/Events/Substrate/MultiTokens/TokenAttributeRemoved.php b/src/Events/Substrate/MultiTokens/TokenAttributeRemoved.php new file mode 100644 index 00000000..c04f8e28 --- /dev/null +++ b/src/Events/Substrate/MultiTokens/TokenAttributeRemoved.php @@ -0,0 +1,34 @@ +broadcastData = [ + 'idempotencyKey' => $transaction?->idempotency_key, + 'collectionId' => $token->collection->collection_chain_id, + 'tokenId' => $token->token_chain_id, + 'key' => $attributeKey, + 'value' => $attributeValue, + ]; + + $this->broadcastChannels = [ + new Channel("collection;{$this->broadcastData['collectionId']}"), + new Channel("token;{$this->broadcastData['tokenId']}"), + new Channel($token->collection->owner->address), + new PlatformAppChannel(), + ]; + } +} diff --git a/src/Events/Substrate/MultiTokens/TokenAttributeSet.php b/src/Events/Substrate/MultiTokens/TokenAttributeSet.php new file mode 100644 index 00000000..7d970cfa --- /dev/null +++ b/src/Events/Substrate/MultiTokens/TokenAttributeSet.php @@ -0,0 +1,34 @@ +broadcastData = [ + 'idempotencyKey' => $transaction?->idempotency_key, + 'collectionId' => $token->collection->collection_chain_id, + 'tokenId' => $token->token_chain_id, + 'key' => $attributeKey, + 'value' => $attributeValue, + ]; + + $this->broadcastChannels = [ + new Channel("collection;{$this->broadcastData['collectionId']}"), + new Channel("token;{$this->broadcastData['tokenId']}"), + new Channel($token->collection->owner->address), + new PlatformAppChannel(), + ]; + } +} diff --git a/src/Events/Substrate/MultiTokens/TokenBurned.php b/src/Events/Substrate/MultiTokens/TokenBurned.php new file mode 100644 index 00000000..7ca4b81c --- /dev/null +++ b/src/Events/Substrate/MultiTokens/TokenBurned.php @@ -0,0 +1,35 @@ +broadcastData = [ + 'idempotencyKey' => $transaction?->idempotency_key, + 'collectionId' => $collection->collection_chain_id, + 'tokenId' => $tokenId, + 'wallet' => $address, + 'amount' => $amount, + ]; + + $this->broadcastChannels = [ + new Channel("collection;{$this->broadcastData['collectionId']}"), + new Channel($collection->owner->address), + new Channel("token;{$this->broadcastData['tokenId']}"), + new Channel($address), + new PlatformAppChannel(), + ]; + } +} diff --git a/src/Events/Substrate/MultiTokens/TokenCreated.php b/src/Events/Substrate/MultiTokens/TokenCreated.php new file mode 100644 index 00000000..ead15cf3 --- /dev/null +++ b/src/Events/Substrate/MultiTokens/TokenCreated.php @@ -0,0 +1,36 @@ +model = $token; + + $this->broadcastData = [ + 'idempotencyKey' => $transaction?->idempotency_key, + 'collectionId' => $token->collection->collection_chain_id, + 'tokenId' => $token->token_chain_id, + 'initialSupply' => $token->supply, + 'issuer' => $issuer->address, + ]; + + $this->broadcastChannels = [ + new Channel("collection;{$this->broadcastData['collectionId']}"), + new PlatformAppChannel(), + new Channel($token->collection->owner->address), + new Channel($issuer->address), + ]; + } +} diff --git a/src/Events/Substrate/MultiTokens/TokenDestroyed.php b/src/Events/Substrate/MultiTokens/TokenDestroyed.php new file mode 100644 index 00000000..141b3217 --- /dev/null +++ b/src/Events/Substrate/MultiTokens/TokenDestroyed.php @@ -0,0 +1,33 @@ +broadcastData = [ + 'idempotencyKey' => $transaction?->idempotency_key, + 'collectionId' => $token->collection->collection_chain_id, + 'tokenId' => $token->token_chain_id, + 'caller' => $caller->address, + ]; + + $this->broadcastChannels = [ + new Channel("collection;{$this->broadcastData['collectionId']}"), + new Channel("token;{$this->broadcastData['tokenId']}"), + new PlatformAppChannel(), + new Channel($caller->address), + ]; + } +} diff --git a/src/Events/Substrate/MultiTokens/TokenFrozen.php b/src/Events/Substrate/MultiTokens/TokenFrozen.php new file mode 100644 index 00000000..6e5e2c8a --- /dev/null +++ b/src/Events/Substrate/MultiTokens/TokenFrozen.php @@ -0,0 +1,32 @@ +broadcastData = [ + 'idempotencyKey' => $transaction?->idempotency_key, + 'collectionId' => $token->collection->collection_chain_id, + 'tokenId' => $token->token_chain_id, + ]; + + $this->broadcastChannels = [ + new Channel($token->collection->owner->address), + new Channel("collection;{$this->broadcastData['collectionId']}"), + new Channel("token;{$this->broadcastData['tokenId']}"), + new PlatformAppChannel(), + ]; + } +} diff --git a/src/Events/Substrate/MultiTokens/TokenMinted.php b/src/Events/Substrate/MultiTokens/TokenMinted.php new file mode 100644 index 00000000..d92fc703 --- /dev/null +++ b/src/Events/Substrate/MultiTokens/TokenMinted.php @@ -0,0 +1,38 @@ +model = $token; + + $this->broadcastData = [ + 'idempotencyKey' => $transaction?->idempotency_key, + 'collectionId' => $token->collection->collection_chain_id, + 'tokenId' => $token->token_chain_id, + 'issuer' => $issuer->address, + 'recipient' => $recipient->address, + 'amount' => $amount, + ]; + + $this->broadcastChannels = [ + new Channel($token->collection->owner->address), + new Channel("collection;{$this->broadcastData['collectionId']}"), + new Channel("token;{$this->broadcastData['tokenId']}"), + new Channel($recipient), + new PlatformAppChannel(), + ]; + } +} diff --git a/src/Events/Substrate/MultiTokens/TokenMutated.php b/src/Events/Substrate/MultiTokens/TokenMutated.php new file mode 100644 index 00000000..d3c3f00c --- /dev/null +++ b/src/Events/Substrate/MultiTokens/TokenMutated.php @@ -0,0 +1,33 @@ +broadcastData = [ + 'idempotencyKey' => $transaction?->idempotency_key, + 'collectionId' => $token->collection->collection_chain_id, + 'tokenId' => $token->token_chain_id, + 'mutation' => $mutation, + ]; + + $this->broadcastChannels = [ + new Channel("collection;{$this->broadcastData['collectionId']}"), + new Channel("token;{$this->broadcastData['tokenId']}"), + new PlatformAppChannel(), + new Channel($token->collection->owner->address), + ]; + } +} diff --git a/src/Events/Substrate/MultiTokens/TokenThawed.php b/src/Events/Substrate/MultiTokens/TokenThawed.php new file mode 100644 index 00000000..69cd6ed5 --- /dev/null +++ b/src/Events/Substrate/MultiTokens/TokenThawed.php @@ -0,0 +1,32 @@ +broadcastData = [ + 'idempotencyKey' => $transaction?->idempotency_key, + 'collectionId' => $token->collection->collection_chain_id, + 'tokenId' => $token->token_chain_id, + ]; + + $this->broadcastChannels = [ + new Channel($token->collection->owner->address), + new Channel("collection;{$this->broadcastData['collectionId']}"), + new Channel("token;{$this->broadcastData['tokenId']}"), + new PlatformAppChannel(), + ]; + } +} diff --git a/src/Events/Substrate/MultiTokens/TokenTransferred.php b/src/Events/Substrate/MultiTokens/TokenTransferred.php new file mode 100644 index 00000000..7f5df082 --- /dev/null +++ b/src/Events/Substrate/MultiTokens/TokenTransferred.php @@ -0,0 +1,37 @@ +broadcastData = [ + 'idempotencyKey' => $transaction?->idempotency_key, + 'collectionId' => $token->collection->collection_chain_id, + 'tokenId' => $token->token_chain_id, + 'from' => $from, + 'recipient' => $recipient, + 'amount' => $amount, + ]; + + $this->broadcastChannels = [ + new Channel($token->collection->owner->address), + new Channel("collection;{$token->collection->collection_chain_id}"), + new Channel("token;{$this->broadcastData['tokenId']}"), + new Channel($from), + new Channel($recipient), + new PlatformAppChannel(), + ]; + } +} diff --git a/src/Events/Substrate/MultiTokens/TokenUnapproved.php b/src/Events/Substrate/MultiTokens/TokenUnapproved.php new file mode 100644 index 00000000..bff5b274 --- /dev/null +++ b/src/Events/Substrate/MultiTokens/TokenUnapproved.php @@ -0,0 +1,37 @@ +broadcastData = [ + 'idempotencyKey' => $transaction?->idempotency_key, + 'collectionId' => $collectionId, + 'tokenId' => $tokenId, + 'operator' => $operator, + ]; + + $this->broadcastChannels = [ + new Channel("collection;{$collectionId}"), + new Channel("token;{$this->broadcastData['tokenId']}"), + new Channel($operator), + new PlatformAppChannel(), + ]; + } +} diff --git a/src/Exceptions/PlatformException.php b/src/Exceptions/PlatformException.php new file mode 100644 index 00000000..d9ba1bda --- /dev/null +++ b/src/Exceptions/PlatformException.php @@ -0,0 +1,24 @@ +attributes()['name']); + } + + /** + * Get the graphql mutation name. + */ + public function getMutationName(): string + { + return $this->attributes()['name']; + } + + /** + * Validate arguments base from the rules. + */ + protected function validateArguments(array $arguments, array $rules): void + { + $validator = $this->getValidator($arguments, $rules); + + if ($validator->stopOnFirstFailure(false)->fails()) { + throw new ValidationError('validation', $validator); + } + } +} diff --git a/src/GraphQL/Base/Query.php b/src/GraphQL/Base/Query.php new file mode 100644 index 00000000..f05d8d18 --- /dev/null +++ b/src/GraphQL/Base/Query.php @@ -0,0 +1,23 @@ +getValidator($arguments, $rules); + + if ($validator->stopOnFirstFailure(false)->fails()) { + throw new ValidationError('validation', $validator); + } + } +} diff --git a/src/GraphQL/Enums/CryptoSignatureTypeEnum.php b/src/GraphQL/Enums/CryptoSignatureTypeEnum.php new file mode 100644 index 00000000..698795e0 --- /dev/null +++ b/src/GraphQL/Enums/CryptoSignatureTypeEnum.php @@ -0,0 +1,22 @@ + 'CryptoSignatureType', + 'values' => CryptoSignatureType::caseNamesAsArray(), + 'description' => __('enjin-platform::enum.crypto_signature.description'), + ]; + } +} diff --git a/src/GraphQL/Enums/EventTypeEnum.php b/src/GraphQL/Enums/EventTypeEnum.php new file mode 100644 index 00000000..8785bab1 --- /dev/null +++ b/src/GraphQL/Enums/EventTypeEnum.php @@ -0,0 +1,22 @@ + 'EventType', + 'values' => MultiTokensEventType::caseNamesAsArray(), + 'description' => __('enjin-platform::enum.event_type.description'), + ]; + } +} diff --git a/src/GraphQL/Enums/FreezeTypeEnum.php b/src/GraphQL/Enums/FreezeTypeEnum.php new file mode 100644 index 00000000..50421309 --- /dev/null +++ b/src/GraphQL/Enums/FreezeTypeEnum.php @@ -0,0 +1,22 @@ + 'FreezeType', + 'values' => FreezeType::caseNamesAsArray(), + 'description' => __('enjin-platform::enum.freezable_type.description'), + ]; + } +} diff --git a/src/GraphQL/Enums/PalletIdentifierEnum.php b/src/GraphQL/Enums/PalletIdentifierEnum.php new file mode 100644 index 00000000..2dad3b6f --- /dev/null +++ b/src/GraphQL/Enums/PalletIdentifierEnum.php @@ -0,0 +1,22 @@ + 'PalletIdentifier', + 'values' => PalletIdentifier::caseNamesAsArray(), + 'description' => __('enjin-platform::enum.pallet_identifier.description'), + ]; + } +} diff --git a/src/GraphQL/Enums/TokenMarketBehaviorTypeEnum.php b/src/GraphQL/Enums/TokenMarketBehaviorTypeEnum.php new file mode 100644 index 00000000..5cb79306 --- /dev/null +++ b/src/GraphQL/Enums/TokenMarketBehaviorTypeEnum.php @@ -0,0 +1,22 @@ + 'TokenMarketBehaviorType', + 'values' => TokenMarketBehavior::caseNamesAsArray(), + 'description' => __('enjin-platform::enum.token_market_behavior_type.description'), + ]; + } +} diff --git a/src/GraphQL/Enums/TokenMintCapTypeEnum.php b/src/GraphQL/Enums/TokenMintCapTypeEnum.php new file mode 100644 index 00000000..dbe36f9b --- /dev/null +++ b/src/GraphQL/Enums/TokenMintCapTypeEnum.php @@ -0,0 +1,22 @@ + 'TokenMintCapType', + 'values' => TokenMintCapType::caseNamesAsArray(), + 'description' => __('enjin-platform::enum.token_mint_cap_type.description'), + ]; + } +} diff --git a/src/GraphQL/Enums/TokenTypeEnum.php b/src/GraphQL/Enums/TokenTypeEnum.php new file mode 100644 index 00000000..c59f56db --- /dev/null +++ b/src/GraphQL/Enums/TokenTypeEnum.php @@ -0,0 +1,22 @@ + 'TokenType', + 'values' => TokenType::caseNamesAsArray(), + 'description' => __('enjin-platform::enum.token_type.description'), + ]; + } +} diff --git a/src/GraphQL/Enums/TransactionMethodEnum.php b/src/GraphQL/Enums/TransactionMethodEnum.php new file mode 100644 index 00000000..07bec86a --- /dev/null +++ b/src/GraphQL/Enums/TransactionMethodEnum.php @@ -0,0 +1,31 @@ +add((new $className())->getMutationName()); + } + } + + return [ + 'name' => 'TransactionMethod', + 'values' => $mutationNames->toArray(), + 'description' => __('enjin-platform::enum.transaction_method.description'), + ]; + } +} diff --git a/src/GraphQL/Enums/TransactionResultEnum.php b/src/GraphQL/Enums/TransactionResultEnum.php new file mode 100644 index 00000000..823fd1ff --- /dev/null +++ b/src/GraphQL/Enums/TransactionResultEnum.php @@ -0,0 +1,25 @@ + 'TransactionResult', + 'values' => [ + SystemEventType::EXTRINSIC_SUCCESS->name, + SystemEventType::EXTRINSIC_FAILED->name, + ], + 'description' => __('enjin-platform::enum.transaction_result.description'), + ]; + } +} diff --git a/src/GraphQL/Enums/TransactionStateEnum.php b/src/GraphQL/Enums/TransactionStateEnum.php new file mode 100644 index 00000000..373dc3b3 --- /dev/null +++ b/src/GraphQL/Enums/TransactionStateEnum.php @@ -0,0 +1,22 @@ + 'TransactionState', + 'values' => TransactionState::caseNamesAsArray(), + 'description' => __('enjin-platform::enum.transaction_state.description'), + ]; + } +} diff --git a/src/GraphQL/Middleware/ResolvePage.php b/src/GraphQL/Middleware/ResolvePage.php new file mode 100644 index 00000000..80684d56 --- /dev/null +++ b/src/GraphQL/Middleware/ResolvePage.php @@ -0,0 +1,23 @@ +resolveCurrentPageForPagination($args['after']); + + return $next($root, $args, $context, $info); + } +} diff --git a/src/GraphQL/Middleware/SingleArgOnly.php b/src/GraphQL/Middleware/SingleArgOnly.php new file mode 100644 index 00000000..4dc48d00 --- /dev/null +++ b/src/GraphQL/Middleware/SingleArgOnly.php @@ -0,0 +1,23 @@ +filter(fn ($arg) => !empty($arg)); + $singleFilterArgs = collect($root->getAttributes()['args'])->where('singleFilter', true); + $otherFilterArgs = collect($root->getAttributes()['args'])->where('filter', true); + + $filledSingleFilters = $filledArgs->intersectByKeys($singleFilterArgs); + $filledOtherFilters = $filledArgs->intersectByKeys($otherFilterArgs); + + if ($filledSingleFilters->isNotEmpty() && $filledOtherFilters->isNotEmpty()) { + $filterOptions = $singleFilterArgs->keys()->implode(', '); + + throw new PlatformException(__('enjin-platform::error.middleware.single_filter_only.only_used_alone', ['filterOptions' => $filterOptions]), 403); + } + + if ($filledSingleFilters->count() > 1) { + $filterOptions = $singleFilterArgs->keys()->implode(', '); + + throw new PlatformException(__('enjin-platform::error.middleware.single_filter_only.only_one_filter', ['filterOptions' => $filterOptions]), 403); + } + + return $next($root, $args, $context, $info); + } +} diff --git a/src/GraphQL/Schemas/Primary/Mutations/AcknowledgeEventsMutation.php b/src/GraphQL/Schemas/Primary/Mutations/AcknowledgeEventsMutation.php new file mode 100644 index 00000000..69a0b212 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Mutations/AcknowledgeEventsMutation.php @@ -0,0 +1,73 @@ + 'AcknowledgeEvents', + 'description' => __('enjin-platform::mutation.acknowledge_events.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Boolean!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + 'uuids' => [ + 'type' => GraphQL::type('[String!]!'), + 'description' => __('enjin-platform::mutation.acknowledge_events.args.uuids'), + ], + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve($root, array $args, $context, ResolveInfo $resolveInfo, Closure $getSelectFields): mixed + { + PendingEvent::query() + ->whereIn('uuid', $args['uuids']) + ->get() + ->each(fn ($pendingEvent) => $pendingEvent->delete()); + + return true; + } + + /** + * Get the validation rules. + */ + protected function rules(array $args = []): array + { + return [ + 'uuids' => ['bail', 'array', 'min:1', 'max:1000', 'distinct'], + 'uuids.*' => ['filled'], + ]; + } +} diff --git a/src/GraphQL/Schemas/Primary/Mutations/CreateWalletMutation.php b/src/GraphQL/Schemas/Primary/Mutations/CreateWalletMutation.php new file mode 100644 index 00000000..88477d26 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Mutations/CreateWalletMutation.php @@ -0,0 +1,62 @@ + 'CreateWallet', + 'description' => __('enjin-platform::mutation.create_wallet.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Boolean!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + 'externalId' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::mutation.create_wallet.args.externalId'), + 'rules' => ['unique:wallets,external_id'], + ], + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve($root, array $args, $context, ResolveInfo $resolveInfo, Closure $getSelectFields, WalletService $walletService): mixed + { + return $walletService->store([ + 'public_key' => null, + 'external_id' => $args['externalId'], + 'managed' => true, + ]); + } +} diff --git a/src/GraphQL/Schemas/Primary/Mutations/MarkAndListPendingTransactionsMutation.php b/src/GraphQL/Schemas/Primary/Mutations/MarkAndListPendingTransactionsMutation.php new file mode 100644 index 00000000..563694c2 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Mutations/MarkAndListPendingTransactionsMutation.php @@ -0,0 +1,102 @@ + 'MarkAndListPendingTransactions', + 'description' => __('enjin-platform::mutation.mark_and_list_pending_transactions.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::paginate('Transaction', 'TransactionConnection'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return ConnectionInput::args([ + 'accounts' => [ + 'type' => GraphQL::type('[String]'), + 'description' => __('enjin-platform::mutation.mark_and_list_pending_transactions.args.accounts'), + ], + 'markAsProcessing' => [ + 'type' => GraphQL::type('Boolean'), + 'defaultValue' => true, + ], + ]); + } + + /** + * Resolve the mutation's request. + */ + public function resolve($root, array $args, $context, ResolveInfo $resolveInfo, Closure $getSelectFields, TransactionService $transactionService): mixed + { + $transactions = Transaction::query() + ->where('state', '=', TransactionState::PENDING->name) + ->when( + $args['accounts'] ?? false, + function (Builder $query) use ($args) { + $publicKeys = array_map(fn ($wallet) => SS58Address::getPublicKey($wallet), $args['accounts']); + + return $query->whereIn('wallet_public_key', $publicKeys); + }, + function (Builder $query) { + return $query->whereIn('wallet_public_key', Account::managedPublicKeys()); + } + )->cursorPaginateWithTotalDesc('id', $args['first'], false); + + if (true === $args['markAsProcessing'] || null === $args['markAsProcessing']) { + $transactionsToMark = clone $transactions['items']->getCollection(); + $transactionsToMark->each(fn ($transaction) => $transactionService->update($transaction, ['state' => TransactionState::PROCESSING->name])); + } + + return $transactions; + } + + /** + * Get the validation rules. + */ + protected function rules(array $args = []): array + { + return [ + 'accounts.*' => [new ValidSubstrateAccount()], + ]; + } +} diff --git a/src/GraphQL/Schemas/Primary/Mutations/SetWalletAccountMutation.php b/src/GraphQL/Schemas/Primary/Mutations/SetWalletAccountMutation.php new file mode 100644 index 00000000..00772754 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Mutations/SetWalletAccountMutation.php @@ -0,0 +1,91 @@ + 'SetWalletAccount', + 'description' => __('enjin-platform::mutation.set_wallet_account.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Boolean!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + 'id' => [ + 'type' => GraphQL::type('Int'), + 'description' => __('enjin-platform::query.get_wallet.args.id'), + 'rules' => ['prohibits:externalId', 'exists:wallets,id'], + ], + 'externalId' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::query.get_wallet.args.externalId'), + 'rules' => ['prohibits:id', 'exists:wallets,external_id'], + ], + 'account' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::query.get_wallet.args.account'), + 'rules' => ['filled', new ValidSubstrateAccount()], + ], + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + WalletService $walletService + ): mixed { + if (Wallet::firstWhere('public_key', '=', SS58Address::getPublicKey($args['account']))) { + throw new PlatformException(__('enjin-platform::error.account_already_taken')); + } + + $column = array_key_first(Arr::except($args, ['account'])); + $wallet = $walletService->get($args[$column], Str::snake($column)); + + if ($wallet->public_key) { + throw new PlatformException(__('enjin-platform::error.wallet_is_immutable'), 403); + } + + return $walletService->update($wallet, ['public_key' => SS58Address::getPublicKey($args['account'])]); + } +} diff --git a/src/GraphQL/Schemas/Primary/Mutations/UpdateTransactionMutation.php b/src/GraphQL/Schemas/Primary/Mutations/UpdateTransactionMutation.php new file mode 100644 index 00000000..cd23336c --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Mutations/UpdateTransactionMutation.php @@ -0,0 +1,92 @@ + 'UpdateTransaction', + 'description' => __('enjin-platform::mutation.update_transaction.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Boolean!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + 'id' => [ + 'type' => GraphQL::type('Int!'), + ], + 'state' => [ + 'type' => GraphQL::type('TransactionState'), + 'description' => __('enjin-platform::mutation.update_transaction.args.state'), + 'rules' => ['required_without_all:transactionId,transactionHash,signedAtBlock'], + ], + 'transactionId' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::mutation.update_transaction.args.transactionId'), + 'alias' => 'transaction_chain_id', + 'rules' => ['nullable', 'required_without_all:state,transactionHash,signedAtBlock', new ValidSubstrateTransactionId()], + ], + 'transactionHash' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::mutation.update_transaction.args.transactionHash'), + 'alias' => 'transaction_chain_hash', + 'rules' => ['nullable', 'required_without_all:state,transactionId,signedAtBlock', new ValidHex(32)], + ], + 'signedAtBlock' => [ + 'type' => GraphQL::type('Int'), + 'description' => __('enjin-platform::mutation.update_transaction.args.signedAtBlock'), + 'alias' => 'signed_at_block', + 'rules' => ['nullable', 'required_without_all:state,transactionId,transactionHash', new MinBigInt(), new MaxBigInt(Hex::MAX_UINT64)], + ], + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve($root, array $args, $context, ResolveInfo $resolveInfo, Closure $getSelectFields, TransactionService $transactionService): mixed + { + $transaction = $transactionService->get($args['id']); + + if ((isset($args['transaction_chain_id']) && $transaction->transaction_chain_id) || (isset($args['transaction_chain_hash']) && $transaction->transaction_chain_hash)) { + throw new PlatformException(__('enjin-platform::mutation.update_transaction.error.hash_and_id_are_immutable'), 403); + } + + return $transactionService->update($transaction, Arr::except($args, 'id')); + } +} diff --git a/src/GraphQL/Schemas/Primary/Mutations/UpdateWalletExternalIdMutation.php b/src/GraphQL/Schemas/Primary/Mutations/UpdateWalletExternalIdMutation.php new file mode 100644 index 00000000..95793a5b --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Mutations/UpdateWalletExternalIdMutation.php @@ -0,0 +1,100 @@ + 'UpdateWalletExternalId', + 'description' => __('enjin-platform::mutation.update_external_id.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Boolean!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + 'id' => [ + 'type' => GraphQL::type('Int'), + 'description' => __('enjin-platform::query.get_wallet.args.id'), + 'rules' => ['required_without_all:externalId,account', 'nullable', 'bail', 'exists:wallets,id', new DaemonProhibited()], + 'singleFilter' => true, + ], + 'externalId' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::query.get_wallet.args.externalId'), + 'rules' => ['required_without_all:id,account', 'nullable', 'exists:wallets,external_id'], + 'singleFilter' => true, + ], + 'newExternalId' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::query.get_wallet.args.newExternalId'), + 'rules' => ['unique:wallets,external_id'], + ], + 'account' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::query.get_wallet.args.account'), + 'rules' => ['required_without_all:id,externalId', 'nullable', 'bail', new ValidSubstrateAccount(), new DaemonProhibited(), new AccountExistsInWallet()], + 'singleFilter' => true, + ], + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + WalletService $walletService + ): mixed { + $column = array_key_first(Arr::except(array_filter($args), ['newExternalId'])); + $wallet = $walletService->get($args[$column], Str::snake($column)); + + if ($wallet->managed) { + throw new PlatformException(__('enjin-platform::mutation.update_wallet_external_id.cannot_update_id_on_managed_wallet'), 403); + } + + return $walletService->update($wallet, ['external_id' => $args['newExternalId'] ?: null]); + } +} diff --git a/src/GraphQL/Schemas/Primary/Mutations/VerifyAccountMutation.php b/src/GraphQL/Schemas/Primary/Mutations/VerifyAccountMutation.php new file mode 100644 index 00000000..f351007f --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Mutations/VerifyAccountMutation.php @@ -0,0 +1,102 @@ + 'VerifyAccount', + 'description' => __('enjin-platform::mutation.verify_account.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Boolean!'); + } + + /** + * Get the mutation's arguments definition.. + */ + public function args(): array + { + return [ + 'verificationId' => [ + 'type' => GraphQL::type('String!'), + 'rules' => ['bail', 'filled', 'exists:verifications,verification_id'], + ], + 'signature' => [ + 'type' => GraphQL::type('String!'), + 'rules' => ['bail', 'filled', new ValidHex()], + ], + 'account' => [ + 'type' => GraphQL::type('String!'), + 'rules' => ['bail', 'filled', new ValidSubstrateAccount()], + ], + 'cryptoSignatureType' => [ + 'type' => GraphQL::type('CryptoSignatureType'), + 'description' => __('enjin-platform::query.verify_message.args.cryptoSignatureType'), + 'defaultValue' => CryptoSignatureType::ED25519->name, + ], + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + VerificationService $verificationService + ): mixed { + $key = "{$args['verificationId']}.{$args['account']}"; + $lock = Cache::lock('enjin-platform:verify-account-cache.' . $key, 5); + if (!$lock->get()) { + throw new PlatformException(__('enjin-platform::error.unable_to_process')); + } + + + $response = false; + + try { + $response = $verificationService->verify( + $args['verificationId'], + $args['signature'], + $args['account'], + $args['cryptoSignatureType'] ?? CryptoSignatureType::ED25519->name + ); + } finally { + $lock->release(); + } + + return $response; + } +} diff --git a/src/GraphQL/Schemas/Primary/Queries/GetAccountVerifiedQuery.php b/src/GraphQL/Schemas/Primary/Queries/GetAccountVerifiedQuery.php new file mode 100644 index 00000000..f1f25fb7 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Queries/GetAccountVerifiedQuery.php @@ -0,0 +1,91 @@ + 'GetAccountVerified', + 'description' => __('enjin-platform::query.get_account_verified.description'), + ]; + } + + /** + * Get the query's return type. + */ + public function type(): Type + { + return GraphQL::type('AccountVerified!'); + } + + /** + * Get the query's arguments definition. + */ + public function args(): array + { + return [ + 'verificationId' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::query.get_account_verified.args.verificationId'), + 'rules' => ['bail', 'required_without:account', 'prohibits:account', new ValidVerificationId()], + ], + 'account' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::query.get_account_verified.args.account'), + 'rules' => [ + 'bail', 'required_without:verificationId', 'prohibits:verificationId', new ValidSubstrateAccount(), + ], + ], + ]; + } + + /** + * Resolve the query's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + VerificationService $verificationService + ): mixed { + $verification = Verification::query() + ->when($args['verificationId'] ?? false, function (Builder $query) use ($args) { + return $query->where('verification_id', '=', $args['verificationId']); + }) + ->when($args['account'] ?? false, function (Builder $query) use ($args) { + return $query->where('public_key', '=', SS58Address::getPublicKey($args['account'])); + }) + ->first(); + + return [ + 'account' => [ + 'publicKey' => $publicKey = $verification?->public_key, + 'address' => null !== $publicKey ? SS58Address::encode($publicKey) : null, + ], + 'verified' => null !== $publicKey, + ]; + } +} diff --git a/src/GraphQL/Schemas/Primary/Queries/GetPendingEventsQuery.php b/src/GraphQL/Schemas/Primary/Queries/GetPendingEventsQuery.php new file mode 100644 index 00000000..5dd64f51 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Queries/GetPendingEventsQuery.php @@ -0,0 +1,75 @@ + 'GetPendingEvents', + 'description' => __('enjin-platform::query.get_pending_events.description'), + ]; + } + + /** + * Get the query's return type. + */ + public function type(): Type + { + return GraphQL::paginate('PendingEvent', 'PendingEventConnection'); + } + + /** + * Get the query's arguments definition. + */ + public function args(): array + { + return ConnectionInput::args([ + 'acknowledgeEvents' => [ + 'type' => GraphQL::type('Boolean'), + 'description' => __('enjin-platform::query.get_pending_events.args.acknowledgeEvents'), + 'defaultValue' => false, + ], + ]); + } + + /** + * Resolve the query's request. + */ + public function resolve($root, array $args, $context, ResolveInfo $resolveInfo, Closure $getSelectFields): mixed + { + $events = PendingEvent::loadSelectFields($resolveInfo, $this->name) + ->cursorPaginateWithTotal('id', $args['first']); + + if (true === $args['acknowledgeEvents']) { + $eventsToClean = $events['items']->getCollection()->pluck('id')->toArray(); + PendingEvent::query() + ->whereIn('id', $eventsToClean) + ->get() + ->each(fn ($pendingEvent) => $pendingEvent->delete()); + } + + return $events; + } +} diff --git a/src/GraphQL/Schemas/Primary/Queries/GetPendingWalletsQuery.php b/src/GraphQL/Schemas/Primary/Queries/GetPendingWalletsQuery.php new file mode 100644 index 00000000..2c328e50 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Queries/GetPendingWalletsQuery.php @@ -0,0 +1,61 @@ + 'GetPendingWallets', + 'description' => __('enjin-platform::query.get_pending_wallets.description'), + ]; + } + + /** + * Get the query's return type. + */ + public function type(): Type + { + return GraphQL::paginate('Wallet', 'WalletConnection'); + } + + /** + * Get the query's arguments definition. + */ + public function args(): array + { + return ConnectionInput::args([]); + } + + /** + * Resolve the query's request. + */ + public function resolve($root, array $args, $context, ResolveInfo $resolveInfo, Closure $getSelectFields): mixed + { + return Wallet::query()->where([ + 'managed' => true, + 'public_key' => null, + ])->cursorPaginateWithTotal('id', $args['first']); + } +} diff --git a/src/GraphQL/Schemas/Primary/Queries/RequestAccountQuery.php b/src/GraphQL/Schemas/Primary/Queries/RequestAccountQuery.php new file mode 100644 index 00000000..eef5d0ee --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Queries/RequestAccountQuery.php @@ -0,0 +1,76 @@ + 'RequestAccount', + 'description' => __('enjin-platform::query.request_account.description'), + ]; + } + + /** + * Get the query's return type. + */ + public function type(): Type + { + return GraphQL::type('AccountRequest!'); + } + + /** + * Get the query's arguments definition. + */ + public function args(): array + { + return [ + 'callback' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::query.request_account.args.callback'), + 'rules' => ['bail', 'filled', 'url'], + ], + ]; + } + + /** + * Resolve the query's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + VerificationService $verificationService, + WalletService $walletService + ): mixed { + $data = $verificationService->generate(); + $verification = $verificationService->store($data); + + return [ + 'qrCode' => $verificationService->qr( + $verification->verification_id, + $verification->code, + $args['callback'] + ), + 'verificationId' => $verification->verification_id, + ]; + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Mutations/ApproveCollectionMutation.php b/src/GraphQL/Schemas/Primary/Substrate/Mutations/ApproveCollectionMutation.php new file mode 100644 index 00000000..6a2745e7 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Mutations/ApproveCollectionMutation.php @@ -0,0 +1,126 @@ + 'ApproveCollection', + 'description' => __('enjin-platform::mutation.approve_collection.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Transaction!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + 'collectionId' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::mutation.approve_collection.args.collectionId'), + ], + 'operator' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::mutation.approve_collection.args.operator'), + ], + 'expiration' => [ + 'type' => GraphQL::type('Int'), + 'description' => __('enjin-platform::mutation.approve_token.args.expiration'), + 'defaultValue' => null, + ], + ...$this->getIdempotencyField(), + ...$this->getSkipValidationField(), + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + SerializationServiceInterface $serializationService, + TransactionService $transactionService, + WalletService $walletService + ): mixed { + $operatorWallet = $walletService->firstOrStore(['account' => $args['operator']]); + $encodedData = $serializationService->encode($this->getMethodName(), [ + 'collectionId' => $args['collectionId'], + 'operator' => $operatorWallet->public_key, + 'expiration' => $args['expiration'], + ]); + + return Transaction::lazyLoadSelectFields( + $transactionService->store([ + 'method' => $this->getMutationName(), + 'encoded_data' => $encodedData, + 'idempotency_key' => $args['idempotencyKey'] ?? Str::uuid()->toString(), + ]), + $resolveInfo + ); + } + + /** + * Get the mutation's validation rules. + */ + protected function rulesWithValidation(array $args): array + { + return [ + 'collectionId' => [new CollectionHasTokens()], + 'operator' => ['filled', new ValidSubstrateAccount(), new DaemonProhibited()], + 'expiration' => ['nullable', 'integer', new FutureBlock()], + ]; + } + + /** + * Get the mutation's validation rules without DB rules. + */ + protected function rulesWithoutValidation(array $args): array + { + return [ + 'operator' => ['filled', new ValidSubstrateAccount()], + 'expiration' => ['nullable', 'integer', 'min:0'], + ]; + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Mutations/ApproveTokenMutation.php b/src/GraphQL/Schemas/Primary/Substrate/Mutations/ApproveTokenMutation.php new file mode 100644 index 00000000..8e2ebb0f --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Mutations/ApproveTokenMutation.php @@ -0,0 +1,149 @@ + 'ApproveToken', + 'description' => __('enjin-platform::mutation.approve_token.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Transaction!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + ...$this->getTokenFields(__('enjin-platform::mutation.approve_token.args.tokenId')), + 'collectionId' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::mutation.approve_token.args.collectionId'), + 'rules' => ['exists:collections,collection_chain_id'], + ], + 'operator' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::mutation.approve_token.args.operator'), + 'rules' => ['filled', new ValidSubstrateAccount(), new DaemonProhibited()], + ], + 'amount' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::mutation.approve_token.args.amount'), + 'rules' => [new MinBigInt(1), new MaxBigInt(Hex::MAX_UINT128)], + ], + 'currentAmount' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::mutation.approve_token.args.currentAmount'), + 'rules' => [new MinBigInt(), new MaxBigInt(Hex::MAX_UINT128)], + ], + 'expiration' => [ + 'type' => GraphQL::type('Int'), + 'description' => __('enjin-platform::mutation.approve_token.args.expiration'), + 'rules' => ['nullable', 'integer', new FutureBlock()], + 'defaultValue' => null, + ], + ...$this->getIdempotencyField(), + ...$this->getSkipValidationField(), + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + SerializationServiceInterface $serializationService, + TransactionService $transactionService, + WalletService $walletService + ): mixed { + $operatorWallet = $walletService->firstOrStore(['account' => $args['operator']]); + + $encodedData = $serializationService->encode($this->getMethodName(), [ + 'collectionId' => $args['collectionId'], + 'tokenId' => $this->encodeTokenId($args), + 'operator' => $operatorWallet->public_key, + 'amount' => $args['amount'], + 'currentAmount' => $args['currentAmount'], + 'expiration' => $args['expiration'], + ]); + + return Transaction::lazyLoadSelectFields( + $transactionService->store([ + 'method' => $this->getMutationName(), + 'encoded_data' => $encodedData, + 'idempotency_key' => $args['idempotencyKey'] ?? Str::uuid()->toString(), + ]), + $resolveInfo + ); + } + + /** + * Get the mutation's validation rules. + */ + protected function rulesWithValidation(array $args): array + { + return $this->getTokenFieldRules( + null, + [new TokenEncodeExists()] + ); + } + + /** + * Get the mutation's validation rules without DB rules. + */ + protected function rulesWithoutValidation(array $args): array + { + return $this->getTokenFieldRules(); + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Mutations/BatchMintMutation.php b/src/GraphQL/Schemas/Primary/Substrate/Mutations/BatchMintMutation.php new file mode 100644 index 00000000..66588062 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Mutations/BatchMintMutation.php @@ -0,0 +1,197 @@ + 'string', 'description' => 'string'])] + public function attributes(): array + { + return [ + 'name' => 'BatchMint', + 'description' => __('enjin-platform::mutation.batch_mint.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Transaction!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + 'collectionId' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::mutation.batch_mint.args.collectionId'), + ], + 'recipients' => [ + 'type' => GraphQL::type('[MintRecipient!]!'), + 'rules' => ['array', 'min:1', 'max:250'], + ], + ...$this->getIdempotencyField(), + ...$this->getSkipValidationField(), + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + Substrate $blockchainService, + SerializationServiceInterface $serializationService, + TransactionService $transactionService, + WalletService $walletService + ): mixed { + $recipients = collect($args['recipients'])->map( + function ($recipient) use ($blockchainService, $walletService) { + $createParams = Arr::get($recipient, 'createParams'); + $mintParams = Arr::get($recipient, 'mintParams'); + + if (Arr::get($createParams, 'cap.type') === TokenMintCapType::SUPPLY->name) { + if (null === Arr::get($createParams, 'cap.amount')) { + throw new PlatformException(__('enjin-platform::error.supply_cap_must_be_set')); + } + if (Arr::get($createParams, 'cap.amount') < Arr::get($createParams, 'initialSupply')) { + throw new PlatformException(__('enjin-platform::error.supply_cap_must_be_greater_than_initial')); + } + } + if (null !== $createParams && null !== $mintParams) { + throw new PlatformException(__('enjin-platform::error.cannot_set_create_and_mint_params_with_same_recipient')); + } + if (null === $createParams && null === $mintParams) { + throw new PlatformException(__('enjin-platform::error.set_either_create_or_mint_param_for_recipient')); + } + + $recipientWallet = $walletService->firstOrStore(['account' => $recipient['account']]); + + return [ + 'accountId' => $recipientWallet->public_key, + 'params' => $blockchainService->getMintOrCreateParams($createParams ?? $mintParams), + ]; + } + ); + + return Transaction::lazyLoadSelectFields( + $transactionService->store([ + 'method' => $this->getMutationName(), + 'encoded_data' => $this->resolveBatch($args['collectionId'], $recipients, false, $serializationService), + 'idempotency_key' => $args['idempotencyKey'] ?? Str::uuid()->toString(), + ]), + $resolveInfo + ); + } + + /** + * Resolve batch mint. + */ + protected function resolveBatch(string $collectionId, Collection $recipients, bool $continueOnFailure, SerializationServiceInterface $serializationService): string + { + if ($continueOnFailure) { + return $this->resolveWithContinueOnFailure($collectionId, $recipients, $serializationService); + } + + return $this->resolveWithoutContinueOnFailure($collectionId, $recipients, $serializationService); + } + + /** + * Resolve batch mint without continue on failure. + */ + protected function resolveWithoutContinueOnFailure(string $collectionId, Collection $recipients, SerializationServiceInterface $serializationService): string + { + return $serializationService->encode($this->getMethodName(), [ + 'collectionId' => $collectionId, + 'recipients' => $recipients->toArray(), + ]); + } + + /** + * Resolve batch mint with continue on failure. + */ + protected function resolveWithContinueOnFailure(string $collectionId, Collection $recipients, SerializationServiceInterface $serializationService): string + { + $encodedData = $recipients->map( + fn ($recipient) => $serializationService->encode('mint', [ + 'collectionId' => $collectionId, + 'recipientId' => $recipient['accountId'], + 'params' => $recipient['params'], + ]) + ); + + return $serializationService->encode('batch', [ + 'calls' => $encodedData->toArray(), + 'continueOnFailure' => true, + ]); + } + + /** + * Get the mutation's validation rules. + */ + protected function rulesWithValidation(array $args): array + { + return [ + 'collectionId' => [ + 'exists:collections,collection_chain_id', + new CheckTokenCount(collect($args['recipients'])->pluck('createParams')->filter()->count()), + ], + ...$this->getTokenFieldRulesDoesntExist('recipients.*.createParams', $args), + ...$this->getTokenFieldRulesExist('recipients.*.mintParams', $args), + ]; + } + + /** + * Get the mutation's validation rules without DB rules. + */ + protected function rulesWithoutValidation(array $args): array + { + return [ + ...$this->getTokenFieldRules('recipients.*.createParams', $args), + ...$this->getTokenFieldRules('recipients.*.mintParams', $args), + ]; + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Mutations/BatchSetAttributeMutation.php b/src/GraphQL/Schemas/Primary/Substrate/Mutations/BatchSetAttributeMutation.php new file mode 100644 index 00000000..6fc778c4 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Mutations/BatchSetAttributeMutation.php @@ -0,0 +1,169 @@ + 'BatchSetAttribute', + 'description' => __('enjin-platform::mutation.batch_set_attribute.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Transaction!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + 'collectionId' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.collectionId'), + ], + ...$this->getTokenFields(__('enjin-platform::args.common.tokenId'), isOptional: true), + 'attributes' => [ + 'type' => GraphQL::type('[AttributeInput!]!'), + ], + ...$this->getIdempotencyField(), + ...$this->getSkipValidationField(), + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + Substrate $blockchainService, + SerializationServiceInterface $serializationService, + TransactionService $transactionService + ): mixed { + return Transaction::lazyLoadSelectFields( + $transactionService->store([ + 'method' => $this->getMutationName(), + 'encoded_data' => $this->resolveBatch($args['collectionId'], $this->encodeTokenId($args), $args['attributes'], false, $serializationService), + 'idempotency_key' => $args['idempotencyKey'] ?? Str::uuid()->toString(), + ]), + $resolveInfo + ); + } + + /** + * Resolve batch set attribute. + */ + protected function resolveBatch(string $collectionId, ?string $tokenId, array $attributes, bool $continueOnFailure, SerializationServiceInterface $serializationService): string + { + if ($continueOnFailure) { + return $this->resolveWithContinueOnFailure($collectionId, $tokenId, $attributes, $serializationService); + } + + return $this->resolveWithoutContinueOnFailure($collectionId, $tokenId, $attributes, $serializationService); + } + + /** + * Resolve batch set attribute without continue on failure. + */ + protected function resolveWithoutContinueOnFailure(string $collectionId, ?string $tokenId, array $attributes, SerializationServiceInterface $serializationService): string + { + return $serializationService->encode($this->getMethodName(), [ + 'collectionId' => $collectionId, + 'tokenId' => $tokenId, + 'attributes' => $attributes, + ]); + } + + /** + * Resolve batch set attribute with continue on failure. + */ + protected function resolveWithContinueOnFailure(string $collectionId, ?string $tokenId, array $attributes, SerializationServiceInterface $serializationService): string + { + $encodedData = collect($attributes)->map( + fn ($attribute) => $serializationService->encode('setAttribute', [ + 'collectionId' => $collectionId, + 'tokenId' => $tokenId, + 'key' => $attribute['key'], + 'value' => $attribute['value'], + ]) + ); + + return $serializationService->encode('batch', [ + 'calls' => $encodedData->toArray(), + 'continueOnFailure' => true, + ]); + } + + /** + * Get the commond rules. + */ + protected function rulesCommon(array $args): array + { + return [ + 'attributes' => ['array', 'min:1', 'max:20'], + 'attributes.*.key' => ['max:32'], + 'attributes.*.value' => ['max:255'], + ]; + } + + /** + * Get the mutation's validation rules. + */ + protected function rulesWithValidation(array $args): array + { + return [ + 'collectionId' => ['bail', 'exists:collections,collection_chain_id', new IsCollectionOwner()], + ...$this->getOptionalTokenFieldRulesExist(), + ]; + } + + /** + * Get the mutation's validation rules without DB rules. + */ + protected function rulesWithoutValidation(array $args): array + { + return $this->getOptionalTokenFieldRules(); + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Mutations/BatchTransferMutation.php b/src/GraphQL/Schemas/Primary/Substrate/Mutations/BatchTransferMutation.php new file mode 100644 index 00000000..43974d79 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Mutations/BatchTransferMutation.php @@ -0,0 +1,208 @@ + 'string', 'description' => 'string'])] + public function attributes(): array + { + return [ + 'name' => 'BatchTransfer', + 'description' => __('enjin-platform::mutation.batch_transfer.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Transaction!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + 'collectionId' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::mutation.batch_transfer.args.collectionId'), + ], + 'recipients' => [ + 'type' => GraphQL::type('[TransferRecipient!]!'), + ], + 'signingAccount' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::mutation.batch_transfer.args.signingAccount'), + ], + ...$this->getIdempotencyField(), + ...$this->getSkipValidationField(), + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + Substrate $blockchainService, + SerializationServiceInterface $serializationService, + TransactionService $transactionService, + WalletService $walletService + ): mixed { + $signingWallet = $walletService->firstOrStore([ + 'account' => $args['signingAccount'] ?? config('enjin-platform.chains.daemon-account'), + ]); + + $recipients = collect($args['recipients'])->map( + function ($recipient) use ($blockchainService, $walletService) { + $simpleParams = Arr::get($recipient, 'simpleParams'); + $operatorParams = Arr::get($recipient, 'operatorParams'); + + if (null !== $simpleParams && null !== $operatorParams) { + throw new PlatformException(__('enjin-platform::error.cannot_set_simple_and_operator_params_for_same_recipient')); + } + if (null === $simpleParams && null === $operatorParams) { + throw new PlatformException(__('enjin-platform::error.set_either_simple_and_operator_params_for_recipient')); + } + + $targetWallet = $walletService->firstOrStore(['account' => $recipient['account']]); + + return [ + 'accountId' => $targetWallet->public_key, + 'params' => $blockchainService->getTransferParams($simpleParams ?? $operatorParams), + ]; + } + ); + + return Transaction::lazyLoadSelectFields( + $transactionService->store( + [ + 'method' => $this->getMutationName(), + 'encoded_data' => $this->resolveBatch($args['collectionId'], $recipients, false, $serializationService), + 'idempotency_key' => $args['idempotencyKey'] ?? Str::uuid()->toString(), + ], + signingWallet: $signingWallet + ), + $resolveInfo + ); + } + + /** + * Resolve batch transfer. + */ + protected function resolveBatch(string $collectionId, Collection $recipients, bool $continueOnFailure, SerializationServiceInterface $serializationService): string + { + if ($continueOnFailure) { + return $this->resolveWithContinueOnFailure($collectionId, $recipients, $serializationService); + } + + return $this->resolveWithoutContinueOnFailure($collectionId, $recipients, $serializationService); + } + + /** + * Resolve batch transfer without continue on failure. + */ + protected function resolveWithoutContinueOnFailure(string $collectionId, Collection $recipients, SerializationServiceInterface $serializationService): string + { + return $serializationService->encode($this->getMethodName(), [ + 'collectionId' => $collectionId, + 'recipients' => $recipients->toArray(), + ]); + } + + /** + * Resolve batch transfer with continue on failure. + */ + protected function resolveWithContinueOnFailure(string $collectionId, Collection $recipients, SerializationServiceInterface $serializationService): string + { + $encodedData = $recipients->map( + fn ($recipient) => $serializationService->encode('transferToken', [ + 'recipient' => $recipient['accountId'], + 'collectionId' => $collectionId, + 'params' => $recipient['params'], + ]) + ); + + return $serializationService->encode('batch', [ + 'calls' => $encodedData->toArray(), + 'continueOnFailure' => true, + ]); + } + + /** + * Get the common rules. + */ + protected function rulesCommon(array $args): array + { + return [ + 'recipients' => ['array', 'min:1', 'max:250'], + ]; + } + + /** + * Get the mutation's validation rules. + */ + protected function rulesWithValidation(array $args): array + { + return [ + 'collectionId' => ['exists:collections,collection_chain_id'], + 'signingAccount' => ['nullable', 'bail', new ValidSubstrateAccount(), new IsManagedWallet()], + ...$this->getTokenFieldRulesExist('recipients.*.simpleParams', $args), + ...$this->getTokenFieldRulesExist('recipients.*.operatorParams', $args), + ]; + } + + /** + * Get the mutation's validation rules withoud DB rules. + */ + protected function rulesWithoutValidation(array $args): array + { + return [ + 'signingAccount' => ['nullable', 'bail', new ValidSubstrateAccount()], + ...$this->getTokenFieldRules('recipients.*.simpleParams', $args), + ...$this->getTokenFieldRules('recipients.*.operatorParams', $args), + ]; + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Mutations/BurnMutation.php b/src/GraphQL/Schemas/Primary/Substrate/Mutations/BurnMutation.php new file mode 100644 index 00000000..dfab486b --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Mutations/BurnMutation.php @@ -0,0 +1,125 @@ + 'Burn', + 'description' => __('enjin-platform::mutation.burn.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Transaction!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + 'collectionId' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::mutation.burn.args.collectionId'), + ], + 'params' => [ + 'type' => GraphQL::type('BurnParamsInput!'), + 'description' => __('enjin-platform::mutation.burn.args.params'), + ], + ...$this->getIdempotencyField(), + ...$this->getSkipValidationField(), + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + SerializationServiceInterface $serializationService, + TransactionService $transactionService, + ): mixed { + $args['params']['tokenId'] = $this->encodeTokenId($args['params']); + unset($args['params']['encodeTokenId']); + $encodedData = $serializationService->encode($this->getMethodName(), [ + 'collectionId' => $args['collectionId'], + 'params' => new BurnParams(...$args['params']), + ]); + + return Transaction::lazyLoadSelectFields( + $transactionService->store([ + 'method' => $this->getMutationName(), + 'encoded_data' => $encodedData, + 'idempotency_key' => $args['idempotencyKey'] ?? Str::uuid()->toString(), + ]), + $resolveInfo + ); + } + + /** + * Get the mutation's validation rules. + */ + protected function rulesWithValidation(array $args): array + { + return [ + 'collectionId' => ['exists:collections,collection_chain_id'], + 'params.amount' => [new MinBigInt(1), new MaxTokenBalance()], + ...$this->getTokenFieldRulesExist('params'), + ]; + } + + /** + * Get the mutation's validation rules withoud DB rules. + */ + protected function rulesWithoutValidation(array $args): array + { + return [ + 'collectionId' => [new MinBigInt(2000), new MaxBigInt(Hex::MAX_UINT128)], + 'params.amount' => [new MinBigInt(1), new MaxBigInt(Hex::MAX_UINT128)], + ...$this->getTokenFieldRules('params'), + ]; + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Mutations/CreateCollectionMutation.php b/src/GraphQL/Schemas/Primary/Substrate/Mutations/CreateCollectionMutation.php new file mode 100644 index 00000000..87bc888c --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Mutations/CreateCollectionMutation.php @@ -0,0 +1,118 @@ + 'CreateCollection', + 'description' => __('enjin-platform::mutation.create_collection.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Transaction!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + 'mintPolicy' => [ + 'type' => GraphQL::type('MintPolicy!'), + 'description' => __('enjin-platform::mutation.create_collection.args.mintPolicy'), + ], + 'marketPolicy' => [ + 'type' => GraphQL::type('MarketPolicy'), + 'description' => __('enjin-platform::mutation.create_collection.args.marketPolicy'), + 'defaultValue' => null, + ], + 'explicitRoyaltyCurrencies' => [ + 'type' => GraphQL::type('[MultiTokenIdInput]'), + 'description' => __('enjin-platform::mutation.create_collection.args.explicitRoyaltyCurrencies'), + 'defaultValue' => [], + ], + 'attributes' => [ + 'type' => GraphQL::type('[AttributeInput]'), + 'description' => __('enjin-platform::mutation.create_collection.args.attributes'), + 'defaultValue' => [], + ], + ...$this->getIdempotencyField(), + ...$this->getSkipValidationField(), + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + Substrate $blockchainService, + SerializationServiceInterface $serializationService, + TransactionService $transactionService + ): mixed { + return Transaction::lazyLoadSelectFields( + $transactionService->store([ + 'method' => $this->getMutationName(), + 'encoded_data' => $serializationService->encode($this->getMethodName(), $blockchainService->getCollectionPolicies($args)), + 'idempotency_key' => $args['idempotencyKey'] ?? Str::uuid()->toString(), + ]), + $resolveInfo + ); + } + + /** + * Get common rules. + */ + protected function rulesCommon(array $args): array + { + return [ + 'explicitRoyaltyCurrencies' => ['nullable', 'bail', 'array', 'min:0', 'max:10', new DistinctMultiAsset()], + 'attributes' => ['nullable', 'bail', 'array', 'min:0', 'max:10', new DistinctAttributes()], + ...$this->getTokenFieldRules('explicitRoyaltyCurrencies.*', $args), + ]; + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Mutations/CreateTokenMutation.php b/src/GraphQL/Schemas/Primary/Substrate/Mutations/CreateTokenMutation.php new file mode 100644 index 00000000..c36019b0 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Mutations/CreateTokenMutation.php @@ -0,0 +1,149 @@ + 'CreateToken', + 'description' => __('enjin-platform::mutation.create_token.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Transaction!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + 'recipient' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::mutation.create_token.args.recipient'), + ], + 'collectionId' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::mutation.burn.args.collectionId'), + ], + 'params' => [ + 'type' => GraphQL::type('CreateTokenParams!'), + 'description' => __('enjin-platform::input_type.create_token_params.description'), + ], + ...$this->getIdempotencyField(), + ...$this->getSkipValidationField(), + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + Substrate $blockchainService, + SerializationServiceInterface $serializationService, + TransactionService $transactionService, + WalletService $walletService + ): mixed { + $recipientWallet = $walletService->firstOrStore(['account' => $args['recipient']]); + + $encodedData = $serializationService->encode($this->getMethodName(), [ + 'recipientId' => $recipientWallet->public_key, + 'collectionId' => $args['collectionId'], + 'params' => $blockchainService->getCreateTokenParams($args['params']), + ]); + + return Transaction::lazyLoadSelectFields( + $transactionService->store([ + 'method' => $this->getMutationName(), + 'encoded_data' => $encodedData, + 'idempotency_key' => $args['idempotencyKey'] ?? Str::uuid()->toString(), + ]), + $resolveInfo + ); + } + + /** + * Get the serialization service method name. + */ + public function getMethodName(): string + { + return 'mint'; + } + + /** + * Get common rules. + */ + protected function rulesCommon(array $args): array + { + return [ + 'recipient' => ['filled', new ValidSubstrateAccount()], + 'params.cap.amount' => [ + 'required_if:params.cap.type,SUPPLY', + 'prohibited_if:params.cap.type,SINGLE_MINT', + 'nullable', + new MinBigInt($args['params']['initialSupply']), + ], + ]; + } + + /** + * Get the mutation's validation rules. + */ + protected function rulesWithValidation(array $args): array + { + return [ + 'collectionId' => ['exists:collections,collection_chain_id', new CheckTokenCount()], + ...$this->getTokenFieldRulesDoesntExist('params'), + ]; + } + + /** + * Get the mutation's validation rules without DB rules. + */ + protected function rulesWithoutValidation(array $args): array + { + return $this->getTokenFieldRules('params'); + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Mutations/DestroyCollectionMutation.php b/src/GraphQL/Schemas/Primary/Substrate/Mutations/DestroyCollectionMutation.php new file mode 100644 index 00000000..227316c5 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Mutations/DestroyCollectionMutation.php @@ -0,0 +1,112 @@ + 'DestroyCollection', + 'description' => __('enjin-platform::mutation.burn.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Transaction!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + 'collectionId' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::mutation.burn.args.collectionId'), + ], + ...$this->getIdempotencyField(), + ...$this->getSkipValidationField(), + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + SerializationServiceInterface $serializationService, + TransactionService $transactionService, + WalletService $walletService + ): mixed { + $encodedData = $serializationService->encode($this->getMethodName(), [ + 'collectionId' => $args['collectionId'], + ]); + + return Transaction::lazyLoadSelectFields( + $transactionService->store([ + 'method' => $this->getMutationName(), + 'encoded_data' => $encodedData, + 'idempotency_key' => $args['idempotencyKey'] ?? Str::uuid()->toString(), + ]), + $resolveInfo + ); + } + + /** + * Get the mutation's validation rules. + */ + protected function rulesWithValidation(array $args): array + { + return [ + 'collectionId' => ['bail', new MinBigInt(2000), new MaxBigInt(Hex::MAX_UINT128), 'exists:collections,collection_chain_id', new IsCollectionOwner(), new NoTokensInCollection()], + ]; + } + + /** + * Get the mutation's validation rules withoud DB rules. + */ + protected function rulesWithoutValidation(array $args): array + { + return [ + 'collectionId' => ['bail', new MinBigInt(2000), new MaxBigInt(Hex::MAX_UINT128)], + ]; + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Mutations/FreezeMutation.php b/src/GraphQL/Schemas/Primary/Substrate/Mutations/FreezeMutation.php new file mode 100644 index 00000000..6d5d8e57 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Mutations/FreezeMutation.php @@ -0,0 +1,147 @@ + 'Freeze', + 'description' => __('enjin-platform::mutation.freeze.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Transaction!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + 'freezeType' => [ + 'type' => GraphQL::type('FreezeType!'), + 'description' => __('enjin-platform::mutation.freeze.args.freezeType'), + ], + 'collectionId' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::mutation.freeze.args.collectionId'), + ], + ...$this->getTokenFields(__('enjin-platform::mutation.freeze.args.tokenId'), true), + 'collectionAccount' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::mutation.freeze.args.collectionAccount'), + ], + 'tokenAccount' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::mutation.freeze.args.tokenAccount'), + ], + ...$this->getIdempotencyField(), + ...$this->getSkipValidationField(), + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + Substrate $blockchainService, + SerializationServiceInterface $serializationService, + TransactionService $transactionService + ): mixed { + $params = $blockchainService->getFreezeOrThawParams($args); + $encodedData = $serializationService->encode($this->getMethodName(), [ + 'collectionId' => $args['collectionId'], + 'params' => $params, + ]); + + return Transaction::lazyLoadSelectFields( + $transactionService->store([ + 'method' => $this->getMutationName(), + 'encoded_data' => $encodedData, + 'idempotency_key' => $args['idempotencyKey'] ?? Str::uuid()->toString(), + ]), + $resolveInfo + ); + } + + /** + * Get the mutation's validation rules. + */ + protected function rulesWithValidation(array $args): array + { + $freezeType = FreezeType::getEnumCase($args['freezeType']); + + return [ + 'collectionId' => ['exists:collections,collection_chain_id'], + ...( + in_array($freezeType, [FreezeType::TOKEN, FreezeType::TOKEN_ACCOUNT], true) + ? $this->getTokenFieldRulesExist(null, [], false) + : ['tokenId' => ['prohibited']] + ), + 'collectionAccount' => FreezeType::COLLECTION_ACCOUNT === $freezeType ? ['bail', 'required', new ValidSubstrateAccount(), new AccountExistsInCollection()] : ['prohibited'], + 'tokenAccount' => FreezeType::TOKEN_ACCOUNT === $freezeType ? ['bail', 'required', new ValidSubstrateAccount(), new AccountExistsInToken()] : ['prohibited'], + ]; + } + + /** + * Get the mutation's validation rules withoud DB rules. + */ + protected function rulesWithoutValidation(array $args): array + { + $freezeType = FreezeType::getEnumCase($args['freezeType']); + + return [ + ...( + in_array($freezeType, [FreezeType::TOKEN, FreezeType::TOKEN_ACCOUNT], true) + ? $this->getTokenFieldRules() + : ['tokenId' => ['prohibited'], 'encodeTokenId' => ['prohibited']] + ), + 'collectionAccount' => FreezeType::COLLECTION_ACCOUNT === $freezeType ? ['bail', 'required', new ValidSubstrateAccount()] : ['prohibited'], + 'tokenAccount' => FreezeType::TOKEN_ACCOUNT === $freezeType ? ['bail', 'required', new ValidSubstrateAccount()] : ['prohibited'], + ]; + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Mutations/MintTokenMutation.php b/src/GraphQL/Schemas/Primary/Substrate/Mutations/MintTokenMutation.php new file mode 100644 index 00000000..22f2d5cc --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Mutations/MintTokenMutation.php @@ -0,0 +1,130 @@ + 'MintToken', + 'description' => __('enjin-platform::mutation.mint_token.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Transaction!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + 'recipient' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::mutation.mint_token.args.recipient'), + 'rules' => ['filled', new ValidSubstrateAccount()], + ], + 'collectionId' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::mutation.mint_token.args.collectionId'), + 'rules' => ['exists:collections,collection_chain_id'], + ], + 'params' => [ + 'type' => GraphQL::type('MintTokenParams!'), + 'description' => __('enjin-platform::input_type.mint_token_params.description'), + ], + ...$this->getIdempotencyField(), + ...$this->getSkipValidationField(), + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + Substrate $blockchainService, + SerializationServiceInterface $serializationService, + TransactionService $transactionService, + WalletService $walletService + ): mixed { + $recipientWallet = $walletService->firstOrStore(['account' => $args['recipient']]); + + $encodedData = $serializationService->encode($this->getMethodName(), [ + 'recipientId' => $recipientWallet->public_key, + 'collectionId' => $args['collectionId'], + 'params' => $blockchainService->getMintTokenParams($args['params']), + ]); + + return Transaction::lazyLoadSelectFields( + $transactionService->store([ + 'method' => $this->getMutationName(), + 'encoded_data' => $encodedData, + 'idempotency_key' => $args['idempotencyKey'] ?? Str::uuid()->toString(), + ]), + $resolveInfo + ); + } + + /** + * Get the serialization service method name. + */ + public function getMethodName(): string + { + return 'mint'; + } + + /** + * Get the mutation's validation rules. + */ + protected function rulesWithValidation(array $args): array + { + return $this->getTokenFieldRulesExist('params'); + } + + /** + * Get the mutation's validation rules withoud DB rules. + */ + protected function rulesWithoutValidation(array $args): array + { + return $this->getTokenFieldRules('params'); + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Mutations/MutateCollectionMutation.php b/src/GraphQL/Schemas/Primary/Substrate/Mutations/MutateCollectionMutation.php new file mode 100644 index 00000000..b1ab6b89 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Mutations/MutateCollectionMutation.php @@ -0,0 +1,167 @@ + 'MutateCollection', + 'description' => __('enjin-platform::mutation.mutate_collection.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Transaction!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + 'collectionId' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::mutation.mutate_collection.args.collectionId'), + ], + 'mutation' => [ + 'type' => GraphQL::type('CollectionMutationInput!'), + 'description' => __('enjin-platform::mutation.mutate_collection.args.mutation'), + ], + ...$this->getIdempotencyField(), + ...$this->getSkipValidationField(), + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + SerializationServiceInterface $serializationService, + TransactionService $transactionService, + WalletService $walletService, + Substrate $blockchainService + ): mixed { + if ($currency = Arr::get($args, 'mutation.explicitRoyaltyCurrencies')) { + Arr::set( + $args, + 'mutation.explicitRoyaltyCurrencies', + collect($currency) + ->map(function ($row) { + $row['tokenId'] = $this->encodeTokenId($row); + unset($row['encodeTokenId']); + + return $row; + })->toArray() + ); + } + + $encodedData = $serializationService->encode($this->getMethodName(), [ + 'collectionId' => $args['collectionId'], + 'owner' => null !== Arr::get($args, 'mutation.owner') + ? $walletService->firstOrStore(['account' => $args['mutation']['owner']])->public_key + : null, + 'royalty' => $blockchainService->getMutateCollectionRoyalty(Arr::get($args, 'mutation')), + 'explicitRoyaltyCurrencies' => Arr::get($args, 'mutation.explicitRoyaltyCurrencies'), + ]); + + return Transaction::lazyLoadSelectFields( + $transactionService->store([ + 'method' => $this->getMutationName(), + 'encoded_data' => $encodedData, + 'idempotency_key' => $args['idempotencyKey'] ?? Str::uuid()->toString(), + ]), + $resolveInfo + ); + } + + /** + * Get the common rules. + */ + protected function rulesCommon(array $args): array + { + $isOwnerEmpty = '' === Arr::get($args, 'mutation.owner'); + $isExplicitRoyaltyEmpty = [] === Arr::get($args, 'mutation.explicitRoyaltyCurrencies'); + $explicitRoyaltyRules = ['nullable', 'bail', 'array', 'min:0', 'max:10', new DistinctMultiAsset()]; + + return [ + 'mutation.owner' => $this->mutationOwnerRule($isOwnerEmpty, $isExplicitRoyaltyEmpty), + 'mutation.royalty' => $isExplicitRoyaltyEmpty ? [] : ['required_without_all:mutation.owner,mutation.explicitRoyaltyCurrencies'], + 'mutation.royalty.beneficiary' => ['nullable', 'bail', 'required_with:mutation.royalty.percentage', new ValidSubstrateAccount()], + 'mutation.royalty.percentage' => ['required_with:mutation.royalty.beneficiary', new ValidRoyaltyPercentage()], + 'mutation.explicitRoyaltyCurrencies' => $isExplicitRoyaltyEmpty ? $explicitRoyaltyRules : [...$explicitRoyaltyRules, 'required_without_all:mutation.owner,mutation.royalty'], + ...$this->getTokenFieldRules('mutation.explicitRoyaltyCurrencies.*', $args), + ]; + } + + /** + * Get the mutation's validation rules. + */ + protected function rulesWithValidation(array $args): array + { + return [ + 'collectionId' => ['exists:collections,collection_chain_id'], + ]; + } + + /** + * Get the owner attribute validation rules. + */ + protected function mutationOwnerRule(bool $isOwnerEmpty, bool $isExplicitRoyaltyEmpty): array + { + $ownerRules = ['nullable', 'bail', new ValidSubstrateAccount()]; + + if ($isOwnerEmpty) { + return [...$ownerRules, 'filled']; + } + + if ($isExplicitRoyaltyEmpty) { + return $ownerRules; + } + + return [...$ownerRules, 'required_without_all:mutation.royalty,mutation.explicitRoyaltyCurrencies']; + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Mutations/MutateTokenMutation.php b/src/GraphQL/Schemas/Primary/Substrate/Mutations/MutateTokenMutation.php new file mode 100644 index 00000000..15fac2ac --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Mutations/MutateTokenMutation.php @@ -0,0 +1,150 @@ + 'MutateToken', + 'description' => __('enjin-platform::mutation.mutate_token.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Transaction!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + 'collectionId' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::mutation.mutate_collection.args.collectionId'), + ], + ...$this->getTokenFields(__('enjin-platform::mutation.mutate_collection.args.tokenId')), + 'mutation' => [ + 'type' => GraphQL::type('TokenMutationInput!'), + 'description' => __('enjin-platform::mutation.mutate_collection.args.mutation'), + ], + ...$this->getIdempotencyField(), + ...$this->getSkipValidationField(), + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + SerializationServiceInterface $serializationService, + TransactionService $transactionService, + Substrate $blockchainService + ): mixed { + $encodedData = $serializationService->encode($this->getMethodName(), [ + 'collectionId' => $args['collectionId'], + 'tokenId' => $this->encodeTokenId($args), + 'behavior' => $blockchainService->getMutateTokenBehavior(Arr::get($args, 'mutation')), + 'listingForbidden' => Arr::get($args, 'mutation.listingForbidden'), + ]); + + + return Transaction::lazyLoadSelectFields( + $transactionService->store([ + 'method' => $this->getMutationName(), + 'encoded_data' => $encodedData, + 'idempotency_key' => $args['idempotencyKey'] ?? Str::uuid()->toString(), + ]), + $resolveInfo + ); + } + + /** + * Get the validation error messages. + */ + public function validationErrorMessages(array $args = []): array + { + return [ + 'mutation.behavior.isCurrency.accepted' => __('enjin-platform::validation.mutation.behavior.isCurrency.accepted'), + ]; + } + + /** + * Get the common rules. + */ + protected function rulesCommon(array $args): array + { + $isBehaviorEmpty = [] === Arr::get($args, 'mutation.behavior'); + + return [ + 'mutation.behavior' => $isBehaviorEmpty ? [] : ['required_without:mutation.listingForbidden'], + 'mutation.behavior.hasRoyalty.beneficiary' => ['nullable', 'bail', 'required_with:mutation.behavior.hasRoyalty.percentage', new ValidSubstrateAccount()], + 'mutation.behavior.hasRoyalty.percentage' => ['required_with:mutation.behavior.hasRoyalty.beneficiary', new ValidRoyaltyPercentage()], + 'mutation.behavior.isCurrency' => ['sometimes', 'accepted'], + 'mutation.listingForbidden' => $isBehaviorEmpty ? [] : ['required_without:mutation.behavior'], + ]; + } + + /** + * Get the mutation's validation rules. + */ + protected function rulesWithValidation(array $args): array + { + return [ + 'collectionId' => ['exists:collections,collection_chain_id'], + ...$this->getTokenFieldRulesExist(), + ]; + } + + /** + * Get the mutation's validation rules withoud DB rules. + */ + protected function rulesWithoutValidation(array $args): array + { + return $this->getTokenFieldRules(); + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Mutations/OperatorTransferTokenMutation.php b/src/GraphQL/Schemas/Primary/Substrate/Mutations/OperatorTransferTokenMutation.php new file mode 100644 index 00000000..5088d406 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Mutations/OperatorTransferTokenMutation.php @@ -0,0 +1,156 @@ + 'OperatorTransferToken', + 'description' => __('enjin-platform::mutation.operator_transfer_token.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Transaction!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + 'collectionId' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.collectionId'), + ], + 'recipient' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.recipient'), + ], + 'params' => [ + 'type' => GraphQL::type('OperatorTransferParams!'), + ], + 'signingAccount' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::mutation.batch_transfer.args.signingAccount'), + ], + ...$this->getIdempotencyField(), + ...$this->getSkipValidationField(), + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + Substrate $blockchainService, + SerializationServiceInterface $serializationService, + TransactionService $transactionService, + WalletService $walletService + ): mixed { + $targetWallet = $walletService->firstOrStore(['account' => $args['recipient']]); + $signingWallet = $walletService->firstOrStore([ + 'account' => Arr::get($args, 'signingAccount') ?: config('enjin-platform.chains.daemon-account'), + ]); + + $encodedData = $serializationService->encode($this->getMethodName(), [ + $targetWallet->public_key, + $args['collectionId'], + $blockchainService->getOperatorTransferParams($args['params']), + ]); + + return Transaction::lazyLoadSelectFields( + $transactionService->store( + [ + 'method' => $this->getMutationName(), + 'encoded_data' => $encodedData, + 'idempotency_key' => $args['idempotencyKey'] ?? Str::uuid()->toString(), + ], + signingWallet: $signingWallet + ), + $resolveInfo + ); + } + + /** + * Get the serialization service method name. + */ + public function getMethodName(): string + { + return 'transferToken'; + } + + /** + * Get the common rules. + */ + protected function rulesCommon(array $args): array + { + return [ + 'recipient' => ['filled', new ValidSubstrateAccount()], + ]; + } + + /** + * Get the mutation's validation rules. + */ + protected function rulesWithValidation(array $args): array + { + return [ + 'collectionId' => ['exists:collections,collection_chain_id'], + 'signingAccount' => '' === Arr::get($args, 'signingAccount') ? ['filled'] : ['nullable', 'bail', new ValidSubstrateAccount(), new IsManagedWallet()], + ...$this->getTokenFieldRulesExist('params'), + ]; + } + + /** + * Get the mutation's validation rules withoud DB rules. + */ + protected function rulesWithoutValidation(array $args): array + { + return [ + 'signingAccount' => '' === Arr::get($args, 'signingAccount') ? ['filled'] : ['nullable', 'bail', new ValidSubstrateAccount()], + ...$this->getTokenFieldRules('params'), + ]; + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Mutations/RemoveAllAttributesMutation.php b/src/GraphQL/Schemas/Primary/Substrate/Mutations/RemoveAllAttributesMutation.php new file mode 100644 index 00000000..d39c848c --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Mutations/RemoveAllAttributesMutation.php @@ -0,0 +1,144 @@ + 'RemoveAllAttributes', + 'description' => __('enjin-platform::mutation.remove_all_attributes.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Transaction!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + 'collectionId' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.collectionId'), + ], + ...$this->getTokenFields(__('enjin-platform::args.common.tokenId'), true), + 'attributeCount' => [ + 'type' => GraphQL::type('Int'), + 'description' => __('enjin-platform::mutation.remove_all_attributes.args.attributeCount'), + ], + ...$this->getIdempotencyField(), + ...$this->getSkipValidationField(), + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + SerializationServiceInterface $serializationService, + TransactionService $transactionService + ): mixed { + $tokenId = $this->encodeTokenId($args); + if (!Arr::get($args, 'attributeCount')) { + $args['attributeCount'] = $this->getAttributeCount($args); + if ($args['attributeCount'] == 0) { + throw new PlatformException(__('enjin-platform::error.attribute_count_empty')); + } + } + + $encodedData = $serializationService->encode($this->name, [ + 'collectionId' => $args['collectionId'], + 'tokenId' => $tokenId, + 'attributeCount' => $args['attributeCount'], + ]); + + return Transaction::lazyLoadSelectFields( + $transactionService->store([ + 'method' => $this->getMutationName(), + 'encoded_data' => $encodedData, + 'idempotency_key' => $args['idempotencyKey'] ?? Str::uuid()->toString(), + ]), + $resolveInfo + ); + } + + /** + * Query the attribute count. + */ + protected function getAttributeCount(array $args): int + { + $tokenId = $this->encodeTokenId($args); + + return Attribute::whereHas('collection', fn ($sub) => $sub->where('collection_chain_id', $args['collectionId'])) + ->when($tokenId, fn ($query) => $query->whereHas('token', fn ($sub) => $sub->where('token_chain_id', $tokenId))) + ->unless($tokenId, fn ($query) => $query->whereNull('token_id')) + ->count(); + } + + /** + * Get the mutation's validation rules. + */ + protected function rulesWithValidation(array $args): array + { + return [ + 'collectionId' => ['bail', 'exists:collections,collection_chain_id', new IsCollectionOwner()], + 'attributeCount' => ['nullable', 'integer', 'min:1', 'max:' . Hex::MAX_UINT32], + ...$this->getOptionalTokenFieldRulesExist(), + ]; + } + + /** + * Get the mutation's validation rules withoud DB rules. + */ + protected function rulesWithoutValidation(array $args): array + { + return $this->getOptionalTokenFieldRules(); + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Mutations/RemoveCollectionAttributeMutation.php b/src/GraphQL/Schemas/Primary/Substrate/Mutations/RemoveCollectionAttributeMutation.php new file mode 100644 index 00000000..a51d7e25 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Mutations/RemoveCollectionAttributeMutation.php @@ -0,0 +1,121 @@ + 'RemoveCollectionAttribute', + 'description' => __('enjin-platform::mutation.remove_collection.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Transaction!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + 'collectionId' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.collectionId'), + ], + 'key' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.key'), + ], + ...$this->getIdempotencyField(), + ...$this->getSkipValidationField(), + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + SerializationServiceInterface $serializationService, + TransactionService $transactionService + ): mixed { + $encodedData = $serializationService->encode($this->getMethodName(), [ + 'collectionId' => $args['collectionId'], + 'tokenId' => null, + 'key' => $args['key'], + ]); + + return Transaction::lazyLoadSelectFields( + $transactionService->store([ + 'method' => $this->getMutationName(), + 'encoded_data' => $encodedData, + 'idempotency_key' => $args['idempotencyKey'] ?? Str::uuid()->toString(), + ]), + $resolveInfo + ); + } + + /** + * Get the serialization service method name. + */ + public function getMethodName(): string + { + return 'removeAttribute'; + } + + /** + * Get the mutation's validation rules. + */ + protected function rulesWithValidation(array $args): array + { + return [ + 'collectionId' => ['exists:collections,collection_chain_id'], + 'key' => ['bail', 'filled', 'alpha_dash', 'max:32', new AttributeExistsInCollection()], + ]; + } + + /** + * Get the mutation's validation rules without DB rules. + */ + protected function rulesWithoutValidation(array $args): array + { + return [ + 'key' => ['bail', 'filled', 'alpha_dash', 'max:32'], + ]; + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Mutations/RemoveTokenAttributeMutation.php b/src/GraphQL/Schemas/Primary/Substrate/Mutations/RemoveTokenAttributeMutation.php new file mode 100644 index 00000000..334f62e2 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Mutations/RemoveTokenAttributeMutation.php @@ -0,0 +1,137 @@ + 'RemoveTokenAttribute', + 'description' => __('enjin-platform::mutation.remove_token_attribute.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Transaction!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + 'collectionId' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.collectionId'), + ], + ...$this->getTokenFields(__('enjin-platform::args.common.tokenId')), + 'key' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.key'), + ], + ...$this->getIdempotencyField(), + ...$this->getSkipValidationField(), + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + SerializationServiceInterface $serializationService, + TransactionService $transactionService + ): mixed { + $encodedData = $serializationService->encode($this->getMethodName(), [ + 'collectionId' => $args['collectionId'], + 'tokenId' => $this->encodeTokenId($args), + 'key' => $args['key'], + ]); + + return Transaction::lazyLoadSelectFields( + $transactionService->store([ + 'method' => $this->getMutationName(), + 'encoded_data' => $encodedData, + 'idempotency_key' => $args['idempotencyKey'] ?? Str::uuid()->toString(), + ]), + $resolveInfo + ); + } + + /** + * Get the serialization service method name. + */ + public function getMethodName(): string + { + return 'removeAttribute'; + } + + /** + * Get the mutation's validation rules. + */ + protected function rulesWithValidation(array $args): array + { + return [ + 'collectionId' => ['exists:collections,collection_chain_id'], + 'key' => ['bail', 'filled', 'alpha_dash', 'max:32', new AttributeExistsInToken()], + ...$this->getTokenFieldRules( + null, + [new TokenEncodeExists()] + )]; + } + + /** + * Get the mutation's validation rules without DB rules. + */ + protected function rulesWithoutValidation(array $args): array + { + return [ + 'collectionId' => [new MinBigInt(2000), new MaxBigInt(Hex::MAX_UINT128)], + 'key' => ['bail', 'filled', 'alpha_dash', 'max:32'], + ...$this->getTokenFieldRules(), + ]; + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Mutations/RetryTransactionsMutation.php b/src/GraphQL/Schemas/Primary/Substrate/Mutations/RetryTransactionsMutation.php new file mode 100644 index 00000000..55f5d492 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Mutations/RetryTransactionsMutation.php @@ -0,0 +1,102 @@ + 'RetryTransactions', + 'description' => __('enjin-platform::mutation.retry_transaction.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Boolean!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + 'ids' => [ + 'type' => GraphQL::type('[BigInt!]'), + 'description' => __('enjin-platform::query.get_transaction.args.id'), + ], + 'idempotencyKeys' => [ + 'type' => GraphQL::type('[String!]'), + 'description' => __('enjin-platform::query.get_transaction.args.idempotencyKey'), + ], + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields + ): mixed { + $prepare = ($ids = Arr::get($args, 'ids')) + ? Transaction::whereIn('id', $ids) + : Transaction::whereIn('idempotency_key', Arr::get($args, 'idempotencyKeys')); + + return (bool) $prepare->update(['state' => TransactionState::PENDING->name, 'transaction_chain_hash' => null]); + } + + /** + * Get the mutation's validation rules. + */ + protected function rules(array $args = []): array + { + return [ + 'ids' => [ + 'required_without:idempotencyKeys', + 'prohibits:idempotencyKeys', + 'array', + 'min:1', + 'max:1000', + Rule::exists('transactions', 'id'), + ], + 'ids.*' => ['bail', 'distinct', new MinBigInt(), new MaxBigInt()], + 'idempotencyKeys' => [ + 'required_without:ids', + 'prohibits:ids', + 'array', + 'min:1', + 'max:1000', + Rule::exists('transactions', 'idempotency_key'), + ], + 'idempotencyKeys.*' => ['bail', 'filled', 'max:255', 'distinct'], + ]; + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Mutations/SetCollectionAttributeMutation.php b/src/GraphQL/Schemas/Primary/Substrate/Mutations/SetCollectionAttributeMutation.php new file mode 100644 index 00000000..c00f1c00 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Mutations/SetCollectionAttributeMutation.php @@ -0,0 +1,125 @@ + 'SetCollectionAttribute', + 'description' => __('enjin-platform::mutation.set_collection_attribute.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Transaction!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + 'collectionId' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.collectionId'), + ], + 'key' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.key'), + ], + 'value' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.value'), + ], + ...$this->getIdempotencyField(), + ...$this->getSkipValidationField(), + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + SerializationServiceInterface $serializationService, + TransactionService $transactionService + ): mixed { + $encodedData = $serializationService->encode($this->getMethodName(), [ + 'collectionId' => $args['collectionId'], + 'tokenId' => null, + 'key' => $args['key'], + 'value' => $args['value'], + ]); + + return Transaction::lazyLoadSelectFields( + $transactionService->store([ + 'method' => $this->getMutationName(), + 'encoded_data' => $encodedData, + 'idempotency_key' => $args['idempotencyKey'] ?? Str::uuid()->toString(), + ]), + $resolveInfo + ); + } + + /** + * Get the serialization service method name. + */ + public function getMethodName(): string + { + return 'setAttribute'; + } + + /** + * Get the mutation's validation rules. + */ + protected function rulesCommon(array $args): array + { + return [ + 'key' => ['filled', 'alpha_dash', 'max:32'], + 'value' => ['filled', 'max:255'], + ]; + } + + /** + * Get the mutation's validation rules without DB rules. + */ + protected function rulesWithValidation(array $args): array + { + return [ + 'collectionId' => ['exists:collections,collection_chain_id'], + ]; + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Mutations/SetTokenAttributeMutation.php b/src/GraphQL/Schemas/Primary/Substrate/Mutations/SetTokenAttributeMutation.php new file mode 100644 index 00000000..64e58e8d --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Mutations/SetTokenAttributeMutation.php @@ -0,0 +1,142 @@ + 'SetTokenAttribute', + 'description' => __('enjin-platform::mutation.set_token_attribute.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Transaction!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + 'collectionId' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.collectionId'), + ], + ...$this->getTokenFields(__('enjin-platform::args.common.tokenId')), + 'key' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.key'), + ], + 'value' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.value'), + ], + ...$this->getIdempotencyField(), + ...$this->getSkipValidationField(), + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + SerializationServiceInterface $serializationService, + TransactionService $transactionService + ): mixed { + $encodedData = $serializationService->encode($this->getMethodName(), [ + 'collectionId' => $args['collectionId'], + 'tokenId' => $this->encodeTokenId($args), + 'key' => $args['key'], + 'value' => $args['value'], + ]); + + + return Transaction::lazyLoadSelectFields( + $transactionService->store([ + 'method' => $this->getMutationName(), + 'encoded_data' => $encodedData, + 'idempotency_key' => $args['idempotencyKey'] ?? Str::uuid()->toString(), + ]), + $resolveInfo + ); + } + + /** + * Get the serialization service method name. + */ + public function getMethodName(): string + { + return 'setAttribute'; + } + + /** + * Get the common rules. + */ + protected function rulesCommon(array $args): array + { + return [ + 'key' => ['filled', 'alpha_dash', 'max:32'], + 'value' => ['filled', 'max:255'], + ]; + } + + /** + * Get the mutation's validation rules. + */ + protected function rulesWithValidation(array $args): array + { + return [ + 'collectionId' => ['exists:collections,collection_chain_id'], + ...$this->getTokenFieldRulesExist(), + ]; + } + + /** + * Get the mutation's validation rules without DB rules. + */ + protected function rulesWithoutValidation(array $args): array + { + return $this->getTokenFieldRules(); + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Mutations/SimpleTransferTokenMutation.php b/src/GraphQL/Schemas/Primary/Substrate/Mutations/SimpleTransferTokenMutation.php new file mode 100644 index 00000000..770615db --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Mutations/SimpleTransferTokenMutation.php @@ -0,0 +1,156 @@ + 'SimpleTransferToken', + 'description' => __('enjin-platform::mutation.simple_transfer_token.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Transaction!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + 'collectionId' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.collectionId'), + ], + 'recipient' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.recipient'), + ], + 'params' => [ + 'type' => GraphQL::type('SimpleTransferParams!'), + ], + 'signingAccount' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::mutation.batch_transfer.args.signingAccount'), + ], + ...$this->getIdempotencyField(), + ...$this->getSkipValidationField(), + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + Substrate $blockchainService, + SerializationServiceInterface $serializationService, + TransactionService $transactionService, + WalletService $walletService + ): mixed { + $targetWallet = $walletService->firstOrStore(['account' => $args['recipient']]); + $signingWallet = $walletService->firstOrStore([ + 'account' => Arr::get($args, 'signingAccount') ?: config('enjin-platform.chains.daemon-account'), + ]); + + $encodedData = $serializationService->encode($this->getMethodName(), [ + $targetWallet->public_key, + $args['collectionId'], + $blockchainService->getSimpleTransferParams($args['params']), + ]); + + return Transaction::lazyLoadSelectFields( + $transactionService->store( + [ + 'method' => $this->getMutationName(), + 'encoded_data' => $encodedData, + 'idempotency_key' => $args['idempotencyKey'] ?? Str::uuid()->toString(), + ], + signingWallet: $signingWallet + ), + $resolveInfo + ); + } + + /** + * Get the serialization service method name. + */ + public function getMethodName(): string + { + return 'transferToken'; + } + + /** + * Get the common rules. + */ + protected function rulesCommon(array $args): array + { + return [ + 'recipient' => ['filled', new ValidSubstrateAccount()], + ]; + } + + /** + * Get the mutation's validation rules. + */ + protected function rulesWithValidation(array $args): array + { + return [ + 'collectionId' => ['exists:collections,collection_chain_id'], + 'signingAccount' => '' === Arr::get($args, 'signingAccount') ? ['filled'] : ['nullable', 'bail', new ValidSubstrateAccount(), new IsManagedWallet()], + ...$this->getTokenFieldRulesExist('params'), + ]; + } + + /** + * Get the mutation's validation rules without DB rules. + */ + protected function rulesWithoutValidation(array $args): array + { + return [ + 'signingAccount' => '' === Arr::get($args, 'signingAccount') ? ['filled'] : ['nullable', 'bail', new ValidSubstrateAccount()], + ...$this->getTokenFieldRules('params'), + ]; + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Mutations/ThawMutation.php b/src/GraphQL/Schemas/Primary/Substrate/Mutations/ThawMutation.php new file mode 100644 index 00000000..153e64cc --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Mutations/ThawMutation.php @@ -0,0 +1,147 @@ + 'Thaw', + 'description' => __('enjin-platform::mutation.thaw.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Transaction!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + 'freezeType' => [ + 'type' => GraphQL::type('FreezeType!'), + 'description' => __('enjin-platform::mutation.thaw.args.freezeType'), + ], + 'collectionId' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::mutation.thaw.args.collectionId'), + ], + ...$this->getTokenFields(__('enjin-platform::mutation.thaw.args.tokenId'), true), + 'collectionAccount' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::mutation.thaw.description'), + ], + 'tokenAccount' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::mutation.thaw.args.tokenAccount'), + ], + ...$this->getIdempotencyField(), + ...$this->getSkipValidationField(), + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + Substrate $blockchainService, + SerializationServiceInterface $serializationService, + TransactionService $transactionService + ): mixed { + $params = $blockchainService->getFreezeOrThawParams($args); + $encodedData = $serializationService->encode($this->getMethodName(), [ + 'collectionId' => $args['collectionId'], + 'params' => $params, + ]); + + return Transaction::lazyLoadSelectFields( + $transactionService->store([ + 'method' => $this->getMutationName(), + 'encoded_data' => $encodedData, + 'idempotency_key' => $args['idempotencyKey'] ?? Str::uuid()->toString(), + ]), + $resolveInfo + ); + } + + /** + * Get the mutation's validation rules. + */ + protected function rulesWithValidation(array $args): array + { + $freezeType = FreezeType::getEnumCase($args['freezeType']); + + return [ + 'collectionId' => ['exists:collections,collection_chain_id'], + ...( + in_array($freezeType, [FreezeType::TOKEN, FreezeType::TOKEN_ACCOUNT], true) + ? $this->getTokenFieldRulesExist(null, [], false) + : ['tokenId' => ['prohibited']] + ), + 'collectionAccount' => FreezeType::COLLECTION_ACCOUNT === $freezeType ? ['bail', 'required', new ValidSubstrateAccount(), new AccountExistsInCollection()] : ['prohibited'], + 'tokenAccount' => FreezeType::TOKEN_ACCOUNT === $freezeType ? ['bail', 'required', new ValidSubstrateAccount(), new AccountExistsInToken()] : ['prohibited'], + ]; + } + + /** + * Get the mutation's validation rules without DB rules. + */ + protected function rulesWithoutValidation(array $args): array + { + $freezeType = FreezeType::getEnumCase($args['freezeType']); + + return [ + ...( + in_array($freezeType, [FreezeType::TOKEN, FreezeType::TOKEN_ACCOUNT], true) + ? $this->getTokenFieldRules() + : ['tokenId' => ['prohibited'], 'encodeTokenId.data' => ['prohibited']] + ), + 'collectionAccount' => FreezeType::COLLECTION_ACCOUNT === $freezeType ? ['bail', 'required', new ValidSubstrateAccount()] : ['prohibited'], + 'tokenAccount' => FreezeType::TOKEN_ACCOUNT === $freezeType ? ['bail', 'required', new ValidSubstrateAccount()] : ['prohibited'], + ]; + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Mutations/TransferAllBalanceMutation.php b/src/GraphQL/Schemas/Primary/Substrate/Mutations/TransferAllBalanceMutation.php new file mode 100644 index 00000000..d868de7a --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Mutations/TransferAllBalanceMutation.php @@ -0,0 +1,140 @@ + 'TransferAllBalance', + 'description' => __('enjin-platform::mutation.transfer_all_balance.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Transaction!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + 'recipient' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.recipient'), + ], + 'keepAlive' => [ + 'type' => GraphQL::type('Boolean'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.keepAlive'), + 'defaultValue' => false, + ], + 'signingAccount' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::mutation.batch_transfer.args.signingAccount'), + ], + ...$this->getIdempotencyField(), + ...$this->getSkipValidationField(), + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + Substrate $blockchainService, + SerializationServiceInterface $serializationService, + TransactionService $transactionService, + WalletService $walletService + ): mixed { + $targetWallet = $walletService->firstOrStore(['account' => $args['recipient']]); + $signingWallet = $walletService->firstOrStore([ + 'account' => Arr::get($args, 'signingAccount') ?: config('enjin-platform.chains.daemon-account'), + ]); + + $encodedData = $serializationService->encode($this->getMethodName(), [ + $targetWallet->public_key, + $args['keepAlive'], + ]); + + return Transaction::lazyLoadSelectFields( + $transactionService->store( + [ + 'method' => $this->getMutationName(), + 'encoded_data' => $encodedData, + 'idempotency_key' => $args['idempotencyKey'] ?? Str::uuid()->toString(), + ], + signingWallet: $signingWallet + ), + $resolveInfo + ); + } + + /** + * Get the common rules. + */ + protected function rulesCommon(array $args): array + { + return [ + 'recipient' => ['filled', new ValidSubstrateAccount()], + ]; + } + + /** + * Get the mutation's validation rules. + */ + protected function rulesWithValidation(array $args): array + { + return [ + 'signingAccount' => '' === Arr::get($args, 'signingAccount') ? ['filled'] : ['nullable', 'bail', new ValidSubstrateAccount(), new IsManagedWallet()], + ]; + } + + /** + * Get the mutation's validation rules without DB rules. + */ + protected function rulesWithoutValidation(array $args): array + { + return [ + 'signingAccount' => '' === Arr::get($args, 'signingAccount') ? ['filled'] : ['nullable', 'bail', new ValidSubstrateAccount()], + ]; + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Mutations/TransferBalanceMutation.php b/src/GraphQL/Schemas/Primary/Substrate/Mutations/TransferBalanceMutation.php new file mode 100644 index 00000000..bbbd3f1e --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Mutations/TransferBalanceMutation.php @@ -0,0 +1,149 @@ + 'TransferBalance', + 'description' => __('enjin-platform::mutation.transfer_balance.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Transaction!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + 'recipient' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.recipient'), + ], + 'amount' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.amount'), + ], + 'keepAlive' => [ + 'type' => GraphQL::type('Boolean'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.keepAlive'), + 'defaultValue' => false, + ], + 'signingAccount' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::mutation.batch_transfer.args.signingAccount'), + ], + ...$this->getIdempotencyField(), + ...$this->getSkipValidationField(), + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + Substrate $blockchainService, + SerializationServiceInterface $serializationService, + TransactionService $transactionService, + WalletService $walletService + ): mixed { + $targetWallet = $walletService->firstOrStore(['account' => $args['recipient']]); + $signingWallet = $walletService->firstOrStore([ + 'account' => Arr::get($args, 'signingAccount') ?: config('enjin-platform.chains.daemon-account'), + ]); + + $method = $this->getMethodName() . ($args['keepAlive'] ? 'KeepAlive' : ''); + $encodedData = $serializationService->encode($method, [ + $targetWallet->public_key, + $args['amount'], + ]); + + return Transaction::lazyLoadSelectFields( + $transactionService->store( + [ + 'method' => $this->getMutationName(), + 'encoded_data' => $encodedData, + 'idempotency_key' => $args['idempotencyKey'] ?? Str::uuid()->toString(), + ], + signingWallet: $signingWallet + ), + $resolveInfo + ); + } + + /** + * Get the common rules. + */ + protected function rulesCommon(array $args): array + { + return [ + 'recipient' => ['filled', new ValidSubstrateAccount()], + 'amount' => [new MinBigInt(1), new MaxBigInt(Hex::MAX_UINT128)], + ]; + } + + /** + * Get the mutation's validation rules. + */ + protected function rulesWithValidation(array $args): array + { + return [ + 'signingAccount' => '' === Arr::get($args, 'signingAccount') ? ['filled'] : ['nullable', 'bail', new ValidSubstrateAccount(), new IsManagedWallet()], + ]; + } + + /** + * Get the mutation's validation rules without DB rules. + */ + protected function rulesWithoutValidation(array $args): array + { + return [ + 'signingAccount' => '' === Arr::get($args, 'signingAccount') ? ['filled'] : ['nullable', 'bail', new ValidSubstrateAccount()], + ]; + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Mutations/UnapproveCollectionMutation.php b/src/GraphQL/Schemas/Primary/Substrate/Mutations/UnapproveCollectionMutation.php new file mode 100644 index 00000000..acbaea48 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Mutations/UnapproveCollectionMutation.php @@ -0,0 +1,117 @@ + 'UnapproveCollection', + 'description' => __('enjin-platform::mutation.unapprove_collection.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Transaction!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + 'collectionId' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::mutation.unapprove_collection.args.collectionId'), + ], + 'operator' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::mutation.unapprove_collection.args.operator'), + ], + ...$this->getIdempotencyField(), + ...$this->getSkipValidationField(), + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + SerializationServiceInterface $serializationService, + TransactionService $transactionService, + WalletService $walletService + ): mixed { + $operatorWallet = $walletService->firstOrStore(['account' => $args['operator']]); + + $encodedData = $serializationService->encode($this->getMethodName(), [ + 'collectionId' => $args['collectionId'], + 'operator' => $operatorWallet->public_key, + ]); + + return Transaction::lazyLoadSelectFields( + $transactionService->store([ + 'method' => $this->getMutationName(), + 'encoded_data' => $encodedData, + 'idempotency_key' => $args['idempotencyKey'] ?? Str::uuid()->toString(), + ]), + $resolveInfo + ); + } + + /** + * Get the mutation's validation rules. + */ + protected function rulesWithValidation(array $args): array + { + return [ + 'collectionId' => ['exists:collections,collection_chain_id'], + 'operator' => ['bail', 'filled', new ValidSubstrateAccount(), new ApprovalExistsInCollection()], + ]; + } + + /** + * Get the mutation's validation rules without DB rules. + */ + protected function rulesWithoutValidation(array $args): array + { + return [ + 'operator' => ['bail', 'filled', new ValidSubstrateAccount()], + ]; + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Mutations/UnapproveTokenMutation.php b/src/GraphQL/Schemas/Primary/Substrate/Mutations/UnapproveTokenMutation.php new file mode 100644 index 00000000..3fbd9568 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Mutations/UnapproveTokenMutation.php @@ -0,0 +1,131 @@ + 'UnapproveToken', + 'description' => __('enjin-platform::mutation.unapprove_token.description'), + ]; + } + + /** + * Get the mutation's return type. + */ + public function type(): Type + { + return GraphQL::type('Transaction!'); + } + + /** + * Get the mutation's arguments definition. + */ + public function args(): array + { + return [ + 'collectionId' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::mutation.unapprove_token.args.collectionId'), + ], + ...$this->getTokenFields(__('enjin-platform::mutation.unapprove_token.args.tokenId')), + 'operator' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::mutation.unapprove_token.args.operator'), + ], + ...$this->getIdempotencyField(), + ...$this->getSkipValidationField(), + ]; + } + + /** + * Resolve the mutation's request. + */ + public function resolve( + $root, + array $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + SerializationServiceInterface $serializationService, + TransactionService $transactionService, + WalletService $walletService + ): mixed { + $operatorWallet = $walletService->firstOrStore(['account' => $args['operator']]); + + $encodedData = $serializationService->encode($this->getMethodName(), [ + 'collectionId' => $args['collectionId'], + 'tokenId' => $this->encodeTokenId($args), + 'operator' => $operatorWallet->public_key, + ]); + + return Transaction::lazyLoadSelectFields( + $transactionService->store([ + 'method' => $this->getMutationName(), + 'encoded_data' => $encodedData, + 'idempotency_key' => $args['idempotencyKey'] ?? Str::uuid()->toString(), + ]), + $resolveInfo + ); + } + + /** + * Get the mutation's validation rules. + */ + protected function rulesWithValidation(array $args): array + { + return [ + 'collectionId' => ['exists:collections,collection_chain_id'], + 'operator' => ['bail', 'filled', new ValidSubstrateAccount(), new ApprovalExistsInToken()], + ...$this->getTokenFieldRules( + null, + [new TokenEncodeExists()] + ), + ]; + } + + /** + * Get the mutation's validation rules without DB rules. + */ + protected function rulesWithoutValidation(array $args): array + { + return [ + 'operator' => ['bail', 'filled', new ValidSubstrateAccount()], + ...$this->getTokenFieldRules(), + ]; + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Queries/GetBlocksQuery.php b/src/GraphQL/Schemas/Primary/Substrate/Queries/GetBlocksQuery.php new file mode 100644 index 00000000..0ffec4ee --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Queries/GetBlocksQuery.php @@ -0,0 +1,84 @@ + 'GetBlocks', + ]; + + protected $middleware = [ + ResolvePage::class, + SingleFilterOnly::class, + ]; + + /** + * Get the query's return type. + */ + public function type(): Type + { + return GraphQL::paginate('Block', 'BlockConnection'); + } + + /** + * Get the query's arguments definition. + */ + public function args(): array + { + return ConnectionInput::args([ + 'numbers' => [ + 'type' => GraphQL::type('[String]'), + 'description' => __('enjin-platform::query.get_blocks.args.number'), + 'singleFilter' => true, + ], + 'hashes' => [ + 'type' => GraphQL::type('[String]'), + 'description' => __('enjin-platform::query.get_blocks.args.hashes'), + 'singleFilter' => true, + ], + ]); + } + + /** + * Resolve the query's request. + */ + public function resolve($root, array $args, $context, ResolveInfo $resolveInfo, Closure $getSelectFields): mixed + { + return Block::loadSelectFields($resolveInfo, $this->name) + ->when(!empty($args['numbers']), fn (Builder $query) => $query->whereIn('number', $args['numbers'])) + ->when(!empty($args['hashes']), fn (Builder $query) => $query->whereIn('hash', $args['hashes'])) + ->cursorPaginateWithTotalDesc('number', $args['first']); + } + + /** + * Get the validatio rules. + */ + protected function rules(array $args = []): array + { + return [ + 'numbers' => ['nullable', 'bail', 'max:100', 'distinct', new MinBigInt(), new MaxBigInt(Hex::MAX_UINT128)], + 'hashes' => ['nullable', 'bail', 'max:100', 'distinct', new ValidHex(32)], + ]; + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Queries/GetCollectionQuery.php b/src/GraphQL/Schemas/Primary/Substrate/Queries/GetCollectionQuery.php new file mode 100644 index 00000000..3e452af1 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Queries/GetCollectionQuery.php @@ -0,0 +1,60 @@ + 'GetCollection', + 'description' => __('enjin-platform::query.get_collection.description'), + ]; + } + + /** + * Get the query's return type. + */ + public function type(): Type + { + return GraphQL::type('Collection!'); + } + + /** + * Get the query's arguments definition. + */ + public function args(): array + { + return [ + 'collectionId' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::query.get_collection.args.collectionId'), + 'rules' => ['exists:collections,collection_chain_id'], + ], + ]; + } + + /** + * Resolve the query's request. + */ + public function resolve($root, $args, $context, ResolveInfo $resolveInfo, Closure $getSelectFields): mixed + { + return Collection::loadSelectFields($resolveInfo, $this->name) + ->where('collection_chain_id', $args['collectionId']) + ->first(); + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Queries/GetCollectionsQuery.php b/src/GraphQL/Schemas/Primary/Substrate/Queries/GetCollectionsQuery.php new file mode 100644 index 00000000..0ddd920e --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Queries/GetCollectionsQuery.php @@ -0,0 +1,89 @@ + 'GetCollections', + 'description' => __('enjin-platform::query.get_collections.description'), + ]; + } + + /** + * Get the query's return type. + */ + public function type(): Type + { + return GraphQL::paginate('Collection', 'CollectionConnection'); + } + + /** + * Get the query's arguments definition. + */ + public function args(): array + { + return ConnectionInput::args([ + 'collectionIds' => [ + 'type' => GraphQL::type('[BigInt]'), + 'description' => __('enjin-platform::query.get_collections.args.collectionIds'), + ], + ]); + } + + /** + * Resolve the query's request. + */ + public function resolve( + $root, + $args, + $context, + ResolveInfo $resolveInfo, + Closure $getSelectFields, + ): mixed { + $collections = Collection::loadSelectFields($resolveInfo, $this->name) + ->addSelect(DB::raw('cast(collection_chain_id as unsigned integer) as collection_id')) + ->when(!empty($args['collectionIds']), fn (Builder $query) => $query->whereIn('collection_chain_id', $args['collectionIds'])) + ->cursorPaginateWithTotalDesc('collection_id', $args['first']); + + return $collections; + } + + /** + * Get the validatio rules. + */ + protected function rules(array $args = []): array + { + return [ + 'collectionIds' => ['nullable', 'bail', 'array', 'min:0', 'max:100', 'distinct'], + 'collectionIds.*' => [new MinBigInt(2000), new MaxBigInt(Hex::MAX_UINT128)], + ]; + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Queries/GetTokenQuery.php b/src/GraphQL/Schemas/Primary/Substrate/Queries/GetTokenQuery.php new file mode 100644 index 00000000..eb937745 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Queries/GetTokenQuery.php @@ -0,0 +1,80 @@ + 'GetToken', + 'description' => __('enjin-platform::query.get_token.description'), + ]; + } + + /** + * Get the query's return type. + */ + public function type(): Type + { + return GraphQL::type('Token!'); + } + + /** + * Get the query's arguments definition. + */ + public function args(): array + { + return [ + 'collectionId' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::query.get_token.args.collectionId'), + 'rules' => ['exists:Enjin\Platform\Models\Collection,collection_chain_id'], + ], + ...$this->getTokenFields(__('enjin-platform::query.get_token.args.tokenId')), + ]; + } + + /** + * Resolve the query's request. + */ + public function resolve($root, $args, $context, ResolveInfo $resolveInfo, Closure $getSelectFields): mixed + { + return Token::loadSelectFields($resolveInfo, $this->name) + ->whereHas('collection', fn ($query) => $query->where('collection_chain_id', $args['collectionId'])) + ->where('token_chain_id', $this->encodeTokenId($args)) + ->first(); + } + + /** + * Get the validatio rules. + */ + protected function rules(array $args = []): array + { + return $this->getTokenFieldRules( + null, + [new TokenEncodeExists()] + ); + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Queries/GetTokensQuery.php b/src/GraphQL/Schemas/Primary/Substrate/Queries/GetTokensQuery.php new file mode 100644 index 00000000..ab3ad7a4 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Queries/GetTokensQuery.php @@ -0,0 +1,82 @@ + 'GetTokens', + 'description' => __('enjin-platform::query.get_tokens.description'), + ]; + } + + /** + * Get the query's return type. + */ + public function type(): Type + { + return GraphQL::paginate('Token', 'TokenConnection'); + } + + /** + * Get the query's arguments definition. + */ + public function args(): array + { + return ConnectionInput::args([ + 'collectionId' => [ + 'type' => GraphQL::type('BigInt'), + 'description' => __('enjin-platform::query.get_tokens.args.collectionId'), + 'rules' => ['required_with:tokenIds', 'exists:Enjin\Platform\Models\Collection,collection_chain_id'], + ], + 'tokenIds' => [ + 'type' => GraphQL::type('[EncodableTokenIdInput]'), + 'description' => __('enjin-platform::query.get_tokens.args.tokenIds'), + 'rules' => ['nullable', 'bail', 'array', 'min:0', 'max:100', 'distinct'], + ], + ]); + } + + /** + * Resolve the query's request. + */ + public function resolve($root, $args, $context, ResolveInfo $resolveInfo, Closure $getSelectFields): mixed + { + if (isset($args['tokenIds'])) { + $args['tokenIds'] = collect($args['tokenIds'])->map(fn ($tokenId) => $this->encodeTokenId(['tokenId' => $tokenId]))->all(); + } + + return Token::loadSelectFields($resolveInfo, $this->name) + ->when($collectionId = Arr::get($args, 'collectionId'), fn ($query) => $query->whereHas( + 'collection', + fn ($query) => $query->where('collection_chain_id', $collectionId) + )) + ->when(Arr::get($args, 'tokenIds'), fn ($query) => $query->whereIn('token_chain_id', $args['tokenIds'])) + ->cursorPaginateWithTotalDesc('collection_id', $args['first']); + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Queries/GetTransactionQuery.php b/src/GraphQL/Schemas/Primary/Substrate/Queries/GetTransactionQuery.php new file mode 100644 index 00000000..4a06e54b --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Queries/GetTransactionQuery.php @@ -0,0 +1,90 @@ + 'GetTransaction', + 'description' => __('enjin-platform::query.get_transaction.description'), + ]; + } + + /** + * Get the query's return type. + */ + public function type(): Type + { + return GraphQL::type('Transaction'); + } + + /** + * Get the query's arguments definition. + */ + public function args(): array + { + return [ + 'id' => [ + 'type' => GraphQL::type('BigInt'), + 'description' => __('enjin-platform::query.get_transaction.args.id'), + 'rules' => ['bail', 'filled', new MinBigInt(1), new MaxBigInt(Hex::MAX_UINT64)], + ], + 'transactionId' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::query.get_transaction.args.transactionId'), + 'rules' => ['bail', 'filled', new ValidSubstrateTransactionId()], + ], + 'transactionHash' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::query.get_transaction.args.transactionHash'), + 'rules' => ['bail', 'filled', new ValidHex(32)], + ], + 'idempotencyKey' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::query.get_transaction.args.idempotencyKey'), + 'rules' => ['bail', 'filled', 'min:36', 'max:255'], + ], + ]; + } + + /** + * Resolve the query's request. + */ + public function resolve($root, array $args, $context, ResolveInfo $resolveInfo, Closure $getSelectFields): mixed + { + return Transaction::loadSelectFields($resolveInfo, $this->name) + ->when(Arr::get($args, 'id'), fn (Builder $query) => $query->where('id', $args['id'])) + ->when(Arr::get($args, 'transactionId'), fn (Builder $query) => $query->where('transaction_chain_id', $args['transactionId'])) + ->when(Arr::get($args, 'transactionHash'), fn (Builder $query) => $query->where('transaction_chain_hash', $args['transactionHash'])) + ->when(Arr::get($args, 'idempotencyKey'), fn (Builder $query) => $query->where('idempotency_key', $args['idempotencyKey'])) + ->first(); + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Queries/GetTransactionsQuery.php b/src/GraphQL/Schemas/Primary/Substrate/Queries/GetTransactionsQuery.php new file mode 100644 index 00000000..a48ef659 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Queries/GetTransactionsQuery.php @@ -0,0 +1,171 @@ + 'GetTransactions', + 'description' => __('enjin-platform::query.get_transactions.description'), + ]; + } + + /** + * Get the query's return type. + */ + public function type(): Type + { + return GraphQL::paginate('Transaction', 'TransactionConnection'); + } + + /** + * Get the query's arguments definition. + */ + public function args(): array + { + return self::resolveArgs(); + } + + /** + * Resolve the query's request. + */ + public function resolve($root, array $args, $context, ResolveInfo $resolveInfo, Closure $getSelectFields): mixed + { + if (!empty($ids = Arr::get($args, 'ids'))) { + return Transaction::loadSelectFields($resolveInfo, 'GetTransactions') + ->whereIn('id', $ids) + ->cursorPaginateWithTotalDesc('id', $args['first']); + } + + if (!empty($transactionIds = Arr::get($args, 'transactionIds'))) { + return Transaction::loadSelectFields($resolveInfo, 'GetTransactions') + ->whereIn('transaction_chain_id', $transactionIds) + ->cursorPaginateWithTotalDesc('id', $args['first']); + } + + return Transaction::loadSelectFields($resolveInfo, 'GetTransactions') + ->when(!empty($args['accounts']), function (Builder $query) use ($args) { + $publicKeys = array_map(fn ($wallet) => SS58Address::getPublicKey($wallet), $args['accounts']); + + return $query->whereIn('wallet_public_key', $publicKeys); + }) + ->when(!empty($args['transactionHashes']), fn (Builder $query) => $query->whereIn('transaction_chain_hash', $args['transactionHashes'])) + ->when(!empty($args['methods']), fn (Builder $query) => $query->whereIn('method', $args['methods'])) + ->when(!empty($args['states']), fn (Builder $query) => $query->whereIn('state', $args['states'])) + ->when(!empty($args['results']), fn (Builder $query) => $query->whereIn('result', $args['results'])) + ->when(!empty($args['signedAtBlocks']), fn (Builder $query) => $query->whereIn('signed_at_block', $args['signedAtBlocks'])) + ->when(!empty($args['idempotencyKeys']), fn (Builder $query) => $query->whereIn('idempotency_key', $args['idempotencyKeys'])) + ->cursorPaginateWithTotalDesc('id', $args['first']); + } + + /** + * Generic function for arguments definition. + */ + public static function resolveArgs(): array + { + return ConnectionInput::args([ + 'ids' => [ + 'type' => GraphQL::type('[BigInt]'), + 'description' => __('enjin-platform::query.get_transactions.args.ids'), + 'singleFilter' => true, + ], + 'transactionIds' => [ + 'type' => GraphQL::type('[String]'), + 'description' => __('enjin-platform::query.get_transactions.args.transactionIds'), + 'singleFilter' => true, + ], + 'transactionHashes' => [ + 'type' => GraphQL::type('[String]'), + 'description' => __('enjin-platform::query.get_transactions.args.hashes'), + 'filter' => true, + ], + 'methods' => [ + 'type' => GraphQL::type('[TransactionMethod]'), + 'description' => __('enjin-platform::query.get_transactions.args.methods'), + 'filter' => true, + ], + 'states' => [ + 'type' => GraphQL::type('[TransactionState]'), + 'description' => __('enjin-platform::query.get_transactions.args.states'), + 'filter' => true, + ], + 'results' => [ + 'type' => GraphQL::type('[TransactionResult]'), + 'description' => __('enjin-platform::query.get_transactions.args.results'), + 'filter' => true, + ], + 'accounts' => [ + 'type' => GraphQL::type('[String]'), + 'description' => __('enjin-platform::query.get_transactions.args.accounts'), + 'filter' => true, + ], + 'signedAtBlocks' => [ + 'type' => GraphQL::type('[Int]'), + 'description' => __('enjin-platform::query.get_transactions.args.signedAtBlocks'), + 'filter' => true, + ], + 'idempotencyKeys' => [ + 'type' => GraphQL::type('[String]'), + 'description' => __('enjin-platform::query.get_transaction.args.idempotencyKey'), + 'singleFilter' => true, + ], + ]); + } + + /** + * Get the validation rules. + */ + protected function rules(array $args = []): array + { + return [ + 'ids' => ['nullable', 'bail', 'max:100', 'distinct'], + 'ids.*' => [new MinBigInt(1), new MaxBigInt(Hex::MAX_UINT64)], + 'transactionIds' => ['nullable', 'bail', 'max:100', 'distinct', new ValidSubstrateTransactionId()], + 'transactionHashes' => ['nullable', 'bail', 'max:100', 'distinct', new ValidHex(32)], + 'methods' => ['nullable', 'bail', 'distinct'], + 'states' => ['nullable', 'bail', 'distinct'], + 'results' => ['nullable', 'bail', 'distinct'], + 'eventIds' => ['nullable', 'bail', 'distinct'], + 'eventTypes' => ['nullable', 'bail', 'distinct'], + 'accounts' => ['nullable', 'bail', 'distinct', new ValidSubstrateAccount()], + 'accounts.*' => ['sometimes', new ValidSubstrateAccount()], + 'signedAtBlocks' => ['nullable', 'bail', 'max:100', 'distinct'], + 'signedAtBlocks.*' => [new MinBigInt(1), new MaxBigInt(Hex::MAX_UINT64)], + 'idempotencyKeys' => ['nullable', 'bail', 'max:100', 'distinct'], + 'idempotencyKeys.*' => ['sometimes', 'min:36', 'max:255'], + ]; + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Queries/GetWalletQuery.php b/src/GraphQL/Schemas/Primary/Substrate/Queries/GetWalletQuery.php new file mode 100644 index 00000000..900e5d63 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Queries/GetWalletQuery.php @@ -0,0 +1,91 @@ + 'GetWallet', + 'description' => __('enjin-platform::query.get_wallet.description'), + ]; + } + + /** + * Get the query's return type. + */ + public function type(): Type + { + return GraphQL::type('Wallet'); + } + + /** + * Get the query's arguments definition. + */ + public function args(): array + { + return [ + 'id' => [ + 'type' => GraphQL::type('Int'), + 'description' => __('enjin-platform::query.get_wallet.args.id'), + 'rules' => ['nullable', 'filled'], + ], + 'externalId' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::query.get_wallet.args.externalId'), + 'rules' => ['nullable', 'filled'], + ], + 'verificationId' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::query.get_wallet.args.verificationId'), + 'rules' => ['bail', 'nullable', 'filled', new ValidVerificationId()], + ], + 'account' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::query.get_wallet.args.account'), + 'rules' => ['bail', 'nullable', 'filled', new ValidSubstrateAccount()], + ], + ]; + } + + /** + * Resolve the query's request. + */ + public function resolve($root, array $args, $context, ResolveInfo $resolveInfo, Closure $getSelectFields, BlockchainServiceInterface $blockchainService): mixed + { + $wallet = Wallet::loadSelectFields($resolveInfo, $this->name) + ->when(Arr::get($args, 'id'), fn (Builder $query) => $query->where('id', $args['id'])) + ->when(Arr::get($args, 'externalId'), fn (Builder $query) => $query->where('external_id', $args['externalId'])) + ->when(Arr::get($args, 'verificationId'), fn (Builder $query) => $query->where('verification_id', $args['verificationId'])) + ->when(Arr::get($args, 'account'), fn (Builder $query) => $query->where('public_key', SS58Address::getPublicKey($args['account']))) + ->first(); + + return $blockchainService->walletWithBalanceAndNonce($wallet ?? Arr::get($args, 'account')); + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Queries/GetWalletsQuery.php b/src/GraphQL/Schemas/Primary/Substrate/Queries/GetWalletsQuery.php new file mode 100644 index 00000000..4e65fcaf --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Queries/GetWalletsQuery.php @@ -0,0 +1,129 @@ + 'GetWallets', + 'description' => __('enjin-platform::query.get_wallets.description'), + ]; + } + + /** + * Get the query's return type. + */ + public function type(): Type + { + return GraphQL::paginate('Wallet', 'WalletConnection'); + } + + /** + * Get the query's arguments definition. + */ + public function args(): array + { + return ConnectionInput::args([ + 'ids' => [ + 'type' => GraphQL::type('[Int!]'), + 'description' => __('enjin-platform::query.get_wallet.args.id'), + ], + 'externalIds' => [ + 'type' => GraphQL::type('[String!]'), + 'description' => __('enjin-platform::query.get_wallet.args.externalId'), + ], + 'verificationIds' => [ + 'type' => GraphQL::type('[String!]'), + 'description' => __('enjin-platform::query.get_wallet.args.verificationId'), + ], + 'accounts' => [ + 'type' => GraphQL::type('[String!]'), + 'description' => __('enjin-platform::query.get_wallet.args.account'), + ], + ]); + } + + /** + * Resolve the query's request. + */ + public function resolve($root, array $args, $context, ResolveInfo $resolveInfo, Closure $getSelectFields, BlockchainServiceInterface $blockchainService): mixed + { + $wallets = Wallet::loadSelectFields($resolveInfo, $this->name) + ->when($ids = Arr::get($args, 'ids'), fn (Builder $query) => $query->whereIn('id', $ids)) + ->when($externalIds = Arr::get($args, 'externalIds'), fn (Builder $query) => $query->whereIn('external_id', $externalIds)) + ->when($verificationIds = Arr::get($args, 'verificationIds'), fn (Builder $query) => $query->whereIn('verification_id', $verificationIds)) + ->when($accounts = Arr::get($args, 'accounts'), fn (Builder $query) => $query->whereIn('public_key', collect($accounts)->map(fn ($val) => SS58Address::getPublicKey($val))->toArray())) + ->cursorPaginateWithTotalDesc('id', $args['first']); + + $fields = Arr::get($resolveInfo->lookAhead()->queryPlan(), 'edges.fields.node.fields', []); + if ($wallets['total'] && (in_array('balance', $fields) || in_array('nonce', $fields))) { + $wallets['items']->each(fn ($wallet) => $blockchainService->walletWithBalanceAndNonce($wallet)); + } + + return $wallets; + } + + /** + * Get the validatio rules. + */ + protected function rules(array $args = []): array + { + return [ + 'ids' => [ + Rule::prohibitedIf(!empty($args['verificationIds']) || !empty($args['externalIds']) || !empty($args['accounts'])), + 'array', + 'min:1', + 'max:100', + ], + 'ids.*' => ['bail', new MinBigInt(), new MaxBigInt()], + 'externalIds' => [ + Rule::prohibitedIf(!empty($args['ids']) || !empty($args['verificationIds']) || !empty($args['accounts'])), + 'array', + 'min:1', + 'max:100', + ], + 'externalIds.*' => ['bail', 'filled', 'max:1000'], + 'verificationIds' => [ + Rule::prohibitedIf(!empty($args['ids']) || !empty($args['externalIds']) || !empty($args['accounts'])), + 'array', + 'min:1', + 'max:100', + ], + 'verificationIds.*' => ['bail', 'filled', new ValidVerificationId()], + 'verificationIds.*' => ['bail', 'filled', 'max:1000'], + 'accounts' => [ + Rule::prohibitedIf(!empty($args['verificationIds']) || !empty($args['externalIds']) || !empty($args['ids'])), + 'array', + 'min:1', + 'max:100', + ], + 'accounts.*' => ['bail', 'filled', 'max:255', new ValidSubstrateAccount()], + ]; + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Queries/VerifyMessageQuery.php b/src/GraphQL/Schemas/Primary/Substrate/Queries/VerifyMessageQuery.php new file mode 100644 index 00000000..3312505d --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Queries/VerifyMessageQuery.php @@ -0,0 +1,75 @@ + 'VerifyMessage', + 'description' => __('enjin-platform::query.verify_message.description'), + ]; + } + + /** + * Get the query's return type. + */ + public function type(): Type + { + return GraphQL::type('Boolean!'); + } + + /** + * Get the query's arguments definition. + */ + public function args(): array + { + return [ + 'message' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::query.verify_message.args.message'), + 'rules' => ['bail', 'filled', new ValidHex()], + ], + 'signature' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::query.verify_message.args.signature'), + 'rules' => ['bail', 'filled', new ValidHex()], + ], + 'publicKey' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::query.verify_message.args.publicKey'), + 'rules' => ['bail', 'filled', new ValidHex(32)], + ], + 'cryptoSignatureType' => [ + 'type' => GraphQL::type('CryptoSignatureType'), + 'description' => __('enjin-platform::query.verify_message.args.cryptoSignatureType'), + 'defaultValue' => CryptoSignatureType::SR25519->name, + ], + ]; + } + + /** + * Resolve the query's request. + */ + public function resolve($root, array $args, $context, ResolveInfo $resolveInfo, Closure $getSelectFields, BlockchainServiceInterface $blockchainService): mixed + { + return $blockchainService->verifyMessage($args['message'], $args['signature'], $args['publicKey'], $args['cryptoSignatureType'] ?? CryptoSignatureType::SR25519->name); + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Traits/HasEncodableTokenId.php b/src/GraphQL/Schemas/Primary/Substrate/Traits/HasEncodableTokenId.php new file mode 100644 index 00000000..5ddf4110 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Traits/HasEncodableTokenId.php @@ -0,0 +1,16 @@ +encode($args) ?? null; + } +} diff --git a/src/GraphQL/Schemas/Primary/Substrate/Traits/InPrimarySubstrateSchema.php b/src/GraphQL/Schemas/Primary/Substrate/Traits/InPrimarySubstrateSchema.php new file mode 100644 index 00000000..f6fdbd01 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Substrate/Traits/InPrimarySubstrateSchema.php @@ -0,0 +1,26 @@ + [ + 'name' => 'skipValidation', + 'description' => __('enjin-platform::mutation.args.skipValidation'), + 'type' => GraphQL::type('Boolean!'), + 'defaultValue' => false, + ], + ]; + } + + /** + * Get the validation rules. + */ + protected function rules(array $args = []): array + { + return $this->getAllRules($args); + } + + /** + * Get all validation rules. + */ + protected function getAllRules(array $args): array + { + if (!isset($args['skipValidation'])) { + throw new PlatformException(__('enjin-platform::error.missing_skip_validation')); + } + + return $args['skipValidation'] + ? [...$this->rulesCommon($args), ...$this->rulesWithoutValidation($args)] + : [...$this->rulesCommon($args), ...$this->rulesWithValidation($args)]; + } + + /** + * Get the common validation rules. + */ + protected function rulesCommon(array $args): array + { + return []; + } + + /** + * Get the rules with validation. + */ + protected function rulesWithValidation(array $args): array + { + return []; + } + + /** + * Get the rules without validation. + */ + protected function rulesWithoutValidation(array $args): array + { + return []; + } +} diff --git a/src/GraphQL/Schemas/Primary/Traits/HasTokenIdFieldArrayRules.php b/src/GraphQL/Schemas/Primary/Traits/HasTokenIdFieldArrayRules.php new file mode 100644 index 00000000..5db21540 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Traits/HasTokenIdFieldArrayRules.php @@ -0,0 +1,123 @@ +getTokenEncoderRules("{$attribute}tokenId"), + "{$attribute}tokenId" => $this->getEncodableTokenIdRules($args, $encodableTokenIdRules), + ]; + } + + /** + * Get token fields rules with exist. + */ + public function getTokenFieldRulesExist( + string $attribute, + array $args = [], + array $encodableTokenIdRules = [] + ): array { + $attribute = $attribute ? "{$attribute}." : $attribute; + + return [ + ...$this->getTokenEncoderRules("{$attribute}tokenId"), + "{$attribute}tokenId" => $this->getEncodableTokenIdRulesExist($args, $encodableTokenIdRules), + ]; + } + + /** + * Get token fields rules with doesn't exist. + */ + public function getTokenFieldRulesDoesntExist( + string $attribute, + array $args = [], + array $encodeTokenIdRules = [] + ): array { + $attribute = $attribute ? "{$attribute}." : $attribute; + + return [ + ...$this->getTokenEncoderRules("{$attribute}tokenId"), + "{$attribute}tokenId" => $this->getEncodableTokenIdRulesDoesntExist($args, $encodeTokenIdRules), + ]; + } + + /** + * Get encode token ID rules. + */ + public function getEncodableTokenIdRules(array $args = [], array $extraRules = []): NestedRules + { + return Rule::forEach(function ($value, $attribute) use ($args, $extraRules) { + return [ + 'bail', + 'filled', + new RequiredIf(Arr::get($args, str_replace('.tokenId', '', $attribute))), + ...$extraRules, + ]; + }); + } + + /** + * Get encode token ID rules with exist rule. + */ + public function getEncodableTokenIdRulesExist(array $args = [], array $extraRules = []): NestedRules + { + return Rule::forEach(function ($value, $attribute) use ($args, $extraRules) { + $rules = [ + 'bail', + 'filled', + new RequiredIf(Arr::get($args, str_replace('.tokenId', '', $attribute))), + new TokenEncodeExistInCollection(), + ...$extraRules, + ]; + + return $rules; + }); + } + + /** + * Get encodeable token rules with doesn't exist rule. + */ + public function getEncodableTokenIdRulesDoesntExist(array $args = [], array $extraRules = []): NestedRules + { + return Rule::forEach(function ($value, $attribute) use ($args, $extraRules) { + return [ + 'bail', + 'filled', + new RequiredIf(Arr::get($args, str_replace('.tokenId', '', $attribute))), + new TokenEncodeDoesNotExistInCollection(), + ...$extraRules, + ]; + }); + } + + /** + * Get token encoder validation rules. + */ + public function getTokenEncoderRules(?string $attribute) + { + $attribute = $attribute ? "{$attribute}." : ''; + $encoders = Package::getClassesThatImplementInterface(Encoder::class); + + return $encoders->mapWithKeys(fn ($encoder) => collect($encoder::getRules())->mapWithKeys(fn ($value, $key) => [$attribute . $key => $value])->all()); + } +} diff --git a/src/GraphQL/Schemas/Primary/Traits/HasTokenIdFieldRules.php b/src/GraphQL/Schemas/Primary/Traits/HasTokenIdFieldRules.php new file mode 100644 index 00000000..b1ff2d46 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Traits/HasTokenIdFieldRules.php @@ -0,0 +1,154 @@ +getTokenEncoderRules("{$attribute}tokenId"), + "{$attribute}tokenId" => $this->getEncodeTokenIdRules($attribute, $encodableTokenIdRules, true), + ]; + } + + /** + * Get token fields rule. + */ + public function getTokenFieldRules( + ?string $attribute = null, + array $encodableTokenIdRules = [], + ): array { + $attribute = $attribute ? "{$attribute}." : $attribute; + + return [ + ...$this->getTokenEncoderRules("{$attribute}tokenId"), + "{$attribute}tokenId" => $this->getEncodeTokenIdRules($attribute, $encodableTokenIdRules), + ]; + } + + /** + * Get optional token fields with exist rule. + */ + public function getOptionalTokenFieldRulesExist( + ?string $attribute = null, + array $encodableTokenIdRules = [], + ): array { + $attribute = $attribute ? "{$attribute}." : $attribute; + + return [ + ...$this->getTokenEncoderRules("{$attribute}tokenId"), + "{$attribute}tokenId" => $this->getEncodeTokenIdRuleExist($attribute, $encodableTokenIdRules, true), + ]; + } + + /** + * Get token fields with exist rule. + */ + public function getTokenFieldRulesExist( + ?string $attribute = null, + array $encodableTokenIdRules = [], + ): array { + $attribute = $attribute ? "{$attribute}." : $attribute; + + return [ + ...$this->getTokenEncoderRules("{$attribute}tokenId"), + "{$attribute}tokenId" => $this->getEncodeTokenIdRuleExist($attribute, $encodableTokenIdRules), + ]; + } + + /** + * Get token fields with doesn't exist rule. + */ + public function getTokenOptionalFieldRulesDoesntExist( + ?string $attribute = null, + array $encodableTokenIdRules = [], + ): array { + $attribute = $attribute ? "{$attribute}." : $attribute; + + return [ + ...$this->getTokenEncoderRules("{$attribute}tokenId"), + "{$attribute}tokenId" => $this->getEncodeTokenIdRuleDoesntExist($attribute, $encodableTokenIdRules, true), + ]; + } + + /** + * Get token fields with doesn't exist rule. + */ + public function getTokenFieldRulesDoesntExist( + ?string $attribute = null, + array $encodableTokenIdRules = [], + ): array { + $attribute = $attribute ? "{$attribute}." : $attribute; + + return [ + ...$this->getTokenEncoderRules("{$attribute}tokenId"), + "{$attribute}tokenId" => $this->getEncodeTokenIdRuleDoesntExist($attribute, $encodableTokenIdRules), + ]; + } + + /** + * Get token ID rules. + */ + public function getEncodeTokenIdRules(?string $attribute = null, array $extraRules = [], ?bool $isOptional = false): array + { + $rules = [ + $isOptional ? 'filled' : 'required', + ...$extraRules, + ]; + + return $rules; + } + + /** + * Get token ID rules with exist. + */ + public function getEncodeTokenIdRuleExist(?string $attribute = null, array $extraRules = [], ?bool $isOptional = false): array + { + $rules = [ + 'bail', + $isOptional ? 'filled' : 'required', + new TokenEncodeExistInCollection(), + ...$extraRules, + ]; + + return $rules; + } + + /** + * Get token ID rules with doesn't exist. + */ + public function getEncodeTokenIdRuleDoesntExist(?string $attribute = null, array $extraRules = [], ?bool $isOptional = false): array + { + $rules = [ + 'bail', + $isOptional ? 'filled' : 'required', + new TokenEncodeDoesNotExistInCollection(), + ...$extraRules, + ]; + + return $rules; + } + + /** + * Get the encodable token id rules. + */ + public function getTokenEncoderRules($attribute) + { + $encoders = Package::getClassesThatImplementInterface(Encoder::class); + + return $encoders->mapWithKeys(fn ($encoder) => collect($encoder::getRules())->mapWithKeys(fn ($value, $key) => ["{$attribute}.{$key}" => $value])->all()); + } +} diff --git a/src/GraphQL/Schemas/Primary/Traits/InPrimarySchema.php b/src/GraphQL/Schemas/Primary/Traits/InPrimarySchema.php new file mode 100644 index 00000000..7df071a2 --- /dev/null +++ b/src/GraphQL/Schemas/Primary/Traits/InPrimarySchema.php @@ -0,0 +1,26 @@ +middleware = array_merge($this->middleware, $resolverMiddleware[class_basename(static::class)] ?? []); + $this->middleware = array_merge($this->middleware, $resolverMiddleware[class_basename(get_parent_class(static::class))] ?? []); + } + + return parent::getMiddleware(); + } +} diff --git a/src/GraphQL/Types/Global/AccountRequestType.php b/src/GraphQL/Types/Global/AccountRequestType.php new file mode 100644 index 00000000..6724b3a4 --- /dev/null +++ b/src/GraphQL/Types/Global/AccountRequestType.php @@ -0,0 +1,41 @@ + 'AccountRequest', + 'description' => __('enjin-platform::type.account_request.description'), + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + 'qrCode' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::type.account_request.field.qrCode'), + ], + 'verificationId' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::type.account_request.field.verificationId'), + ], + ]; + } +} diff --git a/src/GraphQL/Types/Global/AccountVerifiedType.php b/src/GraphQL/Types/Global/AccountVerifiedType.php new file mode 100644 index 00000000..b5fb75df --- /dev/null +++ b/src/GraphQL/Types/Global/AccountVerifiedType.php @@ -0,0 +1,41 @@ + 'AccountVerified', + 'description' => __('enjin-platform::type.account_verified.description'), + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + 'verified' => [ + 'type' => GraphQL::type('Boolean!'), + 'description' => __('enjin-platform::type.account_verified.field.verified'), + ], + 'account' => [ + 'type' => GraphQL::type('Account'), + 'description' => __('enjin-platform::type.account_verified.field.account'), + ], + ]; + } +} diff --git a/src/GraphQL/Types/Global/PendingEventType.php b/src/GraphQL/Types/Global/PendingEventType.php new file mode 100644 index 00000000..ac76b170 --- /dev/null +++ b/src/GraphQL/Types/Global/PendingEventType.php @@ -0,0 +1,62 @@ + 'PendingEvent', + 'description' => __('enjin-platform::type.pending_event.description'), + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + 'id' => [ + 'type' => GraphQL::type('Int!'), + 'description' => __('enjin-platform::type.pending_event.field.id'), + ], + 'uuid' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::type.pending_event.field.uuid'), + ], + 'name' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::type.pending_event.field.name'), + ], + 'sent' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::type.pending_event.field.sent'), + ], + 'channels' => [ + 'type' => GraphQL::type('[String]'), + 'description' => __('enjin-platform::type.pending_event.field.channels'), + 'resolve' => fn ($event) => JSON::decode($event->channels), + ], + 'data' => [ + 'type' => GraphQL::type('Json!'), + 'description' => __('enjin-platform::type.pending_event.field.data'), + 'resolve' => fn ($event) => JSON::decode($event->data), + ], + ]; + } +} diff --git a/src/GraphQL/Types/Input/Substrate/AttributeInputType.php b/src/GraphQL/Types/Input/Substrate/AttributeInputType.php new file mode 100644 index 00000000..dca48fa9 --- /dev/null +++ b/src/GraphQL/Types/Input/Substrate/AttributeInputType.php @@ -0,0 +1,43 @@ + 'AttributeInput', + 'description' => __('enjin-platform::type.attribute_input.description'), + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + 'key' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.key'), + 'rules' => ['filled', 'alpha_dash'], + ], + 'value' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.value'), + 'rules' => ['filled'], + ], + ]; + } +} diff --git a/src/GraphQL/Types/Input/Substrate/BurnParamsInputType.php b/src/GraphQL/Types/Input/Substrate/BurnParamsInputType.php new file mode 100644 index 00000000..7cbfe015 --- /dev/null +++ b/src/GraphQL/Types/Input/Substrate/BurnParamsInputType.php @@ -0,0 +1,50 @@ + 'BurnParamsInput', + 'description' => __('enjin-platform::input_type.burn_params.description'), + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + ...$this->getTokenFields(__('enjin-platform::args.common.tokenId')), + 'amount' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.amount'), + ], + 'keepAlive' => [ + 'type' => GraphQL::type('Boolean'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.keepAlive'), + 'defaultValue' => false, + ], + 'removeTokenStorage' => [ + 'type' => GraphQL::type('Boolean'), + 'description' => __('enjin-platform::input_type.burn_params.field.removeTokenStorage'), + 'defaultValue' => false, + ], + ]; + } +} diff --git a/src/GraphQL/Types/Input/Substrate/CollectionMutationInputType.php b/src/GraphQL/Types/Input/Substrate/CollectionMutationInputType.php new file mode 100644 index 00000000..2ce76469 --- /dev/null +++ b/src/GraphQL/Types/Input/Substrate/CollectionMutationInputType.php @@ -0,0 +1,45 @@ + 'CollectionMutationInput', + 'description' => __('enjin-platform::input_type.collection_mutation.description'), + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + 'owner' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::input_type.collection_mutation.field.owner'), + ], + 'royalty' => [ + 'type' => GraphQL::type('MutationRoyaltyInput'), + 'description' => __('enjin-platform::input_type.collection_mutation.field.royalty'), + ], + 'explicitRoyaltyCurrencies' => [ + 'type' => GraphQL::type('[MultiTokenIdInput]'), + 'description' => __('enjin-platform::mutation.create_collection.args.explicitRoyaltyCurrencies'), + ], + ]; + } +} diff --git a/src/GraphQL/Types/Input/Substrate/CreateTokenParamsInputType.php b/src/GraphQL/Types/Input/Substrate/CreateTokenParamsInputType.php new file mode 100644 index 00000000..736d534f --- /dev/null +++ b/src/GraphQL/Types/Input/Substrate/CreateTokenParamsInputType.php @@ -0,0 +1,71 @@ + 'CreateTokenParams', + 'description' => __('enjin-platform::input_type.create_token_params.description'), + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + ...$this->getTokenFields(), + 'initialSupply' => [ + 'type' => GraphQL::type('BigInt'), + 'description' => __('enjin-platform::input_type.create_token_params.field.initialSupply'), + 'rules' => [new MinBigInt(1), new MaxBigInt(Hex::MAX_UINT128)], + 'defaultValue' => 1, + ], + 'unitPrice' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::input_type.create_token_params.field.unitPrice'), + 'rules' => [new MinTokenDeposit(), new MaxBigInt(Hex::MAX_UINT128)], + ], + 'cap' => [ + 'type' => GraphQL::type('TokenMintCap!'), + 'description' => __('enjin-platform::input_type.create_token_params.field.cap'), + ], + 'behavior' => [ + 'type' => GraphQL::type('TokenMarketBehaviorInput'), + 'description' => __('enjin-platform::input_type.token_market_behavior.description'), + 'defaultValue' => null, + ], + 'listingForbidden' => [ + 'type' => GraphQL::type('Boolean'), + 'description' => __('enjin-platform::input_type.create_token_params.field.listingForbidden'), + 'defaultValue' => false, + ], + 'attributes' => [ + 'type' => GraphQL::type('[AttributeInput]'), + 'description' => __('enjin-platform::input_type.create_token_params.field.attributes'), + 'defaultValue' => [], + 'rules' => ['nullable', 'bail', 'array', 'min:0', 'max:10', new DistinctAttributes()], + ], + ]; + } +} diff --git a/src/GraphQL/Types/Input/Substrate/EncodableTokenIdInputType.php b/src/GraphQL/Types/Input/Substrate/EncodableTokenIdInputType.php new file mode 100644 index 00000000..2caf5b51 --- /dev/null +++ b/src/GraphQL/Types/Input/Substrate/EncodableTokenIdInputType.php @@ -0,0 +1,40 @@ + 'EncodableTokenIdInput', + 'description' => __('enjin-platform::input_type.encodeable_token_id.description'), + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + $encoders = Package::getClassesThatImplementInterface(Encoder::class); + + return $encoders->mapWithKeys(fn ($encoder) => [Str::camel(Str::afterLast($encoder, '\\')) => [ + 'type' => $encoder::getType(), + 'description' => $encoder::getDescription(), + 'rules' => $encoder::getRules(), + ]])->all(); + } +} diff --git a/src/GraphQL/Types/Input/Substrate/MarketPolicyInputType.php b/src/GraphQL/Types/Input/Substrate/MarketPolicyInputType.php new file mode 100644 index 00000000..a1c13cbc --- /dev/null +++ b/src/GraphQL/Types/Input/Substrate/MarketPolicyInputType.php @@ -0,0 +1,37 @@ + 'MarketPolicy', + 'description' => __('enjin-platform::input_type.market_policy.description'), + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + 'royalty' => [ + 'type' => GraphQL::type('RoyaltyInput!'), + 'description' => __('enjin-platform::input_type.market_policy.field.royalty'), + ], + ]; + } +} diff --git a/src/GraphQL/Types/Input/Substrate/MintPolicyInputType.php b/src/GraphQL/Types/Input/Substrate/MintPolicyInputType.php new file mode 100644 index 00000000..8418efbf --- /dev/null +++ b/src/GraphQL/Types/Input/Substrate/MintPolicyInputType.php @@ -0,0 +1,50 @@ + 'MintPolicy', + 'description' => __('enjin-platform::input_type.mint_policy.description'), + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + 'maxTokenCount' => [ + 'type' => GraphQL::type('BigInt'), + 'description' => __('enjin-platform::type.collection_type.field.maxTokenCount'), + 'rules' => ['nullable', new MinBigInt(1), new MaxBigInt(Hex::MAX_UINT64)], + ], + 'maxTokenSupply' => [ + 'type' => GraphQL::type('BigInt'), + 'description' => __('enjin-platform::type.collection_type.field.maxTokenSupply'), + 'rules' => ['nullable', new MinBigInt(1), new MaxBigInt(Hex::MAX_UINT128)], + ], + 'forceSingleMint' => [ + 'type' => GraphQL::type('Boolean!'), + 'description' => __('enjin-platform::input_type.mint_policy.field.forceSingleMint'), + ], + ]; + } +} diff --git a/src/GraphQL/Types/Input/Substrate/MintRecipientInputType.php b/src/GraphQL/Types/Input/Substrate/MintRecipientInputType.php new file mode 100644 index 00000000..0906625e --- /dev/null +++ b/src/GraphQL/Types/Input/Substrate/MintRecipientInputType.php @@ -0,0 +1,45 @@ + 'MintRecipient', + 'description' => __('enjin-platform::input_type.mint_recipient.description'), + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + 'account' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::input_type.mint_recipient.field.account'), + 'rules' => ['filled', new ValidSubstrateAccount()], + ], + 'createParams' => [ + 'type' => GraphQL::type('CreateTokenParams'), + ], + 'mintParams' => [ + 'type' => GraphQL::type('MintTokenParams'), + ], + ]; + } +} diff --git a/src/GraphQL/Types/Input/Substrate/MintTokenParamsInputType.php b/src/GraphQL/Types/Input/Substrate/MintTokenParamsInputType.php new file mode 100644 index 00000000..c25d8fc4 --- /dev/null +++ b/src/GraphQL/Types/Input/Substrate/MintTokenParamsInputType.php @@ -0,0 +1,48 @@ + 'MintTokenParams', + 'description' => __('enjin-platform::input_type.mint_token_params.description'), + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + ...$this->getTokenFields(), + 'amount' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::input_type.create_token_params.field.initialSupply'), + 'rules' => [new MinBigInt(1), new MaxBigInt(Hex::MAX_UINT128)], + ], + 'unitPrice' => [ + 'type' => GraphQL::type('BigInt'), + 'description' => __('enjin-platform::input_type.mint_token_params.field.unitPrice'), + 'rules' => ['nullable', new MinBigInt(1), new MaxBigInt(Hex::MAX_UINT128)], + ], + ]; + } +} diff --git a/src/GraphQL/Types/Input/Substrate/MultiTokenIdInputType.php b/src/GraphQL/Types/Input/Substrate/MultiTokenIdInputType.php new file mode 100644 index 00000000..7f57e774 --- /dev/null +++ b/src/GraphQL/Types/Input/Substrate/MultiTokenIdInputType.php @@ -0,0 +1,44 @@ + 'MultiTokenIdInput', + 'description' => __('enjin-platform::input_type.multi_token_id.description'), + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + 'collectionId' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::input_type.multi_token_id.field.collectionId'), + 'rules' => [new MinBigInt(), new MaxBigInt(Hex::MAX_UINT128)], + ], + ...$this->getTokenFields(__('enjin-platform::input_type.multi_token_id.field.tokenId')), + ]; + } +} diff --git a/src/GraphQL/Types/Input/Substrate/MutationRoyaltyInputType.php b/src/GraphQL/Types/Input/Substrate/MutationRoyaltyInputType.php new file mode 100644 index 00000000..f6139631 --- /dev/null +++ b/src/GraphQL/Types/Input/Substrate/MutationRoyaltyInputType.php @@ -0,0 +1,45 @@ + 'MutationRoyaltyInput', + 'description' => __('enjin-platform::input_type.mutation_royalty.description'), + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + 'beneficiary' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::input_type.mutation_royalty.field.beneficiary'), + 'rules' => ['filled', new ValidSubstrateAccount()], + ], + 'percentage' => [ + 'type' => GraphQL::type('Float'), + 'description' => __('enjin-platform::input_type.mutation_royalty.field.percentage'), + 'rules' => [new ValidRoyaltyPercentage()], + ], + ]; + } +} diff --git a/src/GraphQL/Types/Input/Substrate/OperatorTransferParamsInputType.php b/src/GraphQL/Types/Input/Substrate/OperatorTransferParamsInputType.php new file mode 100644 index 00000000..be75a637 --- /dev/null +++ b/src/GraphQL/Types/Input/Substrate/OperatorTransferParamsInputType.php @@ -0,0 +1,56 @@ + 'OperatorTransferParams', + 'description' => __('enjin-platform::input_type.operator_transfer_params.description'), + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + ...$this->getTokenFields(__('enjin-platform::args.common.tokenId')), + 'source' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::input_type.operator_transfer_params.field.source'), + 'rules' => ['filled', new ValidSubstrateAccount()], + ], + 'amount' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.amount'), + 'rules' => [new MinBigInt(1), new MaxBigInt(Hex::MAX_UINT128), new MaxTokenBalance()], + ], + 'keepAlive' => [ + 'type' => GraphQL::type('Boolean'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.keepAlive'), + 'defaultValue' => false, + ], + ]; + } +} diff --git a/src/GraphQL/Types/Input/Substrate/RoyaltyInputType.php b/src/GraphQL/Types/Input/Substrate/RoyaltyInputType.php new file mode 100644 index 00000000..d02f609a --- /dev/null +++ b/src/GraphQL/Types/Input/Substrate/RoyaltyInputType.php @@ -0,0 +1,45 @@ + 'RoyaltyInput', + 'description' => __('enjin-platform::input_type.mutation_royalty.description'), + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + 'beneficiary' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::input_type.mutation_royalty.field.beneficiary'), + 'rules' => ['filled', new ValidSubstrateAccount()], + ], + 'percentage' => [ + 'type' => GraphQL::type('Float!'), + 'description' => __('enjin-platform::input_type.mutation_royalty.field.percentage'), + 'rules' => [new ValidRoyaltyPercentage()], + ], + ]; + } +} diff --git a/src/GraphQL/Types/Input/Substrate/SimpleTransferParamsInputType.php b/src/GraphQL/Types/Input/Substrate/SimpleTransferParamsInputType.php new file mode 100644 index 00000000..a63b940a --- /dev/null +++ b/src/GraphQL/Types/Input/Substrate/SimpleTransferParamsInputType.php @@ -0,0 +1,50 @@ + 'SimpleTransferParams', + 'description' => __('enjin-platform::input_type.simple_transfer_params.description'), + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + ...$this->getTokenFields(__('enjin-platform::args.common.tokenId')), + 'amount' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.amount'), + 'rules' => [new MinBigInt(1), new MaxBigInt(Hex::MAX_UINT128), new MaxTokenBalance()], + ], + 'keepAlive' => [ + 'type' => GraphQL::type('Boolean'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.keepAlive'), + 'defaultValue' => false, + ], + ]; + } +} diff --git a/src/GraphQL/Types/Input/Substrate/TokenIdEncoders/Erc1155EncoderInputType.php b/src/GraphQL/Types/Input/Substrate/TokenIdEncoders/Erc1155EncoderInputType.php new file mode 100644 index 00000000..e60469b1 --- /dev/null +++ b/src/GraphQL/Types/Input/Substrate/TokenIdEncoders/Erc1155EncoderInputType.php @@ -0,0 +1,47 @@ + 'Erc1155EncoderInput', + 'description' => __('erc1155_encoder.description'), + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + 'tokenId' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::input_type.token_id_encoder.erc1155.token_id.description'), + 'rules' => ['required', new ValidHex(8)], + ], + 'index' => [ + 'type' => GraphQL::type('BigInt'), + 'description' => __('enjin-platform::input_type.token_id_encoder.erc1155.index.description'), + 'rules' => ['sometimes', new MinBigInt(), new MaxBigInt(Hex::MAX_UINT64)], + ], + ]; + } +} diff --git a/src/GraphQL/Types/Input/Substrate/TokenMarketBehaviorInputType.php b/src/GraphQL/Types/Input/Substrate/TokenMarketBehaviorInputType.php new file mode 100644 index 00000000..695100f1 --- /dev/null +++ b/src/GraphQL/Types/Input/Substrate/TokenMarketBehaviorInputType.php @@ -0,0 +1,41 @@ + 'TokenMarketBehaviorInput', + 'description' => __('enjin-platform::input_type.token_market_behavior.description'), + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + 'hasRoyalty' => [ + 'type' => GraphQL::type('RoyaltyInput'), + 'description' => __('enjin-platform::input_type.mutation_royalty.field.beneficiary'), + ], + 'isCurrency' => [ + 'type' => GraphQL::type('Boolean'), + 'description' => __('enjin-platform::input_type.mutation_royalty.field.isCurrency'), + ], + ]; + } +} diff --git a/src/GraphQL/Types/Input/Substrate/TokenMintCapInputType.php b/src/GraphQL/Types/Input/Substrate/TokenMintCapInputType.php new file mode 100644 index 00000000..019ee62f --- /dev/null +++ b/src/GraphQL/Types/Input/Substrate/TokenMintCapInputType.php @@ -0,0 +1,42 @@ + 'TokenMintCap', + 'description' => __('enjin-platform::input_type.token_mint_cap.description'), + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + 'type' => [ + 'type' => GraphQL::type('TokenMintCapType!'), + 'description' => __('enjin-platform::input_type.token_mint_cap.field.type'), + ], + 'amount' => [ + 'type' => GraphQL::type('BigInt'), + 'description' => __('enjin-platform::input_type.token_mint_cap.field.amount'), + 'defaultValue' => null, + ], + ]; + } +} diff --git a/src/GraphQL/Types/Input/Substrate/TokenMutationInputType.php b/src/GraphQL/Types/Input/Substrate/TokenMutationInputType.php new file mode 100644 index 00000000..6e53e8e7 --- /dev/null +++ b/src/GraphQL/Types/Input/Substrate/TokenMutationInputType.php @@ -0,0 +1,41 @@ + 'TokenMutationInput', + 'description' => __('enjin-platform::input_type.token_mutation.description'), + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + 'behavior' => [ + 'type' => GraphQL::type('TokenMarketBehaviorInput'), + 'description' => __('enjin-platform::input_type.token_mutation.field.behavior'), + ], + 'listingForbidden' => [ + 'type' => GraphQL::type('Boolean'), + 'description' => __('enjin-platform::input_type.token_mutation.field.listingForbidden'), + ], + ]; + } +} diff --git a/src/GraphQL/Types/Input/Substrate/Traits/HasIdempotencyField.php b/src/GraphQL/Types/Input/Substrate/Traits/HasIdempotencyField.php new file mode 100644 index 00000000..fc7c2a97 --- /dev/null +++ b/src/GraphQL/Types/Input/Substrate/Traits/HasIdempotencyField.php @@ -0,0 +1,25 @@ + GraphQL::type('String'), + 'description' => $idempotencyKeyDesc ?: __('enjin-platform::args.idempotencyKey'), + 'rules' => ['filled', 'min:36', 'max:255'], + ]; + + return [ + 'idempotencyKey' => $idempotencyKeyType, + ]; + } +} diff --git a/src/GraphQL/Types/Input/Substrate/Traits/HasTokenIdFields.php b/src/GraphQL/Types/Input/Substrate/Traits/HasTokenIdFields.php new file mode 100644 index 00000000..8c5cbc7f --- /dev/null +++ b/src/GraphQL/Types/Input/Substrate/Traits/HasTokenIdFields.php @@ -0,0 +1,26 @@ + GraphQL::type('EncodableTokenIdInput' . ($isOptional ? '' : '!')), + 'description' => $tokenIdDesc ?: __('enjin-platform::args.tokenId'), + 'rules' => ['filled'], + ]; + + return [ + 'tokenId' => $tokenIdType, + ]; + } +} diff --git a/src/GraphQL/Types/Input/Substrate/TransferRecipientInputType.php b/src/GraphQL/Types/Input/Substrate/TransferRecipientInputType.php new file mode 100644 index 00000000..97ef09ca --- /dev/null +++ b/src/GraphQL/Types/Input/Substrate/TransferRecipientInputType.php @@ -0,0 +1,45 @@ + 'TransferRecipient', + 'description' => __('enjin-platform::input_type.transfer_recipient.description'), + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + 'account' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::input_type.mint_recipient.field.account'), + 'rules' => ['filled', new ValidSubstrateAccount()], + ], + 'simpleParams' => [ + 'type' => GraphQL::type('SimpleTransferParams'), + ], + 'operatorParams' => [ + 'type' => GraphQL::type('OperatorTransferParams'), + ], + ]; + } +} diff --git a/src/GraphQL/Types/Pagination/ConnectionInput.php b/src/GraphQL/Types/Pagination/ConnectionInput.php new file mode 100644 index 00000000..4c84a303 --- /dev/null +++ b/src/GraphQL/Types/Pagination/ConnectionInput.php @@ -0,0 +1,31 @@ + [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::connection_input.args.after'), + 'rules' => ['max:255'], + 'defaultValue' => null, + ], + 'first' => [ + 'type' => GraphQL::type('Int'), + 'description' => __('enjin-platform::connection_input.args.first'), + 'rules' => ['nullable', 'integer', 'min:1', 'max:500'], + 'defaultValue' => config('enjin-platform.pagination.limit'), + ], + ]); + } +} diff --git a/src/GraphQL/Types/Pagination/ConnectionType.php b/src/GraphQL/Types/Pagination/ConnectionType.php new file mode 100644 index 00000000..f7843005 --- /dev/null +++ b/src/GraphQL/Types/Pagination/ConnectionType.php @@ -0,0 +1,79 @@ + $customTypeName, + 'fields' => $this->getConnectionFields($typeName), + ]; + + $underlyingType = GraphQL::type($typeName); + + if (isset($underlyingType->config['model'])) { + $config['model'] = $underlyingType->config['model']; + } + + parent::__construct($config); + } + + /** + * Resolve the wrap type. + */ + protected function getConnectionFields(string $typeName): array + { + return [ + 'edges' => [ + 'type' => Type::nonNull( + Type::listOf( + GraphQL::wrapType( + $typeName, + $typeName . 'Edge', + EdgeType::class + ) + ) + ), + 'resolve' => function ($data): Collection { + return $data['items']->getCollection()->map(fn ($item) => [ + 'cursor' => $data['items']->getCursorForItem($item)?->encode() ?? '', + 'node' => $item, + ]); + }, + ], + 'pageInfo' => [ + 'type' => GraphQL::type('PageInfo!'), + 'resolve' => function ($data): array { + return [ + 'hasNextPage' => $data['items']->hasMorePages(), + 'hasPreviousPage' => !$data['items']->onFirstPage(), + 'startCursor' => $data['items']->cursor()?->encode() ?? '', + 'endCursor' => $data['items']->nextCursor()?->encode() ?? '', + ]; + }, + ], + 'totalCount' => [ + 'type' => GraphQL::type('Int!'), + 'resolve' => function ($data): int { + $edgesCount = $data['items']->count(); + $total = Arr::get($data, 'total', 0); + + return max($total, $edgesCount); + }, + ], + ]; + } +} diff --git a/src/GraphQL/Types/Pagination/EdgeType.php b/src/GraphQL/Types/Pagination/EdgeType.php new file mode 100644 index 00000000..8bd6a3a8 --- /dev/null +++ b/src/GraphQL/Types/Pagination/EdgeType.php @@ -0,0 +1,46 @@ + $customTypeName, + 'fields' => $this->getEdgeFields($typeName), + ]; + + $underlyingType = GraphQL::type($typeName); + + if (isset($underlyingType->config['model'])) { + $config['model'] = $underlyingType->config['model']; + } + + parent::__construct($config); + } + + /** + * Resolve the wrap type. + */ + protected function getEdgeFields(string $typeName): array + { + return [ + 'node' => [ + 'type' => GraphQL::type("{$typeName}!"), + 'description' => __('enjin-platform::type.edge.field.node'), + ], + 'cursor' => [ + 'type' => GraphQL::type('String!'), + ], + ]; + } +} diff --git a/src/GraphQL/Types/Pagination/PageInfoType.php b/src/GraphQL/Types/Pagination/PageInfoType.php new file mode 100644 index 00000000..47fac73c --- /dev/null +++ b/src/GraphQL/Types/Pagination/PageInfoType.php @@ -0,0 +1,52 @@ + 'PageInfo', + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + 'hasNextPage' => [ + 'type' => GraphQL::type('Boolean!'), + 'description' => __('enjin-platform::type.page_info.field.hasNextPage'), + 'selectable' => false, + ], + 'hasPreviousPage' => [ + 'type' => GraphQL::type('Boolean!'), + 'description' => __('enjin-platform::type.page_info.field.hasPreviousPage'), + 'selectable' => false, + ], + 'startCursor' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::type.page_info.field.startCursor'), + 'selectable' => false, + ], + 'endCursor' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::type.page_info.field.endCursor'), + 'selectable' => false, + ], + ]; + } +} diff --git a/src/GraphQL/Types/Scalars/BigIntType.php b/src/GraphQL/Types/Scalars/BigIntType.php new file mode 100644 index 00000000..a79ce527 --- /dev/null +++ b/src/GraphQL/Types/Scalars/BigIntType.php @@ -0,0 +1,87 @@ + __('enjin-platform::type.big_int.description')]); + } + + /** + * Serializes an internal value to include in a response. + */ + public function serialize($value) + { + return $value; + } + + /** + * Parses an externally provided value (query variable) to use as an input. + * + * @param mixed $value + */ + public function parseValue($value) + { + if (!$this->isValid($value)) { + throw new Error(__('enjin-platform::error.cannot_represent_uint256', ['value' => Utils::printSafeJson($value)])); + } + + return $value; + } + + /** + * Parses an externally provided literal value (hardcoded in GraphQL query) to use as an input. + * + * @param mixed $valueNode + */ + public function parseLiteral($valueNode, ?array $variables = null) + { + if (!in_array($valueNode->kind, ['IntValue', 'StringValue']) || !$this->isValid($valueNode->value)) { + throw new Error(__('enjin-platform::error.not_valid_uint256'), [$valueNode]); + } + + return $valueNode->value; + } + + /** + * Validate is numeric and within uint256 range. + */ + public function isValid($value): bool + { + if (!is_numeric($value) || Str::contains($value, ['e', 'E']) || !preg_match(static::REGEX, $value)) { + return false; + } + + return bccomp($value, self::MIN_UINT) >= 0 && bccomp($value, self::MAX_UINT) <= 0; + } + + /** + * Self instance. + */ + public function toType(): Type + { + return new static(); + } +} diff --git a/src/GraphQL/Types/Scalars/DateTimeType.php b/src/GraphQL/Types/Scalars/DateTimeType.php new file mode 100644 index 00000000..c03713fc --- /dev/null +++ b/src/GraphQL/Types/Scalars/DateTimeType.php @@ -0,0 +1,57 @@ +toIso8601String(); + } + + /** + * Parses an externally provided value (query variable) to use as an input. + */ + public function parseValue($value): string + { + if (!strtotime($value)) { + throw new Error(__('enjin-platform-beam::error.cannot_represent_datetime', ['value' => Utils::printSafeJson($value)])); + } + + return $value; + } + + /** + * Parses an externally provided literal value (hardcoded in GraphQL query) to use as an input. + */ + public function parseLiteral($valueNode, ?array $variables = null) + { + if (!strtotime($valueNode->value)) { + throw new Error(__('enjin-platform-beam::error.invalid_datetime'), [$valueNode]); + } + + return $valueNode->value; + } + + /** + * Create new type. + */ + public function toType(): Type + { + return new static(); + } +} diff --git a/src/GraphQL/Types/Scalars/IntegerRangeType.php b/src/GraphQL/Types/Scalars/IntegerRangeType.php new file mode 100644 index 00000000..b33cd247 --- /dev/null +++ b/src/GraphQL/Types/Scalars/IntegerRangeType.php @@ -0,0 +1,78 @@ + __('enjin-platform::type.integer_range.description')]); + } + + /** + * Serializes an internal value to include in a response. + */ + public function serialize($value): string + { + $result = $this->serializeValue($value); + + if (count($result) > 1) { + throw new Error(__('enjin-platform::error.not_valid_integer_range')); + } + + return $result[0]; + } + + /** + * Parses an externally provided value (query variable) to use as an input. + */ + public function parseValue($value): array + { + if (!is_string($value) || !$this->isValid($value)) { + throw new Error(__('enjin-platform::error.cannot_represent_integer_range', ['value' => Utils::printSafeJson($value)])); + } + + return $this->expandRanges(Arr::wrap($value)); + } + + /** + * Parses an externally provided literal value (hardcoded in GraphQL query) to use as an input. + */ + public function parseLiteral($valueNode, ?array $variables = null): array + { + if (!in_array($valueNode->kind, ['StringValue']) || !$this->isValid($valueNode->value)) { + throw new Error(__('enjin-platform::error.not_valid_integer_range'), [$valueNode]); + } + + return $this->expandRanges(Arr::wrap($valueNode->value)); + } + + /** + * Validate is the right format to expand into ranges. + */ + public function isValid($value): bool + { + return !$this->validateValue($value); + } + + /** + * Self instance. + */ + public function toType(): Type + { + return new static(); + } +} diff --git a/src/GraphQL/Types/Scalars/IntegerRangesArrayType.php b/src/GraphQL/Types/Scalars/IntegerRangesArrayType.php new file mode 100644 index 00000000..e9f1528c --- /dev/null +++ b/src/GraphQL/Types/Scalars/IntegerRangesArrayType.php @@ -0,0 +1,81 @@ + __('enjin-platform::type.integer_ranges_array.description')]); + } + + /** + * Serializes an internal value to include in a response. + */ + public function serialize($value): array + { + return $this->serializeValue($value); + } + + /** + * Parses an externally provided value (query variable) to use as an input. + */ + public function parseValue($value): array + { + if (!is_array($value) || !$this->isValid($value)) { + throw new Error(__('enjin-platform::error.cannot_represent_integer_ranges_array', ['value' => Utils::printSafeJson($value)])); + } + + return $this->expandRanges($value); + } + + /** + * Parses an externally provided literal value (hardcoded in GraphQL query) to use as an input. + */ + public function parseLiteral($valueNode, ?array $variables = null): array + { + if (!in_array($valueNode->kind, ['ListValue'])) { + throw new Error(__('enjin-platform::error.not_valid_integer_ranges_array'), [$valueNode]); + } + + $values = AST::valueFromAST($valueNode, Type::listOf(Type::string())); + if (!$this->isValid($values)) { + throw new Error(__('enjin-platform::error.not_valid_integer_ranges_array'), [$valueNode]); + } + + return $this->expandRanges($values); + } + + /** + * Validate is the right format to expand into ranges. + */ + public function isValid(array $value): bool + { + return collect($value) + ->sort() + ->filter(function ($range) { + return $this->validateValue($range); + })->isEmpty(); + } + + /** + * Self instance. + */ + public function toType(): Type + { + return new static(); + } +} diff --git a/src/GraphQL/Types/Scalars/JsonType.php b/src/GraphQL/Types/Scalars/JsonType.php new file mode 100644 index 00000000..2c6bd42b --- /dev/null +++ b/src/GraphQL/Types/Scalars/JsonType.php @@ -0,0 +1,104 @@ + __('enjin-platform::type.json.description')]); + } + + /** + * Serializes an internal value to include in a response. + * + * @param mixed $value + * + * @throws Error + * + * @return mixed + */ + public function serialize($value) + { + return $value; + } + + /** + * Parses an externally provided value (query variable) to use as an input. + * + * In the case of an invalid value this method must throw an Exception + * + * @param mixed $value + * + * @throws Error + * + * @return mixed + */ + public function parseValue($value) + { + return $value; + } + + /** + * Validate json data. + * + * @param mixed $data + * + * @throws Exception + * + * @return array + */ + public function decodeJson($data): array + { + $decoded = JSON::decode($data, true); + if (JSON_ERROR_NONE === json_last_error()) { + return $decoded; + } + + throw new Exception(__('enjin-platform::error.invalid_json')); + } + + /** + * Parses an externally provided literal value (hardcoded in GraphQL query) to use as an input. + * + * In the case of an invalid node or value this method must throw an Exception + * + * @param Node $valueNode + * @param mixed[]|null $variables + * + * @throws Exception + * + * @return mixed + */ + public function parseLiteral($valueNode, ?array $variables = null) + { + $current = $valueNode->loc->startToken; + + return $this->decodeJson($current->value); + } + + /** + * Create new instance. + */ + public function toType(): Type + { + return new static(); + } +} diff --git a/src/GraphQL/Types/Scalars/ObjectType.php b/src/GraphQL/Types/Scalars/ObjectType.php new file mode 100644 index 00000000..9eaa9b31 --- /dev/null +++ b/src/GraphQL/Types/Scalars/ObjectType.php @@ -0,0 +1,101 @@ +kind) { + throw new Error(__('enjin-platform::error.not_valid_object'), [$valueNode]); + } + + $data = $this->extractDataFromNodeAsCollection($valueNode); + + return JSON::decode($data->toJson()); + } + + /** + * Createt instance. + * + * @return Type + */ + public function toType(): Type + { + return new static(); + } + + /** + * A recursive function to extract the key/value pairs from an ObjectValue type and arrange them into a collection. + */ + protected function extractDataFromNodeAsCollection(mixed $node): array|Collection + { + if (isset($node->fields)) { + return collect($node->fields->getIterator()) + ->flatMap(fn ($valueNode) => $this->extractDataFromNodeAsCollection($valueNode)); + } + + if (isset($node->value->kind) && 'ListValue' === $node->value->kind) { + return [ + $node->name->value => collect($node->value->values->getIterator()) + ->map(fn ($valueNode) => $this->extractDataFromNodeAsCollection($valueNode)), + ]; + } + + return isset($node->name) ? [$node->name->value => $this->transformByKind($node)] : [$node->value]; + } + + /** + * Cast a node value into an appropriate PHP type based on its kind. + */ + protected function transformByKind(mixed $node): array|Collection|int|string|null + { + return match ($node->value->kind) { + 'IntValue' => (int) ($node->value->value), + 'ObjectValue' => $this->extractDataFromNodeAsCollection($node->value), + default => $node->value->value, + }; + } +} diff --git a/src/GraphQL/Types/Scalars/Traits/HasIntegerRanges.php b/src/GraphQL/Types/Scalars/Traits/HasIntegerRanges.php new file mode 100644 index 00000000..023f75b0 --- /dev/null +++ b/src/GraphQL/Types/Scalars/Traits/HasIntegerRanges.php @@ -0,0 +1,90 @@ +flatten() + ->map(function ($range) { + if (preg_match('/-?[0-9]+(\.\.)-?[0-9]+/', $range)) { + [$start, $end] = explode('..', $range, 2); + $range = []; + while ($start <= $end) { + $range[] = $start; + $start = bcadd($start, '1'); + } + } + + return $range; + }) + ->flatten() + ->transform(fn ($val) => (string) $val) + ->unique() + ->sort() + ->all(); + } + + protected function serializeValue($value): array + { + sort($value, SORT_NUMERIC); + $arrayCount = count($value); + $result = []; + + for ($i = 0; $i < $arrayCount; $i++) { + $currentValue = new BigInteger($value[$i]); + if ($i + 1 != $arrayCount) { + $nextValue = new BigInteger($value[$i + 1]); + } + + if (empty($start) && ($i + 1 != $arrayCount && $nextValue->equals($currentValue->add(new BigInteger(1))))) { + $start = $currentValue->toString(); + $curRange = $start . '..'; + + continue; + } + + if (!empty($start) && ($i + 1 == $arrayCount || !$nextValue->equals($currentValue->add(new BigInteger(1))))) { + $start = null; + $curRange .= $currentValue->toString(); + $result[] = $curRange; + + continue; + } + + if (empty($start)) { + $result[] = $currentValue->toString(); + } + } + + return $result; + } + + protected function validateValue($range): bool + { + if (preg_match('/-?[0-9]+(\.\.)-?[0-9]+/', $range)) { + [$start, $end] = explode('..', $range, 2); + if (!is_numeric($start) || !is_numeric($end)) { + return true; + } + + $start = new BigInteger($start); + $end = new BigInteger($end); + if ($start->compare($end) > 0) { + return true; + } + + return false; + } + + if (!is_numeric($range) || preg_match('/[^-0-9]/', $range)) { + return true; + } + + return false; + } +} diff --git a/src/GraphQL/Types/Substrate/AccountType.php b/src/GraphQL/Types/Substrate/AccountType.php new file mode 100644 index 00000000..ad735320 --- /dev/null +++ b/src/GraphQL/Types/Substrate/AccountType.php @@ -0,0 +1,42 @@ + 'Account', + 'description' => __('enjin-platform::type.account.description'), + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + // Properties + 'publicKey' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::type.account.field.publicKey'), + ], + 'address' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::type.account.field.address'), + ], + ]; + } +} diff --git a/src/GraphQL/Types/Substrate/AttributeType.php b/src/GraphQL/Types/Substrate/AttributeType.php new file mode 100644 index 00000000..135a97fa --- /dev/null +++ b/src/GraphQL/Types/Substrate/AttributeType.php @@ -0,0 +1,51 @@ + 'Attribute', + 'description' => __('enjin-platform::type.attribute.description'), + 'model' => Attribute::class, + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + // Properties + 'key' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.key'), + ], + 'value' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::mutation.batch_set_attribute.args.value'), + 'resolve' => function ($attribute) { + if (strtolower($attribute->key) == 'uri' && strpos($attribute->value, '{id}') !== false && $attribute->token_id) { + return str_replace('{id}', $attribute->token->token_chain_id, $attribute->value); + } + + return $attribute->value; + }, + ], + ]; + } +} diff --git a/src/GraphQL/Types/Substrate/BalancesType.php b/src/GraphQL/Types/Substrate/BalancesType.php new file mode 100644 index 00000000..9dc46006 --- /dev/null +++ b/src/GraphQL/Types/Substrate/BalancesType.php @@ -0,0 +1,50 @@ + 'Balances', + 'description' => __('enjin-platform::type.balances.description'), + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + // Properties + 'free' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => '', + ], + 'reserved' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => '', + ], + 'miscFrozen' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => '', + ], + 'feeFrozen' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => '', + ], + ]; + } +} diff --git a/src/GraphQL/Types/Substrate/BlockType.php b/src/GraphQL/Types/Substrate/BlockType.php new file mode 100644 index 00000000..748e348f --- /dev/null +++ b/src/GraphQL/Types/Substrate/BlockType.php @@ -0,0 +1,61 @@ + 'Block', + 'description' => __('enjin-platform::type.block.description'), + 'model' => Block::class, + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + 'id' => [ + 'type' => GraphQL::type('Int!'), + 'description' => __('enjin-platform::type.block.field.id'), + ], + 'number' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::type.block.field.number'), + ], + 'hash' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::type.block.field.hash'), + ], + 'synced' => [ + 'type' => GraphQL::type('Boolean!'), + 'description' => __('enjin-platform::type.block.field.synced'), + ], + 'failed' => [ + 'type' => GraphQL::type('Boolean!'), + 'description' => __('enjin-platform::type.block.field.failed'), + ], + 'exception' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::type.block.field.exception'), + ], + ]; + } +} diff --git a/src/GraphQL/Types/Substrate/CollectionAccountApprovalType.php b/src/GraphQL/Types/Substrate/CollectionAccountApprovalType.php new file mode 100644 index 00000000..1b5ab253 --- /dev/null +++ b/src/GraphQL/Types/Substrate/CollectionAccountApprovalType.php @@ -0,0 +1,54 @@ + 'CollectionAccountApproval', + 'description' => __('enjin-platform::type.collection_account_approval.description'), + 'model' => CollectionAccountApproval::class, + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + // Properties + 'expiration' => [ + 'type' => GraphQL::type('Int'), + 'description' => __('enjin-platform::type.collection_account_approval.field.expiration'), + ], + + // Related + 'account' => [ + 'type' => GraphQL::type('CollectionAccount!'), + 'description' => __('enjin-platform::type.collection_account_approval.field.account'), + 'is_relation' => true, + ], + 'wallet' => [ + 'type' => GraphQL::type('Wallet!'), + 'description' => __('enjin-platform::type.collection_account_approval.field.wallet'), + 'is_relation' => true, + ], + ]; + } +} diff --git a/src/GraphQL/Types/Substrate/CollectionAccountType.php b/src/GraphQL/Types/Substrate/CollectionAccountType.php new file mode 100644 index 00000000..eb638675 --- /dev/null +++ b/src/GraphQL/Types/Substrate/CollectionAccountType.php @@ -0,0 +1,65 @@ + 'CollectionAccount', + 'description' => __('enjin-platform::type.collection_account.description'), + 'model' => CollectionAccount::class, + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + // Properties + 'accountCount' => [ + 'type' => GraphQL::type('Int!'), + 'description' => __('enjin-platform::type.collection_account.field.accountCount'), + 'alias' => 'account_count', + ], + 'isFrozen' => [ + 'type' => GraphQL::type('Boolean!'), + 'description' => __('enjin-platform::type.collection_account.field.isFrozen'), + 'alias' => 'is_frozen', + ], + + // Related + 'collection' => [ + 'type' => GraphQL::type('Collection!'), + 'description' => __('enjin-platform::type.collection_account.field.collection'), + 'is_relation' => true, + ], + 'wallet' => [ + 'type' => GraphQL::type('Wallet'), + 'description' => __('enjin-platform::type.collection_account.field.wallet'), + 'is_relation' => true, + ], + 'approvals' => [ + 'type' => GraphQL::type('[CollectionAccountApproval]'), + 'description' => __('enjin-platform::type.collection_account.field.approvals'), + 'is_relation' => true, + ], + ]; + } +} diff --git a/src/GraphQL/Types/Substrate/CollectionType.php b/src/GraphQL/Types/Substrate/CollectionType.php new file mode 100644 index 00000000..55ec1767 --- /dev/null +++ b/src/GraphQL/Types/Substrate/CollectionType.php @@ -0,0 +1,134 @@ + 'Collection', + 'description' => __('enjin-platform::type.collection.description'), + 'model' => Collection::class, + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + // Properties + 'collectionId' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::type.collection_type.field.collectionId'), + 'alias' => 'collection_chain_id', + ], + 'maxTokenCount' => [ + 'type' => GraphQL::type('Int'), + 'description' => __('enjin-platform::type.collection_type.field.maxTokenCount'), + 'alias' => 'max_token_count', + ], + 'maxTokenSupply' => [ + 'type' => GraphQL::type('BigInt'), + 'description' => __('enjin-platform::type.collection_type.field.maxTokenSupply'), + 'alias' => 'max_token_supply', + ], + 'forceSingleMint' => [ + 'type' => GraphQL::type('Boolean'), + 'description' => __('enjin-platform::type.collection_type.field.forceSingleMint'), + 'alias' => 'force_single_mint', + ], + 'frozen' => [ + 'type' => GraphQL::type('Boolean'), + 'description' => __('enjin-platform::type.collection_type.field.frozen'), + 'alias' => 'is_frozen', + ], + 'royalty' => [ + 'type' => GraphQL::type('Royalty'), + 'description' => __('enjin-platform::type.collection_type.field.royalty'), + 'resolve' => function ($collection) { + if (null === $collection->royaltyBeneficiary) { + return; + } + + return [ + 'beneficiary' => $collection->royaltyBeneficiary, + 'percentage' => $collection->royalty_percentage, + ]; + }, + 'is_relation' => false, + 'selectable' => false, + 'always' => ['royalty_wallet_id', 'royalty_percentage'], + ], + 'network' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::type.collection_type.field.network'), + ], + + // Related + 'owner' => [ + 'type' => GraphQL::type('Wallet!'), + 'description' => __('enjin-platform::type.collection_type.field.owner'), + 'is_relation' => true, + ], + 'attributes' => [ + 'type' => GraphQL::type('[Attribute]'), + 'description' => __('enjin-platform::type.collection_type.field.attributes'), + 'is_relation' => true, + ], + 'accounts' => [ + 'type' => GraphQL::paginate('CollectionAccount', 'CollectionAccountConnection'), + 'description' => __('enjin-platform::type.collection_type.field.accounts'), + 'args' => ConnectionInput::args(), + 'resolve' => function ($collection, $args, $context, $info) { + return [ + 'items' => new CursorPaginator( + $collection?->accounts, + $args['first'], + Arr::get($args, 'after') ? Cursor::fromEncoded($args['after']) : null, + ['parameters'=>['id']] + ), + 'total' => (int) $collection?->accounts_count, + ]; + }, + 'is_relation' => true, + ], + 'tokens' => [ + 'type' => GraphQL::paginate('Token', 'TokenConnection'), + 'description' => __('enjin-platform::type.collection_type.field.tokens'), + 'args' => ConnectionInput::args(), + 'resolve' => function ($collection, $args) { + return [ + 'items' => new CursorPaginator( + $collection?->tokens, + $args['first'], + Arr::get($args, 'after') ? Cursor::fromEncoded($args['after']) : null, + ['parameters'=>['id']] + ), + 'total' => (int) $collection?->tokens_count, + ]; + }, + 'is_relation' => true, + ], + ]; + } +} diff --git a/src/GraphQL/Types/Substrate/EventParamType.php b/src/GraphQL/Types/Substrate/EventParamType.php new file mode 100644 index 00000000..b9e0a3cf --- /dev/null +++ b/src/GraphQL/Types/Substrate/EventParamType.php @@ -0,0 +1,41 @@ + 'EventParam', + 'description' => __('enjin-platform::type.event_param.description'), + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + 'type' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::type.event_param.field.type'), + ], + 'value' => [ + 'type' => GraphQL::type('Json'), + 'description' => __('enjin-platform::type.event_param.field.value'), + ], + ]; + } +} diff --git a/src/GraphQL/Types/Substrate/EventType.php b/src/GraphQL/Types/Substrate/EventType.php new file mode 100644 index 00000000..791dc346 --- /dev/null +++ b/src/GraphQL/Types/Substrate/EventType.php @@ -0,0 +1,64 @@ + 'Event', + 'description' => __('enjin-platform::type.event.description'), + 'model' => Event::class, + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + 'phase' => [ + 'type' => GraphQL::type('Int!'), + 'description' => __('enjin-platform::type.event.field.phase'), + ], + 'lookUp' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::type.event.field.lookUp'), + 'alias' => 'look_up', + ], + 'moduleId' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::type.event.field.moduleId'), + 'alias' => 'module_id', + ], + 'eventId' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::type.event.field.eventId'), + 'alias' => 'event_id', + ], + 'params' => [ + 'type' => GraphQL::type('[EventParam]'), + 'description' => __('enjin-platform::type.event.field.params'), + 'resolve' => function ($event) { + return JSON::decode($event['params']); + }, + ], + ]; + } +} diff --git a/src/GraphQL/Types/Substrate/RoyaltyType.php b/src/GraphQL/Types/Substrate/RoyaltyType.php new file mode 100644 index 00000000..58658805 --- /dev/null +++ b/src/GraphQL/Types/Substrate/RoyaltyType.php @@ -0,0 +1,40 @@ + 'Royalty', + 'description' => __('enjin-platform::type.royalty.description'), + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + // Properties + 'beneficiary' => [ + 'type' => GraphQL::type('Wallet!'), + ], + 'percentage' => [ + 'type' => GraphQL::type('Float!'), + ], + ]; + } +} diff --git a/src/GraphQL/Types/Substrate/TokenAccountApprovalType.php b/src/GraphQL/Types/Substrate/TokenAccountApprovalType.php new file mode 100644 index 00000000..786ad277 --- /dev/null +++ b/src/GraphQL/Types/Substrate/TokenAccountApprovalType.php @@ -0,0 +1,58 @@ + 'TokenAccountApproval', + 'description' => __('enjin-platform::type.token_account_approval.description'), + 'model' => TokenAccount::class, + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + // Properties + 'amount' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::type.token_account_approval.args.amount'), + ], + 'expiration' => [ + 'type' => GraphQL::type('Int'), + 'description' => __('enjin-platform::type.collection_account_approval.field.expiration'), + ], + + // Related + 'account' => [ + 'type' => GraphQL::type('TokenAccount!'), + 'description' => __('enjin-platform::type.collection_account_approval.field.account'), + 'is_relation' => true, + ], + 'wallet' => [ + 'type' => GraphQL::type('Wallet!'), + 'description' => __('enjin-platform::type.collection_account_approval.field.wallet'), + 'is_relation' => true, + ], + ]; + } +} diff --git a/src/GraphQL/Types/Substrate/TokenAccountNamedReserveType.php b/src/GraphQL/Types/Substrate/TokenAccountNamedReserveType.php new file mode 100644 index 00000000..9baa57ad --- /dev/null +++ b/src/GraphQL/Types/Substrate/TokenAccountNamedReserveType.php @@ -0,0 +1,46 @@ + 'TokenAccountNamedReserve', + 'description' => __('enjin-platform::type.token_account_named_reserve.description'), + 'model' => TokenAccountNamedReserve::class, + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + // Properties + 'pallet' => [ + 'type' => GraphQL::type('PalletIdentifier!'), + 'description' => __('enjin-platform::type.token_account_named_reserve.args.pallet'), + ], + 'amount' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::type.token_account_named_reserve.args.amount'), + ], + ]; + } +} diff --git a/src/GraphQL/Types/Substrate/TokenAccountType.php b/src/GraphQL/Types/Substrate/TokenAccountType.php new file mode 100644 index 00000000..59aeb296 --- /dev/null +++ b/src/GraphQL/Types/Substrate/TokenAccountType.php @@ -0,0 +1,79 @@ + 'TokenAccount', + 'description' => __('enjin-platform::type.token_account.description'), + 'model' => TokenAccount::class, + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + // Properties + 'balance' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::type.token_account.field.balance'), + ], + 'reservedBalance' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::type.token_account.field.reservedBalance'), + 'alias' => 'reserved_balance', + ], + 'isFrozen' => [ + 'type' => GraphQL::type('Boolean!'), + 'description' => __('enjin-platform::type.token_account.field.isFrozen'), + 'alias' => 'is_frozen', + ], + + // Related + 'collection' => [ + 'type' => GraphQL::type('Collection!'), + 'description' => __('enjin-platform::type.token_account.field.collection'), + 'is_relation' => true, + ], + 'wallet' => [ + 'type' => GraphQL::type('Wallet'), + 'description' => __('enjin-platform::type.token_account.field.wallet'), + 'is_relation' => true, + ], + 'token' => [ + 'type' => GraphQL::type('Token!'), + 'description' => __('enjin-platform::type.token_account.field.token'), + 'is_relation' => true, + ], + 'approvals' => [ + 'type' => GraphQL::type('[TokenAccountApproval]'), + 'description' => __('enjin-platform::type.collection_account.field.approvals'), + 'is_relation' => true, + ], + 'namedReserves' => [ + 'type' => GraphQL::type('[TokenAccountNamedReserve]'), + 'description' => __('enjin-platform::type.collection_account.field.namedReserves'), + 'is_relation' => true, + ], + ]; + } +} diff --git a/src/GraphQL/Types/Substrate/TokenType.php b/src/GraphQL/Types/Substrate/TokenType.php new file mode 100644 index 00000000..e35cf11a --- /dev/null +++ b/src/GraphQL/Types/Substrate/TokenType.php @@ -0,0 +1,148 @@ + 'Token', + 'description' => __('enjin-platform::type.token.description'), + 'model' => Token::class, + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + // Properties + 'tokenId' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::type.token.field.tokenId'), + 'alias' => 'token_chain_id', + ], + 'supply' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::type.token.field.supply'), + ], + 'cap' => [ + 'type' => GraphQL::type('TokenMintCapType'), + 'description' => __('enjin-platform::type.token.field.cap'), + ], + 'capSupply' => [ + 'type' => GraphQL::type('BigInt'), + 'description' => __('enjin-platform::type.token.field.cap'), + 'alias' => 'cap_supply', + ], + 'isFrozen' => [ + 'type' => GraphQL::type('Boolean!'), + 'description' => __('enjin-platform::type.token.field.isFrozen'), + 'alias' => 'is_frozen', + ], + 'isCurrency' => [ + 'type' => GraphQL::type('Boolean!'), + 'description' => __('enjin-platform::type.token.field.isCurrency'), + 'alias' => 'is_currency', + ], + 'royalty' => [ + 'type' => GraphQL::type('Royalty'), + 'description' => __('enjin-platform::type.token.field.royalty'), + 'resolve' => function ($token) { + if (null === $token->royaltyBeneficiary) { + return; + } + + return [ + 'beneficiary' => $token->royaltyBeneficiary, + 'percentage' => $token->royalty_percentage, + ]; + }, + 'is_relation' => false, + 'selectable' => false, + 'always' => ['royalty_wallet_id', 'royalty_percentage'], + ], + 'minimumBalance' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::type.token.field.minimumBalance'), + 'alias' => 'minimum_balance', + ], + 'unitPrice' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::type.token.field.unitPrice'), + 'alias' => 'unit_price', + ], + 'mintDeposit' => [ + 'type' => GraphQL::type('BigInt!'), + 'description' => __('enjin-platform::type.token.field.mintDeposit'), + 'alias' => 'mint_deposit', + ], + 'attributeCount' => [ + 'type' => GraphQL::type('Int!'), + 'description' => __('enjin-platform::type.token.field.attributeCount'), + 'alias' => 'attribute_count', + ], + + // Related + 'collection' => [ + 'type' => GraphQL::type('Collection!'), + 'description' => __('enjin-platform::type.token.field.collection'), + 'is_relation' => true, + ], + 'attributes' => [ + 'type' => GraphQL::type('[Attribute]'), + 'description' => __('enjin-platform::type.token.field.attributes'), + 'is_relation' => true, + ], + 'accounts' => [ + 'type' => GraphQL::paginate('TokenAccount', 'TokenAccountConnection'), + 'description' => __('enjin-platform::type.token.field.accounts'), + 'args' => ConnectionInput::args(), + 'resolve' => function ($token, $args) { + return [ + 'items' => new CursorPaginator( + $token?->accounts, + $args['first'], + Arr::get($args, 'after') ? Cursor::fromEncoded($args['after']) : null, + ['parameters'=>['id']] + ), + 'total' => (int) $token?->accounts_count, + ]; + }, + 'is_relation' => true, + ], + + // Computed + 'metadata' => [ + 'type' => GraphQL::type('Object'), + 'selectable' => false, + ], + 'nonFungible' => [ + 'type' => GraphQL::type('Boolean'), + 'description' => __('enjin-platform::type.token.field.nonFungible'), + 'alias' => 'non_fungible', + 'selectable' => false, + ], + ]; + } +} diff --git a/src/GraphQL/Types/Substrate/TransactionType.php b/src/GraphQL/Types/Substrate/TransactionType.php new file mode 100644 index 00000000..23782831 --- /dev/null +++ b/src/GraphQL/Types/Substrate/TransactionType.php @@ -0,0 +1,116 @@ + 'Transaction', + 'description' => __('enjin-platform::type.transaction.description'), + 'model' => Transaction::class, + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + 'id' => [ + 'type' => GraphQL::type('Int!'), + 'description' => __('enjin-platform::query.get_transaction.args.id'), + ], + 'transactionId' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::type.transaction.field.transactionId'), + 'alias' => 'transaction_chain_id', + ], + 'transactionHash' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::type.transaction.field.transactionHash'), + 'alias' => 'transaction_chain_hash', + ], + 'method' => [ + 'type' => GraphQL::type('TransactionMethod'), + 'description' => __('enjin-platform::type.transaction.field.method'), + ], + 'state' => [ + 'type' => GraphQL::type('TransactionState!'), + 'description' => __('enjin-platform::type.transaction.field.state'), + ], + 'result' => [ + 'type' => GraphQL::type('TransactionResult'), + 'description' => __('enjin-platform::type.transaction.field.result'), + ], + 'encodedData' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::type.transaction.field.encodedData'), + 'alias' => 'encoded_data', + ], + 'wallet' => [ + 'type' => GraphQL::type('Wallet'), + 'description' => __('enjin-platform::type.transaction.field.wallet'), + 'is_relation' => true, + ], + 'idempotencyKey' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::type.transaction.field.idempotencyKey'), + 'alias' => 'idempotency_key', + ], + 'signedAtBlock' => [ + 'type' => GraphQL::type('Int'), + 'description' => __('enjin-platform::type.transaction.field.signedAtBlock'), + 'alias' => 'signed_at_block', + ], + 'createdAt' => [ + 'type' => GraphQL::type('DateTime!'), + 'description' => __('enjin-platform::type.transaction.field.createdAt'), + 'alias' => 'created_at', + ], + 'updatedAt' => [ + 'type' => GraphQL::type('DateTime!'), + 'description' => __('enjin-platform::type.transaction.field.updatedAt'), + 'alias' => 'updated_at', + ], + + // Related + 'events' => [ + 'type' => GraphQL::paginate('Event', 'EventConnection'), + 'description' => __('enjin-platform::type.transaction.field.events'), + 'args' => ConnectionInput::args(), + 'resolve' => function ($transaction, $args) { + return [ + 'items' => new CursorPaginator( + $transaction?->events, + $args['first'], + Arr::get($args, 'after') ? Cursor::fromEncoded($args['after']) : null, + ['parameters'=>['id']] + ), + 'total' => (int) $transaction?->events_count, + ]; + }, + 'is_relation' => true, + ], + ]; + } +} diff --git a/src/GraphQL/Types/Substrate/WalletType.php b/src/GraphQL/Types/Substrate/WalletType.php new file mode 100644 index 00000000..f6ab7771 --- /dev/null +++ b/src/GraphQL/Types/Substrate/WalletType.php @@ -0,0 +1,222 @@ + 'Wallet', + 'description' => __('enjin-platform::type.wallet.description'), + 'model' => Wallet::class, + ]; + } + + /** + * Get the type's fields definition. + */ + public function fields(): array + { + return [ + // Properties + 'id' => [ + 'type' => GraphQL::type('Int!'), + 'description' => __('enjin-platform::type.wallet.field.id'), + ], + 'account' => [ + 'type' => GraphQL::type('Account'), + 'description' => __('enjin-platform::type.wallet.field.account'), + 'resolve' => function ($wallet) { + return [ + 'publicKey' => $wallet->public_key, + 'address' => $wallet->address, + ]; + }, + 'is_relation' => false, + 'selectable' => false, + 'always' => ['public_key'], + ], + 'externalId' => [ + 'type' => GraphQL::type('String'), + 'description' => __('enjin-platform::type.wallet.field.externalId'), + 'alias' => 'external_id', + ], + 'managed' => [ + 'type' => GraphQL::type('Boolean!'), + 'description' => __('enjin-platform::type.wallet.field.managed'), + ], + 'network' => [ + 'type' => GraphQL::type('String!'), + 'description' => __('enjin-platform::type.wallet.field.network'), + ], + + // Related + 'nonce' => [ + 'type' => GraphQL::type('Int'), + 'description' => __('enjin-platform::type.wallet.field.nonce'), + 'resolve' => function ($wallet) { + return $this->blockchainService->walletWithBalanceAndNonce($wallet)->nonce; + }, + 'selectable' => false, + ], + 'balances' => [ + 'type' => GraphQL::type('Balances'), + 'description' => __('enjin-platform::type.wallet.field.balances'), + 'resolve' => function ($wallet) { + return $this->blockchainService->walletWithBalanceAndNonce($wallet)->balances; + }, + 'selectable' => false, + 'is_relation' => false, + ], + 'collectionAccounts' => [ + 'type' => GraphQL::paginate('CollectionAccount', 'CollectionAccountConnection'), + 'description' => __('enjin-platform::type.wallet.field.collectionAccounts'), + 'args' => ConnectionInput::args([ + 'collectionIds' => [ + 'type' => GraphQL::type('[BigInt]'), + 'description' => __('enjin-platform::type.wallet.field.collectionIds'), + ], + ]), + 'resolve' => function ($wallet, $args) { + return [ + 'items' => new CursorPaginator( + $wallet?->collectionAccounts, + $args['first'], + Arr::get($args, 'after') ? Cursor::fromEncoded($args['after']) : null, + ['parameters'=>['id']] + ), + 'total' => (int) $wallet?->collectionAccounts_count, + ]; + }, + 'is_relation' => true, + ], + 'tokenAccounts' => [ + 'type' => GraphQL::paginate('TokenAccount', 'TokenAccountConnection'), + 'description' => __('enjin-platform::type.wallet.field.tokenAccounts'), + 'args' => ConnectionInput::args([ + 'collectionIds' => [ + 'type' => GraphQL::type('[BigInt]'), + 'description' => __('enjin-platform::query.get_tokens.args.collectionId'), + ], + 'tokenIds' => [ + 'type' => GraphQL::type('[BigInt]'), + 'description' => __('enjin-platform::query.get_tokens.args.tokenIds'), + ], + ]), + 'resolve' => function ($wallet, $args) { + return [ + 'items' => new CursorPaginator( + $wallet?->tokenAccounts, + $args['first'], + Arr::get($args, 'after') ? Cursor::fromEncoded($args['after']) : null, + ['parameters'=>['id']] + ), + 'total' => (int) $wallet?->tokenAccounts_count, + ]; + }, + 'is_relation' => true, + ], + 'collectionAccountApprovals' => [ + 'type' => GraphQL::paginate('CollectionAccountApproval', 'CollectionAccountApprovalConnection'), + 'description' => __('enjin-platform::type.wallet.field.collectionAccountApprovals'), + 'args' => ConnectionInput::args(), + 'resolve' => function ($wallet, $args) { + return [ + 'items' => new CursorPaginator( + $wallet?->collectionAccountApprovals, + $args['first'], + Arr::get($args, 'after') ? Cursor::fromEncoded($args['after']) : null, + ['parameters'=>['id']] + ), + 'total' => (int) $wallet?->collectionAccountApprovals_count, + ]; + }, + 'is_relation' => true, + ], + 'tokenAccountApprovals' => [ + 'type' => GraphQL::paginate('TokenAccountApproval', 'TokenAccountApprovalConnection'), + 'description' => __('enjin-platform::type.wallet.field.tokenAccountApprovals'), + 'args' => ConnectionInput::args(), + 'resolve' => function ($wallet, $args) { + return [ + 'items' => new CursorPaginator( + $wallet?->tokenAccountApprovals, + $args['first'], + Arr::get($args, 'after') ? Cursor::fromEncoded($args['after']) : null, + ['parameters'=>['id']] + ), + 'total' => (int) $wallet?->tokenAccountApprovals_count, + ]; + }, + 'is_relation' => true, + ], + 'transactions' => [ + 'type' => GraphQL::paginate('Transaction', 'TransactionConnection'), + 'description' => __('enjin-platform::type.wallet.field.transactions'), + 'args' => Arr::except(GetTransactionsQuery::resolveArgs(), ['accounts']), + 'resolve' => function ($wallet, array $args) { + return [ + 'items' => new CursorPaginator( + $wallet?->transactions, + $args['first'], + Arr::get($args, 'after') ? Cursor::fromEncoded($args['after']) : null, + ['parameters'=>['id']] + ), + 'total' => (int) $wallet?->transactions_count, + ]; + }, + 'is_relation' => true, + ], + 'ownedCollections' => [ + 'type' => GraphQL::paginate('Collection', 'CollectionConnection'), + 'description' => __('enjin-platform::type.wallet.field.ownedCollections'), + 'args' => ConnectionInput::args([ + 'collectionIds' => [ + 'type' => GraphQL::type('[BigInt]'), + 'description' => __('enjin-platform::type.wallet.field.collectionIds'), + ], + ]), + 'resolve' => function ($wallet, $args) { + return [ + 'items' => new CursorPaginator( + $wallet?->ownedCollections, + $args['first'], + Arr::get($args, 'after') ? Cursor::fromEncoded($args['after']) : null, + ['parameters'=>['id']] + ), + 'total' => (int) $wallet?->owned_collections_count, + ]; + }, + 'is_relation' => true, + ], + ]; + } +} diff --git a/src/GraphQL/Types/Traits/InGlobalSchema.php b/src/GraphQL/Types/Traits/InGlobalSchema.php new file mode 100644 index 00000000..f0df47fd --- /dev/null +++ b/src/GraphQL/Types/Traits/InGlobalSchema.php @@ -0,0 +1,22 @@ +isIntrospection($request, $parser)) { + return $this->translateVendorTexts($response); + } + + return $response; + } + + /** + * Translate vendor texts. + */ + protected function translateVendorTexts(JsonResponse $response): JsonResponse + { + $data = $response->getData(true); + + $this->translateSchemaTypes($data); + $this->translateSchemaDirectives($data); + + return response()->json( + $data, + 200, + config('graphql.headers', []), + config('graphql.json_encoding_options', 0) + ); + } + + /** + * Translate schema types texts. + */ + protected function translateSchemaTypes(array &$data): void + { + foreach (Arr::get($data, 'data.__schema.types', []) as $i => $type) { + if (in_array($type['kind'], ['OBJECT', 'ENUM', 'SCALAR'])) { + match ($type['name']) { + Type::INT => Arr::set($data, "data.__schema.types.{$i}.description", __('enjin-platform::scalar.int.description')), + Type::BOOLEAN => Arr::set($data, "data.__schema.types.{$i}.description", __('enjin-platform::scalar.boolean.description')), + Type::STRING => Arr::set($data, "data.__schema.types.{$i}.description", __('enjin-platform::scalar.string.description')), + Type::FLOAT => Arr::set($data, "data.__schema.types.{$i}.description", __('enjin-platform::scalar.float.description')), + Type::ID => Arr::set($data, "data.__schema.types.{$i}.description", __('enjin-platform::scalar.id.description')), + '__Schema' => Arr::set($data, "data.__schema.types.{$i}.description", __('enjin-platform::schema.description')), + '__Type' => Arr::set($data, "data.__schema.types.{$i}.description", __('enjin-platform::type.description')), + '__Directive' => Arr::set($data, "data.__schema.types.{$i}.description", __('enjin-platform::directive.description')), + '__Field' => Arr::set($data, "data.__schema.types.{$i}.description", __('enjin-platform::field.description')), + '__InputValue' => Arr::set($data, "data.__schema.types.{$i}.description", __('enjin-platform::inputvalue.description')), + '__EnumValue' => Arr::set($data, "data.__schema.types.{$i}.description", __('enjin-platform::enumvalue.description')), + '__TypeKind' => Arr::set($data, "data.__schema.types.{$i}.description", __('enjin-platform::typekind.description')), + '__DirectiveLocation' => Arr::set($data, "data.__schema.types.{$i}.description", __('enjin-platform::directivelocation.description')), + default => '' + }; + + if (isset($type['fields'])) { + foreach ($type['fields'] as $k => $field) { + match ($field['name']) { + 'queryType' => Arr::set($data, "data.__schema.types.{$i}.fields.{$k}.description", __('enjin-platform::schema.types.field.queryType')), + 'mutationType' => Arr::set($data, "data.__schema.types.{$i}.fields.{$k}.description", __('enjin-platform::schema.types.field.mutationType')), + 'subscriptionType' => Arr::set($data, "data.__schema.types.{$i}.fields.{$k}.description", __('enjin-platform::schema.types.field.subscriptionType')), + 'directives' => Arr::set($data, "data.__schema.types.{$i}.fields.{$k}.description", __('enjin-platform::schema.types.field.directives')), + 'types' => Arr::set($data, "data.__schema.types.{$i}.fields.{$k}.description", __('enjin-platform::schema.types.field.types')), + 'defaultValue' => Arr::set($data, "data.__schema.types.{$i}.fields.{$k}.description", __('enjin-platform::schema.inputvalue.field.defaultValue')), + default => '' + }; + } + } + + if (isset($type['enumValues'])) { + foreach ($type['enumValues'] as $k => $value) { + match ($value['name']) { + TypeKind::SCALAR => Arr::set($data, "data.__schema.types.{$i}.enumValues.{$k}.description", __('enjin-platform::schema.types.enumValues.SCALAR')), + TypeKind::OBJECT => Arr::set($data, "data.__schema.types.{$i}.enumValues.{$k}.description", __('enjin-platform::schema.types.enumValues.OBJECT')), + TypeKind::INTERFACE => Arr::set($data, "data.__schema.types.{$i}.enumValues.{$k}.description", __('enjin-platform::schema.types.enumValues.INTERFACE')), + TypeKind::UNION => Arr::set($data, "data.__schema.types.{$i}.enumValues.{$k}.description", __('enjin-platform::schema.types.enumValues.UNION')), + TypeKind::ENUM => Arr::set($data, "data.__schema.types.{$i}.enumValues.{$k}.description", __('enjin-platform::schema.types.enumValues.ENUM')), + TypeKind::INPUT_OBJECT => Arr::set($data, "data.__schema.types.{$i}.enumValues.{$k}.description", __('enjin-platform::schema.types.enumValues.INPUT_OBJECT')), + TypeKind::LIST => Arr::set($data, "data.__schema.types.{$i}.enumValues.{$k}.description", __('enjin-platform::schema.types.enumValues.LIST')), + TypeKind::NON_NULL => Arr::set($data, "data.__schema.types.{$i}.enumValues.{$k}.description", __('enjin-platform::schema.types.enumValues.NON_NULL')), + DirectiveLocation::QUERY => Arr::set($data, "data.__schema.types.{$i}.enumValues.{$k}.description", __('enjin-platform::schema.types.directiveLocation.QUERY')), + DirectiveLocation::MUTATION => Arr::set($data, "data.__schema.types.{$i}.enumValues.{$k}.description", __('enjin-platform::schema.types.directiveLocation.MUTATION')), + DirectiveLocation::SUBSCRIPTION => Arr::set($data, "data.__schema.types.{$i}.enumValues.{$k}.description", __('enjin-platform::schema.types.directiveLocation.SUBSCRIPTION')), + DirectiveLocation::FIELD => Arr::set($data, "data.__schema.types.{$i}.enumValues.{$k}.description", __('enjin-platform::schema.types.directiveLocation.FIELD')), + DirectiveLocation::FRAGMENT_DEFINITION => Arr::set($data, "data.__schema.types.{$i}.enumValues.{$k}.description", __('enjin-platform::schema.types.directiveLocation.FRAGMENT_DEFINITION')), + DirectiveLocation::FRAGMENT_SPREAD => Arr::set($data, "data.__schema.types.{$i}.enumValues.{$k}.description", __('enjin-platform::schema.types.directiveLocation.FRAGMENT_SPREAD')), + DirectiveLocation::INLINE_FRAGMENT => Arr::set($data, "data.__schema.types.{$i}.enumValues.{$k}.description", __('enjin-platform::schema.types.directiveLocation.INLINE_FRAGMENT')), + DirectiveLocation::VARIABLE_DEFINITION => Arr::set($data, "data.__schema.types.{$i}.enumValues.{$k}.description", __('enjin-platform::schema.types.directiveLocation.VARIABLE_DEFINITION')), + DirectiveLocation::SCHEMA => Arr::set($data, "data.__schema.types.{$i}.enumValues.{$k}.description", __('enjin-platform::schema.types.directiveLocation.SCHEMA')), + DirectiveLocation::SCALAR => Arr::set($data, "data.__schema.types.{$i}.enumValues.{$k}.description", __('enjin-platform::schema.types.directiveLocation.SCALAR')), + DirectiveLocation::OBJECT => Arr::set($data, "data.__schema.types.{$i}.enumValues.{$k}.description", __('enjin-platform::schema.types.directiveLocation.OBJECT')), + DirectiveLocation::FIELD_DEFINITION => Arr::set($data, "data.__schema.types.{$i}.enumValues.{$k}.description", __('enjin-platform::schema.types.directiveLocation.FIELD_DEFINITION')), + DirectiveLocation::ARGUMENT_DEFINITION => Arr::set($data, "data.__schema.types.{$i}.enumValues.{$k}.description", __('enjin-platform::schema.types.directiveLocation.ARGUMENT_DEFINITION')), + DirectiveLocation::IFACE => Arr::set($data, "data.__schema.types.{$i}.enumValues.{$k}.description", __('enjin-platform::schema.types.directiveLocation.INTERFACE')), + DirectiveLocation::UNION => Arr::set($data, "data.__schema.types.{$i}.enumValues.{$k}.description", __('enjin-platform::schema.types.directiveLocation.UNION')), + DirectiveLocation::ENUM => Arr::set($data, "data.__schema.types.{$i}.enumValues.{$k}.description", __('enjin-platform::schema.types.directiveLocation.ENUM')), + DirectiveLocation::ENUM_VALUE => Arr::set($data, "data.__schema.types.{$i}.enumValues.{$k}.description", __('enjin-platform::schema.types.directiveLocation.ENUM_VALUE')), + DirectiveLocation::INPUT_OBJECT => Arr::set($data, "data.__schema.types.{$i}.enumValues.{$k}.description", __('enjin-platform::schema.types.directiveLocation.INPUT_OBJECT')), + DirectiveLocation::INPUT_FIELD_DEFINITION => Arr::set($data, "data.__schema.types.{$i}.enumValues.{$k}.description", __('enjin-platform::schema.types.directiveLocation.INPUT_FIELD_DEFINITION')), + default => '' + }; + } + } + } + } + } + + /** + * Translate schema directives texts. + */ + protected function translateSchemaDirectives(array &$data): void + { + foreach (Arr::get($data, 'data.__schema.directives', []) as $i => $directive) { + match ($directive['name']) { + 'include' => Arr::set($data, "data.__schema.directives.{$i}.description", __('enjin-platform::schema.directives.include')), + 'skip' => Arr::set($data, "data.__schema.directives.{$i}.description", __('enjin-platform::schema.directives.skip')), + 'deprecated' => Arr::set($data, "data.__schema.directives.{$i}.description", __('enjin-platform::schema.directives.deprecated')), + default => '' + }; + + foreach ($directive['args'] as $k => $arg) { + match (true) { + 'if' == $arg['name'] && 'include' == $directive['name'] => Arr::set($data, "data.__schema.directives.{$i}.args.{$k}.description", __('enjin-platform::schema.directives.include.args.if')), + 'if' == $arg['name'] && 'skip' == $directive['name'] => Arr::set($data, "data.__schema.directives.{$i}.args.{$k}.description", __('enjin-platform::schema.directives.skip.args.if')), + 'reason' == $arg['name'] && 'deprecated' == $directive['name'] => Arr::set($data, "data.__schema.directives.{$i}.args.{$k}.description", __('enjin-platform::schema.directives.deprecated.reason')), + default => '' + }; + } + } + } + + /** + * Check if query is doing introspection. + */ + protected function isIntrospection(Request $request, RequestParser $parser): bool + { + if (!$requests = $parser->parseRequest($request)) { + return false; + } + + foreach (Arr::wrap($requests) as $operation) { + if (!$operation->query) { + return false; + } + if ($node = Parser::parse($operation->query)) { + if ('__schema' == $node->definitions->offsetGet(0)?->selectionSet?->selections?->offsetGet(0)?->name?->value) { + return true; + } + } + } + + return false; + } +} diff --git a/src/Http/Controllers/PlatformController.php b/src/Http/Controllers/PlatformController.php new file mode 100644 index 00000000..cd60862a --- /dev/null +++ b/src/Http/Controllers/PlatformController.php @@ -0,0 +1,56 @@ +filter(fn ($packageName) => preg_match("/^enjin\/platform-/", $packageName)); + + $packages = $installedPackages->mapWithKeys(function ($package) { + $packageName = Str::studly(Str::afterLast($package, '-')); + if ($packageName == 'Core') { + $packageClass = 'Enjin\\Platform\\Package'; + } else { + $packageClass = 'Enjin\\Platform\\' . Str::studly(Str::afterLast($package, '-')) . '\\Package'; + } + + $info = [ + 'version' => InstalledVersions::getVersion($package), + 'revision' => InstalledVersions::getReference($package), + ]; + + if (class_exists($packageClass) && !empty($routes = $packageClass::getPackageRoutes())) { + $info['routes'] = $routes; + } + + return [$package => $info]; + }); + + $platformData = [ + 'root' => 'enjin/platform-core', + 'url' => config('app.url'), + 'chain' => config('enjin-platform.chains.selected'), + 'network' => config('enjin-platform.chains.network'), + 'packages' => $packages, + ]; + + return response() + ->json($platformData, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) + ->setCache([ + 'public' => true, + 'max_age' => 10, + 's_maxage' => 60, + ]); + } +} diff --git a/src/Interfaces/PlatformBlockchainTransaction.php b/src/Interfaces/PlatformBlockchainTransaction.php new file mode 100644 index 00000000..050e1014 --- /dev/null +++ b/src/Interfaces/PlatformBlockchainTransaction.php @@ -0,0 +1,16 @@ +except = Package::getGraphQlFieldsThatImplementInterface(PlatformPublicGraphQlOperation::class)->all(); + } + + /** + * Handle an incoming request. + */ + public function handle(Request $request, Closure $next): JsonResponse|RedirectResponse|Response + { + if ($this->bypass($request) || $this->manager->authenticate($request)) { + return $next($request); + } + + return response()->json(['error' => $this->manager->getError()], 401); + } + + /** + * Handle an incoming request. + */ + protected function bypass(Request $request): bool + { + if (!$requests = $this->parser->parseRequest($request)) { + return false; + } + + foreach (Arr::wrap($requests) as $operation) { + if (!$operation->query) { + return false; + } + if ($node = Parser::parse($operation->query)) { + if (!in_array( + $node->definitions->offsetGet(0)?->selectionSet?->selections?->offsetGet(0)?->name?->value, + $this->except + )) { + return false; + } + } + } + + return true; + } +} diff --git a/src/Middlewares/UniqueFieldNamesArray.php b/src/Middlewares/UniqueFieldNamesArray.php new file mode 100644 index 00000000..bb91af31 --- /dev/null +++ b/src/Middlewares/UniqueFieldNamesArray.php @@ -0,0 +1,64 @@ +query); + $operationName = $params->operation; + $operationDefinitionNode = collect($documentNode->definitions)->where('name.value', '=', $operationName)->first(); + if (isset($operationDefinitionNode)) { + collect($operationDefinitionNode->selectionSet->selections[0]->arguments) + ->each(fn ($args) => $this->validateUniqueFieldNames(collect(Arr::wrap($args)))); + } + + if ($this->errors) { + return new ExecutionResult(null, $this->errors); + } + + return $next($schemaName, $schema, $params, $rootValue, $contextValue); + } + + protected function validateUniqueFieldNames($nodes) + { + $fieldNames = $nodes->map(fn ($node) => $node->name->value); + $duplicates = $fieldNames->duplicates(); + if ($duplicates->isNotEmpty()) { + $duplicates->each(fn ($duplicate) => $this->errors[$duplicate] = new Error(__('enjin-platform::error.there_can_only_one_input_name', ['name' => $duplicate]))); + } + + $nodes->each(function ($node) { + switch ($node->value->kind) { + case NodeKind::OBJECT: + $this->validateUniqueFieldNames(collect($node->value->fields)); + + break; + case NodeKind::LST: + collect($node->value->values->getIterator()) + ->each(fn ($value) => $this->validateUniqueFieldNames(collect($value->fields ?? []))); + + break; + } + }); + } +} diff --git a/src/Models/Attribute.php b/src/Models/Attribute.php new file mode 100644 index 00000000..34c0870d --- /dev/null +++ b/src/Models/Attribute.php @@ -0,0 +1,7 @@ + '', + ); + } +} diff --git a/src/Models/Block.php b/src/Models/Block.php new file mode 100644 index 00000000..8ac0ad71 --- /dev/null +++ b/src/Models/Block.php @@ -0,0 +1,7 @@ + + */ + public $fillable = [ + 'collection_id', + 'token_id', + 'key', + 'value', + 'created_at', + 'updated_at', + ]; + + /** + * The attributes that aren't mass assignable. + * + * @var array|bool + */ + protected $guarded = []; + + /** + * Create a new factory instance for the model. + */ + protected static function newFactory(): AttributeFactory + { + return AttributeFactory::new(); + } +} diff --git a/src/Models/Laravel/Block.php b/src/Models/Laravel/Block.php new file mode 100644 index 00000000..4f5b85e8 --- /dev/null +++ b/src/Models/Laravel/Block.php @@ -0,0 +1,49 @@ +|bool + */ + public $guarded = []; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + public $fillable = [ + 'number', + 'hash', + 'synced', + 'failed', + 'exception', + 'retried', + 'events', + 'extrinsics', + 'created_at', + 'updated_at', + ]; + + /** + * Create a new factory instance for the model. + */ + protected static function newFactory(): BlockFactory + { + return BlockFactory::new(); + } +} diff --git a/src/Models/Laravel/Collection.php b/src/Models/Laravel/Collection.php new file mode 100644 index 00000000..a81785dc --- /dev/null +++ b/src/Models/Laravel/Collection.php @@ -0,0 +1,76 @@ +|bool + */ + public $guarded = []; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'collection_chain_id', + 'owner_wallet_id', + 'max_token_count', + 'max_token_supply', + 'force_single_mint', + 'is_frozen', + 'royalty_wallet_id', + 'royalty_percentage', + 'token_count', + 'attribute_count', + 'total_deposit', + 'network', + 'created_at', + 'updated_at', + ]; + + /** + * The model's attributes. + * + * @var array + */ + protected $attributes = [ + 'force_single_mint' => false, + 'is_frozen' => false, + 'token_count' => '0', + 'attribute_count' => '0', + 'total_deposit' => '0', + ]; + + /** + * Create a new factory instance for the model. + */ + protected static function newFactory(): CollectionFactory + { + return CollectionFactory::new(); + } + + protected function pivotIdentifier(): Attribute + { + return Attribute::make( + get: fn () => $this->collection_chain_id, + ); + } +} diff --git a/src/Models/Laravel/CollectionAccount.php b/src/Models/Laravel/CollectionAccount.php new file mode 100644 index 00000000..cd0a06ea --- /dev/null +++ b/src/Models/Laravel/CollectionAccount.php @@ -0,0 +1,58 @@ +|bool + */ + public $guarded = []; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + public $fillable = [ + 'collection_id', + 'wallet_id', + 'is_frozen', + 'account_count', + 'created_at', + 'updated_at', + ]; + + /** + * The model's attributes. + * + * @var array + */ + protected $attributes = [ + 'is_frozen' => false, + 'account_count' => 0, + ]; + + /** + * Create a new factory instance for the model. + */ + protected static function newFactory(): CollectionAccountFactory + { + return CollectionAccountFactory::new(); + } +} diff --git a/src/Models/Laravel/CollectionAccountApproval.php b/src/Models/Laravel/CollectionAccountApproval.php new file mode 100644 index 00000000..9bcb4799 --- /dev/null +++ b/src/Models/Laravel/CollectionAccountApproval.php @@ -0,0 +1,45 @@ +|bool + */ + public $guarded = []; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + public $fillable = [ + 'collection_account_id', + 'wallet_id', + 'expiration', + 'created_at', + 'updated_at', + ]; + + /** + * Create a new factory instance for the model. + */ + protected static function newFactory(): CollectionAccountApprovalFactory + { + return CollectionAccountApprovalFactory::new(); + } +} diff --git a/src/Models/Laravel/CollectionRoyaltyCurrency.php b/src/Models/Laravel/CollectionRoyaltyCurrency.php new file mode 100644 index 00000000..d65a2b71 --- /dev/null +++ b/src/Models/Laravel/CollectionRoyaltyCurrency.php @@ -0,0 +1,45 @@ +|bool + */ + public $guarded = []; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + public $fillable = [ + 'collection_id', + 'currency_collection_chain_id', + 'currency_token_chain_id', + 'created_at', + 'updated_at', + ]; + + /** + * Create a new factory instance for the model. + */ + protected static function newFactory(): CollectionRoyaltyCurrencyFactory + { + return CollectionRoyaltyCurrencyFactory::new(); + } +} diff --git a/src/Models/Laravel/Event.php b/src/Models/Laravel/Event.php new file mode 100644 index 00000000..ec38e450 --- /dev/null +++ b/src/Models/Laravel/Event.php @@ -0,0 +1,55 @@ +|bool + */ + public $guarded = []; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + public $fillable = [ + 'transaction_id', + 'phase', + 'look_up', + 'module_id', + 'event_id', + 'params', + ]; + + /** + * Create a new factory instance for the model. + */ + protected static function newFactory(): EventFactory + { + return EventFactory::new(); + } +} diff --git a/src/Models/Laravel/PendingEvent.php b/src/Models/Laravel/PendingEvent.php new file mode 100644 index 00000000..03b7223c --- /dev/null +++ b/src/Models/Laravel/PendingEvent.php @@ -0,0 +1,46 @@ +|bool + */ + public $guarded = []; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + public $fillable = [ + 'uuid', + 'name', + 'sent', + 'channels', + 'data', + ]; + + protected function pivotIdentifier(): Attribute + { + return Attribute::make( + get: fn () => $this->uuid, + ); + } +} diff --git a/src/Models/Laravel/Token.php b/src/Models/Laravel/Token.php new file mode 100644 index 00000000..2d0ac1ab --- /dev/null +++ b/src/Models/Laravel/Token.php @@ -0,0 +1,156 @@ +|bool + */ + public $guarded = []; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + public $fillable = [ + 'collection_id', + 'token_chain_id', + 'supply', + 'cap', + 'cap_supply', + 'is_frozen', + 'royalty_wallet_id', + 'royalty_percentage', + 'is_currency', + 'listing_forbidden', + 'minimum_balance', + 'unit_price', + 'mint_deposit', + 'attribute_count', + 'created_at', + 'updated_at', + ]; + + /** + * The model's attributes. + * + * @var array + */ + protected $attributes = [ + 'supply' => '1', + 'is_frozen' => false, + 'is_currency' => false, + 'listing_forbidden' => false, + 'minimum_balance' => '1', + 'unit_price' => '0', + 'mint_deposit' => '0', + 'attribute_count' => 0, + ]; + + /** + * The non-fungible attribute accessor. + */ + public function nonFungible(): Attribute + { + return new Attribute( + get: fn () => $this->isNonFungible() + ); + } + + /** + * Checks if the token is non-fungible. + */ + protected function isNonFungible(): bool + { + if ($this->is_currency) { + // If the token is a currency it is fungible. + return false; + } + + if ($this->collection->max_token_supply === '1') { + // If the collection has a rule of maxTokenSupply of 1 means all tokens are NFT + return true; + } + + if ($this->collection->force_single_mint && $this->supply === '1') { + // If the collection has a rule of forceSingleMint and there is only one unit of the token means it is a NFT + return true; + } + + if ($this->cap === TokenMintCapType::SUPPLY->name) { + // If token has a cap of Supply 1, it is non-fungible. + // If the cap Supply is more than 1, it is fungible. + return $this->cap_supply === '1'; + } + + if ($this->cap === TokenMintCapType::SINGLE_MINT->name) { + // If the token is set as SingleMint and only one was minted it is non-fungible + // If more than one was minted it is fungible. + return $this->supply === '1'; + } + + // All other cases we will consider the token is fungible. + return false; + } + + /** + * The metadata attribute accessor. + */ + protected function fetchMetadata(): Attribute + { + return new Attribute( + get: fn () => $this->attributes['fetch_metadata'] ?? false, + set: function ($value) { + if ($value === true) { + $this->attributes['metadata'] = MetadataService::fetch($this->getRelation('attributes')->first()); + } + $this->attributes['fetch_metadata'] = $value; + } + ); + } + + /** + * The metadata attribute accessor. + */ + protected function metadata(): Attribute + { + return new Attribute( + get: fn () => $this->attributes['metadata'] ?? MetadataService::fetch($this->getRelation('attributes')->first()), + ); + } + + /** + * Create a new factory instance for the model. + */ + protected static function newFactory(): TokenFactory + { + return TokenFactory::new(); + } + + protected function pivotIdentifier(): Attribute + { + return Attribute::make( + get: fn () => $this->token_chain_id, + ); + } +} diff --git a/src/Models/Laravel/TokenAccount.php b/src/Models/Laravel/TokenAccount.php new file mode 100644 index 00000000..3092183b --- /dev/null +++ b/src/Models/Laravel/TokenAccount.php @@ -0,0 +1,61 @@ +|bool + */ + public $guarded = []; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + public $fillable = [ + 'wallet_id', + 'collection_id', + 'token_id', + 'balance', + 'reserved_balance', + 'is_frozen', + 'created_at', + 'updated_at', + ]; + + /** + * The model's attributes. + * + * @var array + */ + protected $attributes = [ + 'balance' => '1', + 'reserved_balance' => '0', + 'is_frozen' => false, + ]; + + /** + * Create a new factory instance for the model. + */ + protected static function newFactory(): TokenAccountFactory + { + return TokenAccountFactory::new(); + } +} diff --git a/src/Models/Laravel/TokenAccountApproval.php b/src/Models/Laravel/TokenAccountApproval.php new file mode 100644 index 00000000..0052b96c --- /dev/null +++ b/src/Models/Laravel/TokenAccountApproval.php @@ -0,0 +1,46 @@ +|bool + */ + public $guarded = []; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + public $fillable = [ + 'token_account_id', + 'wallet_id', + 'amount', + 'expiration', + 'created_at', + 'updated_at', + ]; + + /** + * Create a new factory instance for the model. + */ + protected static function newFactory(): TokenAccountApprovalFactory + { + return TokenAccountApprovalFactory::new(); + } +} diff --git a/src/Models/Laravel/TokenAccountNamedReserve.php b/src/Models/Laravel/TokenAccountNamedReserve.php new file mode 100644 index 00000000..8be52bfc --- /dev/null +++ b/src/Models/Laravel/TokenAccountNamedReserve.php @@ -0,0 +1,45 @@ +|bool + */ + public $guarded = []; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + public $fillable = [ + 'token_account_id', + 'pallet', + 'amount', + 'created_at', + 'updated_at', + ]; + + /** + * Create a new factory instance for the model. + */ + protected static function newFactory(): TokenAccountNamedReserveFactory + { + return TokenAccountNamedReserveFactory::new(); + } +} diff --git a/src/Models/Laravel/Traits/Attribute.php b/src/Models/Laravel/Traits/Attribute.php new file mode 100644 index 00000000..fd22b4e8 --- /dev/null +++ b/src/Models/Laravel/Traits/Attribute.php @@ -0,0 +1,26 @@ +belongsTo(Collection::class); + } + + /** + * The token relationship. + */ + public function token(): BelongsTo + { + return $this->belongsTo(Token::class); + } +} diff --git a/src/Models/Laravel/Traits/Collection.php b/src/Models/Laravel/Traits/Collection.php new file mode 100644 index 00000000..70a21c24 --- /dev/null +++ b/src/Models/Laravel/Traits/Collection.php @@ -0,0 +1,62 @@ +belongsTo(Wallet::class, 'owner_wallet_id'); + } + + /** + * The royalty beneficiary relationship. + */ + public function royaltyBeneficiary(): BelongsTo + { + return $this->belongsTo(Wallet::class, 'royalty_wallet_id'); + } + + /** + * The royalty currencies relationship. + */ + public function royaltyCurrencies(): HasMany + { + return $this->hasMany(CollectionRoyaltyCurrency::class); + } + + /** + * The tokens relationship. + */ + public function tokens(): HasMany + { + return $this->hasMany(Token::class); + } + + /** + * The collection account relationsip. + */ + public function accounts(): HasMany + { + return $this->hasMany(CollectionAccount::class); + } + + /** + * The attributes relationship. + */ + public function attributes(): HasMany + { + return $this->hasMany(Attribute::class, 'collection_id')->whereNull('token_id'); + } +} diff --git a/src/Models/Laravel/Traits/CollectionAccount.php b/src/Models/Laravel/Traits/CollectionAccount.php new file mode 100644 index 00000000..2808ee93 --- /dev/null +++ b/src/Models/Laravel/Traits/CollectionAccount.php @@ -0,0 +1,36 @@ +belongsTo(Collection::class); + } + + /** + * The wallet relationship. + */ + public function wallet(): BelongsTo + { + return $this->belongsTo(Wallet::class); + } + + /** + * The collection account approvals relationship. + */ + public function approvals(): HasMany + { + return $this->hasMany(CollectionAccountApproval::class); + } +} diff --git a/src/Models/Laravel/Traits/CollectionAccountApproval.php b/src/Models/Laravel/Traits/CollectionAccountApproval.php new file mode 100644 index 00000000..5e17b39c --- /dev/null +++ b/src/Models/Laravel/Traits/CollectionAccountApproval.php @@ -0,0 +1,26 @@ +belongsTo(CollectionAccount::class, 'collection_account_id'); + } + + /** + * The wallet relationship. + */ + public function wallet(): BelongsTo + { + return $this->belongsTo(Wallet::class); + } +} diff --git a/src/Models/Laravel/Traits/CollectionRoyaltyCurrency.php b/src/Models/Laravel/Traits/CollectionRoyaltyCurrency.php new file mode 100644 index 00000000..40eaedba --- /dev/null +++ b/src/Models/Laravel/Traits/CollectionRoyaltyCurrency.php @@ -0,0 +1,27 @@ +belongsTo(Collection::class); + } + + /** + * The currency relationship. + */ + public function currency(): HasOne + { + return $this->hasOne(Token::class); + } +} diff --git a/src/Models/Laravel/Traits/EagerLoadSelectFields.php b/src/Models/Laravel/Traits/EagerLoadSelectFields.php new file mode 100644 index 00000000..b16334f0 --- /dev/null +++ b/src/Models/Laravel/Traits/EagerLoadSelectFields.php @@ -0,0 +1,689 @@ +load($with)->loadCount($withCount); + } + + /** + * Load collection's select and relationship fields. + */ + public static function loadCollection( + array $selections, + string $attribute, + array $args = [], + ?string $key = null, + bool $isParent = false + ): array { + $fields = static::$query == 'GetTokens' && !isset($selections['collection']) + ? [] + : Arr::get($selections, $attribute, $selections); + $hasBeneficiary = (bool) Arr::get($fields, 'royalty.fields.beneficiary'); + $select = array_filter([ + 'id', + 'max_token_supply', + 'force_single_mint', + isset($fields['owner']) || static::$query == 'GetWallet' ? 'owner_wallet_id' : null, + $hasBeneficiary ? 'royalty_wallet_id' : null, + Arr::get($fields, 'royalty.fields.percentage') ? 'royalty_percentage' : null, + ...CollectionType::getSelectFields($fieldKeys = array_keys($fields)), + ]); + + $with = []; + $withCount = []; + + if (!$isParent) { + $with = [ + $key => function ($query) use ($select, $args) { + $query->select(array_unique($select)) + ->when(Arr::get($args, 'after'), fn ($q) => $q->where('id', '>', Cursor::fromEncoded($args['after'])->parameter('id'))) + ->when(Arr::get($args, 'collectionIds'), fn ($q) => $q->whereIn('collection_chain_id', $args['collectionIds'])); + // This must be done this way to load eager limit correctly. + if ($limit = Arr::get($args, 'first')) { + $query->limit($limit + 1); + } + }, + ]; + } + + foreach ([ + ...CollectionType::getRelationFields($fieldKeys), + ...($hasBeneficiary ? ['royaltyBeneficiary'] : []), + ] as $relation) { + if ($isParent && in_array($relation, ['tokens', 'accounts'])) { + $withCount[] = $relation; + } + + $with = array_merge( + $with, + static::getRelationQuery( + CollectionType::class, + $relation, + $fields, + $key, + $with + ) + ); + } + + return [$select, $with, $withCount]; + } + + /** + * Load token's select and relationship fields. + */ + public static function loadToken( + array $selections, + string $attribute, + array $args = [], + ?string $key = null, + bool $isParent = false + ): array { + $fields = Arr::get($selections, $attribute, $selections); + $hasBeneficiary = (bool) Arr::get($fields, 'royalty.fields.beneficiary'); + $select = array_filter([ + 'id', + 'collection_id', + ...(isset($fields['nonFungible']) ? ['is_currency', 'supply', 'cap', 'cap_supply'] : []), + $hasBeneficiary ? 'royalty_wallet_id' : null, + Arr::get($fields, 'royalty.fields.percentage') ? 'royalty_percentage' : null, + ...TokenType::getSelectFields($fieldKeys = array_keys($fields)), + ]); + + $with = []; + $withCount = []; + + if (!$isParent) { + $with = [ + $key => function ($query) use ($select, $args) { + $query->select(array_unique($select)) + ->when( + Arr::get($args, 'after'), + fn ($q) => $q->where('id', '>', Cursor::fromEncoded($args['after'])->parameter('id')) + ); + // This must be done this way to load eager limit correctly. + if ($limit = Arr::get($args, 'first')) { + $query->limit($limit + 1); + } + }, + ]; + } + + $relations = array_filter([ + isset($fields['nonFungible']) ? 'collection' : null, + isset($fields['metadata']) ? 'attributes' : null, + $hasBeneficiary ? 'royaltyBeneficiary' : null, + ...TokenType::getRelationFields($fieldKeys), + ]); + foreach ($relations as $relation) { + if ($relation == 'accounts') { + $withCount[] = $relation; + } + + $with = array_merge( + $with, + static::getRelationQuery( + TokenType::class, + $relation, + $fields, + $key, + $with + ) + ); + } + + return [$select, $with, $withCount]; + } + + /** + * Load transaction's select and relationship fields. + */ + public static function loadTransaction( + array $selections, + string $attribute, + array $args = [], + ?string $key = null, + bool $isParent = false + ): array { + $fields = Arr::get($selections, $attribute, $selections); + $select = array_filter([ + 'id', + isset($fields['wallet']) || static::$query == 'GetWallet' ? 'wallet_public_key' : null, + ...TransactionType::getSelectFields($fieldKeys = array_keys($fields)), + ]); + + $with = []; + $withCount = []; + + if (!$isParent) { + $with = [ + $key => function ($query) use ($select, $args) { + $query->select(array_unique($select)) + ->when(Arr::get($args, 'after'), fn ($q) => $q->where('id', '>', Cursor::fromEncoded($args['after'])->parameter('id'))) + ->when(Arr::get($args, 'transactionIds'), fn ($q) => $q->whereIn('transaction_chain_id', $args['transactionIds'])) + ->when(Arr::get($args, 'transactionHashes'), fn ($q) => $q->whereIn('transaction_chain_hash', $args['transactionHashes'])) + ->when(Arr::get($args, 'methods'), fn ($q) => $q->whereIn('method', $args['methods'])) + ->when(Arr::get($args, 'states'), fn ($q) => $q->whereIn('state', $args['states'])) + ->when(Arr::get($args, 'signedAtBlocks'), fn ($q) => $q->whereIn('signed_at_block', $args['signedAtBlocks'])); + + // This must be done this way to load eager limit correctly. + if ($limit = Arr::get($args, 'first')) { + $query->limit($limit + 1); + } + }, + ]; + } + + foreach (TransactionType::getRelationFields($fieldKeys) as $relation) { + if ($relation == 'events') { + $withCount[] = $relation; + } + + $with = array_merge( + $with, + static::getRelationQuery( + TransactionType::class, + $relation, + $fields, + $key, + $with + ) + ); + } + + return [$select, $with, $withCount]; + } + + /** + * Load wallet's select and relationship fields. + */ + public static function loadWallet( + array $selections, + string $attribute, + array $args = [], + ?string $key = null, + bool $isParent = false + ): array { + $fields = Arr::get($selections, $attribute, $selections); + $select = array_filter([ + 'id', + 'public_key', + ...WalletType::getSelectFields($fieldKeys = array_keys($fields)), + ]); + + $with = []; + $withCount = []; + + if (!$isParent) { + $with = [ + $key => function ($query) use ($select, $args) { + $query->select(array_unique($select)) + ->when(Arr::get($args, 'after'), fn ($q) => $q->where('id', '>', Cursor::fromEncoded($args['after'])->parameter('id'))) + ->when(Arr::get($args, 'transactionIds'), fn ($q) => $q->whereIn('transaction_chain_id', $args['transactionIds'])) + ->when(Arr::get($args, 'transactionHashes'), fn ($q) => $q->whereIn('transaction_chain_hash', $args['transactionIds'])) + ->when(Arr::get($args, 'methods'), fn ($q) => $q->whereIn('method', $args['methods'])) + ->when(Arr::get($args, 'states'), fn ($q) => $q->whereIn('state', $args['states'])); + + // This must be done this way to load eager limit correctly. + if ($limit = Arr::get($args, 'first')) { + $query->limit($limit + 1); + } + }, + ]; + } + + foreach (WalletType::getRelationFields($fieldKeys) as $relation) { + switch($relation) { + case 'collectionAccounts': + $withCount[$relation] = fn ($query) => $query->when( + Arr::get($args, 'collectionIds'), + fn ($q) => $q->whereIn( + 'collection_id', + DB::table('collections')->select('id')->whereIn('collection_chain_id', $args['collectionIds']) + ) + ); + + break; + case 'tokenAccounts': + $withCount[$relation] = fn ($query) => $query->when( + Arr::get($args, 'collectionIds'), + fn ($q) => $q->whereIn( + 'collection_id', + DB::table('collections')->select('id')->whereIn('collection_chain_id', $args['collectionIds']) + ) + )->when( + Arr::get($args, 'tokenIds'), + fn ($q) => $q->whereIn( + 'token_id', + DB::table('tokens')->select('id')->whereIn('token_chain_id', $args['tokenIds']) + ) + ); + + break; + case 'transactions': + $withCount[$relation] = fn ($query) => $query->when(Arr::get($args, 'transactionIds'), fn ($q) => $q->whereIn('transaction_chain_id', $args['transactionIds'])) + ->when(Arr::get($args, 'transactionHashes'), fn ($q) => $q->whereIn('transaction_chain_hash', $args['transactionIds'])) + ->when(Arr::get($args, 'methods'), fn ($q) => $q->whereIn('method', $args['methods'])) + ->when(Arr::get($args, 'states'), fn ($q) => $q->whereIn('state', $args['states'])); + + break; + case 'ownedCollections': + $withCount[$relation] = fn ($query) => $query->when( + Arr::get($args, 'collectionIds'), + fn ($q) => $q->whereIn('collection_id', $args['collectionIds']) + ); + + break; + case 'tokenAccountApprovals': + case 'collectionAccountApprovals': + $withCount[] = $relation; + + break; + } + + $with = array_merge( + $with, + static::getRelationQuery( + WalletType::class, + $relation, + $fields, + $key, + $with + ) + ); + } + + return [$select, $with, $withCount]; + } + + /** + * Load select and relationship fields. + */ + public static function selectFields(ResolveInfo $resolveInfo, string $query): array + { + $select = ['*']; + $with = []; + $withCount = []; + static::$query = $query; + $queryPlan = $resolveInfo->lookAhead()->queryPlan(); + + switch($query) { + case 'GetBlocks': + $fields = Arr::get($queryPlan, 'edges.fields.node.fields', []); + $select = BlockType::getSelectFields(array_keys($fields)); + // Number must always be selected as pagination is done based on that + in_array('number', $select) || $select[] = 'number'; + + break; + case 'GetPendingEvents': + $fields = Arr::get($queryPlan, 'edges.fields.node.fields', []); + $select = array_unique([ + 'id', + ...PendingEventType::getSelectFields(array_keys($fields)), + ]); + + break; + case 'GetCollections': + case 'GetCollection': + [$select, $with, $withCount] = static::loadCollection( + $queryPlan, + $query == 'GetCollections' ? 'edges.fields.node.fields' : '', + [], + null, + true + ); + + break; + case 'GetToken': + case 'GetTokens': + [$select, $with, $withCount] = static::loadToken( + $queryPlan, + $query == 'GetTokens' ? 'edges.fields.node.fields' : '', + [], + null, + true + ); + + break; + case 'GetTransaction': + case 'GetTransactions': + [$select, $with, $withCount] = static::loadTransaction( + $queryPlan, + $query == 'GetTransactions' ? 'edges.fields.node.fields' : '', + [], + null, + true + ); + + break; + case 'GetWallet': + case 'GetWallets': + [$select, $with, $withCount] = static::loadWallet( + $queryPlan, + $query == 'GetWallets' ? 'edges.fields.node.fields' : '', + [], + null, + true + ); + + break; + } + + + return [$select, $with, $withCount]; + } + + /** + * Eager load selects and relationships. + */ + public static function loadSelectFields(ResolveInfo $resolveInfo, string $query): Builder + { + [$select, $with, $withCount] = static::selectFields($resolveInfo, $query); + + return static::query()->select($select)->with($with)->withCount($withCount); + } + + /** + * Get attribute alias. + */ + public static function getAlias(string $name, ?string $type = null): string + { + return match (true) { + $name == 'owner' || $name == 'royaltyBeneficiary' => 'wallet', + $name == 'approvals' && $type == TokenAccountType::class => 'tokenAccountApprovals', + $name == 'account' && $type == TokenAccountApprovalType::class => 'tokenAccount', + $name == 'accounts' && $type == TokenType::class => 'tokenAccounts', + $name == 'approvals' && $type == CollectionAccountType::class => 'collectionAccountApprovals', + $name == 'account' && $type == CollectionAccountApprovalType::class => 'collectionAccount', + $name == 'accounts' && $type == CollectionType::class => 'collectionAccounts', + $name == 'ownedCollections' && $type == WalletType::class => 'collections', + default => $name + }; + } + + /** + * Get relationship query. + */ + public static function getRelationQuery( + string $parentType, + string $attribute, + array $selections, + ?string $parent = null, + array $withs = [] + ): array { + $key = $parent ? "{$parent}.{$attribute}" : $attribute; + $alias = static::getAlias($attribute, $parentType); + $args = Arr::get($selections, $attribute . '.args', []); + switch($alias) { + case 'collection': + case 'collections': + $relations = static::loadCollection( + $selections, + $alias == 'collections' ? $attribute . '.fields.edges.fields.node.fields' : $attribute . '.fields', + $args, + $key + ); + $withs = array_merge($withs, $relations[1]); + + break; + case 'token': + case 'tokens': + $relations = static::loadToken( + $selections, + $alias == 'tokens' ? $attribute . '.fields.edges.fields.node.fields' : $attribute . '.fields', + $args, + $key + ); + $withs = array_merge($withs, $relations[1]); + + break; + case 'attributes': + $withs = array_merge( + $withs, + [$key => fn ($query) => $query->select(['id', 'key', 'value', 'collection_id', 'token_id'])] + ); + + break; + case 'events': + $fields = Arr::get($selections, $attribute . '.fields.edges.fields.node.fields', []); + $select = array_filter([ + 'id', + 'transaction_id', + ...EventType::getSelectFields(array_keys($fields)), + ]); + $withs = array_merge($withs, [$key => fn ($query) => $query->select(array_unique($select))]); + + break; + case 'wallet': + $relations = static::loadWallet( + $selections, + $attribute == 'royaltyBeneficiary' + ? 'royalty.fields.beneficiary.fields' + : $attribute . '.fields', + $args, + $key + ); + $withs = array_merge($withs, $relations[1]); + + break; + case 'tokenAccountApproval': + case 'tokenAccountApprovals': + $fields = Arr::get( + $selections, + static::$query == 'GetWallet' && $attribute != 'approvals' ? $attribute . '.fields.edges.fields.node.fields' : $attribute . '.fields', + [] + ); + $select = array_filter([ + 'id', + 'token_account_id', + isset($fields['wallet']) || static::$query == 'GetWallet' ? 'wallet_id' : null, + ...TokenAccountApprovalType::getSelectFields($fieldKeys = array_keys($fields)), + ]); + $withs = array_merge($withs, [$key => fn ($query) => $query->select(array_unique($select))]); + + foreach (TokenAccountApprovalType::getRelationFields($fieldKeys) as $relation) { + $withs = array_merge( + $withs, + static::getRelationQuery( + TokenAccountApprovalType::class, + $relation, + $fields, + $key, + $withs + ) + ); + } + + break; + case 'tokenAccount': + case 'tokenAccounts': + $fields = Arr::get( + $selections, + $alias == 'tokenAccounts' ? $attribute . '.fields.edges.fields.node.fields' : $attribute . '.fields', + [] + ); + $select = array_filter([ + 'id', + 'token_id', + isset($fields['collection']) ? 'collection_id' : null, + isset($fields['wallet']) || static::$query == 'GetWallet' ? 'wallet_id' : null, + ...TokenAccountType::getSelectFields($fieldKeys = array_keys($fields)), + ]); + + $withs = array_merge( + $withs, + [$key => function ($query) use ($select, $args) { + $query->select(array_unique($select)) + ->when(Arr::get($args, 'after'), fn ($q) => $q->where('id', '>', Cursor::fromEncoded($args['after'])->parameter('id'))) + ->when( + Arr::get($args, 'collectionIds'), + fn ($q) => $q->whereIn( + 'collection_id', + DB::table('collections')->select('id')->whereIn('collection_chain_id', $args['collectionIds']) + ) + ) + ->when( + Arr::get($args, 'tokenIds'), + fn ($q) => $q->whereIn( + 'token_id', + DB::table('tokens')->select('id')->whereIn('token_chain_id', $args['tokenIds']) + ) + ); + // This must be done this way to load eager limit correctly. + if ($limit = Arr::get($args, 'first')) { + $query->limit($limit + 1); + } + }] + ); + + foreach (TokenAccountType::getRelationFields($fieldKeys) as $relation) { + $withs = array_merge( + $withs, + static::getRelationQuery( + TokenAccountType::class, + $relation, + $fields, + $key, + $withs + ) + ); + } + + break; + case 'collectionAccount': + case 'collectionAccounts': + $fields = Arr::get( + $selections, + $alias == 'collectionAccounts' ? $attribute . '.fields.edges.fields.node.fields' : $attribute . '.fields', + [] + ); + $select = array_filter([ + 'id', + 'collection_id', + isset($fields['wallet']) || static::$query == 'GetWallet' ? 'wallet_id' : null, + ...CollectionAccountType::getSelectFields($fieldKeys = array_keys($fields)), + ]); + + $withs = array_merge( + $withs, + [$key => function ($query) use ($select, $args) { + $query->select(array_unique($select)) + ->when(Arr::get($args, 'after'), fn ($q) => $q->where('id', '>', Cursor::fromEncoded($args['after'])->parameter('id'))) + ->when( + Arr::get($args, 'collectionIds'), + fn ($q) => $q->whereIn( + 'collection_id', + DB::table('collections')->select('id')->whereIn('collection_chain_id', $args['collectionIds']) + ) + ); + if ($limit = Arr::get($args, 'first')) { + $query->limit($limit + 1); + } + }] + ); + + foreach (CollectionAccountType::getRelationFields($fieldKeys) as $relation) { + $withs = array_merge( + $withs, + static::getRelationQuery( + CollectionAccountType::class, + $relation, + $fields, + $key, + $withs + ) + ); + } + + break; + case 'collectionAccountApproval': + case 'collectionAccountApprovals': + $fields = Arr::get( + $selections, + $alias == 'collectionAccountApprovals' && $attribute != 'approvals' ? $attribute . '.fields.edges.fields.node.fields' : $attribute . '.fields', + [] + ); + $select = array_filter([ + 'id', + 'collection_account_id', + isset($fields['wallet']) || static::$query == 'GetWallet' ? 'wallet_id' : null, + ...CollectionAccountApprovalType::getSelectFields($fieldKeys = array_keys($fields)), + ]); + + $withs = array_merge($withs, [$key => fn ($query) => $query->select($select)]); + foreach (CollectionAccountApprovalType::getRelationFields($fieldKeys) as $relation) { + $withs = array_merge( + $withs, + static::getRelationQuery( + CollectionAccountApprovalType::class, + $relation, + $fields, + $key, + $withs + ) + ); + } + + break; + case 'namedReserves': + $fields = Arr::get($selections, $attribute . '.fields', []); + $select = array_filter([ + 'id', + 'token_account_id', + ...TokenAccountNamedReserveType::getSelectFields(array_keys($fields)), + ]); + $withs = array_merge($withs, [$key => fn ($query) => $query->select($select)]); + + break; + case 'transactions': + $relations = static::loadTransaction( + $selections, + $attribute . '.fields.edges.fields.node.fields', + $args, + $key + ); + $withs = array_merge($withs, $relations[1]); + + break; + } + + return $withs; + } +} diff --git a/src/Models/Laravel/Traits/Event.php b/src/Models/Laravel/Traits/Event.php new file mode 100644 index 00000000..60b0e09a --- /dev/null +++ b/src/Models/Laravel/Traits/Event.php @@ -0,0 +1,17 @@ +belongsTo(Transaction::class); + } +} diff --git a/src/Models/Laravel/Traits/Token.php b/src/Models/Laravel/Traits/Token.php new file mode 100644 index 00000000..74f70a1d --- /dev/null +++ b/src/Models/Laravel/Traits/Token.php @@ -0,0 +1,45 @@ +belongsTo(Collection::class); + } + + /** + * The royalty benificiary relationship. + */ + public function royaltyBeneficiary(): BelongsTo + { + return $this->belongsTo(Wallet::class, 'royalty_wallet_id'); + } + + /** + * The token accounts relationship. + */ + public function accounts(): HasMany + { + return $this->hasMany(TokenAccount::class); + } + + /** + * The attributes relationship. + */ + public function attributes(): HasMany + { + return $this->hasMany(Attribute::class, 'token_id'); + } +} diff --git a/src/Models/Laravel/Traits/TokenAccount.php b/src/Models/Laravel/Traits/TokenAccount.php new file mode 100644 index 00000000..3d1a434b --- /dev/null +++ b/src/Models/Laravel/Traits/TokenAccount.php @@ -0,0 +1,54 @@ +belongsTo(Collection::class); + } + + /** + * The token relationship. + */ + public function token(): BelongsTo + { + return $this->belongsTo(Token::class); + } + + /** + * The wallet relationship. + */ + public function wallet(): BelongsTo + { + return $this->belongsTo(Wallet::class); + } + + /** + * The token account approvals relationship. + */ + public function approvals(): HasMany + { + return $this->hasMany(TokenAccountApproval::class); + } + + /** + * The token account named reserves relationship. + */ + public function namedReserves(): HasMany + { + return $this->hasMany(TokenAccountNamedReserve::class); + } +} diff --git a/src/Models/Laravel/Traits/TokenAccountApproval.php b/src/Models/Laravel/Traits/TokenAccountApproval.php new file mode 100644 index 00000000..22baae92 --- /dev/null +++ b/src/Models/Laravel/Traits/TokenAccountApproval.php @@ -0,0 +1,26 @@ +belongsTo(TokenAccount::class, 'token_account_id'); + } + + /** + * The wallet relationship. + */ + public function wallet(): BelongsTo + { + return $this->belongsTo(Wallet::class); + } +} diff --git a/src/Models/Laravel/Traits/TokenAccountNamedReserve.php b/src/Models/Laravel/Traits/TokenAccountNamedReserve.php new file mode 100644 index 00000000..05f1b934 --- /dev/null +++ b/src/Models/Laravel/Traits/TokenAccountNamedReserve.php @@ -0,0 +1,17 @@ +belongsTo(TokenAccount::class, 'token_account_id'); + } +} diff --git a/src/Models/Laravel/Traits/Transaction.php b/src/Models/Laravel/Traits/Transaction.php new file mode 100644 index 00000000..c7586e4b --- /dev/null +++ b/src/Models/Laravel/Traits/Transaction.php @@ -0,0 +1,27 @@ +belongsTo(Wallet::class, 'wallet_public_key', 'public_key'); + } + + /** + * The events relationship. + */ + public function events(): HasMany + { + return $this->hasMany(Event::class); + } +} diff --git a/src/Models/Laravel/Traits/Verification.php b/src/Models/Laravel/Traits/Verification.php new file mode 100644 index 00000000..d6654156 --- /dev/null +++ b/src/Models/Laravel/Traits/Verification.php @@ -0,0 +1,17 @@ +hasOne(Wallet::class); + } +} diff --git a/src/Models/Laravel/Traits/Wallet.php b/src/Models/Laravel/Traits/Wallet.php new file mode 100644 index 00000000..4229b668 --- /dev/null +++ b/src/Models/Laravel/Traits/Wallet.php @@ -0,0 +1,80 @@ +hasMany(Collection::class, 'owner_wallet_id'); + } + + /** + * The owned collections relationship. + */ + public function ownedCollections() + { + return $this->hasMany(Collection::class, 'owner_wallet_id'); + } + + /** + * The collection accounts relationship. + */ + public function collectionAccounts(): HasMany + { + return $this->hasMany(CollectionAccount::class); + } + + /** + * The token accounts relationship. + */ + public function tokenAccounts(): HasMany + { + return $this->hasMany(TokenAccount::class); + } + + /** + * The transactions relationship. + */ + public function transactions(): HasMany + { + return $this->hasMany(Transaction::class, 'wallet_public_key', 'public_key'); + } + + /** + * The verification relationship. + */ + public function verification(): HasOne + { + return $this->hasOne(Verification::class); + } + + /** + * The collection account approvals relationship. + */ + public function collectionAccountApprovals(): HasMany + { + return $this->hasMany(CollectionAccountApproval::class); + } + + /** + * The token account approvals relationship. + */ + public function tokenAccountApprovals(): HasMany + { + return $this->hasMany(TokenAccountApproval::class); + } +} diff --git a/src/Models/Laravel/Transaction.php b/src/Models/Laravel/Transaction.php new file mode 100644 index 00000000..e8d539a5 --- /dev/null +++ b/src/Models/Laravel/Transaction.php @@ -0,0 +1,89 @@ +|bool + */ + public $guarded = []; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + public $fillable = [ + 'transaction_chain_id', + 'wallet_public_key', + 'transaction_chain_hash', + 'method', + 'state', + 'result', + 'events', + 'encoded_data', + 'idempotency_key', + 'signed_at_block', + 'created_at', + 'updated_at', + ]; + + /** + * The accessors to append to the model's array form. + * + * @var array + */ + protected $appends = ['wallet_address']; + + /** + * Create a new model instance. + */ + public function __construct(array $attributes = []) + { + $attributes['state'] = $attributes['state'] ?? TransactionState::PENDING->name; + + parent::__construct($attributes); + } + + /** + * The wallet address attribute accessor. + */ + protected function walletAddress(): Attribute + { + return new Attribute( + get: fn () => SS58Address::encode($this->wallet_public_key) + ); + } + + protected function pivotIdentifier(): Attribute + { + return Attribute::make( + get: fn () => $this->idempotency_key, + ); + } + + /** + * Create a new factory instance for the model. + */ + protected static function newFactory(): TransactionFactory + { + return TransactionFactory::new(); + } +} diff --git a/src/Models/Laravel/Verification.php b/src/Models/Laravel/Verification.php new file mode 100644 index 00000000..cf20d90e --- /dev/null +++ b/src/Models/Laravel/Verification.php @@ -0,0 +1,62 @@ +|bool + */ + public $guarded = []; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + public $fillable = [ + 'verification_id', + 'code', + 'public_key', + ]; + + /** + * The accessors to append to the model's array form. + * + * @var array + */ + protected $appends = ['address']; + + /** + * The address attribute accessor. + */ + protected function address(): Attribute + { + return new Attribute( + get: fn () => SS58Address::encode($this->public_key) + ); + } + + /** + * Create a new factory instance for the model. + */ + protected static function newFactory(): VerificationFactory + { + return VerificationFactory::new(); + } +} diff --git a/src/Models/Laravel/Wallet.php b/src/Models/Laravel/Wallet.php new file mode 100644 index 00000000..1042d35a --- /dev/null +++ b/src/Models/Laravel/Wallet.php @@ -0,0 +1,98 @@ +|bool + */ + public $guarded = []; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + public $fillable = [ + 'public_key', + 'external_id', + 'managed', + 'verification_id', + 'network', + ]; + + /** + * The model's attributes. + * + * @var array + */ + protected $attributes = [ + 'managed' => false, + ]; + + /** + * The accessors to append to the model's array form. + * + * @var array + */ + protected $appends = ['address']; + + /** + * The tokens attribute accessor. + */ + public function tokens(): Attribute + { + return new Attribute( + get: function () { + return $this->tokenAccounts->pluck('token'); + } + ); + } + + /** + * The address attribute accessor. + */ + protected function address(): Attribute + { + return new Attribute( + get: fn () => is_null($this->public_key) ? null : SS58Address::encode($this->public_key) + ); + } + + /** + * Create a new factory instance for the model. + */ + protected static function newFactory(): WalletFactory + { + return WalletFactory::new(); + } + + /** + * Bootstrap the model and its traits. + * + * @return void + */ + protected static function boot() + { + parent::boot(); + + self::observe(new WalletObserver()); + } +} diff --git a/src/Models/ModelResolver.php b/src/Models/ModelResolver.php new file mode 100644 index 00000000..4707cc83 --- /dev/null +++ b/src/Models/ModelResolver.php @@ -0,0 +1,64 @@ +model = new $class(); + } + + /** + * Dynamically pass methods to the model. + */ + public function __call($method, $parameters) + { + return $this->model->{$method}(...$parameters); + } + + /** + * Dynamically pass static methods to the model. + */ + public static function __callStatic($method, $parameters) + { + $class = static::resolveClassFqn(static::class); + + return $class::$method(...$parameters); + } + + /** + * Resolve the class function. + */ + public static function resolveClassFqn($classReference = null): string + { + if (null == $classReference) { + $classReference = static::class; + } + + $className = class_basename($classReference); + $driverName = config('database.default'); + $driver = in_array($driverName, DB::supportedDrivers()) ? 'Laravel' : Str::ucfirst($driverName); + + if (!isset(static::$resolvedClassNamespace[$classReference])) { + static::$resolvedClassNamespace[$classReference] = (new ReflectionClass($classReference))->getNamespaceName(); + } + + return static::$resolvedClassNamespace[$classReference] . "\\{$driver}\\{$className}"; + } +} diff --git a/src/Models/PendingEvent.php b/src/Models/PendingEvent.php new file mode 100644 index 00000000..89fa997c --- /dev/null +++ b/src/Models/PendingEvent.php @@ -0,0 +1,7 @@ + gmp_init($this->tokenId), + 'amount' => gmp_init($this->amount), + 'keepAlive' => $this->keepAlive, + 'removeTokenStorage' => $this->removeTokenStorage, + ]; + } + + /** + * Get the array representation. + */ + public function toArray(): array + { + return [ + 'tokenId' => $this->tokenId, + 'amount' => $this->amount, + 'keepAlive' => $this->keepAlive, + 'removeTokenStorage' => $this->removeTokenStorage, + ]; + } +} diff --git a/src/Models/Substrate/CreateTokenParams.php b/src/Models/Substrate/CreateTokenParams.php new file mode 100644 index 00000000..1b17510a --- /dev/null +++ b/src/Models/Substrate/CreateTokenParams.php @@ -0,0 +1,116 @@ +keys()->first()) ?? TokenMintCapType::INFINITE, + supply: ($supply = Arr::get($params, 'cap.Supply')) !== null ? gmp_strval($supply) : null, + behavior: ($behavior = Arr::get($params, 'behavior')) !== null ? TokenMarketBehaviorParams::fromEncodable($behavior) : null, + listingForbidden: Arr::get($params, 'listingForbidden'), + attributes: Arr::get($params, 'attributes'), + ); + } + + /** + * Create new instance from array. + */ + public static function fromArray(array $params): self + { + return new self( + tokenId: Arr::get($params, 'tokenId'), + initialSupply: Arr::get($params, 'initialSupply'), + unitPrice: Arr::get($params, 'unitPrice'), + cap: TokenMintCapType::tryFrom(collect(Arr::get($params, 'cap'))?->keys()->first()) ?? TokenMintCapType::INFINITE, + supply: ($supply = Arr::get($params, 'cap.Supply')) !== null ? $supply : null, + behavior: Arr::get($params, 'behavior'), + listingForbidden: Arr::get($params, 'listingForbidden'), + attributes: Arr::get($params, 'attributes'), + ); + } + + /** + * Get the GMP encoded data. + */ + public function toEncodable(): array + { + return [ + 'CreateToken' => [ + 'tokenId' => gmp_init($this->tokenId), + 'initialSupply' => gmp_init($this->initialSupply), + 'sufficiency' => [ + 'Insufficient' => $this->unitPrice ? gmp_init($this->unitPrice) : null, + ], + 'cap' => $this->cap === TokenMintCapType::INFINITE ? null : [ + $this->cap->value => $this->cap === TokenMintCapType::SINGLE_MINT ? null : gmp_init($this->supply), + ], + 'behavior' => $this->behavior?->toEncodable(), + 'listingForbidden' => $this->listingForbidden, + 'freezeState' => null, + 'attributes' => array_map( + fn ($attribute) => [ + 'key' => HexConverter::stringToHexPrefixed(Arr::get($attribute, 'key')), + 'value' => HexConverter::stringToHexPrefixed(Arr::get($attribute, 'value')), + ], + $this->attributes + ), + 'foreignParams' => null, + ], + ]; + } + + /** + * Get the array representation. + */ + public function toArray(): array + { + return [ + 'CreateToken' => [ + 'tokenId' => $this->tokenId, + 'initialSupply' => $this->initialSupply, + 'unitPrice' => $this->unitPrice, + 'cap' => [ + 'type' => $this->cap->name, + 'amount' => $this->supply, + ], + 'behavior' => $this->behavior?->toArray(), + 'listingForbidden' => $this->listingForbidden, + 'attributes' => array_map( + fn ($attribute) => [ + 'key' => HexConverter::hexToString(Arr::get($attribute, 'key')), + 'value' => HexConverter::hexToString(Arr::get($attribute, 'value')), + ], + $this->attributes + ), + ], + ]; + } +} diff --git a/src/Models/Substrate/FreezeTypeParams.php b/src/Models/Substrate/FreezeTypeParams.php new file mode 100644 index 00000000..4fdb6ac9 --- /dev/null +++ b/src/Models/Substrate/FreezeTypeParams.php @@ -0,0 +1,84 @@ +keys()->first()), + token: ($token = Arr::get($params, 'Token') ?? Arr::get($params, 'TokenAccount.0')) !== null ? gmp_strval($token) : null, + account: ($account = Arr::get($params, 'CollectionAccount') ?? Arr::get($params, 'TokenAccount.1')) !== null ? HexConverter::prefix($account) : null, + ); + } + + /** + * Get the GMP encoded data. + */ + public function toEncodable(): array + { + return match ($this->type) { + FreezeType::COLLECTION => [ + 'Collection' => null, + ], + FreezeType::TOKEN => [ + 'Token' => [ + 'tokenId' => gmp_init($this->token), + 'freezeState' => FreezeStateType::TEMPORARY->value, + ], + ], + FreezeType::COLLECTION_ACCOUNT => [ + 'CollectionAccount' => HexConverter::unPrefix($this->account), + ], + FreezeType::TOKEN_ACCOUNT => [ + 'TokenAccount' => [ + gmp_init($this->token), + HexConverter::unPrefix($this->account), + ], + ] + }; + } + + /** + * Get the array representation. + */ + public function toArray(): array + { + return match ($this->type) { + FreezeType::COLLECTION => [ + 'Collection' => null, + ], + FreezeType::TOKEN => [ + 'Token' => $this->token, + ], + FreezeType::COLLECTION_ACCOUNT => [ + 'CollectionAccount' => $this->account, + ], + FreezeType::TOKEN_ACCOUNT => [ + 'TokenAccount' => [ + $this->token, + $this->account, + ], + ] + }; + } +} diff --git a/src/Models/Substrate/MintParams.php b/src/Models/Substrate/MintParams.php new file mode 100644 index 00000000..5458fb27 --- /dev/null +++ b/src/Models/Substrate/MintParams.php @@ -0,0 +1,70 @@ + [ + 'tokenId' => gmp_init($this->tokenId), + 'amount' => gmp_init($this->amount), + 'unitPrice' => $this->unitPrice !== null ? gmp_init($this->unitPrice) : null, + ], + ]; + } + + /** + * Get the array representation. + */ + public function toArray(): array + { + return [ + 'Mint' => [ + 'tokenId' => $this->tokenId, + 'amount' => $this->amount, + 'unitPrice' => $this->unitPrice, + ], + ]; + } +} diff --git a/src/Models/Substrate/MintPolicyParams.php b/src/Models/Substrate/MintPolicyParams.php new file mode 100644 index 00000000..315417ac --- /dev/null +++ b/src/Models/Substrate/MintPolicyParams.php @@ -0,0 +1,66 @@ + $this->forceSingleMint, + 'maxTokenCount' => $this->maxTokenCount, + 'maxTokenSupply' => $this->maxTokenSupply !== null ? gmp_init($this->maxTokenSupply) : null, + ]; + } + + /** + * Get the array representation. + */ + public function toArray(): array + { + return [ + 'forceSingleMint' => $this->forceSingleMint, + 'maxTokenCount' => $this->maxTokenCount, + 'maxTokenSupply' => $this->maxTokenSupply, + ]; + } +} diff --git a/src/Models/Substrate/OperatorTransferParams.php b/src/Models/Substrate/OperatorTransferParams.php new file mode 100644 index 00000000..22877003 --- /dev/null +++ b/src/Models/Substrate/OperatorTransferParams.php @@ -0,0 +1,76 @@ + [ + 'tokenId' => gmp_init($this->tokenId), + 'source' => SS58Address::getPublicKey($this->source), + 'amount' => gmp_init($this->amount), + 'keepAlive' => $this->keepAlive, + ], + ]; + } + + /** + * Get the array representation. + */ + public function toArray(): array + { + return [ + 'Operator' => [ + 'tokenId' => $this->tokenId, + 'source' => $this->source, + 'amount' => $this->amount, + 'keepAlive' => $this->keepAlive, + ], + ]; + } +} diff --git a/src/Models/Substrate/RoyaltyPolicyParams.php b/src/Models/Substrate/RoyaltyPolicyParams.php new file mode 100644 index 00000000..cb7ed0f6 --- /dev/null +++ b/src/Models/Substrate/RoyaltyPolicyParams.php @@ -0,0 +1,63 @@ + HexConverter::unPrefix(SS58Address::getPublicKey($this->beneficiary)), + 'percentage' => (int) $this->percentage * 10 ** 7, + ]; + } + + /** + * Get the array representation. + */ + public function toArray(): array + { + return [ + 'beneficiary' => $this->beneficiary, + 'percentage' => $this->percentage, + ]; + } +} diff --git a/src/Models/Substrate/SimpleTransferParams.php b/src/Models/Substrate/SimpleTransferParams.php new file mode 100644 index 00000000..6e33613e --- /dev/null +++ b/src/Models/Substrate/SimpleTransferParams.php @@ -0,0 +1,70 @@ + [ + 'tokenId' => gmp_init($this->tokenId), + 'amount' => gmp_init($this->amount), + 'keepAlive' => $this->keepAlive, + ], + ]; + } + + /** + * Get the array representation. + */ + public function toArray(): array + { + return [ + 'Simple' => [ + 'tokenId' => $this->tokenId, + 'amount' => $this->amount, + 'keepAlive' => $this->keepAlive, + ], + ]; + } +} diff --git a/src/Models/Substrate/TokenMarketBehaviorParams.php b/src/Models/Substrate/TokenMarketBehaviorParams.php new file mode 100644 index 00000000..376a26b6 --- /dev/null +++ b/src/Models/Substrate/TokenMarketBehaviorParams.php @@ -0,0 +1,69 @@ +isCurrency === true) { + return [ + 'IsCurrency' => null, + ]; + } + + return [ + 'HasRoyalty' => $this->hasRoyalty?->toEncodable(), + ]; + } + + /** + * Get the array representation. + */ + public function toArray(): array + { + if ($this->hasRoyalty !== null) { + return [ + 'hasRoyalty' => $this->hasRoyalty->toArray(), + ]; + } + + return $this->isCurrency !== null ? ['isCurrency' => $this->isCurrency] : []; + } +} diff --git a/src/Models/Token.php b/src/Models/Token.php new file mode 100644 index 00000000..d633b89c --- /dev/null +++ b/src/Models/Token.php @@ -0,0 +1,7 @@ +managed === true) { + Cache::forget(PlatformCache::MANAGED_ACCOUNTS->key()); + } + } +} diff --git a/src/Package.php b/src/Package.php new file mode 100644 index 00000000..b1dde7f5 --- /dev/null +++ b/src/Package.php @@ -0,0 +1,85 @@ +basePath('../../../autoload.php') + return app()->runningUnitTests() ? require app()->basePath('../../../autoload.php') : require app()->basePath('vendor/autoload.php'); + } + + /** + * Get any routes that have been set up for this package. + */ + public static function getPackageRoutes(): array + { + return CoreRoute::caseValuesAsArray(); + } + + /** + * Get a list of package and app classes. + */ + public static function getPackageClasses(): Collection + { + return collect(self::getAutoloader()->getClassMap()) + ->keys() + ->filter(function ($className) { + $appNamespace = trim(app()->getNamespace(), '\\'); + $namespaceFilter = "/^(Enjin\\\\Platform|{$appNamespace})\\\\/"; + + return preg_match($namespaceFilter, $className) + && class_exists($className) + && !(new \ReflectionClass($className))->isAbstract(); + }); + } + + public static function getClass(string $className) + { + return self::getPackageClasses()->first(fn ($class) => Str::afterLast($class, '\\') == $className); + } + + public static function classImplementsInterface($class, $interface) + { + return in_array($interface, class_implements($class)); + } + + /** + * Get a list of classes that implement a specific interface. + */ + public static function getClassesThatImplementInterface(string $interface): Collection + { + return self::getPackageClasses()->filter(fn ($className) => self::classImplementsInterface($className, $interface)); + } + + /** + * Get a list of class names that implement a specific interface. + */ + public static function getClassNamesThatImplementInterface(string $interface): Collection + { + return self::getClassesThatImplementInterface($interface) + ->transform(fn ($class) => Str::afterLast($class, '\\')) + ->unique(); + } + + /** + * Get a list of GraphQL fields that implement a specific interface. + */ + public static function getGraphQlFieldsThatImplementInterface(string $interface): Collection + { + return self::getClassNamesThatImplementInterface($interface) + ->transform(fn ($class) => Str::replace(['Query', 'Mutation'], '', $class)) + ->unique(); + } +} diff --git a/src/Providers/AuthServiceProvider.php b/src/Providers/AuthServiceProvider.php new file mode 100644 index 00000000..45c0dd3e --- /dev/null +++ b/src/Providers/AuthServiceProvider.php @@ -0,0 +1,19 @@ +app->singleton(AuthManager::class, function ($app) { + return new AuthManager($app); + }); + } +} diff --git a/src/Providers/Deferred/BlockchainServiceProvider.php b/src/Providers/Deferred/BlockchainServiceProvider.php new file mode 100644 index 00000000..847b650c --- /dev/null +++ b/src/Providers/Deferred/BlockchainServiceProvider.php @@ -0,0 +1,36 @@ + Substrate::class, + ]; + + $driverKey = config('enjin-platform.chains.selected'); + $driverClass = $map[$driverKey]; + $this->app->singleton( + BlockchainServiceInterface::class, + $driverClass, + ); + } + + /** + * Get the services provided by the provider. + */ + public function provides() + { + return [BlockchainServiceInterface::class]; + } +} diff --git a/src/Providers/Deferred/SerializationServiceProvider.php b/src/Providers/Deferred/SerializationServiceProvider.php new file mode 100644 index 00000000..31473462 --- /dev/null +++ b/src/Providers/Deferred/SerializationServiceProvider.php @@ -0,0 +1,36 @@ + Substrate::class, + ]; + + $driverKey = config('enjin-platform.chains.selected'); + $driverClass = $map[$driverKey]; + $this->app->singleton( + SerializationServiceInterface::class, + $driverClass + ); + } + + /** + * Get the services provided by the provider. + */ + public function provides() + { + return [SerializationServiceInterface::class]; + } +} diff --git a/src/Providers/Deferred/WebsocketClientProvider.php b/src/Providers/Deferred/WebsocketClientProvider.php new file mode 100644 index 00000000..2672f96c --- /dev/null +++ b/src/Providers/Deferred/WebsocketClientProvider.php @@ -0,0 +1,36 @@ + SubstrateWebsocket::class, + ]; + + $driverKey = config('enjin-platform.chains.selected'); + $driverClass = $map[$driverKey]; + $this->app->singleton( + WebsocketAbstract::class, + $driverClass, + ); + } + + /** + * Get the services provided by the provider. + */ + public function provides() + { + return [WebsocketAbstract::class]; + } +} diff --git a/src/Providers/Faker/Erc1155Provider.php b/src/Providers/Faker/Erc1155Provider.php new file mode 100644 index 00000000..963bb72c --- /dev/null +++ b/src/Providers/Faker/Erc1155Provider.php @@ -0,0 +1,43 @@ +erc1155_token_id(), 48) . HexConverter::padLeft($this->erc1155_token_index(), 16)); + } +} diff --git a/src/Providers/Faker/PlatformProvider.php b/src/Providers/Faker/PlatformProvider.php new file mode 100644 index 00000000..41637ba1 --- /dev/null +++ b/src/Providers/Faker/PlatformProvider.php @@ -0,0 +1,9 @@ +signWithCode($message, $keypair) : $this->signWithMessage($message, $keypair); + $address = SS58Address::encode(HexConverter::hexToBytes($publicKey = bin2hex($publicKey))); + + return [ + 'address' => $address, + 'publicKey' => HexConverter::prefix($publicKey), + 'signature' => $signature, + ]; + } catch (\Exception $e) { + } + } + } + + /** + * Get a random substrate address with signed message using sr25519. + */ + public function sr25519_signature(string $message, ?bool $isCode = false): array + { + $sr = new sr25519(); + $seed = sodium_crypto_sign_publickey(sodium_crypto_sign_keypair()); + $pair = $sr->InitKeyPair(bin2hex($seed)); + + $message = $isCode ? Blake2::hash(HexConverter::stringToHex('Enjin Signed Message:' . $message)) : $message; + + return [ + 'address' => SS58Address::encode($pair->publicKey), + 'publicKey' => HexConverter::prefix($pair->publicKey), + 'signature' => $sr->Sign($pair, HexConverter::prefix($message)), + ]; + } + + /** + * Sign a message using the provided keypair. + */ + public function sign(string $message, string $keypair): string + { + $privateKey = sodium_crypto_sign_secretkey($keypair); + + return HexConverter::prefix(bin2hex(sodium_crypto_sign_detached(sodium_hex2bin(HexConverter::unPrefix($message)), $privateKey))); + } + + /** + * Sign a code using the provided keypair. + */ + public function signWithCode(string $code, ?string $keypair = null): string + { + $keypair = $keypair ?? sodium_crypto_sign_keypair(); + $message = Blake2::hash(HexConverter::stringToHex('Enjin Signed Message:' . $code)); + + return $this->sign($message, $keypair); + } + + /** + * Sign a message using the provided keypair. + */ + public function signWithMessage(string $message, ?string $keypair = null): string + { + $keypair = $keypair ?? sodium_crypto_sign_keypair(); + + return $this->sign($message, $keypair); + } +} diff --git a/src/Providers/FakerServiceProvider.php b/src/Providers/FakerServiceProvider.php new file mode 100644 index 00000000..b44253c4 --- /dev/null +++ b/src/Providers/FakerServiceProvider.php @@ -0,0 +1,26 @@ +app->singleton(Generator::class, function () { + $faker = Factory::create(); + $faker->addProvider(new SubstrateProvider($faker)); + $faker->addProvider(new Erc1155Provider($faker)); + + return $faker; + }); + } +} diff --git a/src/Providers/GraphQlServiceProvider.php b/src/Providers/GraphQlServiceProvider.php new file mode 100644 index 00000000..650be297 --- /dev/null +++ b/src/Providers/GraphQlServiceProvider.php @@ -0,0 +1,251 @@ + ConnectionType::class]); + + $this->setNetwork(); + + $this->graphqlClasses = Package::getPackageClasses(); + + $this->graphQlEnums(); + $this->graphQlGlobalTypes(); + $this->graphQlUnions(); + $this->graphQlSchemas(); + $this->registerGraphQlHttpMiddleware(); + $this->registerGraphQlExecutionMiddleware(); + $this->registerExternalResolverMiddleware(); + } + + /** + * Register graphql union types. + */ + protected function graphQlUnions() + { + $this->graphqlClasses + ->filter( + fn ($className) => in_array(static::UNION, class_implements($className)) + ) + ->each(fn ($className) => GraphQL::addType($className)); + } + + /** + * Register graphql network agnostic global types. + */ + protected function graphQlNetworkAgnosticGlobalTypes() + { + $this->graphqlClasses + ->filter( + fn ($className) => in_array(static::TYPE, class_implements($className)) + && !empty($className::getSchemaName()) + && empty($className::getSchemaNetwork()) + ) + ->each(fn ($className) => GraphQL::addType($className)); + } + + /** + * Register graphql enum types. + */ + protected function graphQlEnums() + { + $this->graphqlClasses + ->filter( + fn ($className) => in_array(static::ENUM, class_implements($className)) + ) + ->each(fn ($className) => GraphQL::addType($className)); + } + + /** + * Register graphql global types. + */ + protected function graphQlGlobalTypes() + { + $this->graphqlClasses + ->filter( + fn ($className) => in_array(static::TYPE, class_implements($className)) + && empty($className::getSchemaName()) + && empty($className::getSchemaNetwork()) + ) + ->each(fn ($className) => GraphQL::addType($className)); + + $this->graphQlNetworkAgnosticGlobalTypes(); + $this->graphQlNetworkSpecificGlobalTypes(); + } + + /** + * Register graphql network specific global types. + */ + protected function graphQlNetworkSpecificGlobalTypes() + { + $this->graphqlClasses + ->filter( + fn ($className) => in_array(static::TYPE, class_implements($className)) + && empty($className::getSchemaName()) + && $className::getSchemaNetwork() == config('enjin-platform.chains.selected') + ) + ->each(fn ($className) => GraphQL::addType($className)); + } + + /** + * Register graphql schemas. + */ + protected function graphQlSchemas() + { + // Schema Queries and Mutations + $queries = $this->graphqlClasses->filter( + fn ($className) => in_array(static::QUERY, class_implements($className)) + && (empty($className::getSchemaNetwork()) || $className::getSchemaNetwork() == config('enjin-platform.chains.selected')) + ); + + $mutations = $this->graphqlClasses->filter( + fn ($className) => in_array(static::MUTATION, class_implements($className)) + && (empty($className::getSchemaNetwork()) || $className::getSchemaNetwork() == config('enjin-platform.chains.selected')) + ); + + $schemas = []; + + $queries->each(function ($query) use (&$schemas) { + $schemas[$query::getSchemaName()]['query'][] = $query; + }); + + $mutations->each(function ($mutation) use (&$schemas) { + $schemas[$mutation::getSchemaName()]['mutation'][] = $mutation; + }); + + // Schema specific Types + $types = $this->graphqlClasses->filter( + fn ($className) => in_array(static::TYPE, class_implements($className)) + && !empty($className::getSchemaName()) + && $className::getSchemaNetwork() == config('enjin-platform.chains.selected') + ); + + $types->each(function ($type) use (&$schemas) { + $schemas[$type::getSchemaName()]['types'][] = $type; + }); + + foreach ($schemas as $schemaName => $schema) { + config(["graphql.schemas.{$schemaName}" => $schema]); + } + + // Manually add UploadType after schema has been built. + GraphQL::addType(UploadType::class); + } + + protected function registerGraphQlHttpMiddleware() + { + $httpMiddlewares = Package::getClassesThatImplementInterface(PlatformGraphQlHttpMiddleware::class); + + [$globalHttpMiddleware, $schemaHttpMiddleware] = $httpMiddlewares->partition(fn ($middleware) => empty($middleware::forSchema()) || 'global' === $middleware::forSchema()); + + $globalHttpMiddleware + ->each(function ($middleware) { + $graphQlHttpMiddleware = config('graphql.route.middleware'); + $graphQlHttpMiddleware[] = $middleware; + config(['graphql.route.middleware' => $graphQlHttpMiddleware]); + }); + + $schemaHttpMiddleware + ->each(function ($middleware) { + $schema = $middleware::forSchema(); + $graphQlHttpMiddleware = config('graphql.route.middleware'); + + $graphQlSchemaHttpMiddleware = config("graphql.schemas.{$schema}.middleware") ?? []; + $graphQlSchemaHttpMiddleware[] = $middleware; + + config(["graphql.schemas.{$schema}.middleware" => array_merge($graphQlHttpMiddleware, $graphQlSchemaHttpMiddleware)]); + }); + } + + protected function registerGraphQlExecutionMiddleware() + { + $executionMiddlewares = Package::getClassesThatImplementInterface(PlatformGraphQlExecutionMiddleware::class); + + [$globalExecutionMiddleware, $schemaExecutionMiddleware] = $executionMiddlewares->partition(fn ($middleware) => empty($middleware::forSchema()) || 'global' === $middleware::forSchema()); + + $globalExecutionMiddleware + ->each(function ($middleware) { + $graphQlExecutionMiddleware = config('graphql.execution_middleware'); + $graphQlExecutionMiddleware[] = $middleware; + config(['graphql.execution_middleware' => $graphQlExecutionMiddleware]); + }); + + $schemaExecutionMiddleware + ->each(function ($middleware) { + $schema = $middleware::forSchema(); + $graphQlExecutionMiddleware = config('graphql.execution_middleware'); + + $graphQlSchemaExecutionMiddleware = config("graphql.schemas.{$schema}.execution_middleware") ?? []; + $graphQlSchemaExecutionMiddleware[] = $middleware; + + config(["graphql.schemas.{$schema}.execution_middleware" => array_merge($graphQlExecutionMiddleware, $graphQlSchemaExecutionMiddleware)]); + }); + } + + protected function registerExternalResolverMiddleware() + { + $resolverMiddlewares = Package::getClassesThatImplementInterface(PlatformGraphQlResolverMiddleware::class) + ->mapWithKeys(function ($resolverMiddleware) { + $excludeFrom = collect($resolverMiddleware::excludeFrom())->transform(fn ($class) => class_basename($class)); + + return collect($resolverMiddleware::registerOn()) + ->map(fn ($model, $class) => ['operation' => class_basename($class), 'middleware' => $resolverMiddleware]) + ->whereNotIn('operation', $excludeFrom) + ->all(); + }) + ->mapToGroups(fn ($class, $key) => [$class['operation'] => $class['middleware']]) + ->toArray(); + + $graphQlResolverMiddleware = config('graphql.resolver_middleware') ?? []; + + config(['graphql.resolver_middleware' => array_merge($graphQlResolverMiddleware, $resolverMiddlewares)]); + } + + /** + * Set the network for the graphql. + */ + private function setNetwork() + { + $segments = request()->segments(); + $network = array_values(array_intersect($segments, array_keys(config('enjin-platform.chains.supported')))); + + if (!empty($network)) { + $network = $network[0]; + $currentGraphQlRoutePrefix = config('graphql.route.prefix'); + $currentGraphiQlRoutePrefix = config('graphql.graphiql.prefix'); + + config(['graphql.route.prefix' => "{$network}/{$currentGraphQlRoutePrefix}"]); + config(['graphql.graphiql.prefix' => "{$network}/{$currentGraphiQlRoutePrefix}"]); + config(['enjin-platform.networks.selected' => $network]); + } + } +} diff --git a/src/Rules/AccountExistsInCollection.php b/src/Rules/AccountExistsInCollection.php new file mode 100644 index 00000000..31b300e3 --- /dev/null +++ b/src/Rules/AccountExistsInCollection.php @@ -0,0 +1,65 @@ +collectionService = app()->make(CollectionService::class); + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * + * @return bool + */ + public function passes($attribute, $value) + { + return $this->collectionService->accountExistsInCollection($this->data['collectionId'], $value); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return __('enjin-platform::validation.account_exists_in_collection', ['account' => $this->data['collectionAccount'], 'collectionId' => $this->data['collectionId']]); + } + + /** + * Set the data under validation. + * + * @param array $data + * + * @return $this + */ + public function setData($data) + { + $this->data = $data; + + return $this; + } +} diff --git a/src/Rules/AccountExistsInToken.php b/src/Rules/AccountExistsInToken.php new file mode 100644 index 00000000..d36e8c59 --- /dev/null +++ b/src/Rules/AccountExistsInToken.php @@ -0,0 +1,80 @@ +tokenService = app()->make(TokenService::class); + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * + * @return bool + */ + public function passes($attribute, $value) + { + $tokenId = $this->encodeTokenId($this->data); + $this->message = __('enjin-platform::validation.account_exists_in_token', ['account' => $this->data['tokenAccount'], 'collectionId' => $this->data['collectionId'], 'tokenId' => $tokenId]); + + if (!$tokenId) { + return false; + } + + return $this->tokenService->accountExistsInToken($this->data['collectionId'], $tokenId, $value); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return $this->message; + } + + /** + * Set the data under validation. + * + * @param array $data + * + * @return $this + */ + public function setData($data) + { + $this->data = $data; + + return $this; + } +} diff --git a/src/Rules/AccountExistsInWallet.php b/src/Rules/AccountExistsInWallet.php new file mode 100644 index 00000000..a056d710 --- /dev/null +++ b/src/Rules/AccountExistsInWallet.php @@ -0,0 +1,45 @@ +walletService = app()->make(WalletService::class); + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * + * @return bool + */ + public function passes($attribute, $value) + { + return $this->walletService->accountExistsInWallet($value); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return __('enjin-platform::validation.account_exists_in_wallet'); + } +} diff --git a/src/Rules/ApprovalExistsInCollection.php b/src/Rules/ApprovalExistsInCollection.php new file mode 100644 index 00000000..0cc4250a --- /dev/null +++ b/src/Rules/ApprovalExistsInCollection.php @@ -0,0 +1,65 @@ +collectionService = app()->make(CollectionService::class); + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * + * @return bool + */ + public function passes($attribute, $value) + { + return $this->collectionService->approvalExistsInCollection($this->data['collectionId'], $value); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return __('enjin-platform::validation.approval_exists_in_collection', ['operator' => $this->data['operator'], 'collectionId' => $this->data['collectionId']]); + } + + /** + * Set the data under validation. + * + * @param array $data + * + * @return $this + */ + public function setData($data) + { + $this->data = $data; + + return $this; + } +} diff --git a/src/Rules/ApprovalExistsInToken.php b/src/Rules/ApprovalExistsInToken.php new file mode 100644 index 00000000..aef63486 --- /dev/null +++ b/src/Rules/ApprovalExistsInToken.php @@ -0,0 +1,80 @@ +tokenService = app()->make(TokenService::class); + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * + * @return bool + */ + public function passes($attribute, $value) + { + $tokenId = $this->encodeTokenId($this->data); + $this->message = __('enjin-platform::validation.approval_exists_in_token', ['operator' => $this->data['operator'], 'collectionId' => $this->data['collectionId'], 'tokenId' => $tokenId]); + + if (!$tokenId) { + return false; + } + + return $this->tokenService->approvalExistsInToken($this->data['collectionId'], $tokenId, $value); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return $this->message; + } + + /** + * Set the data under validation. + * + * @param array $data + * + * @return $this + */ + public function setData($data) + { + $this->data = $data; + + return $this; + } +} diff --git a/src/Rules/AttributeExistsInCollection.php b/src/Rules/AttributeExistsInCollection.php new file mode 100644 index 00000000..7a9d234e --- /dev/null +++ b/src/Rules/AttributeExistsInCollection.php @@ -0,0 +1,65 @@ +collectionService = app()->make(CollectionService::class); + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * + * @return bool + */ + public function passes($attribute, $value) + { + return $this->collectionService->attributeExistsInCollection($this->data['collectionId'], $value); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return __('enjin-platform::validation.attribute_exists_in_collection'); + } + + /** + * Set the data under validation. + * + * @param array $data + * + * @return $this + */ + public function setData($data) + { + $this->data = $data; + + return $this; + } +} diff --git a/src/Rules/AttributeExistsInToken.php b/src/Rules/AttributeExistsInToken.php new file mode 100644 index 00000000..f07a0f53 --- /dev/null +++ b/src/Rules/AttributeExistsInToken.php @@ -0,0 +1,72 @@ +tokenService = app()->make(TokenService::class); + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * + * @return bool + */ + public function passes($attribute, $value) + { + if (!$tokenId = $this->encodeTokenId($this->data)) { + return true; + } + + return $this->tokenService->attributeExistsInToken($this->data['collectionId'], $tokenId, $value); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return __('enjin-platform::validation.key_doesnt_exit_in_token'); + } + + /** + * Set the data under validation. + * + * @param array $data + * + * @return $this + */ + public function setData($data) + { + $this->data = $data; + + return $this; + } +} diff --git a/src/Rules/CheckTokenCount.php b/src/Rules/CheckTokenCount.php new file mode 100644 index 00000000..73b817f2 --- /dev/null +++ b/src/Rules/CheckTokenCount.php @@ -0,0 +1,55 @@ +firstWhere('collection_chain_id', '=', $value) + ) { + $total = ($collection->tokens_count + $this->offset); + if (null !== $collection->max_token_count && (0 === $collection->max_token_count || $total > $collection->max_token_count)) { + $this->message = __('enjin-platform::validation.check_token_count', ['total' => $total, 'maxToken' => $collection->max_token_count]); + + return false; + } + } + + return true; + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return $this->message; + } +} diff --git a/src/Rules/CollectionHasTokens.php b/src/Rules/CollectionHasTokens.php new file mode 100644 index 00000000..05837fae --- /dev/null +++ b/src/Rules/CollectionHasTokens.php @@ -0,0 +1,29 @@ +firstWhere(['collection_chain_id' => $value]); + if (!$collection) { + $fail('validation.exists')->translate(); + + return; + } + + if (!$collection->tokens_count) { + $fail('enjin-platform::validation.collection_has_tokens')->translate(); + } + } +} diff --git a/src/Rules/DaemonProhibited.php b/src/Rules/DaemonProhibited.php new file mode 100644 index 00000000..be673183 --- /dev/null +++ b/src/Rules/DaemonProhibited.php @@ -0,0 +1,34 @@ +address, config('enjin-platform.chains.daemon-account')); + } + + return !SS58Address::isSameAddress($value, config('enjin-platform.chains.daemon-account')); + } + + /** + * Get the validation error message. + */ + public function message() + { + return __('enjin-platform::validation.daemon_prohibited'); + } +} diff --git a/src/Rules/DistinctAttributes.php b/src/Rules/DistinctAttributes.php new file mode 100644 index 00000000..21e6c2ca --- /dev/null +++ b/src/Rules/DistinctAttributes.php @@ -0,0 +1,31 @@ +pluck('key')->unique()->count() === count($value); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return __('enjin-platform::validation.distinct_attribute'); + } +} diff --git a/src/Rules/DistinctMultiAsset.php b/src/Rules/DistinctMultiAsset.php new file mode 100644 index 00000000..df4c1950 --- /dev/null +++ b/src/Rules/DistinctMultiAsset.php @@ -0,0 +1,31 @@ +unique()->count() === count($value); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return __('enjin-platform::validation.distinct_multi_asset'); + } +} diff --git a/src/Rules/FutureBlock.php b/src/Rules/FutureBlock.php new file mode 100644 index 00000000..cf1a49b0 --- /dev/null +++ b/src/Rules/FutureBlock.php @@ -0,0 +1,51 @@ +latestBlock = app()->runningUnitTests() + ? (int) Block::max('number') + : (int) ((new BlockProcessor())->latestBlock() ?: Block::max('number')); + + return $this->latestBlock < $value; + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return __('enjin-platform::validation.future_block', ['block' => $this->latestBlock]); + } +} diff --git a/src/Rules/IsCollectionOwner.php b/src/Rules/IsCollectionOwner.php new file mode 100644 index 00000000..75470488 --- /dev/null +++ b/src/Rules/IsCollectionOwner.php @@ -0,0 +1,35 @@ +owner->public_key; + + return SS58Address::isSameAddress($owner, config('enjin-platform.chains.daemon-account')); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return __('enjin-platform::validation.is_collection_owner'); + } +} diff --git a/src/Rules/IsCollectionOwnerOrApproved.php b/src/Rules/IsCollectionOwnerOrApproved.php new file mode 100644 index 00000000..8d9f86d1 --- /dev/null +++ b/src/Rules/IsCollectionOwnerOrApproved.php @@ -0,0 +1,50 @@ +owner->public_key, $daemonAccount)) { + return true; + } + + return app(CollectionService::class)->approvalExistsInCollection( + $collection->collection_chain_id, + $daemonAccount, + false, + ); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return __('enjin-platform::validation.is_collection_owner_or_approved'); + } +} diff --git a/src/Rules/IsManagedWallet.php b/src/Rules/IsManagedWallet.php new file mode 100644 index 00000000..40c1a748 --- /dev/null +++ b/src/Rules/IsManagedWallet.php @@ -0,0 +1,37 @@ +flatten()->every(fn ($item) => $this->isValidMaxBigInt($item)); + } + + return $this->isValidMaxBigInt($value); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return $this->message; + } + + /** + * Determine if the value is a valid max big int. + */ + protected function isValidMaxBigInt($value): bool + { + if (!is_numeric($value)) { + $this->message = __('validation.numeric'); + + return false; + } + + $this->message = __('enjin-platform::validation.max_big_int', ['max' => $this->max]); + + return bccomp($this->max, $value) >= 0; + } +} diff --git a/src/Rules/MaxTokenBalance.php b/src/Rules/MaxTokenBalance.php new file mode 100644 index 00000000..3b2afbbd --- /dev/null +++ b/src/Rules/MaxTokenBalance.php @@ -0,0 +1,91 @@ +tokenService = app()->make(TokenService::class); + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * + * @return bool + */ + public function passes($attribute, $value) + { + // Parse tokenId when there's an adatper + $chunks = explode('.', $attribute); + array_pop($chunks); + if (!$tokenId = $this->encodeTokenId(Arr::get($this->data, implode('.', $chunks)))) { + // bypass when no tokenId + return true; + } + + $tokenAccountBalance = gmp_init($this->tokenService->tokenBalanceForAccount( + collectionId: Arr::get($this->data, 'collectionId'), + tokenId: // This gets the ID when the rule is used in a Simple Mutation. + $tokenId + // This gets the ID when the rule is used in a Batch Mutation. + ?? Arr::get($this->data, str_replace('amount', 'tokenId', $attribute)), + address: // This gets the address for OperatorTransfer when the rule is used in a Simple Mutation. + Arr::get($this->data, 'params.source') + // This gets the address for OperatorTransfer when the rule is used in a Batch Mutation. + ?? Arr::get($this->data, str_replace('amount', 'source', $attribute)) + // This gets the address when using SimpleTransfer either on Simple or Batch Mutation. + ?? Arr::get($this->data, 'signingAccount'), + )); + + return gmp_sub($tokenAccountBalance, gmp_init($value)) >= 0; + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return __('enjin-platform::validation.max_token_balance'); + } + + /** + * Set the data under validation. + * + * @param array $data + * + * @return $this + */ + public function setData($data) + { + $this->data = $data; + + return $this; + } +} diff --git a/src/Rules/MinBigInt.php b/src/Rules/MinBigInt.php new file mode 100644 index 00000000..e2f923fe --- /dev/null +++ b/src/Rules/MinBigInt.php @@ -0,0 +1,64 @@ +min = $min; + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * + * @return bool + */ + public function passes($attribute, $value) + { + if (is_array($value)) { + return collect($value)->flatten()->every(fn ($item) => $this->isValidMinBigInt($item)); + } + + return $this->isValidMinBigInt($value); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return $this->message; + } + + /** + * Determine if the value is a valid min big int. + */ + protected function isValidMinBigInt($value): bool + { + if (!is_numeric($value)) { + $this->message = __('validation.numeric'); + + return false; + } + + $this->message = __('enjin-platform::validation.min_big_int', ['min' => $this->min]); + + return bccomp($this->min, $value) <= 0; + } +} diff --git a/src/Rules/MinTokenDeposit.php b/src/Rules/MinTokenDeposit.php new file mode 100644 index 00000000..7494cbb5 --- /dev/null +++ b/src/Rules/MinTokenDeposit.php @@ -0,0 +1,60 @@ +data, str_replace('.unitPrice', '.initialSupply', $attribute)); + $tokenDeposit = gmp_mul($initialSupply, $value); + + return gmp_sub($tokenDeposit, $this->minTokenDeposit) >= 0; + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return __('enjin-platform::validation.min_token_deposit'); + } + + /** + * Set the data under validation. + * + * @param array $data + * + * @return $this + */ + public function setData($data) + { + $this->data = $data; + + return $this; + } +} diff --git a/src/Rules/NoTokensInCollection.php b/src/Rules/NoTokensInCollection.php new file mode 100644 index 00000000..535594dd --- /dev/null +++ b/src/Rules/NoTokensInCollection.php @@ -0,0 +1,34 @@ +withCount('tokens')->firstWhere('collection_chain_id', '=', $value); + + return 0 === $collection?->tokens_count; + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return __('enjin-platform::validation.no_tokens_in_collection'); + } +} diff --git a/src/Rules/TokenDoesNotExistInCollection.php b/src/Rules/TokenDoesNotExistInCollection.php new file mode 100644 index 00000000..3f44a243 --- /dev/null +++ b/src/Rules/TokenDoesNotExistInCollection.php @@ -0,0 +1,65 @@ +tokenService = app()->make(TokenService::class); + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * + * @return bool + */ + public function passes($attribute, $value) + { + return !$this->tokenService->tokenExistsInCollection($value, $this->data['collectionId']); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return __('enjin-platform::validation.token_doesnt_exist_in_collection'); + } + + /** + * Set the data under validation. + * + * @param array $data + * + * @return $this + */ + public function setData($data) + { + $this->data = $data; + + return $this; + } +} diff --git a/src/Rules/TokenEncodeDoesNotExistInCollection.php b/src/Rules/TokenEncodeDoesNotExistInCollection.php new file mode 100644 index 00000000..6cd33730 --- /dev/null +++ b/src/Rules/TokenEncodeDoesNotExistInCollection.php @@ -0,0 +1,79 @@ +tokenService = app()->make(TokenService::class); + $this->tokenIdManager = app()->make(TokenIdManager::class); + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * + * @return bool + */ + public function passes($attribute, $value) + { + $data = Arr::get($this->data, Str::beforeLast($attribute, '.')); + + return !$this->tokenService->tokenExistsInCollection( + $this->tokenIdManager->encode($data), + $this->data['collectionId'] + ); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return __('enjin-platform::validation.token_encode_doesnt_exist_in_collection'); + } + + /** + * Set the data under validation. + * + * @param array $data + * + * @return $this + */ + public function setData($data) + { + $this->data = $data; + + return $this; + } +} diff --git a/src/Rules/TokenEncodeExistInCollection.php b/src/Rules/TokenEncodeExistInCollection.php new file mode 100644 index 00000000..60ad2248 --- /dev/null +++ b/src/Rules/TokenEncodeExistInCollection.php @@ -0,0 +1,77 @@ +tokenService = app()->make(TokenService::class); + $this->tokenIdManager = app()->make(TokenIdManager::class); + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * + * @return bool + */ + public function passes($attribute, $value) + { + return $this->tokenService->tokenExistsInCollection( + $this->tokenIdManager->encode(Arr::wrap(['tokenId' => $value])), + $this->data['collectionId'] + ); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return __('enjin-platform::validation.token_encode_exist_in_collection'); + } + + /** + * Set the data under validation. + * + * @param array $data + * + * @return $this + */ + public function setData($data) + { + $this->data = $data; + + return $this; + } +} diff --git a/src/Rules/TokenEncodeExists.php b/src/Rules/TokenEncodeExists.php new file mode 100644 index 00000000..fc543cab --- /dev/null +++ b/src/Rules/TokenEncodeExists.php @@ -0,0 +1,68 @@ +tokenIdManager = app()->make(TokenIdManager::class); + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * + * @return bool + */ + public function passes($attribute, $value) + { + return Token::whereTokenChainId($this->tokenIdManager->encode($this->data))->exists(); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return __('enjin-platform::validation.token_encode_exists'); + } + + /** + * Set the data under validation. + * + * @param array $data + * + * @return $this + */ + public function setData($data) + { + $this->data = $data; + + return $this; + } +} diff --git a/src/Rules/TokenExistsInCollection.php b/src/Rules/TokenExistsInCollection.php new file mode 100644 index 00000000..8de4c767 --- /dev/null +++ b/src/Rules/TokenExistsInCollection.php @@ -0,0 +1,65 @@ +tokenService = app()->make(TokenService::class); + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * + * @return bool + */ + public function passes($attribute, $value) + { + return $this->tokenService->tokenExistsInCollection($value, $this->data['collectionId']); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return __('enjin-platform::validation.token_exists_in_collection'); + } + + /** + * Set the data under validation. + * + * @param array $data + * + * @return $this + */ + public function setData($data) + { + $this->data = $data; + + return $this; + } +} diff --git a/src/Rules/ValidHex.php b/src/Rules/ValidHex.php new file mode 100644 index 00000000..86fbb393 --- /dev/null +++ b/src/Rules/ValidHex.php @@ -0,0 +1,54 @@ +every(fn ($item) => $this->isValidHex($item)); + } + + return $this->isValidHex($value); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return __('enjin-platform::validation.valid_hex'); + } + + /** + * Determine if the value is a valid hex. + */ + protected function isValidHex($value): bool + { + if (!is_string($value) || (null !== $this->bytesLength && strlen($value) !== ((2 * $this->bytesLength) + 2))) { + return false; + } + + return preg_match('/^0x[a-fA-F0-9]*$/', $value) >= 1; + } +} diff --git a/src/Rules/ValidRoyaltyPercentage.php b/src/Rules/ValidRoyaltyPercentage.php new file mode 100644 index 00000000..da85a55f --- /dev/null +++ b/src/Rules/ValidRoyaltyPercentage.php @@ -0,0 +1,35 @@ + 50) { + return false; + } + + return preg_match('/^(\d*\.)?\d{0,7}$/', $value) >= 1; + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return __('enjin-platform::validation.valid_royalty_percentage'); + } +} diff --git a/src/Rules/ValidSubstrateAccount.php b/src/Rules/ValidSubstrateAccount.php new file mode 100644 index 00000000..174a1204 --- /dev/null +++ b/src/Rules/ValidSubstrateAccount.php @@ -0,0 +1,44 @@ +every(fn ($item) => $this->isValidAddress($item)); + } + + return $this->isValidAddress($value); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return __('enjin-platform::validation.valid_substrate_account'); + } + + /** + * Determine if the value is a valid address. + */ + protected function isValidAddress($value): bool + { + return SS58Address::isValidAddress($value); + } +} diff --git a/src/Rules/ValidSubstrateAddress.php b/src/Rules/ValidSubstrateAddress.php new file mode 100644 index 00000000..f6e3d0e3 --- /dev/null +++ b/src/Rules/ValidSubstrateAddress.php @@ -0,0 +1,32 @@ +every(fn ($item) => $this->isValidTransactionId($item)); + } + + return $this->isValidTransactionId($value); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return __('enjin-platform::validation.valid_substrate_transaction_id'); + } + + /** + * Determine if the value is a valid transaction id. + */ + protected function isValidTransactionId($value): bool + { + return preg_match('/^\d*-\d*$/', $value) >= 1; + } +} diff --git a/src/Rules/ValidVerificationId.php b/src/Rules/ValidVerificationId.php new file mode 100644 index 00000000..0c0e53a7 --- /dev/null +++ b/src/Rules/ValidVerificationId.php @@ -0,0 +1,35 @@ += 1; + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return __('enjin-platform::validation.valid_verification_id'); + } +} diff --git a/src/Services/Auth/AuthManager.php b/src/Services/Auth/AuthManager.php new file mode 100755 index 00000000..d413a1fc --- /dev/null +++ b/src/Services/Auth/AuthManager.php @@ -0,0 +1,95 @@ +driver()->{$method}(...$parameters); + } + + /** + * Get an auth driver instance by name. + */ + public function driver($name = null): Authenticator + { + $name = $name ?: $this->getDefaultDriver(); + + return $this->drivers[$name] = $this->get($name); + } + + /** + * Get the default cache driver name. + */ + public function getDefaultDriver(): ?string + { + return $this->app['config']['enjin-platform.auth']; + } + + /** + * Set the default cache driver name. + */ + public function setDefaultDriver(string $name): void + { + $this->app['config']['enjin-platform.auth_driver'] = $name; + } + + /** + * Unset the given driver instances. + */ + public function forgetDriver(array|string|null $name = null): self + { + $name ??= $this->getDefaultDriver(); + + foreach ((array) $name as $cacheName) { + if (isset($this->drivers[$cacheName])) { + unset($this->drivers[$cacheName]); + } + } + + return $this; + } + + /** + * Attempt to get the store from the local platform. + */ + protected function get(?string $name): Authenticator + { + return $this->drivers[$name] ?? $this->resolve($name); + } + + /** + * Resolve the given store. + */ + protected function resolve(?string $name): Authenticator + { + $driver = Package::getClass(Str::studly($name ?? 'Null') . 'Auth'); + + if (!isset($driver)) { + throw new InvalidArgumentException(__('enjin-platform::error.auth.auth_not_defined')); + } + + return $driver::create(); + } +} diff --git a/src/Services/Auth/Authenticator.php b/src/Services/Auth/Authenticator.php new file mode 100755 index 00000000..2b5011fa --- /dev/null +++ b/src/Services/Auth/Authenticator.php @@ -0,0 +1,22 @@ +token)) { + return false; + } + + return $request->header(self::HEADER) === $this->token; + } + + /** + * Get authorization token. + */ + public function getToken(): string + { + return $this->token; + } + + public function getError(): string + { + return $this->token ? __('enjin-platform::error.unauthorized_header') : __('enjin-platform::error.auth.basic_token.token_not_defined'); + } + + public static function create(): Authenticator + { + $token = config('enjin-platform.auth_drivers.basic_token.token'); + + return new static($token); + } +} diff --git a/src/Services/Auth/Drivers/NullAuth.php b/src/Services/Auth/Drivers/NullAuth.php new file mode 100755 index 00000000..365cb3bd --- /dev/null +++ b/src/Services/Auth/Drivers/NullAuth.php @@ -0,0 +1,35 @@ +codec = new Codec(); + } + + /** + * Get the client. + */ + public function getClient(): WebsocketAbstract + { + return $this->client; + } + + /** + * Call the method in the client service. + */ + public function callMethod(string $name, array $args = []): mixed + { + return $this->client->send($name, $args); + } + + /** + * Get the collection policies. + */ + public function getCollectionPolicies(array $args): array + { + if (Arr::get($args, 'explicitRoyaltyCurrencies')) { + $args['explicitRoyaltyCurrencies'] = collect($args['explicitRoyaltyCurrencies']) + ->map(function ($row) { + $row['tokenId'] = $this->encodeTokenId($row); + unset($row['encodeTokenId']); + + return $row; + })->toArray(); + } + + $mintPolicy = new MintPolicyParams(...$args['mintPolicy']); + if (null !== $args['marketPolicy']) { + $args['marketPolicy']['royalty']['beneficiary'] = SS58Address::getPublicKey($args['marketPolicy']['royalty']['beneficiary']); + } + + $marketPolicy = null !== $args['marketPolicy'] ? new RoyaltyPolicyParams(...$args['marketPolicy']['royalty']) : null; + + return [ + 'mintPolicy' => $mintPolicy, + 'marketPolicy' => $marketPolicy, + 'explicitRoyaltyCurrencies' => $args['explicitRoyaltyCurrencies'], + 'attributes' => Arr::get($args, 'attributes', []), + ]; + } + + /** + * Get mint or create params object. + */ + public function getMintOrCreateParams(array $args): CreateTokenParams|MintParams + { + if (isset($args['initialSupply'])) { + return $this->getCreateTokenParams($args); + } + + return $this->getMintTokenParams($args); + } + + /** + * Create a new mint token params object. + */ + public function getMintTokenParams(array $args): MintParams + { + $data = [ + $this->encodeTokenId($args), + $args['amount'], + ]; + if (isset($args['unitPrice'])) { + $data['unitPrice'] = $args['unitPrice']; + } + + return new MintParams(...$data); + } + + /** + * Create a new create token params object. + */ + public function getCreateTokenParams(array $args): CreateTokenParams + { + $data = [ + $this->encodeTokenId($args), + $args['initialSupply'], + $args['unitPrice'], + ]; + + if (null !== $args['cap']) { + $data['cap'] = TokenMintCapType::getEnumCase($args['cap']['type']); + $data['supply'] = $args['cap']['amount']; + } + + if (($beneficiary = Arr::get($args, 'behavior.hasRoyalty.beneficiary')) !== null) { + $args['behavior']['hasRoyalty']['beneficiary'] = SS58Address::getPublicKey($beneficiary); + $data['behavior'] = new TokenMarketBehaviorParams(hasRoyalty: new RoyaltyPolicyParams(...$args['behavior']['hasRoyalty'])); + } + if (true === Arr::get($args, 'behavior.isCurrency')) { + $data['behavior'] = new TokenMarketBehaviorParams(isCurrency: true); + } + + $data['listingForbidden'] = $args['listingForbidden']; + $data['attributes'] = Arr::get($args, 'attributes', []); + + return new CreateTokenParams(...$data); + } + + /** + * Create a new royalty policy params object. + */ + public function getMutateCollectionRoyalty(array $args): null|array|RoyaltyPolicyParams + { + if (!isset($args['royalty'])) { + return null; + } + + if (null == Arr::get($args, 'royalty.beneficiary') && null == Arr::get($args, 'royalty.percentage')) { + return []; + } + + return new RoyaltyPolicyParams(...$args['royalty']); + } + + /** + * Create a new create token market behavior object. + */ + public function getMutateTokenBehavior(array $args): null|array|TokenMarketBehaviorParams + { + if (!isset($args['behavior'])) { + return null; + } + + if ($args['behavior'] === []) { + return []; + } + + if (($beneficiary = Arr::get($args, 'behavior.hasRoyalty.beneficiary')) !== null) { + $args['behavior']['hasRoyalty']['beneficiary'] = SS58Address::getPublicKey($beneficiary); + + return new TokenMarketBehaviorParams(hasRoyalty: new RoyaltyPolicyParams(...$args['behavior']['hasRoyalty'])); + } + + return new TokenMarketBehaviorParams(isCurrency: true); + } + + /** + * Create a new simple transfer or operator transfer params object. + */ + public function getTransferParams(array $args): SimpleTransferParams|OperatorTransferParams + { + if (isset($args['source'])) { + return $this->getOperatorTransferParams($args); + } + + return $this->getSimpleTransferParams($args); + } + + /** + * Create a new operator transfer params object. + */ + public function getOperatorTransferParams(array $args): OperatorTransferParams + { + $data = [ + $this->encodeTokenId($args), + $args['source'], + $args['amount'], + $args['keepAlive'], + ]; + + return new OperatorTransferParams(...$data); + } + + /** + * Create a new simple transfer params object. + */ + public function getSimpleTransferParams(array $args): SimpleTransferParams + { + $data = [ + $this->encodeTokenId($args), + $args['amount'], + $args['keepAlive'], + ]; + + return new SimpleTransferParams(...$data); + } + + /** + * Create a new freeze or thaw params object. + */ + public function getFreezeOrThawParams(array $args): FreezeTypeParams + { + $data = [ + 'type' => FreezeType::getEnumCase($args['freezeType']), + 'token' => $this->encodeTokenId($args), + 'account' => null, + ]; + + if (isset($args['collectionAccount']) || isset($args['tokenAccount'])) { + $accountWallet = WalletService::firstOrStore(['public_key' => SS58Address::getPublicKey(Arr::get($args, 'collectionAccount') ?? Arr::get($args, 'tokenAccount'))]); + $data['account'] = $accountWallet->public_key; + } + + return new FreezeTypeParams(...$data); + } + + /** + * Append balance details to the wallet object. + */ + public function walletWithBalanceAndNonce(mixed $wallet): mixed + { + if (!$wallet) { + return null; + } + + if (!is_string($wallet) && null === $wallet->public_key) { + return $wallet; + } + + if (is_string($wallet)) { + $wallet = WalletService::firstOrStore(['public_key' => SS58Address::getPublicKey($wallet)]); + } + + return Cache::remember(PlatformCache::BALANCE->key($wallet->public_key), now()->addSeconds(12), function () use ($wallet) { + $storage = $this->fetchSystemAccount($wallet->public_key); + $accountInfo = $this->codec->decode()->systemAccount($storage); + $wallet->nonce = Arr::get($accountInfo, 'nonce'); + $wallet->balances = Arr::get($accountInfo, 'balances'); + + return $wallet; + }); + } + + /** + * Verify a message signature. + */ + public function verifyMessage(string $message, string $signature, string $publicKey, string $cryptoSignatureType): bool + { + if ($cryptoSignatureType === CryptoSignatureType::SR25519->name) { + $sr = new sr25519(); + + return $sr->VerifySign( + HexConverter::prefix($publicKey), + $message, + HexConverter::prefix($signature) + ); + } + + try { + return sodium_crypto_sign_verify_detached( + sodium_hex2bin(HexConverter::unPrefix($signature)), + sodium_hex2bin(HexConverter::unPrefix($message)), + sodium_hex2bin(HexConverter::unPrefix($publicKey)), + ); + } catch (\Exception $e) { + return false; + } + } + + /** + * Fetch the system account from the chain. + */ + protected function fetchSystemAccount(string $publicKey): mixed + { + return Cache::remember( + PlatformCache::SYSTEM_ACCOUNT->key($publicKey), + now()->addSeconds(12), + fn () => $this->callMethod('state_getStorage', [ + $this->codec->encode()->systemAccountStorageKey($publicKey), + ]) + ); + } +} diff --git a/src/Services/Blockchain/Interfaces/BlockchainServiceInterface.php b/src/Services/Blockchain/Interfaces/BlockchainServiceInterface.php new file mode 100644 index 00000000..08305b0a --- /dev/null +++ b/src/Services/Blockchain/Interfaces/BlockchainServiceInterface.php @@ -0,0 +1,21 @@ +firstOrFail(); + } + + /** + * Create a new collection. + */ + public function store(array $data): Model + { + return Collection::create($data); + } + + /** + * Insert a new collection. + */ + public function insert(array $data): bool + { + return Collection::insert($data); + } + + /** + * Check if the attribute key exists in the collection. + */ + public function attributeExistsInCollection(string $collectionId, string $key): bool + { + return Attribute::with('collection') + ->whereRelation('collection', 'collection_chain_id', $collectionId) + ->where('key', '=', $key) + ->exists(); + } + + /** + * Check if the account exists in the collection. + */ + public function accountExistsInCollection(string $collectionId, string $account): bool + { + $accountWallet = $this->walletService->firstOrStore(['public_key' => SS58Address::getPublicKey($account)]); + + return CollectionAccount::with('collection') + ->whereRelation('collection', 'collection_chain_id', $collectionId) + ->where('wallet_id', '=', $accountWallet->id) + ->exists(); + } + + /** + * Check if the approval exists in the collection. + */ + public function approvalExistsInCollection( + string $collectionId, + string $operator, + bool $hasAccountForDaemon = true, + ): bool { + $operatorWallet = $this->walletService->firstOrStore(['public_key' => SS58Address::getPublicKey($operator)]); + + $collectionAccount = CollectionAccount::with(['collection', 'wallet']) + ->whereRelation('collection', 'collection_chain_id', $collectionId) + ->when( + $hasAccountForDaemon, + fn ($query) => $query->where('wallet_id', '=', Account::daemon()->id) + ) + ->get(); + + if ($collectionAccount->isEmpty()) { + return false; + } + + return CollectionAccountApproval::with(['account', 'wallet']) + ->whereBelongsTo($collectionAccount, 'account') + ->whereBelongsTo($operatorWallet, 'wallet') + ->exists(); + } +} diff --git a/src/Services/Database/MetadataService.php b/src/Services/Database/MetadataService.php new file mode 100644 index 00000000..25980dc4 --- /dev/null +++ b/src/Services/Database/MetadataService.php @@ -0,0 +1,30 @@ +value, FILTER_VALIDATE_URL)) { + return null; + } + + $response = $this->client->fetch($attribute->value); + + return $response ?: null; + } +} diff --git a/src/Services/Database/TokenService.php b/src/Services/Database/TokenService.php new file mode 100644 index 00000000..ad52ebea --- /dev/null +++ b/src/Services/Database/TokenService.php @@ -0,0 +1,173 @@ +public_key; + if (!($accountWallet = Wallet::firstWhere(['public_key' => $publicKey])) + || !($collection = Collection::firstWhere(['collection_chain_id' => $collectionId])) + || !($token = Token::firstWhere(['token_chain_id' => $tokenId, 'collection_id' => $collection->id])) + ) { + return '0'; + } + + $tokenAccount = TokenAccount::whereCollectionId($collection->id) + ->whereTokenId($token->id) + ->whereWalletId($accountWallet->id) + ->first(); + + return (string) ($tokenAccount?->balance ?: 0); + } + + /** + * Check if an account exists in a token. + */ + public function accountExistsInToken(string $collectionId, string $tokenId, string $account): bool + { + $accountWallet = $this->walletService->firstOrStore(['public_key' => SS58Address::getPublicKey($account)]); + if (!($collection = Collection::firstWhere(['collection_chain_id' => $collectionId])) + || !($token = Token::firstWhere(['token_chain_id' => $tokenId, 'collection_id' => $collection->id])) + ) { + return false; + } + + return TokenAccount::whereCollectionId($collection->id) + ->whereTokenId($token->id) + ->whereWalletId($accountWallet->id) + ->exists(); + } + + /** + * Check if an attribute key exists in a token. + */ + public function attributeExistsInToken(string $collectionId, string $tokenId, string $key): bool + { + if (!($collection = Collection::firstWhere(['collection_chain_id' => $collectionId])) + || !($token = Token::firstWhere(['token_chain_id' => $tokenId, 'collection_id' => $collection->id])) + ) { + return false; + } + + return Attribute::whereCollectionId($collection->id) + ->whereTokenId($token->id) + ->where('key', '=', $key) + ->exists(); + } + + /** + * Check if a token exists in a collection. + */ + public function tokenExistsInCollection(string $tokenId, $collectionId) + { + return $this->inCollection($collectionId) + ->where('token_chain_id', $tokenId) + ->exists(); + } + + /** + * Check if an operator has approval for an account in a token. + */ + public function approvalExistsInToken(string $collectionId, string $tokenId, string $operator): bool + { + $operatorWallet = $this->walletService->firstOrStore(['public_key' => SS58Address::getPublicKey($operator)]); + if (!($collection = Collection::firstWhere(['collection_chain_id' => $collectionId])) + || !($token = Token::firstWhere(['token_chain_id' => $tokenId, 'collection_id' => $collection->id])) + ) { + return false; + } + + $tokenAccount = TokenAccount::whereCollectionId($collection->id) + ->whereTokenId($token->id) + ->where('wallet_id', '=', Account::daemon()->id) + ->first(); + + if (!$tokenAccount) { + return false; + } + + return TokenAccountApproval::where('token_account_id', $tokenAccount->id) + ->where('wallet_id', $operatorWallet->id) + ->exists(); + } + + /** + * Get a token from a collection. + */ + public function getTokenFromCollection(string $tokenId, $collectionId): Model + { + $token = $this->inCollection($collectionId) + ->where('token_chain_id', $tokenId) + ->first(); + + if (!$token) { + throw new PlatformException(__('enjin-platform::error.token_not_found'), 404); + } + + return $token; + } + + public function getTokensFromCollection($collectionId, ?array $tokenIds = null, $paginationLimit = null): array + { + return $this->inCollection($collectionId) + ->when($tokenIds ?? false, function (Builder $query) use ($tokenIds) { + $query->whereIn('token_chain_id', $tokenIds); + })->cursorPaginateWithTotalDesc('id', $paginationLimit ?? config('enjin-platform.pagination.limit')); + } + + /** + * Get the generic collection query builder. + */ + protected function inCollection($collectionId): Builder + { + return Token::with('collection')->whereRelation('collection', 'collection_chain_id', $collectionId); + } +} diff --git a/src/Services/Database/TransactionService.php b/src/Services/Database/TransactionService.php new file mode 100644 index 00000000..9f8ba75a --- /dev/null +++ b/src/Services/Database/TransactionService.php @@ -0,0 +1,67 @@ + $key])->first(); + + if (!$transaction) { + throw new PlatformException(__('enjin-platform::error.transaction_not_found'), 404); + } + + return $transaction; + } + + /** + * Create a new transaction. + */ + public function store(array $data, ?Wallet $signingWallet = null): Model + { + if ($transaction = Transaction::firstWhere(['idempotency_key' => $data['idempotency_key']])) { + return $transaction; + } + + $data['wallet_public_key'] = $signingWallet->public_key ?? Account::daemon()->public_key; + $data['method'] = $data['method'] ?? ''; + + $transaction = Transaction::create($data); + + TransactionCreated::safeBroadcast($transaction); + + return $transaction; + } + + /** + * Update a transaction. + */ + public function update($transaction, array $data): bool + { + $transaction->fill($data)->save(); + + TransactionUpdated::safeBroadcast($transaction->refresh()); + + return $transaction->wasChanged(); + } +} diff --git a/src/Services/Database/VerificationService.php b/src/Services/Database/VerificationService.php new file mode 100644 index 00000000..dde9d159 --- /dev/null +++ b/src/Services/Database/VerificationService.php @@ -0,0 +1,172 @@ +network = config("enjin-platform.chains.supported.{$chain}.{$network}.network-id"); + $this->platform = config("enjin-platform.chains.supported.{$chain}.{$network}.platform-id"); + + $this->walletService = $walletService; + $this->blockchainService = $blockchainService; + } + + /** + * Get a verification by column and value. + */ + public function get(string $key, string $column = 'verification_id'): Model + { + $verification = Verification::find([$column => $key]); + if (!$verification) { + throw new PlatformException(__('enjin-platform::error.verification.verification_not_found'), 404); + } + + return $verification; + } + + /** + * Create a new verification. + */ + public function store(array $data): Model + { + return Verification::create($data); + } + + /** + * Update a verification. + */ + public function update(Model $verification, array $data): bool + { + return $verification + ->fill($data) + ->save(); + } + + /** + * Verify a verification. + */ + public function verify(string $verificationId, string $signature, string $address, string $cryptoSignatureType): bool + { + $verification = Verification::where(['verification_id' => $verificationId])->firstOrFail(); + $publicKey = SS58Address::getPublicKey($address); + $message = HexConverter::prefix(Blake2::hash(HexConverter::stringToHex('Enjin Signed Message:' . $verification->code))); + $isValid = $this->blockchainService->verifyMessage($message, $signature, $publicKey, $cryptoSignatureType); + + if (!$isValid) { + throw new PlatformException(__('enjin-platform::error.verification.invalid_signature')); + } + + $wallet = Wallet::query()->firstWhere(['public_key' => $publicKey]); + if (empty($wallet)) { + $wallet = $this->walletService->firstOrStore(['verification_id' => $verificationId]); + } + + $this->update($verification, ['public_key' => $publicKey]); + $this->walletService->update($wallet, [ + 'public_key' => $publicKey, + 'verification_id' => $verificationId, + ]); + + return true; + } + + /** + * Generate a readable string using all upper case letters that are easy to recognize. + */ + public function generate(): array + { + $verificationId = $this->generateVerificationId(); + + while (Verification::firstWhere(['verification_id' => $verificationId])) { + // TODO: Should report this as in theory this should not happen. + $verificationId = $this->generateVerificationId(); + } + + return [ + 'verification_id' => $verificationId, + 'code' => $this->generateCode(), + ]; + } + + /** + * Generate a QR code for a verification. + */ + public function qr(string $verificationId, string $code, string $callback, int $size = 512): string + { + $encodedCallback = base64_encode($callback); + $deepLink = config('enjin-platform.deep_links.proof') . "{$verificationId}:{$code}:{$encodedCallback}"; + + return "https://chart.googleapis.com/chart?chs={$size}x{$size}&cht=qr&chl={$deepLink}"; + } + + /** + * Generate a random verification ID. + */ + private function generateVerificationId(): string + { + try { + $randomBytes = random_bytes(SODIUM_CRYPTO_SIGN_SEEDBYTES); + sodium_crypto_sign_seed_keypair($randomBytes); + $keypair = sodium_crypto_sign_keypair(); + $key = sodium_crypto_sign_publickey($keypair); + $hexed = sodium_bin2hex($key); + + return HexConverter::prefix($hexed); + } catch (\Exception $e) { + throw new PlatformException(__('enjin-platform::error.verification.unable_to_generate_verification_id')); + } + } + + /** + * Generate a random code. + */ + private function generateCode(): string + { + $code = $this->num2alpha($this->platform) . $this->network . $this->letters[random_int(0, strlen($this->letters) - 1)]; + + for ($i = 0; $i < 3; $i++) { + $code .= $this->alphanumerics[random_int(0, strlen($this->alphanumerics) - 1)]; + } + + return $code; + } + + /** + * Converts an integer into the alphabet base (A-Z). + */ + private function num2alpha($n): string + { + $r = ''; + + for ($i = 1; $n >= 0 && $i < 10; $i++) { + $r = chr(0x41 + ($n % 26 ** $i / 26 ** ($i - 1))) . $r; + $n -= 26 ** $i; + } + + return $r; + } +} diff --git a/src/Services/Database/WalletService.php b/src/Services/Database/WalletService.php new file mode 100644 index 00000000..057135b4 --- /dev/null +++ b/src/Services/Database/WalletService.php @@ -0,0 +1,64 @@ + Wallet::where(['public_key' => SS58Address::getPublicKey($key)])->firstOrFail(), + default => Wallet::where([$column => $key])->firstOrFail(), + }; + } + + /** + * Create a new wallet. + */ + public function store(array $data): Model + { + $data['network'] = $data['network'] ?? config('enjin-platform.chains.network'); + + return Wallet::create($data); + } + + /** + * Find or insert a new wallet. + */ + public function firstOrStore(array $key, $data = []): Model + { + if (isset($key['account'])) { + $key['public_key'] = SS58Address::getPublicKey($key['account']); + unset($key['account']); + } + + $data['network'] = $data['network'] ?? config('enjin-platform.chains.network'); + + return Wallet::firstOrCreate($key, $data); + } + + /** + * Check if the account exists in the wallet. + */ + public function accountExistsInWallet(string $account): bool + { + return Wallet::where(['public_key' => SS58Address::getPublicKey($account)])->exists(); + } + + /** + * Update a wallet. + */ + public function update(Model $wallet, array $data): bool + { + return $wallet + ->fill($data) + ->save(); + } +} diff --git a/src/Services/Processor/Substrate/BlockProcessor.php b/src/Services/Processor/Substrate/BlockProcessor.php new file mode 100644 index 00000000..8b835e2f --- /dev/null +++ b/src/Services/Processor/Substrate/BlockProcessor.php @@ -0,0 +1,332 @@ +input = new ArgvInput(); + $this->output = new BufferedConsoleOutput(); + $this->codec = new Codec(); + $this->persistedClient = new Substrate(new SubstrateWebsocket()); + } + + public function latestBlock(): int|null + { + try { + if ($currentBlock = $this->persistedClient->callMethod('chain_getHeader')) { + return (int) HexConverter::hexToUInt($currentBlock['number']); + } + } catch (Throwable $e) { + $this->error($e->getMessage()); + } + + return null; + } + + public function latestSyncedBlock(): int + { + return Block::where('synced', true)->max('number') ?? 0; + } + + public function checkParentBlocks(string $heightHexed) + { + $this->warn('Making sure no blocks were left behind'); + $lastBlockSynced = $this->latestSyncedBlock(); + $blockBeforeSubscription = HexConverter::hexToUInt($heightHexed) - 1; + $this->warn("Last block synced: {$lastBlockSynced}"); + $this->warn("Block before subscription: {$blockBeforeSubscription}"); + + if ($blockBeforeSubscription > $lastBlockSynced) { + $this->warn('Processing blocks left behind'); + $this->fetchPastHeads($lastBlockSynced, $blockBeforeSubscription); + $this->warn('Finished processing blocks left behind'); + } + + $this->hasCheckedSubBlocks = true; + $this->warn('Starting processing blocks from subscription'); + } + + public function getHashWhenBlockIsFinalized(int $blockNumber): string + { + while (true) { + $blockHash = $this->persistedClient->callMethod('chain_getBlockHash', [$blockNumber]); + if ($blockHash) { + $this->persistedClient->getClient()->close(); + + return $blockHash; + } + usleep(100000); + } + } + + public function subscribeToNewHeads(): void + { + $sub = new Substrate(new SubstrateWebsocket()); + $this->warn('Starting subscription to new heads'); + + try { + $sub->callMethod('chain_subscribeNewHeads'); + while (true) { + if ($response = $sub->getClient()->receive()) { + $syncTime = now(); + $result = Arr::get(JSON::decode($response, true), 'params.result'); + $heightHexed = Arr::get($result, 'number'); + + if (null === $heightHexed) { + continue; + } + + if (!$this->hasCheckedSubBlocks) { + $this->checkParentBlocks($heightHexed); + } + + $blockNumber = HexConverter::hexToUInt($heightHexed); + $blockHash = $this->getHashWhenBlockIsFinalized($blockNumber); + + $this->pauseWhenSynching(); + + $block = Block::updateOrCreate( + ['number' => $blockNumber], + ['hash' => $blockHash], + ); + + PlatformBlockIngesting::dispatch($block); + + $this->info(sprintf('Ingested header for block #%s in %s seconds', $blockNumber, now()->diffInMilliseconds($syncTime) / 1000)); + + $this->fetchEvents($block); + $this->fetchExtrinsics($block); + $this->process($block); + + PlatformBlockIngested::dispatch($block); + } + } + } finally { + $sub->getClient()->close(); + } + } + + public function ingest(): void + { + $currentHeight = $this->latestBlock(); + $lastBlockSynced = $this->latestSyncedBlock(); + + $this->info('================ Starting Substrate Ingest ================'); + $this->info("Current block on-chain: {$currentHeight}"); + $this->info('Last ingested block: ' . $lastBlockSynced ?: 'No blocks ingested'); + $this->info('========================================================='); + + $this->startIngest($lastBlockSynced, $currentHeight); + + $this->info('An error has occurred the ingest process has been stopped.'); + } + + public function fetchPastHeads(int $startingHeight, int $currentHeight): void + { + $numOfBlocks = $currentHeight - $startingHeight; + if ($numOfBlocks <= 0) { + return; + } + + $startBlock = $startingHeight + 1; + $this->fetchPreviousBlockHeads($startBlock, $currentHeight); + + $newCurrentHeight = $this->latestBlock(); + $this->warn("Current block on-chain: {$newCurrentHeight} - Last block processed: {$currentHeight}"); + $this->fetchPastHeads($currentHeight, $newCurrentHeight); + } + + public function process(Block $block): Block|null + { + try { + $blockNumber = $block->number; + $syncTime = now(); + + if ($block->synced) { + $this->info("Block #{$blockNumber} already processed, skipping"); + + return $block; + } + + $this->info("Processing block #{$blockNumber} ({$block->hash})"); + + (new EventProcessor($block, $this->codec))->run(); + (new ExtrinsicProcessor($block, $this->codec))->run(); + + $block->fill(['synced' => true, 'failed' => false, 'exception' => null])->save(); + $this->info(sprintf("Process completed for block #{$blockNumber} in %s seconds", now()->diffInMilliseconds($syncTime) / 1000)); + } catch (Throwable $exception) { + $this->error("Failed processing block #{$blockNumber}"); + $exception = sprintf('%s: %s (Line %s in %s)', get_class($exception), $exception->getMessage(), $exception->getLine(), $exception->getFile()); + $block->fill(['synced' => true, 'failed' => true, 'exception' => $exception])->save(); + } + + return $block; + } + + /** + * Check if synching is in progress. + */ + public static function isSynching(): bool + { + return (bool) Cache::get(PlatformCache::SYNCING_IN_PROGRESS->key()); + } + + /** + * Set flag to indicate synching is in progress. + */ + public static function synching(): void + { + Cache::put(PlatformCache::SYNCING_IN_PROGRESS->key(), true); + } + + /** + * Remove flag to indicate synching is done. + */ + public static function synchingDone(): void + { + Cache::forget(PlatformCache::SYNCING_IN_PROGRESS->key()); + } + + protected function startIngest(int $lastBlockSynced, int $currentHeight): void + { + try { + $this->fetchPastHeads($lastBlockSynced, $currentHeight); + $this->subscribeToNewHeads(); + } catch(RestartIngestException) { + $this->startIngest( + $this->latestSyncedBlock(), + $this->latestBlock() + ); + } + } + + protected function fetchPreviousBlockHeads(int $blockNumber, int $blockLimit): void + { + while ($blockNumber <= $blockLimit) { + $this->pauseWhenSynching(); + + $syncTime = now(); + $block = Block::updateOrCreate( + ['number' => $blockNumber], + ['hash' => $this->persistedClient->callMethod('chain_getBlockHash', [$blockNumber])], + ); + + PlatformBlockIngesting::dispatch($block); + + $this->info(sprintf('Ingested header for block #%s in %s seconds', $block->number, now()->diffInMilliseconds($syncTime) / 1000)); + + $this->fetchEvents($block); + $this->fetchExtrinsics($block); + $this->process($block); + + PlatformBlockIngested::dispatch($block); + + $blockNumber++; + } + + $this->warn('Finished fetching past block heads'); + } + + protected function setBlockEvent(Substrate $blockchain, Block $block): Block + { + if ($events = $blockchain->callMethod('state_getStorage', [StorageKey::EVENTS->value, $block->hash])) { + $block->events = State::eventsForBlock(['number' => $block->number, 'events' => $events]) ?? []; + } + + return $block; + } + + protected function setBlockExtrinsic(Substrate $blockchain, Block $block): Block + { + $data = $blockchain->callMethod('chain_getBlock', [$block->hash]); + if ($extrinsics = Arr::get($data, 'block.extrinsics')) { + $block->extrinsics = State::extrinsicsForBlock(['number' => $block->number, 'extrinsics' => json_encode($extrinsics)]) ?? []; + } + + return $block; + } + + protected function fetchEvents(Block $block): Block + { + $syncTime = now(); + $block = $this->setBlockEvent($this->persistedClient, $block); + + $this->info(sprintf('Ingested events for block #%s in %s seconds', $block->number, now()->diffInMilliseconds($syncTime) / 1000)); + + return $block; + } + + protected function fetchExtrinsics(Block $block): Block + { + $syncTime = now(); + $block = $this->setBlockExtrinsic($this->persistedClient, $block); + + $this->info(sprintf('Ingested extrinsics for block #%s in %s seconds', $block->number, now()->diffInMilliseconds($syncTime) / 1000)); + + return $block; + } + + /** + * Pause the ingest process when the sync is running. + */ + protected function pauseWhenSynching(): void + { + if (static::isSynching()) { + $this->info('Pausing ingest, waiting for sync to complete...'); + $counter = 1; + while (static::isSynching()) { + sleep(static::SYNC_WAIT_DELAY); + if ($counter * static::SYNC_WAIT_DELAY >= config('enjin-platform.sync_max_wait_timeout')) { + $this->warn('Sync has taken too long, forcing to restart ingest...'); + + $result = Process::pipe(function (Pipe $pipe) { + $pipe->command('ps aux'); + $pipe->command('grep platform:sync'); + }); + if ($result->successful() && empty($result->output())) { + $this->warn('Sync is not running, updating flag to false...'); + static::synchingDone(); + } + + break; + } + $counter++; + } + $this->info('Sync completed, restarting ingest...'); + + throw new RestartIngestException(); + } + } +} diff --git a/src/Services/Processor/Substrate/Codec/Codec.php b/src/Services/Processor/Substrate/Codec/Codec.php new file mode 100644 index 00000000..74e787f4 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Codec.php @@ -0,0 +1,55 @@ +loadCustomTypes()); + $this->scaleInstance = new ScaleInstance($generator); + $this->decoder = new Decoder($this->scaleInstance); + $this->encoder = new Encoder($this->scaleInstance); + } + + public function decode(): Decoder + { + return $this->decoder; + } + + public function encode(): Encoder + { + return $this->encoder; + } + + private function loadCustomTypes() + { + return Cache::remember(PlatformCache::CUSTOM_TYPES->key(), 86400, function () { + $moduleFiles = array_filter(Utils::getDirContents(__DIR__ . '/Types/'), function ($var) { + $slice = explode('.', $var); + + return $slice[count($slice) - 1] === 'json'; + }); + $moduleTypes = []; + foreach ($moduleFiles as $file) { + $content = JSON::decode(file_get_contents($file), true); + // merge all array to one $moduleTypes array + $moduleTypes = array_merge($moduleTypes, $content); + } + // reg custom type + return $moduleTypes; + }); + } +} diff --git a/src/Services/Processor/Substrate/Codec/Decoder.php b/src/Services/Processor/Substrate/Codec/Decoder.php new file mode 100644 index 00000000..543a9eff --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Decoder.php @@ -0,0 +1,302 @@ +codec = $codec; + } + + public function compact(string $data) + { + return $this->codec->process('Compact', new ScaleBytes($data)); + } + + public function systemAccount(?string $data = null): array + { + $decoded = $data === null ? null : $this->codec->process('AccountInfoWithTripleRefCount', new ScaleBytes($data)); + + return [ + 'nonce' => Arr::get($decoded, 'nonce', 0), + 'consumers' => Arr::get($decoded, 'consumers', 0), + 'providers' => Arr::get($decoded, 'providers', 0), + 'sufficients' => Arr::get($decoded, 'sufficients', 0), + 'balances' => [ + 'free' => gmp_strval(Arr::get($decoded, 'data.free', '0')), + 'reserved' => gmp_strval(Arr::get($decoded, 'data.reserved', '0')), + 'miscFrozen' => gmp_strval(Arr::get($decoded, 'data.miscFrozen', '0')), + 'feeFrozen' => gmp_strval(Arr::get($decoded, 'data.feeFrozen', '0')), + ], + ]; + } + + public function createCollection(string $data): array + { + $decoded = $this->codec->process('CreateCollection', new ScaleBytes($data)); + + return [ + 'mintPolicy' => MintPolicyParams::fromEncodable(Arr::get($decoded, 'descriptor.policy.mint'))->toArray(), + 'marketPolicy' => ($royalty = Arr::get($decoded, 'descriptor.policy.market')) !== null + ? RoyaltyPolicyParams::fromEncodable($royalty)->toArray() + : null, + ]; + } + + public function destroyCollection(string $data): array + { + $decoded = $this->codec->process('DestroyCollection', new ScaleBytes($data)); + + return [ + 'collectionId' => gmp_strval(Arr::get($decoded, 'collectionId')), + ]; + } + + public function mint(string $data): array + { + $decoded = $this->codec->process('Mint', new ScaleBytes($data)); + $params = Arr::get($decoded, 'params'); + + return [ + 'recipientId' => ($recipient = Arr::get($decoded, 'recipient.Id')) !== null ? HexConverter::prefix($recipient) : null, + 'collectionId' => gmp_strval(Arr::get($decoded, 'collectionId')), + 'params' => Arr::exists($params, 'CreateToken') ? + CreateTokenParams::fromEncodable(Arr::get($params, 'CreateToken'))->toArray() + : + MintParams::fromEncodable(Arr::get($params, 'Mint'))->toArray(), + ]; + } + + public function burn(string $data): array + { + $decoded = $this->codec->process('Burn', new ScaleBytes($data)); + + return [ + 'collectionId' => gmp_strval(Arr::get($decoded, 'collectionId')), + 'tokenId' => gmp_strval(Arr::get($decoded, 'params.tokenId')), + 'amount' => gmp_strval(Arr::get($decoded, 'params.amount')), + 'keepAlive' => Arr::get($decoded, 'params.keepAlive'), + 'removeTokenStorage' => Arr::get($decoded, 'params.removeTokenStorage'), + ]; + } + + public function freeze(string $data): array + { + $decoded = $this->codec->process('Freeze', new ScaleBytes($data)); + + return [ + 'collectionId' => gmp_strval(Arr::get($decoded, 'collectionId')), + 'freezeType' => FreezeTypeParams::fromEncodable(Arr::get($decoded, 'freezeType'))->toArray(), + ]; + } + + public function thaw(string $data): array + { + $decoded = $this->codec->process('Thaw', new ScaleBytes($data)); + + return [ + 'collectionId' => gmp_strval(Arr::get($decoded, 'collectionId')), + 'freezeType' => FreezeTypeParams::fromEncodable(Arr::get($decoded, 'freezeType'))->toArray(), + ]; + } + + public function setAttribute(string $data): array + { + $decoded = $this->codec->process('SetAttribute', new ScaleBytes($data)); + + return [ + 'collectionId' => gmp_strval(Arr::get($decoded, 'collectionId')), + 'tokenId' => ($value = Arr::get($decoded, 'tokenId')) !== null ? gmp_strval($value) : null, + 'key' => HexConverter::hexToString(Arr::get($decoded, 'key')), + 'value' => HexConverter::hexToString(Arr::get($decoded, 'value')), + ]; + } + + public function removeAttribute(string $data): array + { + $decoded = $this->codec->process('RemoveAttribute', new ScaleBytes($data)); + + return [ + 'collectionId' => gmp_strval(Arr::get($decoded, 'collectionId')), + 'tokenId' => ($value = Arr::get($decoded, 'tokenId')) !== null ? gmp_strval($value) : null, + 'key' => HexConverter::hexToString(Arr::get($decoded, 'key')), + ]; + } + + public function bytes(string $data) + { + return $this->codec->process('Bytes', new ScaleBytes($data)); + } + + public function attributeStorageKey(string $data): array + { + $decoded = $this->codec->process('AttributeStorage', new ScaleBytes($data)); + + return [ + 'collectionId' => gmp_strval(Arr::get($decoded, 'collectionId')), + 'tokenId' => gmp_strval(Arr::get($decoded, 'tokenId')), + 'attribute' => Arr::get($decoded, 'attribute'), + ]; + } + + public function collectionStorageKey(string $data): array + { + $decoded = $this->codec->process('CollectionStorageKey', new ScaleBytes($data)); + + return [ + 'collectionId' => gmp_strval(Arr::get($decoded, 'collectionId')), + ]; + } + + public function collectionStorageData(string $data): array + { + $decoded = $this->codec->process('CollectionStorageData', new ScaleBytes($data)); + + return [ + 'owner' => ($owner = Arr::get($decoded, 'owner')) !== null ? HexConverter::prefix($owner) : null, + 'maxTokenCount' => ($value = Arr::get($decoded, 'policy.mint.maxTokenCount')) !== null ? gmp_strval($value) : null, + 'maxTokenSupply' => ($value = Arr::get($decoded, 'policy.mint.maxTokenSupply')) !== null ? gmp_strval($value) : null, + 'forceSingleMint' => Arr::get($decoded, 'policy.mint.forceSingleMint'), + 'burn' => Arr::get($decoded, 'policy.burn'), + 'isFrozen' => Arr::get($decoded, 'policy.transfer.isFrozen'), + 'royaltyBeneficiary' => ($beneficiary = Arr::get($decoded, 'policy.market.royalty.beneficiary')) !== null ? HexConverter::prefix($beneficiary) : null, + 'royaltyPercentage' => ($percentage = Arr::get($decoded, 'policy.market.royalty.percentage')) !== null ? $percentage / 10 ** 7 : null, + 'attribute' => Arr::get($decoded, 'policy.attribute'), + 'tokenCount' => gmp_strval(Arr::get($decoded, 'tokenCount')), + 'attributeCount' => gmp_strval(Arr::get($decoded, 'attributeCount')), + 'totalDeposit' => gmp_strval(Arr::get($decoded, 'totalDeposit')), + 'explicitRoyaltyCurrencies' => Arr::get($decoded, 'explicitRoyaltyCurrencies'), + ]; + } + + public function tokenStorageKey(string $data): array + { + $decoded = $this->codec->process('TokenStorageKey', new ScaleBytes($data)); + + return [ + 'collectionId' => gmp_strval(Arr::get($decoded, 'collectionId')), + 'tokenId' => gmp_strval(Arr::get($decoded, 'tokenId')), + ]; + } + + public function tokenStorageData(string $data): array + { + $decoded = $this->codec->process('CanaryTokenStorageData', new ScaleBytes($data)); + $cap = TokenMintCapType::tryFrom(collect(Arr::get($decoded, 'cap'))->keys()->first()) ?? TokenMintCapType::INFINITE; + $isCurrency = Arr::exists(Arr::get($decoded, 'marketBehavior') ?: [], 'IsCurrency'); + $isFrozen = in_array(Arr::get($decoded, 'freezeState'), ['Permanent', 'Temporary']); + $unitPrice = Arr::get($decoded, 'sufficiency.Insufficient'); + + return [ + 'supply' => gmp_strval(Arr::get($decoded, 'supply')), + 'cap' => $cap, + 'capSupply' => ($supply = Arr::get($decoded, 'cap.Supply')) !== null ? gmp_strval($supply) : null, + 'isFrozen' => $isFrozen, + 'royaltyBeneficiary' => ($beneficiary = Arr::get($decoded, 'marketBehavior.HasRoyalty.beneficiary')) !== null ? HexConverter::prefix($beneficiary) : null, + 'royaltyPercentage' => ($percentage = Arr::get($decoded, 'marketBehavior.HasRoyalty.percentage')) !== null ? $percentage / 10 ** 7 : null, + 'isCurrency' => $isCurrency, + 'listingForbidden' => Arr::get($decoded, 'listingForbidden'), + 'minimumBalance' => gmp_strval(Arr::get($decoded, 'minimumBalance')), + 'unitPrice' => gmp_strval($unitPrice), + 'mintDeposit' => gmp_strval(Arr::get($decoded, 'mintDeposit')), + 'attributeCount' => gmp_strval(Arr::get($decoded, 'attributeCount')), + ]; + } + + public function collectionAccountStorageKey(string $data): array + { + $decoded = $this->codec->process('CollectionAccountsStorageKey', new ScaleBytes($data)); + + return [ + 'collectionId' => gmp_strval(Arr::get($decoded, 'collectionId')), + 'accountId' => HexConverter::prefix(Arr::get($decoded, 'accountId')), + ]; + } + + public function collectionAccountStorageData(string $data): array + { + $decoded = $this->codec->process('CollectionAccountsStorageData', new ScaleBytes($data)); + + $approvals = collect(Arr::get($decoded, 'approvals'))->map( + fn ($expiration, $account) => [ + 'accountId' => HexConverter::prefix($account), + 'expiration' => $expiration !== null ? gmp_strval($expiration) : null, + ] + )->values()->toArray(); + + return [ + 'isFrozen' => Arr::get($decoded, 'isFrozen'), + 'approvals' => $approvals, + 'accountCount' => gmp_strval(Arr::get($decoded, 'accountCount')), + ]; + } + + public function tokenAccountStorageKey(string $data): array + { + $decoded = $this->codec->process('CanaryTokenAccountsStorageKey', new ScaleBytes($data)); + + return [ + 'accountId' => HexConverter::prefix(Arr::get($decoded, 'accountId')), + 'collectionId' => gmp_strval(Arr::get($decoded, 'collectionId')), + 'tokenId' => gmp_strval(Arr::get($decoded, 'tokenId')), + ]; + } + + public function tokenAccountStorageData(string $data): array + { + $decoded = $this->codec->process('TokenAccountsStorageData', new ScaleBytes($data)); + + $approvals = collect(Arr::get($decoded, 'approvals'))->map( + fn ($approval, $account) => [ + 'accountId' => HexConverter::prefix($account), + 'amount' => gmp_strval($approval['amount']), + 'expiration' => ($expiration = $approval['expiration']) !== null ? gmp_strval($expiration) : null, + ] + )->values()->toArray(); + + $namedReserves = collect(Arr::get($decoded, 'namedReserves'))->map( + fn ($reserve, $pallet) => [ + 'pallet' => PalletIdentifier::fromHex($pallet), + 'amount' => gmp_strval($reserve), + ] + )->values()->toArray(); + + return [ + 'balance' => gmp_strval(Arr::get($decoded, 'balance')), + 'reservedBalance' => gmp_strval(Arr::get($decoded, 'reservedBalance')), + 'lockedBalance' => gmp_strval(Arr::get($decoded, 'lockedBalance')), + 'namedReserves' => $namedReserves, + 'approvals' => $approvals, + 'isFrozen' => Arr::get($decoded, 'isFrozen'), + ]; + } + + public function getMethodFromEncoded(string $data) + { + $metadata = Cache::remember(PlatformCache::CALL_INDEXES->key(), 86400, function () { + return collect($this->codec->process('metadata', new ScaleBytes(metadata('metadata')))['metadata']['call_index']); + }); + + $callIndex = substr($data, 2, 4); + + return Str::studly($metadata[$callIndex]['call']['name']); + } +} diff --git a/src/Services/Processor/Substrate/Codec/Encoder.php b/src/Services/Processor/Substrate/Codec/Encoder.php new file mode 100644 index 00000000..fdfaac17 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Encoder.php @@ -0,0 +1,480 @@ +scaleInstance = $scaleInstance; + $this->callIndexes = $this->loadCallIndexes(); + } + + public function transferBalance(string $recipient, string $value): string + { + $encoded = $this->scaleInstance->createTypeByTypeString('TransferBalance')->encode([ + 'callIndex' => $this->getCallIndex('Balances.transfer'), + 'dest' => [ + 'Id' => HexConverter::unPrefix($recipient), + ], + 'value' => gmp_init($value), + ]); + + return HexConverter::prefix($encoded); + } + + public function transferBalanceKeepAlive(string $recipient, string $value): string + { + $encoded = $this->scaleInstance->createTypeByTypeString('TransferBalanceKeepAlive')->encode([ + 'callIndex' => $this->getCallIndex('Balances.transfer_keep_alive'), + 'dest' => [ + 'Id' => HexConverter::unPrefix($recipient), + ], + 'value' => gmp_init($value), + ]); + + return HexConverter::prefix($encoded); + } + + public function transferAllBalance(string $recipient, ?bool $keepAlive = false): string + { + $encoded = $this->scaleInstance->createTypeByTypeString('TransferAllBalance')->encode([ + 'callIndex' => $this->getCallIndex('Balances.transfer_all'), + 'dest' => [ + 'Id' => HexConverter::unPrefix($recipient), + ], + 'keepAlive' => $keepAlive, + ]); + + return HexConverter::prefix($encoded); + } + + public function systemAccountStorageKey(string $publicKey): string + { + $publicKey = HexConverter::unPrefix($publicKey); + $keyHashed = Blake2::hash($publicKey, 128); + $key = StorageKey::SYSTEM_ACCOUNT->value . $keyHashed . $publicKey; + + return HexConverter::prefix($key); + } + + public function approveCollection(string $collectionId, string $operator, ?int $expiration = null): string + { + $encoded = $this->scaleInstance->createTypeByTypeString('ApproveCollection')->encode([ + 'callIndex' => $this->getCallIndex('MultiTokens.approve_collection'), + 'collectionId' => gmp_init($collectionId), + 'operator' => HexConverter::unPrefix($operator), + 'expiration' => $expiration, + ]); + + return HexConverter::prefix($encoded); + } + + public function unapproveCollection(string $collectionId, string $operator): string + { + $encoded = $this->scaleInstance->createTypeByTypeString('UnapproveCollection')->encode([ + 'callIndex' => $this->getCallIndex('MultiTokens.unapprove_collection'), + 'collectionId' => gmp_init($collectionId), + 'operator' => HexConverter::unPrefix($operator), + ]); + + return HexConverter::prefix($encoded); + } + + public function approveToken(string $collectionId, string $tokenId, string $operator, string $amount, string $currentAmount, ?int $expiration = null): string + { + $encoded = $this->scaleInstance->createTypeByTypeString('ApproveToken')->encode([ + 'callIndex' => $this->getCallIndex('MultiTokens.approve_token'), + 'collectionId' => gmp_init($collectionId), + 'tokenId' => gmp_init($tokenId), + 'operator' => HexConverter::unPrefix($operator), + 'amount' => gmp_init($amount), + 'expiration' => $expiration, + 'currentAmount' => gmp_init($currentAmount), + ]); + + return HexConverter::prefix($encoded); + } + + public function unapproveToken(string $collectionId, string $tokenId, string $operator): string + { + $encoded = $this->scaleInstance->createTypeByTypeString('UnapproveToken')->encode([ + 'callIndex' => $this->getCallIndex('MultiTokens.unapprove_token'), + 'collectionId' => gmp_init($collectionId), + 'tokenId' => gmp_init($tokenId), + 'operator' => HexConverter::unPrefix($operator), + ]); + + return HexConverter::prefix($encoded); + } + + public function batch(array $calls, bool $continueOnFailure): string + { + $callIndex = $this->callIndexes['EfinityUtility.batch']; + $numberOfCalls = $this->scaleInstance->createTypeByTypeString('Compact')->encode(count($calls)); + $calls = str_replace('0x', '', implode('', $calls)); + $continueOnFailure = $continueOnFailure ? '01' : '00'; + $encoded = $callIndex . $numberOfCalls . $calls . $continueOnFailure; + + return HexConverter::prefix($encoded); + } + + public function batchSetAttribute(string $collectionId, ?string $tokenId, array $attributes) + { + $encoded = $this->scaleInstance->createTypeByTypeString('BatchSetAttribute')->encode([ + 'callIndex' => $this->getCallIndex('MultiTokens.batch_set_attribute'), + 'collectionId' => gmp_init($collectionId), + 'tokenId' => $tokenId !== null ? gmp_init($tokenId) : null, + 'attributes' => array_map( + fn ($attribute) => [ + 'key' => HexConverter::stringToHexPrefixed($attribute['key']), + 'value' => HexConverter::stringToHexPrefixed($attribute['value']), + ], + $attributes + ), + ]); + + return HexConverter::prefix($encoded); + } + + public function batchTransfer(string $collectionId, array $recipients) + { + $encoded = $this->scaleInstance->createTypeByTypeString('BatchTransfer')->encode([ + 'callIndex' => $this->getCallIndex('MultiTokens.batch_transfer'), + 'collectionId' => gmp_init($collectionId), + 'recipients' => array_map( + fn ($item) => [ + 'accountId' => HexConverter::unPrefix($item['accountId']), + 'params' => $item['params']->toEncodable(), + ], + $recipients + ), + ]); + + return HexConverter::prefix($encoded); + } + + public function transferToken(string $recipient, string $collectionId, SimpleTransferParams|OperatorTransferParams $params): string + { + $encoded = $this->scaleInstance->createTypeByTypeString('Transfer')->encode([ + 'callIndex' => $this->getCallIndex('MultiTokens.transfer'), + 'recipient' => [ + 'Id' => HexConverter::unPrefix($recipient), + ], + 'collectionId' => gmp_init($collectionId), + 'params' => $params->toEncodable(), + ]); + + return HexConverter::prefix($encoded); + } + + public function createCollection(MintPolicyParams $mintPolicy, ?RoyaltyPolicyParams $marketPolicy = null, ?array $explicitRoyaltyCurrencies = [], ?array $attributes = []): string + { + $encoded = $this->scaleInstance->createTypeByTypeString('CreateCollection')->encode( + [ + 'callIndex' => $this->getCallIndex('MultiTokens.create_collection'), + 'descriptor' => [ + 'policy' => [ + 'mint' => $mintPolicy->toEncodable(), + 'market' => $marketPolicy?->toEncodable(), + ], + 'explicitRoyaltyCurrencies' => array_map( + fn ($multiToken) => [ + 'collectionId' => gmp_init($multiToken['collectionId']), + 'tokenId' => gmp_init($multiToken['tokenId']), + ], + $explicitRoyaltyCurrencies + ), + 'attributes' => array_map( + fn ($attribute) => [ + 'key' => HexConverter::stringToHexPrefixed($attribute['key']), + 'value' => HexConverter::stringToHexPrefixed($attribute['value']), + ], + $attributes + ), + ], + ] + ); + + return HexConverter::prefix($encoded); + } + + public function destroyCollection(string $collectionId): string + { + $encoded = $this->scaleInstance->createTypeByTypeString('DestroyCollection')->encode([ + 'callIndex' => $this->getCallIndex('MultiTokens.destroy_collection'), + 'collectionId' => gmp_init($collectionId), + ]); + + return HexConverter::prefix($encoded); + } + + public function mutateCollection(string $collectionId, ?string $owner = null, null|array|RoyaltyPolicyParams $royalty = null, ?array $explicitRoyaltyCurrencies = null): string + { + $encoded = $this->scaleInstance->createTypeByTypeString('MutateCollection')->encode([ + 'callIndex' => $this->getCallIndex('MultiTokens.mutate_collection'), + 'collectionId' => gmp_init($collectionId), + 'mutation' => [ + 'owner' => $owner !== null ? HexConverter::unPrefix($owner) : null, + 'royalty' => is_array($royalty) ? ['NoMutation' => null] : ['SomeMutation' => $royalty?->toEncodable()], + 'explicitRoyaltyCurrencies' => $explicitRoyaltyCurrencies !== null ? array_map( + fn ($multiToken) => [ + 'collectionId' => gmp_init($multiToken['collectionId']), + 'tokenId' => gmp_init($multiToken['tokenId']), + ], + $explicitRoyaltyCurrencies + ) : null, + ], + ]); + + return HexConverter::prefix($encoded); + } + + public function mutateToken(string $collectionId, string $tokenId, null|array|TokenMarketBehaviorParams $behavior = null, ?bool $listingForbidden = null): string + { + $encoded = $this->scaleInstance->createTypeByTypeString('MutateToken')->encode([ + 'callIndex' => $this->getCallIndex('MultiTokens.mutate_token'), + 'collectionId' => gmp_init($collectionId), + 'tokenId' => gmp_init($tokenId), + 'mutation' => [ + 'behavior' => is_array($behavior) ? ['NoMutation' => null] : ['SomeMutation' => $behavior?->toEncodable()], + 'listingForbidden' => $listingForbidden, + 'metadata' => null, + ], + ]); + + return HexConverter::prefix($encoded); + } + + public static function collectionStorageKey(string $collectionId): string + { + $hashAndEncode = Blake2::hashAndEncode($collectionId); + $key = StorageKey::COLLECTIONS->value . $hashAndEncode; + + return HexConverter::prefix($key); + } + + public static function tokenStorageKey(string $collectionId, string $tokenId): string + { + $key = StorageKey::TOKENS->value . Blake2::hashAndEncode($collectionId) . Blake2::hashAndEncode($tokenId); + + return HexConverter::prefix($key); + } + + public static function collectionAccountStorageKey(string $collectionId, string $accountId): string + { + $accountId = HexConverter::unPrefix($accountId); + $key = StorageKey::COLLECTION_ACCOUNTS->value . Blake2::hashAndEncode($collectionId) . Blake2::hash($accountId, 128) . $accountId; + + return HexConverter::prefix($key); + } + + public function attributeStorageKey(string $collectionId, ?string $tokenId, string $key): string + { + $storageKey = StorageKey::ATTRIBUTES->value . Blake2::hashAndEncode($collectionId); + + $encodedToken = $this->scaleInstance->createTypeByTypeString('Option')->encode($tokenId); + $storageKey .= Blake2::hash($encodedToken, 128) . $encodedToken; + + $encodedKey = $this->scaleInstance->createTypeByTypeString('Bytes')->encode($key); + $storageKey .= Blake2::hash($encodedKey, 128) . $encodedKey; + + return HexConverter::prefix($storageKey); + } + + public static function tokenAccountStorageKey(string $accountId, string $collectionId, string $tokenId): string + { + $accountId = HexConverter::unPrefix($accountId); + $key = StorageKey::TOKEN_ACCOUNTS->value . Blake2::hashAndEncode($collectionId) . Blake2::hashAndEncode($tokenId) . Blake2::hash($accountId, 128) . $accountId; + + return HexConverter::prefix($key); + } + + public function mint(string $recipientId, string $collectionId, CreateTokenParams|MintParams $params): string + { + $encoded = $this->scaleInstance->createTypeByTypeString('CanaryMint')->encode([ + 'callIndex' => $this->getCallIndex('MultiTokens.mint'), + 'recipient' => [ + 'Id' => HexConverter::unPrefix($recipientId), + ], + 'collectionId' => gmp_init($collectionId), + 'params' => $params->toEncodable(), + ]); + + return HexConverter::prefix($encoded); + } + + public function batchMint(string $collectionId, array $recipients) + { + $encoded = $this->scaleInstance->createTypeByTypeString('CanaryBatchMint')->encode([ + 'callIndex' => $this->getCallIndex('MultiTokens.batch_mint'), + 'collectionId' => gmp_init($collectionId), + 'recipients' => array_map( + fn ($item) => [ + 'accountId' => HexConverter::unPrefix($item['accountId']), + 'params' => $item['params']->toEncodable(), + ], + $recipients + ), + ]); + + return HexConverter::prefix($encoded); + } + + public function burn(string $collectionId, BurnParams $params): string + { + $encoded = $this->scaleInstance->createTypeByTypeString('Burn')->encode([ + 'callIndex' => $this->getCallIndex('MultiTokens.burn'), + 'collectionId' => gmp_init($collectionId), + 'params' => $params->toEncodable(), + ]); + + return HexConverter::prefix($encoded); + } + + public function freeze(string $collectionId, FreezeTypeParams $params): string + { + $encoded = $this->scaleInstance->createTypeByTypeString('CanaryFreeze')->encode([ + 'callIndex' => $this->getCallIndex('MultiTokens.freeze'), + 'collectionId' => gmp_init($collectionId), + 'freezeType' => $params->toEncodable(), + ]); + + return HexConverter::prefix($encoded); + } + + public function thaw(string $collectionId, FreezeTypeParams $params): string + { + $encoded = $this->scaleInstance->createTypeByTypeString('CanaryThaw')->encode([ + 'callIndex' => $this->getCallIndex('MultiTokens.thaw'), + 'collectionId' => gmp_init($collectionId), + 'freezeType' => $params->toEncodable(), + ]); + + return HexConverter::prefix($encoded); + } + + public function setRoyalty(string $collectionId, ?string $tokenId, RoyaltyPolicyParams $royalty): string + { + $encoded = $this->scaleInstance->createTypeByTypeString('SetRoyalty')->encode([ + 'callIndex' => $this->getCallIndex('MultiTokens.set_royalty'), + 'collectionId' => gmp_init($collectionId), + 'tokenId' => $tokenId, + 'descriptor' => $royalty->toEncodable(), + ]); + + return HexConverter::prefix($encoded); + } + + public function setAttribute(string $collectionId, ?string $tokenId, string $key, string $value): string + { + $encoded = $this->scaleInstance->createTypeByTypeString('SetAttribute')->encode([ + 'callIndex' => $this->getCallIndex('MultiTokens.set_attribute'), + 'collectionId' => gmp_init($collectionId), + 'tokenId' => $tokenId !== null ? gmp_init($tokenId) : null, + 'key' => HexConverter::stringToHexPrefixed($key), + 'value' => HexConverter::stringToHexPrefixed($value), + ]); + + return HexConverter::prefix($encoded); + } + + public function removeAttribute(string $collectionId, ?string $tokenId, string $key): string + { + $encoded = $this->scaleInstance->createTypeByTypeString('RemoveAttribute')->encode([ + 'callIndex' => $this->getCallIndex('MultiTokens.remove_attribute'), + 'collectionId' => gmp_init($collectionId), + 'tokenId' => $tokenId !== null ? gmp_init($tokenId) : null, + 'key' => HexConverter::stringToHex($key), + ]); + + return HexConverter::prefix($encoded); + } + + public function removeAllAttributes(string $collectionId, ?string $tokenId, int $attributeCount): string + { + $encoded = $this->scaleInstance->createTypeByTypeString('RemoveAllAttributes')->encode([ + 'callIndex' => $this->getCallIndex('MultiTokens.remove_all_attributes'), + 'collectionId' => gmp_init($collectionId), + 'tokenId' => $tokenId !== null ? gmp_init($tokenId) : null, + 'attributeCount' => $attributeCount, + ]); + + return HexConverter::prefix($encoded); + } + + public function attributeStorage(int $module, int $method): string + { + $encoded = $this->scaleInstance->createTypeByTypeString('AttributeStorage')->encode([ + 'module' => $module, + 'method' => $method, + ]); + + return HexConverter::prefix($encoded); + } + + protected function loadCallIndexes(): array + { + $metadata = Cache::remember(PlatformCache::METADATA->key(), 3600, function () { + if (app()->runningUnitTests()) { + return Metadata::staging3014(); + } + + $blockchain = new SubstrateWebsocket(); + $response = $blockchain->send('state_getMetadata'); + $blockchain->close(); + + return $response; + }); + + if (!$metadata) { + return []; + } + + return Cache::rememberForever( + PlatformCache::CALL_INDEXES->key(config('enjin-platform.chains.selected') . config('enjin-platform.chains.network')), + function () use ($metadata) { + $decode = $this->scaleInstance->process('metadata', new ScaleBytes($metadata)); + + $callIndexes = collect(Arr::get($decode, 'metadata.call_index'))->mapWithKeys( + fn ($call, $key) => [ + sprintf('%s.%s', Arr::get($call, 'module.name'), Arr::get($call, 'call.name')) => $key, + ] + ); + + return $callIndexes->toArray(); + } + ); + } + + protected function getCallIndex(string $call): array + { + $index = str_split($this->callIndexes[$call], 2); + + return [HexConverter::hexToInt($index[0]), HexConverter::hexToInt($index[1])]; + } +} diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/AccountAdded.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/AccountAdded.php new file mode 100644 index 00000000..32e207bc --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/AccountAdded.php @@ -0,0 +1,66 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->tankId = is_string($key = Arr::get($data, 'event.FuelTanks.AccountAdded.tank_id')) ? $key : HexConverter::bytesToHex($key); + $self->userId = is_string($key = Arr::get($data, 'event.FuelTanks.AccountAdded.user_id')) ? $key : HexConverter::bytesToHex($key); + $self->tankDeposit = Arr::get($data, 'event.FuelTanks.AccountAdded.tank_deposit'); + $self->userDeposit = Arr::get($data, 'event.FuelTanks.AccountAdded.user_deposit'); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'tankId', 'value' => $this->tankId], + ['type' => 'userId', 'value' => $this->userId], + ['type' => 'tankDeposit', 'value' => $this->tankDeposit], + ['type' => 'userDeposit', 'value' => $this->userDeposit], + ]; + } +} + +/* Example 1 + { + "phase": { + "ApplyExtrinsic": 5 + }, + "event": { + "FuelTanks": { + "AccountAdded": { + "tank_id": "5b1c2bf7e279af55f31ff1c4a95330745efd3916bc2973e0ae377efd06aa3e68", + "user_id": "820c985a18d2a2ec3d7f96cb7429fd745299d121097f66f4acf0c2449d98d70c", + "tank_deposit": "2000000000000000000", + "user_deposit": "0" + } + } + }, + "topics": [] + }, + */ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/AccountRemoved.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/AccountRemoved.php new file mode 100644 index 00000000..0c276260 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/AccountRemoved.php @@ -0,0 +1,58 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->tankId = is_string($key = Arr::get($data, 'event.FuelTanks.AccountRemoved.tank_id')) ? $key : HexConverter::bytesToHex($key); + $self->userId = is_string($key = Arr::get($data, 'event.FuelTanks.AccountRemoved.user_id')) ? $key : HexConverter::bytesToHex($key); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'tankId', 'value' => $this->tankId], + ['type' => 'userId', 'value' => $this->userId], + ]; + } +} + +/* Example 1 + { + "phase": { + "ApplyExtrinsic": 5 + }, + "event": { + "FuelTanks": { + "AccountRemoved": { + "tank_id": "d6925288a4bee08ef3bc8432b0e87da18d2ae866f35b2042fc0ef0b4ed864d76", + "user_id": "f856030303d0f372281a365824e63d63d3c94000e7f4141c32655937bdc63d54" + } + } + }, + "topics": [] + }, + */ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/AccountRuleDataRemoved.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/AccountRuleDataRemoved.php new file mode 100644 index 00000000..36d1d62c --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/AccountRuleDataRemoved.php @@ -0,0 +1,66 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->tankId = is_string($key = Arr::get($data, 'event.FuelTanks.AccountRuleDataRemoved.tank_id')) ? $key : HexConverter::bytesToHex($key); + $self->userId = is_string($key = Arr::get($data, 'event.FuelTanks.AccountRuleDataRemoved.user_id')) ? $key : HexConverter::bytesToHex($key); + $self->ruleSetId = Arr::get($data, 'event.FuelTanks.AccountRuleDataRemoved.rule_set_id'); + $self->isFrozen = Arr::get($data, 'event.FuelTanks.AccountRuleDataRemoved.rule_kind'); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'tankId', 'value' => $this->tankId], + ['type' => 'userId', 'value' => $this->userId], + ['type' => 'ruleSetId', 'value' => $this->ruleSetId], + ['type' => 'ruleKind', 'value' => $this->ruleKind], + ]; + } +} + +/* Example 1 + { + "phase": { + "ApplyExtrinsic": 2 + }, + "event": { + "FuelTanks": { + "AccountRuleDataRemoved": { + "tank_id": "6b4df13dc3d4b7c5de2b334ec76e6fe4eee513f4668661cdf9e0ffc6dcc2927f", + "user_id": "1231e16a5f2793e7d48452ecccd17f1a83e1f0776ea5679443a0759f0b43dd40", + "rule_set_id": 1, + "rule_kind": "UserFuelBudget" + } + } + }, + "topics": [] + }, + */ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/CallDispatched.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/CallDispatched.php new file mode 100644 index 00000000..2004460d --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/CallDispatched.php @@ -0,0 +1,58 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->caller = is_string($key = Arr::get($data, 'event.FuelTanks.CallDispatched.caller')) ? $key : HexConverter::bytesToHex($key); + $self->tankId = is_string($key = Arr::get($data, 'event.FuelTanks.CallDispatched.tank_id')) ? $key : HexConverter::bytesToHex($key); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'called', 'value' => $this->caller], + ['type' => 'tankId', 'value' => $this->tankId], + ]; + } +} + +/* Example 1 + { + "phase": { + "ApplyExtrinsic": 2 + }, + "event": { + "FuelTanks": { + "CallDispatched": { + "caller": "4006f30f72abea5d8a641e55a780138016f0a6f762fc1892c19cdc05056d2c66", + "tank_id": "4a6feb98fea168c9f50ab74221cdad28d84b2fca01feab89564bda0131f7cc8a" + } + } + }, + "topics": [] + }, + */ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/FreezeStateMutated.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/FreezeStateMutated.php new file mode 100644 index 00000000..10f8d846 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/FreezeStateMutated.php @@ -0,0 +1,62 @@ +extrinsicIndex = Arr::get($data, 'phase'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->tankId = is_string($key = Arr::get($data, 'event.FuelTanks.FreezeStateMutated.tank_id')) ? $key : HexConverter::bytesToHex($key); + $self->ruleSetId = Arr::get($data, 'event.FuelTanks.FreezeStateMutated.rule_set_id.Some'); // TODO: Check + $self->isFrozen = Arr::get($data, 'event.FuelTanks.FreezeStateMutated.is_frozen'); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'tankId', 'value' => $this->tankId], + ['type' => 'ruleSetId', 'value' => $this->ruleSetId], + ['type' => 'isFrozen', 'value' => $this->isFrozen], + ]; + } +} + +/* Example 1 + { + "phase": "Finalization", + "event": { + "FuelTanks": { + "FreezeStateMutated": { + "tank_id": "5b1c2bf7e279af55f31ff1c4a95330745efd3916bc2973e0ae377efd06aa3e68", + "rule_set_id": { + "None": null + }, + "is_frozen": true + } + } + }, + "topics": [] + } + */ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/FuelTankCreated.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/FuelTankCreated.php new file mode 100644 index 00000000..e09a66be --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/FuelTankCreated.php @@ -0,0 +1,71 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->owner = Arr::get($data, 'event.FuelTanks.FuelTankCreated.owner'); + $self->tankName = is_string($key = Arr::get($data, 'event.FuelTanks.FuelTankCreated.name')) ? $key : HexConverter::bytesToHex($key); + $self->tankId = is_string($key = Arr::get($data, 'event.FuelTanks.FuelTankCreated.tank_id')) ? $key : HexConverter::bytesToHex($key); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'owner', 'value' => $this->owner], + ['type' => 'tankName', 'value' => $this->tankName], + ['type' => 'tankId', 'value' => $this->tankId], + ]; + } +} + +/* Example 1 + { + "phase": { + "ApplyExtrinsic": 4 + }, + "event": { + "FuelTanks": { + "FuelTankCreated": { + "owner": "56fba7af9da63a74853ced5555fec97ce993bd02060ed5954938f72636bb0800", + "name": [ + 108, + 102, + 109, + 121, + 107, + 114, + 116, + 50 + ], + "tank_id": "15937c9a8d71e75037ec8cf3d870e4302735b8c9fc0f703ec4298548d4dff5a6" + } + } + }, + "topics": [] + }, + */ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/FuelTankDestroyed.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/FuelTankDestroyed.php new file mode 100644 index 00000000..06b810ec --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/FuelTankDestroyed.php @@ -0,0 +1,54 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->tankId = is_string($key = Arr::get($data, 'event.FuelTanks.FuelTankDestroyed.tank_id')) ? $key : HexConverter::bytesToHex($key); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'tankId', 'value' => $this->tankId], + ]; + } +} + +/* Example 1 + { + "phase": { + "ApplyExtrinsic": 6 + }, + "event": { + "FuelTanks": { + "FuelTankDestroyed": { + "tank_id": "06ce2ac56eab3948f24a3a8613d38d58dec3bd796cd4c1035c08b7034b4d5d3e" + } + } + }, + "topics": [] + }, + */ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/FuelTankMutated.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/FuelTankMutated.php new file mode 100644 index 00000000..8eafbf99 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/FuelTankMutated.php @@ -0,0 +1,108 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->tankId = is_string($key = Arr::get($data, 'event.FuelTanks.FuelTankMutated.tank_id')) ? $key : HexConverter::bytesToHex($key); + $self->userAccountManagement = Arr::get($data, 'event.FuelTanks.FuelTankMutated.mutation.user_account_management.SomeMutation'); + $self->providesDeposit = Arr::get($data, 'event.FuelTanks.FuelTankMutated.mutation.provides_deposit.Some'); + $self->accountRules = Arr::get($data, 'event.FuelTanks.FuelTankMutated.mutation.account_rules.Some'); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'tankId', 'value' => $this->tankId], + ['type' => 'userAccountManagement', 'value' => $this->userAccountManagement], + ['type' => 'providesDeposit', 'value' => $this->providesDeposit], + ['type' => 'accountRules', 'value' => $this->accountRules], + ]; + } +} + +/* Example 1 + { + "phase": { + "ApplyExtrinsic": 3 + }, + "event": { + "FuelTanks": { + "FuelTankMutated": { + "tank_id": "11827d8f669d703144b335e1583f9b735ac60b0eeab34b74481836d151d9f698", + "mutation": { + "user_account_management": { + "SomeMutation": { + "Some": { + "tank_reserves_existential_deposit": true, + "tank_reserves_account_creation_deposit": true + } + } + }, + "provides_deposit": { + "None": null + }, + "account_rules": { + "None": null + } + } + } + } + }, + "topics": [] + }, + + Example 2: + { + "phase": { + "ApplyExtrinsic": 2 + }, + "event": { + "FuelTanks": { + "FuelTankMutated": { + "tank_id": "11827d8f669d703144b335e1583f9b735ac60b0eeab34b74481836d151d9f698", + "mutation": { + "user_account_management": { + "SomeMutation": { + "None": null + } + }, + "provides_deposit": { + "None": null + }, + "account_rules": { + "None": null + } + } + } + } + }, + "topics": [] + }, + */ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/MutateFreezeStateScheduled.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/MutateFreezeStateScheduled.php new file mode 100644 index 00000000..becac996 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/MutateFreezeStateScheduled.php @@ -0,0 +1,64 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->tankId = is_string($key = Arr::get($data, 'event.FuelTanks.MutateFreezeStateScheduled.tank_id')) ? $key : HexConverter::bytesToHex($key); + $self->ruleSetId = Arr::get($data, 'event.FuelTanks.MutateFreezeStateScheduled.rule_set_id.Some'); + $self->isFrozen = Arr::get($data, 'event.FuelTanks.MutateFreezeStateScheduled.is_frozen'); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'tankId', 'value' => $this->tankId], + ['type' => 'ruleSetId', 'value' => $this->ruleSetId], + ['type' => 'isFrozen', 'value' => $this->isFrozen], + ]; + } +} + +/* Example 1 + { + "phase": { + "ApplyExtrinsic": 3 + }, + "event": { + "FuelTanks": { + "MutateFreezeStateScheduled": { + "tank_id": "5b1c2bf7e279af55f31ff1c4a95330745efd3916bc2973e0ae377efd06aa3e68", + "rule_set_id": { + "None": null + }, + "is_frozen": true + } + } + }, + "topics": [] + }, + */ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/RuleSetInserted.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/RuleSetInserted.php new file mode 100644 index 00000000..e08aaff7 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/RuleSetInserted.php @@ -0,0 +1,58 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->tankId = is_string($key = Arr::get($data, 'event.FuelTanks.RuleSetInserted.tank_id')) ? $key : HexConverter::bytesToHex($key); + $self->ruleSetId = Arr::get($data, 'event.FuelTanks.RuleSetInserted.rule_set_id'); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'tankId', 'value' => $this->tankId], + ['type' => 'ruleSetId', 'value' => $this->ruleSetId], + ]; + } +} + +/* Example 1 + { + "phase": { + "ApplyExtrinsic": 2 + }, + "event": { + "FuelTanks": { + "RuleSetInserted": { + "tank_id": "3ccc633f7f7a8e4e4a24e72962465e3eac5bd67d313ea9bab4584e771d01cbb0", + "rule_set_id": 95022 + } + } + }, + "topics": [] + }, + */ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/RuleSetRemoved.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/RuleSetRemoved.php new file mode 100644 index 00000000..8b42c00f --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/FuelTanks/RuleSetRemoved.php @@ -0,0 +1,58 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->tankId = is_string($key = Arr::get($data, 'event.FuelTanks.RuleSetRemoved.tank_id')) ? $key : HexConverter::bytesToHex($key); + $self->ruleSetId = Arr::get($data, 'event.FuelTanks.RuleSetRemoved.rule_set_id'); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'tankId', 'value' => $this->tankId], + ['type' => 'ruleSetId', 'value' => $this->ruleSetId], + ]; + } +} + +/* Example 1 + { + "phase": { + "ApplyExtrinsic": 2 + }, + "event": { + "FuelTanks": { + "RuleSetRemoved": { + "tank_id": "ef8a434c13749766ea5857e1802bd735dba5b73ba6704a6700e835a2f2544dd2", + "rule_set_id": 1 + } + } + }, + "topics": [] + }, + */ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/Generic.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/Generic.php new file mode 100644 index 00000000..cad45b5d --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/Generic.php @@ -0,0 +1,63 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = is_string($eventId = Arr::get($data, 'event.' . $self->module)) ? $eventId : array_key_first($eventId); + $self->data = Arr::get($data, 'event.' . $self->module . '.' . $self->name); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + if (!$this->data) { + return []; + } + + return array_map( + fn ($k, $v) => [ + 'type' => is_string($k) ? $k : json_encode($k), + 'value' => is_string($v) ? $v : json_encode($v), + ], + array_keys($this->data), + array_values($this->data) + ); + } +} + +/* Example 1 + [ + "phase" => [ + "ApplyExtrinsic" => 2, + ], + "event" => [ + "Balances" => [ + "Deposit" => [ + "who" => "6d6f646c65662f66656469730000000000000000000000000000000000000000", + "amount" => "14130724955336550", + ], + ], + ], + "topics" => [], + ] + */ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/Marketplace/AuctionFinalized.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/Marketplace/AuctionFinalized.php new file mode 100644 index 00000000..c31e28bc --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/Marketplace/AuctionFinalized.php @@ -0,0 +1,73 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->listingId = Arr::get($data, 'event.Marketplace.AuctionFinalized.listing_id'); + $self->winningBidder = Arr::get($data, 'event.Marketplace.AuctionFinalized.winning_bid.Some.bidder'); + $self->price = Arr::get($data, 'event.Marketplace.AuctionFinalized.winning_bid.Some.price'); + $self->protocolFee = Arr::get($data, 'event.Marketplace.AuctionFinalized.protocol_fee'); + $self->royalty = Arr::get($data, 'event.Marketplace.AuctionFinalized.royalty'); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'listing_id', 'value' => $this->listingId], + ['type' => 'winning_bidder', 'value' => $this->winningBidder], + ['type' => 'price', 'value' => $this->price], + ['type' => 'protocol_fee', 'value' => $this->protocolFee], + ['type' => 'royalty', 'value' => $this->royalty], + ]; + } +} + +/* Example 1 + { + "phase": { + "ApplyExtrinsic": 6 + }, + "event": { + "Marketplace": { + "AuctionFinalized": { + "listing_id": "5abb7f8eb36bfa505e43564d5b9d8657d75537d7509e4683442e41209ba9a326", + "winning_bid": { + "Some": { + "bidder": "1e7462f65c593827ea042101d5d2befdb877883ce72b363b33d46a2a054d4a52", + "price": "1000000000000000000" + } + }, + "protocol_fee": "25000000000000000", + "royalty": "0" + } + } + }, + "topics": [] + }, +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/Marketplace/BidPlaced.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/Marketplace/BidPlaced.php new file mode 100644 index 00000000..ca0242e5 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/Marketplace/BidPlaced.php @@ -0,0 +1,63 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->listingId = Arr::get($data, 'event.Marketplace.BidPlaced.listing_id'); + $self->bidder = Arr::get($data, 'event.Marketplace.BidPlaced.bid.bidder'); + $self->price = Arr::get($data, 'event.Marketplace.BidPlaced.bid.price'); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'listing_id', 'value' => $this->listingId], + ['type' => 'bidder', 'value' => $this->bidder], + ['type' => 'price', 'value' => $this->price], + ]; + } +} + +/* Example 1 + { + "phase": { + "ApplyExtrinsic": 4 + }, + "event": { + "Marketplace": { + "BidPlaced": { + "listing_id": "5abb7f8eb36bfa505e43564d5b9d8657d75537d7509e4683442e41209ba9a326", + "bid": { + "bidder": "1e7462f65c593827ea042101d5d2befdb877883ce72b363b33d46a2a054d4a52", + "price": "1000000000000000000" + } + } + } + }, + "topics": [] + }, +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/Marketplace/ListingCancelled.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/Marketplace/ListingCancelled.php new file mode 100644 index 00000000..ff8ce896 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/Marketplace/ListingCancelled.php @@ -0,0 +1,53 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->listingId = Arr::get($data, 'event.Marketplace.ListingCancelled.listing_id'); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'listing_id', 'value' => $this->listingId], + ]; + } +} + +/* Example 1 + { + "phase": { + "ApplyExtrinsic": 25 + }, + "event": { + "Marketplace": { + "ListingCancelled": { + "listing_id": "23f6172d569c15f67ad4a9ba7207e237f48cc0f01ce1ddd12121a66eb30d2444" + } + } + }, + "topics": [] + }, +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/Marketplace/ListingCreated.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/Marketplace/ListingCreated.php new file mode 100644 index 00000000..fbd51728 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/Marketplace/ListingCreated.php @@ -0,0 +1,164 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->listingId = Arr::get($data, 'event.Marketplace.ListingCreated.listing_id'); + $self->seller = Arr::get($data, 'event.Marketplace.ListingCreated.listing.seller'); + $self->makeAssetId = Arr::get($data, 'event.Marketplace.ListingCreated.listing.make_asset_id'); + $self->takeAssetId = Arr::get($data, 'event.Marketplace.ListingCreated.listing.take_asset_id'); + $self->amount = Arr::get($data, 'event.Marketplace.ListingCreated.listing.amount'); + $self->price = Arr::get($data, 'event.Marketplace.ListingCreated.listing.price'); + $self->minTakeValue = Arr::get($data, 'event.Marketplace.ListingCreated.listing.min_take_value'); + $self->feeSide = Arr::get($data, 'event.Marketplace.ListingCreated.listing.fee_side'); + $self->creationBlock = Arr::get($data, 'event.Marketplace.ListingCreated.listing.creation_block'); + $self->deposit = Arr::get($data, 'event.Marketplace.ListingCreated.listing.deposit'); + $self->salt = Arr::get($data, 'event.Marketplace.ListingCreated.listing.salt'); + $self->data = Arr::get($data, 'event.Marketplace.ListingCreated.listing.data'); + $self->state = Arr::get($data, 'event.Marketplace.ListingCreated.listing.state'); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'listing_id', 'value' => $this->listingId], + ]; + } +} + +/* Example 1 + { + "phase": { + "ApplyExtrinsic": 31 + }, + "event": { + "Marketplace": { + "ListingCreated": { + "listing_id": "3a9d2f540a276f59104c6c2057903dff1c1d1a481ff87315d4b0017b9d7bed42", + "listing": { + "seller": "b882d3135b23eefc56ff0fd9e7d3f87c732040b49282cbd836f142c2435c0d11", + "make_asset_id": { + "collection_id": "89793", + "token_id": "0" + }, + "take_asset_id": { + "collection_id": "0", + "token_id": "0" + }, + "amount": "1", + "price": "1", + "min_take_value": "0", + "fee_side": "Take", + "creation_block": 642082, + "deposit": "2025700000000000000", + "salt": [ + 115, + 97, + 108, + 116, + 49, + 50, + 51 + ], + "data": "FixedPrice", + "state": { + "FixedPrice": { + "amount_filled": "0" + } + } + } + } + } + }, + "topics": [] + }, + */ + +/* Example 2 + { + "phase": { + "ApplyExtrinsic": 34 + }, + "event": { + "Marketplace": { + "ListingCreated": { + "listing_id": "5abb7f8eb36bfa505e43564d5b9d8657d75537d7509e4683442e41209ba9a326", + "listing": { + "seller": "e4569fb538b1cb511472919417e748d96aaab546f15d89f3d387122ab72eef79", + "make_asset_id": { + "collection_id": "89800", + "token_id": "0" + }, + "take_asset_id": { + "collection_id": "0", + "token_id": "0" + }, + "amount": "1", + "price": "1000000000000000000", + "min_take_value": "975000000000000000", + "fee_side": "Take", + "creation_block": 642082, + "deposit": "2025700000000000000", + "salt": [ + 115, + 97, + 108, + 116, + 49, + 50, + 51 + ], + "data": { + "Auction": { + "start_block": 642088, + "end_block": 642092 + } + }, + "state": { + "Auction": { + "high_bid": { + "None": null + } + } + } + } + } + } + }, + "topics": [] + }, +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/Marketplace/ListingFilled.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/Marketplace/ListingFilled.php new file mode 100644 index 00000000..1d55191b --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/Marketplace/ListingFilled.php @@ -0,0 +1,73 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->listingId = Arr::get($data, 'event.Marketplace.ListingFilled.listing_id'); + $self->buyer = Arr::get($data, 'event.Marketplace.ListingFilled.buyer'); + $self->amountFilled = Arr::get($data, 'event.Marketplace.ListingFilled.amount_filled'); + $self->amountRemaining = Arr::get($data, 'event.Marketplace.ListingFilled.amount_remaining'); + $self->protocolFee = Arr::get($data, 'event.Marketplace.ListingFilled.protocol_fee'); + $self->royalty = Arr::get($data, 'event.Marketplace.ListingFilled.royalty'); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'listing_id', 'value' => $this->listingId], + ['type' => 'buyer', 'value' => $this->buyer], + ['type' => 'amount_filled', 'value' => $this->amountFilled], + ['type' => 'amount_remaining', 'value' => $this->amountRemaining], + ['type' => 'protocol_fee', 'value' => $this->protocolFee], + ['type' => 'royalty', 'value' => $this->royalty], + ]; + } +} + +/* Example 1 + { + "phase": { + "ApplyExtrinsic": 5 + }, + "event": { + "Marketplace": { + "ListingFilled": { + "listing_id": "9102fb9f5e5d05051caad813aa0e12e9a4317fa5da41391d2ef0987705e704ea", + "buyer": "8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48", + "amount_filled": "1", + "amount_remaining": "0", + "protocol_fee": "25000000000000000", + "royalty": "0" + } + } + }, + "topics": [] + }, +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/Approved.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/Approved.php new file mode 100644 index 00000000..ced6b277 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/Approved.php @@ -0,0 +1,79 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->collectionId = Arr::get($data, 'event.MultiTokens.Approved.collection_id'); + $self->tokenId = Arr::get($data, 'event.MultiTokens.Approved.token_id.Some'); + $self->owner = Arr::get($data, 'event.MultiTokens.Approved.owner'); + $self->operator = Arr::get($data, 'event.MultiTokens.Approved.operator'); + $self->amount = Arr::get($data, 'event.MultiTokens.Approved.amount.Some'); + $self->expiration = Arr::get($data, 'event.MultiTokens.Approved.expiration.Some'); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'collection_id', 'value' => $this->collectionId], + ['type' => 'token_id', 'value' => $this->tokenId], + ['type' => 'owner', 'value' => $this->owner], + ['type' => 'operator', 'value' => $this->operator], + ['type' => 'amount', 'value' => $this->amount], + ['type' => 'expiration', 'value' => $this->expiration], + ]; + } +} + +/* Example 1 + [ + "phase" => [ + "ApplyExtrinsic" => 22, + ], + "event" => [ + "MultiTokens" => [ + "Approved" => [ + "collection_id" => "6499", + "token_id" => [ + "None" => null, + ], + "owner" => "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "operator" => "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", + "amount" => [ + "None" => null, + ], + "expiration" => [ + "None" => null, + ], + ], + ], + ], + "topics" => [], + ] + */ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/AttributeRemoved.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/AttributeRemoved.php new file mode 100644 index 00000000..5211fec4 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/AttributeRemoved.php @@ -0,0 +1,68 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->collectionId = Arr::get($data, 'event.MultiTokens.AttributeRemoved.collection_id'); + $self->tokenId = Arr::get($data, 'event.MultiTokens.AttributeRemoved.token_id.Some'); + $self->key = is_string($key = Arr::get($data, 'event.MultiTokens.AttributeRemoved.key')) ? $key : HexConverter::bytesToHex($key); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'collection_id', 'value' => $this->collectionId], + ['type' => 'token_id', 'value' => $this->tokenId], + ['type' => 'key', 'value' => $this->key], + ]; + } +} + +/* Example 1 + [ + "phase" => [ + "ApplyExtrinsic" => 2, + ], + "event" => [ + "MultiTokens" => [ + "AttributeRemoved" => [ + "collection_id" => "9248", + "token_id" => [ + "None" => null, + ], + "key" => [ + 0 => 110, + 1 => 97, + ..., + ], + ], + ], + ], + "topics" => [], + ] + */ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/AttributeSet.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/AttributeSet.php new file mode 100644 index 00000000..91f0cd0f --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/AttributeSet.php @@ -0,0 +1,76 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->collectionId = Arr::get($data, 'event.MultiTokens.AttributeSet.collection_id'); + $self->tokenId = Arr::get($data, 'event.MultiTokens.AttributeSet.token_id.Some'); + $self->key = is_string($key = Arr::get($data, 'event.MultiTokens.AttributeSet.key')) ? $key : HexConverter::bytesToHex($key); + $self->value = is_string($value = Arr::get($data, 'event.MultiTokens.AttributeSet.value')) ? $value : HexConverter::bytesToHex($value); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'collection_id', 'value' => $this->collectionId], + ['type' => 'token_id', 'value' => $this->tokenId], + ['type' => 'key', 'value' => $this->key], + ['type' => 'value', 'value' => $this->value], + ]; + } +} + +/* Example 1 + [ + "phase" => [ + "ApplyExtrinsic" => 2, + ], + "event" => [ + "MultiTokens" => [ + "AttributeSet" => [ + "collection_id" => "9248", + "token_id" => [ + "None" => null, + ], + "key" => [ + 0 => 110, + 1 => 97, + ..., + ], + "value" => [ + 0 => 84, + 1 => 101, + ..., + ], + ], + ], + ], + "topics" => [], + ] + */ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/Burned.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/Burned.php new file mode 100644 index 00000000..9b7d74e2 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/Burned.php @@ -0,0 +1,65 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->collectionId = Arr::get($data, 'event.MultiTokens.Burned.collection_id'); + $self->tokenId = Arr::get($data, 'event.MultiTokens.Burned.token_id'); + $self->account = Arr::get($data, 'event.MultiTokens.Burned.account_id'); + $self->amount = Arr::get($data, 'event.MultiTokens.Burned.amount'); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'collection_id', 'value' => $this->collectionId], + ['type' => 'token_id', 'value' => $this->tokenId], + ['type' => 'account', 'value' => $this->account], + ['type' => 'amount', 'value' => $this->amount], + ]; + } +} + +/* Example 1 + [ + "phase" => [ + "ApplyExtrinsic" => 2, + ], + "event" => [ + "MultiTokens" => [ + "Burned" => [ + "collection_id" => "10133", + "token_id" => "1", + "account_id" => "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", + "amount" => "1", + ], + ], + ], + "topics" => [], + ] +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/CollectionAccountCreated.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/CollectionAccountCreated.php new file mode 100644 index 00000000..d955f1e2 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/CollectionAccountCreated.php @@ -0,0 +1,57 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->collectionId = Arr::get($data, 'event.MultiTokens.CollectionAccountCreated.collection_id'); + $self->account = Arr::get($data, 'event.MultiTokens.CollectionAccountCreated.account_id'); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'collection_id', 'value' => $this->collectionId], + ['type' => 'account', 'value' => $this->account], + ]; + } +} + +/* Example 1 + [ + "phase" => [ + "ApplyExtrinsic" => 2, + ], + "event" => [ + "MultiTokens" => [ + "CollectionAccountCreated" => [ + "collection_id" => "9248", + "account_id" => "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", + ], + ], + ], + "topics" => [], + ] + */ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/CollectionAccountDestroyed.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/CollectionAccountDestroyed.php new file mode 100644 index 00000000..68f453fd --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/CollectionAccountDestroyed.php @@ -0,0 +1,57 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->collectionId = Arr::get($data, 'event.MultiTokens.CollectionAccountDestroyed.collection_id'); + $self->account = Arr::get($data, 'event.MultiTokens.CollectionAccountDestroyed.account_id'); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'collection_id', 'value' => $this->collectionId], + ['type' => 'account', 'value' => $this->account], + ]; + } +} + +/* Example 1 + [ + "phase" => [ + "ApplyExtrinsic" => 2, + ], + "event" => [ + "MultiTokens" => [ + "CollectionAccountDestroyed" => [ + "collection_id" => "10133", + "account_id" => "8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48", + ], + ], + ], + "topics" => [], + ] + */ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/CollectionCreated.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/CollectionCreated.php new file mode 100644 index 00000000..6863b698 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/CollectionCreated.php @@ -0,0 +1,57 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->collectionId = Arr::get($data, 'event.MultiTokens.CollectionCreated.collection_id'); + $self->owner = Arr::get($data, 'event.MultiTokens.CollectionCreated.owner'); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'collection_id', 'value' => $this->collectionId], + ['type' => 'owner', 'value' => $this->owner], + ]; + } +} + +/* Example 1 + [ + "phase" => [ + "ApplyExtrinsic" => 2, + ], + "event" => [ + "MultiTokens" => [ + "CollectionCreated" => [ + "collection_id" => "9248", + "owner" => "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", + ], + ], + ], + "topics" => [], + ] + */ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/CollectionDestroyed.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/CollectionDestroyed.php new file mode 100644 index 00000000..786366bb --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/CollectionDestroyed.php @@ -0,0 +1,57 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->collectionId = Arr::get($data, 'event.MultiTokens.CollectionDestroyed.collection_id'); + $self->caller = Arr::get($data, 'event.MultiTokens.CollectionDestroyed.caller'); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'collection_id', 'value' => $this->collectionId], + ['type' => 'caller', 'value' => $this->caller], + ]; + } +} + +/* Example 1 + [ + "phase" => [ + "ApplyExtrinsic" => 2, + ], + "event" => [ + "MultiTokens" => [ + "CollectionDestroyed" => [ + "collection_id" => "10133", + "caller" => "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", + ], + ], + ], + "topics" => [], + ] + */ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/CollectionMutated.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/CollectionMutated.php new file mode 100644 index 00000000..e327d1c8 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/CollectionMutated.php @@ -0,0 +1,113 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->collectionId = Arr::get($data, 'event.MultiTokens.CollectionMutated.collection_id'); + $self->owner = Arr::get($data, 'event.MultiTokens.CollectionMutated.mutation.owner.Some'); + $self->royalty = $royalty = self::getRoyalty($data); + $self->beneficiary = self::getBeneficiary($data, $royalty); + $self->percentage = self::getPercentage($data, $royalty); + $self->explicitRoyaltyCurrencies = Arr::get($data, 'event.MultiTokens.CollectionMutated.mutation.explicit_royalty_currencies.Some'); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'collection_id', 'value' => $this->collectionId], + ['type' => 'owner', 'value' => $this->owner], + ['type' => 'royalty', 'value' => $this->royalty], + ['type' => 'beneficiary', 'value' => $this->beneficiary], + ['type' => 'percentage', 'value' => $this->percentage], + ['type' => 'explicit_royalty_currencies', 'value' => is_array($this->explicitRoyaltyCurrencies) + ? json_encode($this->explicitRoyaltyCurrencies) + : $this->explicitRoyaltyCurrencies, + ], + ]; + } + + protected static function getRoyalty($data): string + { + $royalty = Arr::get($data, 'event.MultiTokens.CollectionMutated.mutation.royalty'); + + if ($royalty === null || $royalty === 'NoMutation') { + return 'NoMutation'; + } + + return array_key_first($royalty); + } + + protected static function getBeneficiary($data, $royalty): ?string + { + if ($royalty === 'NoMutation') { + return null; + } + + return Arr::get($data, 'event.MultiTokens.CollectionMutated.mutation.royalty.SomeMutation.Some.beneficiary'); + } + + protected static function getPercentage($data, $royalty): ?string + { + if ($royalty === 'NoMutation') { + return null; + } + + return Arr::get($data, 'event.MultiTokens.CollectionMutated.mutation.royalty.SomeMutation.Some.percentage'); + } +} + +/* Example 1 + [ + "phase" => [ + "ApplyExtrinsic" => 2, + ], + "event" => [ + "MultiTokens" => [ + "CollectionMutated" => [ + "collection_id" => "10685", + "mutation" => [ + "owner" => [ + "Some" => "8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48", + ], + "royalty" => [ + "SomeMutation" => [ + "None" => null, + ], + ], + "explicit_royalty_currencies" => [ + "Some" => [], + ], + ], + ], + ], + ], + "topics" => [], + ] +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/Frozen.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/Frozen.php new file mode 100644 index 00000000..c08d43c8 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/Frozen.php @@ -0,0 +1,91 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->collectionId = Arr::get($data, 'event.MultiTokens.Frozen.collection_id'); + $self->freezeType = is_string($freezeType = Arr::get($data, 'event.MultiTokens.Frozen.freeze_type')) ? $freezeType : array_key_first($freezeType); + $self->tokenId = self::getTokenId($data, $self->freezeType); + $self->account = self::getAccount($data, $self->freezeType); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'collection_id', 'value' => $this->collectionId], + ['type' => 'token_id', 'value' => $this->tokenId], + ['type' => 'account', 'value' => $this->account], + ['type' => 'freeze_type', 'value' => $this->freezeType], + ]; + } + + protected static function getTokenId(array $data, string $freezeType): ?string + { + if (!in_array($freezeType, ['Token', 'TokenAccount'])) { + return null; + } + + // We can use only freeze_type.Token.token_id when Substrate is upgraded + return $freezeType === 'Token' + ? Arr::get($data, 'event.MultiTokens.Frozen.freeze_type.Token.token_id') ?? Arr::get($data, 'event.MultiTokens.Frozen.freeze_type.Token') + : Arr::get($data, 'event.MultiTokens.Frozen.freeze_type.TokenAccount.token_id'); + } + + protected static function getAccount(array $data, string $freezeType): ?string + { + if (!in_array($freezeType, ['CollectionAccount', 'TokenAccount'])) { + return null; + } + + return $freezeType === 'CollectionAccount' + ? Arr::get($data, 'event.MultiTokens.Frozen.freeze_type.CollectionAccount') + : Arr::get($data, 'event.MultiTokens.Frozen.freeze_type.TokenAccount.account_id'); + } +} + +/* Example 1 + [ + "phase" => [ + "ApplyExtrinsic" => 2, + ], + "event" => [ + "MultiTokens" => [ + "Frozen" => [ + "collection_id" => "10133", + "freeze_type" => [ + "TokenAccount" => [ + "token_id" => "1", + "account_id" => "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", + ], + ], + ], + ], + ], + "topics" => [], + ] + */ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/Minted.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/Minted.php new file mode 100644 index 00000000..5eabb1e6 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/Minted.php @@ -0,0 +1,71 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->collectionId = Arr::get($data, 'event.MultiTokens.Minted.collection_id'); + $self->tokenId = Arr::get($data, 'event.MultiTokens.Minted.token_id'); + $self->issuer = Arr::get($data, 'event.MultiTokens.Minted.issuer.Signed'); + $self->recipient = Arr::get($data, 'event.MultiTokens.Minted.recipient'); + $self->amount = Arr::get($data, 'event.MultiTokens.Minted.amount'); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'collection_id', 'value' => $this->collectionId], + ['type' => 'token_id', 'value' => $this->tokenId], + ['type' => 'issuer', 'value' => $this->issuer], + ['type' => 'recipient', 'value' => $this->recipient], + ['type' => 'amount', 'value' => $this->amount], + ]; + } +} + +/* Example 1 + [ + "phase" => [ + "ApplyExtrinsic" => 2, + ], + "event" => [ + "MultiTokens" => [ + "Minted" => [ + "collection_id" => "9248", + "token_id" => "1", + "issuer" => [ + "Signed" => "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", + ], + "recipient" => "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", + "amount" => "1", + ], + ], + ], + "topics" => [] + ] + */ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/Thawed.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/Thawed.php new file mode 100644 index 00000000..1665888b --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/Thawed.php @@ -0,0 +1,91 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->collectionId = Arr::get($data, 'event.MultiTokens.Thawed.collection_id'); + $self->freezeType = is_string($freezeType = Arr::get($data, 'event.MultiTokens.Thawed.freeze_type')) ? $freezeType : array_key_first($freezeType); + $self->tokenId = self::getTokenId($data, $self->freezeType); + $self->account = self::getAccount($data, $self->freezeType); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'collection_id', 'value' => $this->collectionId], + ['type' => 'token_id', 'value' => $this->tokenId], + ['type' => 'account', 'value' => $this->account], + ['type' => 'freeze_type', 'value' => $this->freezeType], + ]; + } + + protected static function getTokenId(array $data, string $freezeType): ?string + { + if (!in_array($freezeType, ['Token', 'TokenAccount'])) { + return null; + } + + // We can use only freeze_type.Token.token_id when Substrate is upgraded + return $freezeType === 'Token' + ? Arr::get($data, 'event.MultiTokens.Thawed.freeze_type.Token.token_id') ?? Arr::get($data, 'event.MultiTokens.Thawed.freeze_type.Token') + : Arr::get($data, 'event.MultiTokens.Thawed.freeze_type.TokenAccount.token_id'); + } + + protected static function getAccount(array $data, string $freezeType): ?string + { + if (!in_array($freezeType, ['CollectionAccount', 'TokenAccount'])) { + return null; + } + + return $freezeType === 'CollectionAccount' + ? Arr::get($data, 'event.MultiTokens.Thawed.freeze_type.CollectionAccount') + : Arr::get($data, 'event.MultiTokens.Thawed.freeze_type.TokenAccount.account_id'); + } +} + +/* Example 1 + [ + "phase" => [ + "ApplyExtrinsic" => 2, + ], + "event" => [ + "MultiTokens" => [ + "Thawed" => [ + "collection_id" => "10133", + "freeze_type" => [ + "TokenAccount" => [ + "token_id" => "1", + "account_id" => "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", + ], + ], + ], + ], + ], + "topics" => [], + ] + */ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/TokenAccountCreated.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/TokenAccountCreated.php new file mode 100644 index 00000000..6f590101 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/TokenAccountCreated.php @@ -0,0 +1,65 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->collectionId = Arr::get($data, 'event.MultiTokens.TokenAccountCreated.collection_id'); + $self->tokenId = Arr::get($data, 'event.MultiTokens.TokenAccountCreated.token_id'); + $self->account = Arr::get($data, 'event.MultiTokens.TokenAccountCreated.account_id'); + $self->balance = Arr::get($data, 'event.MultiTokens.TokenAccountCreated.balance'); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'collection_id', 'value' => $this->collectionId], + ['type' => 'token_id', 'value' => $this->tokenId], + ['type' => 'account', 'value' => $this->account], + ['type' => 'balance', 'value' => $this->balance], + ]; + } +} + +/* Example 1 + [ + "phase" => [ + "ApplyExtrinsic" => 2, + ], + "event" => [ + "MultiTokens" => [ + "TokenAccountCreated" => [ + "collection_id" => "9248", + "token_id" => "1", + "account_id" => "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", + "balance" => "1", + ], + ], + ], + "topics" => [], + ] +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/TokenAccountDestroyed.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/TokenAccountDestroyed.php new file mode 100644 index 00000000..b1beeeba --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/TokenAccountDestroyed.php @@ -0,0 +1,61 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->collectionId = Arr::get($data, 'event.MultiTokens.TokenAccountDestroyed.collection_id'); + $self->tokenId = Arr::get($data, 'event.MultiTokens.TokenAccountDestroyed.token_id'); + $self->account = Arr::get($data, 'event.MultiTokens.TokenAccountDestroyed.account_id'); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'collection_id', 'value' => $this->collectionId], + ['type' => 'token_id', 'value' => $this->tokenId], + ['type' => 'account', 'value' => $this->account], + ]; + } +} + +/* + [ + "phase" => [ + "ApplyExtrinsic" => 2, + ], + "event" => [ + "MultiTokens" => [ + "TokenAccountDestroyed" => [ + "collection_id" => "10133", + "token_id" => "1", + "account_id" => "8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48", + ], + ], + ], + "topics" => [], + ] + */ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/TokenCreated.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/TokenCreated.php new file mode 100644 index 00000000..daba2268 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/TokenCreated.php @@ -0,0 +1,67 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->collectionId = Arr::get($data, 'event.MultiTokens.TokenCreated.collection_id'); + $self->tokenId = Arr::get($data, 'event.MultiTokens.TokenCreated.token_id'); + $self->issuer = Arr::get($data, 'event.MultiTokens.TokenCreated.issuer.Signed'); + $self->initialSupply = Arr::get($data, 'event.MultiTokens.TokenCreated.initial_supply'); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'collection_id', 'value' => $this->collectionId], + ['type' => 'token_id', 'value' => $this->tokenId], + ['type' => 'issuer', 'value' => $this->issuer], + ['type' => 'initial_supply', 'value' => $this->initialSupply], + ]; + } +} + +/* Example 1 + [ + "phase" => [ + "ApplyExtrinsic" => 2, + ], + "event" => [ + "MultiTokens" => [ + "TokenCreated" => [ + "collection_id" => "9248", + "token_id" => "1", + "issuer" => [ + "Signed" => "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", + ], + "initial_supply" => "1", + ], + ], + ], + "topics" => [], + ] +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/TokenDestroyed.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/TokenDestroyed.php new file mode 100644 index 00000000..3826f4e5 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/TokenDestroyed.php @@ -0,0 +1,61 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->collectionId = Arr::get($data, 'event.MultiTokens.TokenDestroyed.collection_id'); + $self->tokenId = Arr::get($data, 'event.MultiTokens.TokenDestroyed.token_id'); + $self->caller = Arr::get($data, 'event.MultiTokens.TokenDestroyed.caller'); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'collection_id', 'value' => $this->collectionId], + ['type' => 'token_id', 'value' => $this->tokenId], + ['type' => 'caller', 'value' => $this->caller], + ]; + } +} + +/* Example 1 + [ + "phase" => [ + "ApplyExtrinsic" => 2, + ], + "event" => [ + "MultiTokens" => [ + "TokenDestroyed" => [ + "collection_id" => "10133", + "token_id" => "1", + "caller" => "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", + ], + ], + ], + "topics" => [], + ] + */ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/TokenMutated.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/TokenMutated.php new file mode 100644 index 00000000..2074090d --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/TokenMutated.php @@ -0,0 +1,88 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->collectionId = Arr::get($data, 'event.MultiTokens.TokenMutated.collection_id'); + $self->tokenId = Arr::get($data, 'event.MultiTokens.TokenMutated.token_id'); + $self->listingForbidden = Arr::get($data, 'event.MultiTokens.TokenMutated.mutation.listing_forbidden.SomeMutation'); + $self->behaviorMutation = is_string($behavior = Arr::get($data, 'event.MultiTokens.TokenMutated.mutation.behavior')) ? $behavior : array_key_first($behavior); + $self->isCurrency = Arr::get($data, 'event.MultiTokens.TokenMutated.mutation.behavior.SomeMutation.Some') === 'IsCurrency'; + $self->beneficiary = Arr::get($data, 'event.MultiTokens.TokenMutated.mutation.behavior.SomeMutation.Some.HasRoyalty.beneficiary'); + $self->percentage = Arr::get($data, 'event.MultiTokens.TokenMutated.mutation.behavior.SomeMutation.Some.HasRoyalty.percentage'); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'collection_id', 'value' => $this->collectionId], + ['type' => 'token_id', 'value' => $this->tokenId], + ['type' => 'listing_forbidden', 'value' => $this->listingForbidden], + ['type' => 'behavior_mutation', 'value' => $this->behaviorMutation], + ['type' => 'is_currency', 'value' => $this->isCurrency], + ['type' => 'beneficiary', 'value' => $this->beneficiary], + ['type' => 'percentage', 'value' => $this->percentage], + ]; + } +} + +/* Example 1 + [ + "phase" => [ + "ApplyExtrinsic" => 2, + ], + "event" => [ + "MultiTokens" => [ + "TokenMutated" => [ + "collection_id" => "10685", + "token_id" => "1", + "mutation" => [ + "behavior" => [ + "SomeMutation" => [ + "Some" => [ + "HasRoyalty" => [ + "beneficiary" => "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", + "percentage" => 10000000, + ], + ], + ], + ], + "listing_forbidden" => [ + "SomeMutation" => true, + ], + "metadata" => "NoMutation", + ], + ], + ], + ], + "topics" => [], + ] + */ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/Transferred.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/Transferred.php new file mode 100644 index 00000000..510d1de1 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/Transferred.php @@ -0,0 +1,73 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->collectionId = Arr::get($data, 'event.MultiTokens.Transferred.collection_id'); + $self->tokenId = Arr::get($data, 'event.MultiTokens.Transferred.token_id'); + $self->operator = Arr::get($data, 'event.MultiTokens.Transferred.operator'); + $self->from = Arr::get($data, 'event.MultiTokens.Transferred.from'); + $self->to = Arr::get($data, 'event.MultiTokens.Transferred.to'); + $self->amount = Arr::get($data, 'event.MultiTokens.Transferred.amount'); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'collection_id', 'value' => $this->collectionId], + ['type' => 'token_id', 'value' => $this->tokenId], + ['type' => 'operator', 'value' => $this->operator], + ['type' => 'from', 'value' => $this->from], + ['type' => 'to', 'value' => $this->to], + ['type' => 'amount', 'value' => $this->amount], + ]; + } +} + +/* Example 1 + [ + "phase" => [ + "ApplyExtrinsic" => 2, + ], + "event" => [ + "MultiTokens" => [ + "Transferred" => [ + "collection_id" => "10133", + "token_id" => "1", + "operator" => "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", + "from" => "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", + "to" => "8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48", + "amount" => "1", + ], + ], + ], + "topics" => [], + ] + */ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/Unapproved.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/Unapproved.php new file mode 100644 index 00000000..31731e01 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/MultiTokens/Unapproved.php @@ -0,0 +1,67 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->collectionId = Arr::get($data, 'event.MultiTokens.Unapproved.collection_id'); + $self->tokenId = Arr::get($data, 'event.MultiTokens.Unapproved.token_id.Some'); + $self->owner = Arr::get($data, 'event.MultiTokens.Unapproved.owner'); + $self->operator = Arr::get($data, 'event.MultiTokens.Unapproved.operator'); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return [ + ['type' => 'collection_id', 'value' => $this->collectionId], + ['type' => 'token_id', 'value' => $this->tokenId], + ['type' => 'owner', 'value' => $this->owner], + ['type' => 'operator', 'value' => $this->operator], + ]; + } +} + +/* Example 1 + [ + "phase" => [ + "ApplyExtrinsic" => 5, + ], + "event" => [ + "MultiTokens" => [ + "Unapproved" => [ + "collection_id" => "10685", + "token_id" => [ + "None" => null, + ], + "owner" => "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", + "operator" => "8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48", + ], + ], + ], + "topics" => [], + ] +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/System/ExtrinsicFailed.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/System/ExtrinsicFailed.php new file mode 100644 index 00000000..9a435269 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/System/ExtrinsicFailed.php @@ -0,0 +1,45 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->data = Arr::get($data, 'event.' . $self->module . '.' . $self->name); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return array_map( + fn ($k, $v) => [ + 'type' => is_string($k) ? $k : json_encode($k), + 'value' => is_string($v) ? $v : json_encode($v), + ], + array_keys($this->data), + array_values($this->data) + ); + } +} + +/* Example 1 + */ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Events/System/ExtrinsicSuccess.php b/src/Services/Processor/Substrate/Codec/Polkadart/Events/System/ExtrinsicSuccess.php new file mode 100644 index 00000000..0ac6df4b --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Events/System/ExtrinsicSuccess.php @@ -0,0 +1,45 @@ +extrinsicIndex = Arr::get($data, 'phase.ApplyExtrinsic'); + $self->module = array_key_first(Arr::get($data, 'event')); + $self->name = array_key_first(Arr::get($data, 'event.' . $self->module)); + $self->data = Arr::get($data, 'event.' . $self->module . '.' . $self->name); + + return $self; + } + + public function getPallet(): string + { + return $this->module; + } + + public function getParams(): array + { + return array_map( + fn ($k, $v) => [ + 'type' => is_string($k) ? $k : json_encode($k), + 'value' => is_string($v) ? $v : json_encode($v), + ], + array_keys($this->data), + array_values($this->data) + ); + } +} + +/* Example 1 + */ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/AddAccount.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/AddAccount.php new file mode 100644 index 00000000..b8931bb3 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/AddAccount.php @@ -0,0 +1,65 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +[ + { + "extrinsic_length": 172, + "version": 4, + "signature": { + "address": { + "Id": "56fba7af9da63a74853ced5555fec97ce993bd02060ed5954938f72636bb0800" + }, + "signature": { + "Sr25519": "4e8406a1ffd29b460218bdb3552db2b44994c5ed50415a174931f62d318260215412e58e0de5c2a0b08f0c9781609f8c9458bcb9cbe8d4886f239537302ab184" + }, + "signedExtensions": { + "CheckMortality": { + "Mortal244": 1 + }, + "CheckNonce": 5775, + "ChargeTransactionPayment": "0" + } + }, + "call": { + "FuelTanks": { + "add_account": { + "tank_id": { + "Id": "37b9c0ddac0ce0fb116dfcd8f9ba7e27f89bfd1a47fdd1c9d4a07fdd69c2dab7" + }, + "user_id": { + "Id": "427c2fe497c02e0ee7812fc183fb2a07b3c821b91c58b827ab301ed5674ce120" + } + } + } + }, + "extrinsic_hash": "0xd7338bfb2de30dcae4fd57d3765b47616e84d7051c026d73fe6baab010961edf" + } +] +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/BatchAddAccount.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/BatchAddAccount.php new file mode 100644 index 00000000..ca0d4c15 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/BatchAddAccount.php @@ -0,0 +1,67 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +[ + { + "extrinsic_length": 173, + "version": 4, + "signature": { + "address": { + "Id": "3274a0b6662b3cab47da58afd6549b17f0cbf5b7a977bb7fed481ce76ea8af74" + }, + "signature": { + "Sr25519": "eec0a671e5a832a25f5caa8bbd78cea850df3f6a2e9b4d453251159710b7de7041cca76f541b73409985d548a47098654bc4e1fce58d149217625a1995f9aa83" + }, + "signedExtensions": { + "CheckMortality": { + "Mortal84": 0 + }, + "CheckNonce": 12011, + "ChargeTransactionPayment": "0" + } + }, + "call": { + "FuelTanks": { + "batch_add_account": { + "tank_id": { + "Id": "5baca881467045ad17d4b46a034fd0e24fad6139b65cb75a2ed76cf23d5a3aca" + }, + "user_ids": [ + { + "Id": "d262026b9f63cff14e06d54e85485e2c4d6458de2cf4858b4ce365a519fa3e51" + } + ] + } + } + }, + "extrinsic_hash": "0x48a8e23dc59af033cff424d7c15b44a38d4e6b411f0c8447699fd2765f302571" + } +] +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/BatchRemoveAccount.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/BatchRemoveAccount.php new file mode 100644 index 00000000..ebb10e64 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/BatchRemoveAccount.php @@ -0,0 +1,70 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +[ + { + "extrinsic_length": 206, + "version": 4, + "signature": { + "address": { + "Id": "3274a0b6662b3cab47da58afd6549b17f0cbf5b7a977bb7fed481ce76ea8af74" + }, + "signature": { + "Sr25519": "72d800d429b02167668a7b82fbf6eb6054a86b57d442018eb9eb1ac5fc965d0b9dbce2feaebe4a6338bfb705bd42affde0d3ceae24ece5838e6d9d531bcc1080" + }, + "signedExtensions": { + "CheckMortality": { + "Mortal20": 1 + }, + "CheckNonce": 12018, + "ChargeTransactionPayment": "0" + } + }, + "call": { + "FuelTanks": { + "batch_remove_account": { + "tank_id": { + "Id": "f8d962353abb6d609d0a7c566d6f4a94271f7ddd68d8f1a1b9c2baf7ae173da8" + }, + "user_ids": [ + { + "Id": "d262026b9f63cff14e06d54e85485e2c4d6458de2cf4858b4ce365a519fa3e51" + }, + { + "Id": "4e0ff7b256ec986362ef446f67cf28d851496ac8d74d3777ed75be8548621952" + } + ] + } + } + }, + "extrinsic_hash": "0xbc9a7b281d046db1dc42494bae0bf8c16ba98eb616e9b5ca14efe603df6509d0" + } +] +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/CreateFuelTank.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/CreateFuelTank.php new file mode 100644 index 00000000..fb03dd1a --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/CreateFuelTank.php @@ -0,0 +1,138 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* Example 1 +[ + { + "extrinsic_length": 138, + "version": 4, + "signature": { + "address": { + "Id": "56fba7af9da63a74853ced5555fec97ce993bd02060ed5954938f72636bb0800" + }, + "signature": { + "Sr25519": "5072bfce18686680908bc383e170db94d1b0a05360d605b3ab65c2c921ec7a035a75363adda0c5a5c247e6b48d3691801de48108f80d3547df8ecfd48cee5e8b" + }, + "signedExtensions": { + "CheckMortality": { + "Mortal116": 0 + }, + "CheckNonce": 5778, + "ChargeTransactionPayment": "0" + } + }, + "call": { + "FuelTanks": { + "create_fuel_tank": { + "descriptor": { + "name": [ + 108, + 102, + 109, + 119, + 112, + 120, + 119, + 107 + ], + "user_account_management": { + "None": null + }, + "rule_sets": [ + [ + 1, + [ + { + "UserFuelBudget": { + "amount": "100000000000000000", + "reset_period": 5 + } + } + ] + ] + ], + "provides_deposit": false, + "account_rules": [] + } + } + } + }, + "extrinsic_hash": "0x34ae20154ab4d6e804a2ffa3bfca14a000f9ec02c765f9e54a1cf152c87ce172" + } +] + +Example 2 +{ + "extrinsic_length": 121, + "version": 4, + "signature": { + "address": { + "Id": "3274a0b6662b3cab47da58afd6549b17f0cbf5b7a977bb7fed481ce76ea8af74" + }, + "signature": { + "Sr25519": "684155414ab95477307bd4e0ad20388db978297c1fdc8d824e6d226974cad03372e23cf3cf408a3575044d3b2ee73bacb12ee783a56ec4653adc0a8f1a603b82" + }, + "signedExtensions": { + "CheckMortality": { + "Mortal52": 1 + }, + "CheckNonce": 12927, + "ChargeTransactionPayment": "0" + } + }, + "call": { + "FuelTanks": { + "create_fuel_tank": { + "descriptor": { + "name": [ + 108, + 102, + 112, + 115, + 108, + 53, + 48, + 116 + ], + "user_account_management": { + "Some": { + "tank_reserves_existential_deposit": false, + "tank_reserves_account_creation_deposit": false + } + }, + "rule_sets": [], + "provides_deposit": false, + "account_rules": [] + } + } + } + }, + "extrinsic_hash": "0xcfbf46a534901582260467827723eb7538367d55d910f63dde4d3f1fc720f234" +} +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/DestroyFuelTank.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/DestroyFuelTank.php new file mode 100644 index 00000000..de701b24 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/DestroyFuelTank.php @@ -0,0 +1,62 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +[ + { + "extrinsic_length": 139, + "version": 4, + "signature": { + "address": { + "Id": "3274a0b6662b3cab47da58afd6549b17f0cbf5b7a977bb7fed481ce76ea8af74" + }, + "signature": { + "Sr25519": "b0537327ee2a1cdadf0cf52a0a9d8afe336dfec421758522382eed228fad1c0144cba65c7184aeba248dd21862df910bc50b4efcd211583db19045492f090d81" + }, + "signedExtensions": { + "CheckMortality": { + "Mortal244": 1 + }, + "CheckNonce": 11950, + "ChargeTransactionPayment": "0" + } + }, + "call": { + "FuelTanks": { + "destroy_fuel_tank": { + "tank_id": { + "Id": "9fcf27d80fb439424f312d42e608bc7469b3000d4b7a8bb5ccc7000c086a8a33" + } + } + } + }, + "extrinsic_hash": "0xfa4ba778209af8803f019dccb7f27dcc69e13d9de85de7c6bf72376857da29f0" + } +] +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/Dispatch.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/Dispatch.php new file mode 100644 index 00000000..d5173b02 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/Dispatch.php @@ -0,0 +1,100 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +[ + { + "extrinsic_length": 174, + "version": 4, + "signature": { + "address": { + "Id": "9443c3a49629e05c4f40e8cdfdc2d099fb1bb57b4afc58e6faacecfae76e272c" + }, + "signature": { + "Sr25519": "241fbfd18f12825f8d16b14b2980aeca90cff17a3485229c1149e0a3d0fedf2271504517b9412bc41fe2a3456df017331dcf1cadf6aef890033e7a9a1159fd8e" + }, + "signedExtensions": { + "CheckMortality": { + "Mortal212": 0 + }, + "CheckNonce": 0, + "ChargeTransactionPayment": "0" + } + }, + "call": { + "FuelTanks": { + "dispatch": { + "tank_id": { + "Id": "f0f239d473822cfb453b5a034d8836987066f8755a1d8759ef49d06c55fe6695" + }, + "rule_set_id": 0, + "call": { + "System": { + "remark": { + "remark": [ + 119, + 105, + 116, + 104, + 32, + 116, + 101, + 115, + 116, + 32, + 97, + 99, + 99, + 111, + 117, + 110, + 116, + 32, + 119, + 105, + 116, + 104, + 32, + 48, + 32, + 69, + 70, + 73 + ] + } + } + }, + "pays_remaining_fee": false + } + } + }, + "extrinsic_hash": "0xc74e55720cad4896296a403ba59e97c3d7dc3c2c4ee99f8c9f5745c0c0df06fd" + } +] +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/DispatchAndTouch.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/DispatchAndTouch.php new file mode 100644 index 00000000..89ac2d2f --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/DispatchAndTouch.php @@ -0,0 +1,89 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +[ + { + "extrinsic_length": 164, + "version": 4, + "signature": { + "address": { + "Id": "3274a0b6662b3cab47da58afd6549b17f0cbf5b7a977bb7fed481ce76ea8af74" + }, + "signature": { + "Sr25519": "be966395b6b605aabfa591697c08c5ae6b5f67f4b1185c1715c57c6bac620d77483d7f09e4a020bc2c40bf217edc00d0dc5d338fd03164ec17920ee265867488" + }, + "signedExtensions": { + "CheckMortality": { + "Mortal148": 1 + }, + "CheckNonce": 12057, + "ChargeTransactionPayment": "0" + } + }, + "call": { + "FuelTanks": { + "dispatch_and_touch": { + "tank_id": { + "Id": "933b3489626796e32b08602d248f9c0e9132dd20bdf588ef7bfef04d098539f6" + }, + "rule_set_id": 0, + "call": { + "System": { + "remark": { + "remark": [ + 119, + 105, + 116, + 104, + 32, + 116, + 101, + 115, + 116, + 32, + 97, + 99, + 99, + 111, + 117, + 110, + 116 + ] + } + } + }, + "pays_remaining_fee": false + } + } + }, + "extrinsic_hash": "0xec69fcd4328c8f201576d6a3419732f69ecc59ae45bdc063db88f5064442b0c6" + } +] +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/ForceSetConsumption.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/ForceSetConsumption.php new file mode 100644 index 00000000..d24855de --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/ForceSetConsumption.php @@ -0,0 +1,31 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/InsertRuleSet.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/InsertRuleSet.php new file mode 100644 index 00000000..3ca857cc --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/InsertRuleSet.php @@ -0,0 +1,79 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +[ + { + "extrinsic_length": 160, + "version": 4, + "signature": { + "address": { + "Id": "3274a0b6662b3cab47da58afd6549b17f0cbf5b7a977bb7fed481ce76ea8af74" + }, + "signature": { + "Sr25519": "78143729c27c7cc3d4c174ff7f0cd0d5e375295f25913474f884a3b2eb05f02e6980b63f24af15ba002c539cf3a1996b0b3d814d8ce4eab97009e3508acb1088" + }, + "signedExtensions": { + "CheckMortality": { + "Mortal52": 1 + }, + "CheckNonce": 11960, + "ChargeTransactionPayment": "0" + } + }, + "call": { + "FuelTanks": { + "insert_rule_set": { + "tank_id": { + "Id": "bf6059d424bd518ef7a80e70c83d69a7a030ec60a4a6ab6460f5e99e3fffa260" + }, + "rule_set_id": 12849, + "rules": [ + { + "TankFuelBudget": { + "budget": { + "amount": "1000000000000000000", + "reset_period": 123 + }, + "consumption": { + "total_consumed": "0", + "last_reset_block": { + "None": null + } + } + } + } + ] + } + } + }, + "extrinsic_hash": "0x64fe01cb6fb13c6edb3663ce7064473cbdb5065e11a3325255475adaff2b428d" + } +] +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/MutateFuelTank.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/MutateFuelTank.php new file mode 100644 index 00000000..48e6c945 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/MutateFuelTank.php @@ -0,0 +1,78 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +[ + { + "extrinsic_length": 145, + "version": 4, + "signature": { + "address": { + "Id": "3274a0b6662b3cab47da58afd6549b17f0cbf5b7a977bb7fed481ce76ea8af74" + }, + "signature": { + "Sr25519": "00c4cfdc6a303d434d46766266270bebd28c0fc238983b69cabcfe182b7b615dd199c9d8b8c126b164606aed6aaa5f402d2e82a620b326187c1089264a923f8d" + }, + "signedExtensions": { + "CheckMortality": { + "Mortal228": 0 + }, + "CheckNonce": 12078, + "ChargeTransactionPayment": "0" + } + }, + "call": { + "FuelTanks": { + "mutate_fuel_tank": { + "tank_id": { + "Id": "590198166858712848c921f27329babcb1f73ebbd8ff6cc1a9dfc74679b36cc7" + }, + "mutation": { + "user_account_management": { + "SomeMutation": { + "Some": { + "tank_reserves_existential_deposit": true, + "tank_reserves_account_creation_deposit": true + } + } + }, + "provides_deposit": { + "None": null + }, + "account_rules": { + "None": null + } + } + } + } + }, + "extrinsic_hash": "0x3a96aa133878f15d1534b953dd1348aa9beca9b76c9d14bb34a5f9c3104b0cd1" + } +] +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/RemoveAccount.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/RemoveAccount.php new file mode 100644 index 00000000..07087ec4 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/RemoveAccount.php @@ -0,0 +1,65 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +[ + { + "extrinsic_length": 172, + "version": 4, + "signature": { + "address": { + "Id": "3274a0b6662b3cab47da58afd6549b17f0cbf5b7a977bb7fed481ce76ea8af74" + }, + "signature": { + "Sr25519": "9a04bb98afdaf308d35b0292457616b5839dec5b65b2b386c453ad72d47d2f7f5f5ba4d2f1fedde37cabdcee2d8333c8b50e2f47320bc8efb0e573c5cc06128c" + }, + "signedExtensions": { + "CheckMortality": { + "Mortal4": 0 + }, + "CheckNonce": 12096, + "ChargeTransactionPayment": "0" + } + }, + "call": { + "FuelTanks": { + "remove_account": { + "tank_id": { + "Id": "3bcde4366eeb3372ae358fc725842e62d5b6ee9e2feb606ebfc36c27a3b23925" + }, + "user_id": { + "Id": "d262026b9f63cff14e06d54e85485e2c4d6458de2cf4858b4ce365a519fa3e51" + } + } + } + }, + "extrinsic_hash": "0x980b49f65b2ad4735de79e24081fa8191797e87b6ef593196a1aea0093587223" + } +] +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/RemoveAccountRuleData.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/RemoveAccountRuleData.php new file mode 100644 index 00000000..d4d3eef1 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/RemoveAccountRuleData.php @@ -0,0 +1,67 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +[ + { + "extrinsic_length": 177, + "version": 4, + "signature": { + "address": { + "Id": "56fba7af9da63a74853ced5555fec97ce993bd02060ed5954938f72636bb0800" + }, + "signature": { + "Sr25519": "0885f16f2790e7d2f61a7ed0293f3339b69654dc15d1e455ebe6b0f0c43ca72622d8eee06d934910ff6254d832135d519a5fe8c01a6db3317d9fa169d6fce488" + }, + "signedExtensions": { + "CheckMortality": { + "Mortal20": 1 + }, + "CheckNonce": 5782, + "ChargeTransactionPayment": "0" + } + }, + "call": { + "FuelTanks": { + "remove_account_rule_data": { + "tank_id": { + "Id": "889b33804eba04edd50c777128a3770df880421b5de226467f51156df9e631ea" + }, + "user_id": { + "Id": "aeb382bcd8115adde20353278557f4ff79bf95506634368d642f8e694c9e2743" + }, + "rule_set_id": 1, + "rule_kind": "UserFuelBudget" + } + } + }, + "extrinsic_hash": "0x3777d7a9d9217a4f2c4b82d4990ec63ecba29ff2d3fc284b89c6b1fa147355b9" + } +] +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/RemoveRuleSet.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/RemoveRuleSet.php new file mode 100644 index 00000000..799df407 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/RemoveRuleSet.php @@ -0,0 +1,63 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +[ + { + "extrinsic_length": 143, + "version": 4, + "signature": { + "address": { + "Id": "3274a0b6662b3cab47da58afd6549b17f0cbf5b7a977bb7fed481ce76ea8af74" + }, + "signature": { + "Sr25519": "dc782a370cc7f27171d23030c7bcbf68e905e3df4515d85ecca4b6cbdff18b00b338fd57b84b6ed563b0342b0d7dc6befde0084643f05d75653956f5662b8c87" + }, + "signedExtensions": { + "CheckMortality": { + "Mortal68": 1 + }, + "CheckNonce": 11977, + "ChargeTransactionPayment": "0" + } + }, + "call": { + "FuelTanks": { + "remove_rule_set": { + "tank_id": { + "Id": "019c63df7d06220d8f42e4a04eeafc69c01849c1b4df1270f455e8fb49531368" + }, + "rule_set_id": 1 + } + } + }, + "extrinsic_hash": "0xa412c28574ad79b95c8de7a964879ad3f27a054166fc7508b806c3fa592808de" + } +] +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/ScheduleMutateFreezeState.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/ScheduleMutateFreezeState.php new file mode 100644 index 00000000..4ad7e594 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/FuelTanks/ScheduleMutateFreezeState.php @@ -0,0 +1,66 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +[ + { + "extrinsic_length": 141, + "version": 4, + "signature": { + "address": { + "Id": "3274a0b6662b3cab47da58afd6549b17f0cbf5b7a977bb7fed481ce76ea8af74" + }, + "signature": { + "Sr25519": "8c5f779ef9cdaca71c58411af7a7e6f5d540e48113f8b74d66576dd39b82dc358be65c1281ae5edb3ee6d271dfb0813987b7f0e03110b19959fa4bd95a250083" + }, + "signedExtensions": { + "CheckMortality": { + "Mortal244": 0 + }, + "CheckNonce": 12017, + "ChargeTransactionPayment": "0" + } + }, + "call": { + "FuelTanks": { + "schedule_mutate_freeze_state": { + "tank_id": { + "Id": "f8d962353abb6d609d0a7c566d6f4a94271f7ddd68d8f1a1b9c2baf7ae173da8" + }, + "rule_set_id": { + "None": null + }, + "is_frozen": true + } + } + }, + "extrinsic_hash": "0x6dd970effbc06c6a1e13f0a7ef856ba0747d7a42eac05b088e17459dc914c9ee" + } +] +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/Generic.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/Generic.php new file mode 100644 index 00000000..4c63f4d3 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/Generic.php @@ -0,0 +1,31 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = is_string($callId = Arr::get($data, 'call.' . $self->module)) ? $callId : array_key_first($callId); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/Marketplace/CancelListing.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/Marketplace/CancelListing.php new file mode 100644 index 00000000..d6c3d77b --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/Marketplace/CancelListing.php @@ -0,0 +1,58 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +{ + "extrinsic_length": 138, + "version": 4, + "signature": { + "address": { + "Id": "b882d3135b23eefc56ff0fd9e7d3f87c732040b49282cbd836f142c2435c0d11" + }, + "signature": { + "Sr25519": "561821de4f8b95efa598fbd78ab82c4016a851d35edf312c846b5df7e6bdfc228f48f0ed7a0a922a6d286ab2334d7ef79ea242c3b9cafa8dd924798e3d083583" + }, + "signedExtensions": { + "CheckMortality": { + "Mortal244": 1 + }, + "CheckNonce": 111, + "ChargeTransactionPayment": "0" + } + }, + "call": { + "Marketplace": { + "cancel_listing": { + "listing_id": "a7511f79d0fba9bd3e4239672bdf1ae7429596035d0d2cc04ae9b0d73d49290b" + } + } + }, + "extrinsic_hash": "0x960e7ad6fce0f579c9ae8c95df7a23d92fad5d31742d2624016ae59025068d1e" +} +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/Marketplace/CreateListing.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/Marketplace/CreateListing.php new file mode 100644 index 00000000..f6788033 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/Marketplace/CreateListing.php @@ -0,0 +1,79 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +{ + "extrinsic_length": 126, + "version": 4, + "signature": { + "address": { + "Id": "3274a0b6662b3cab47da58afd6549b17f0cbf5b7a977bb7fed481ce76ea8af74" + }, + "signature": { + "Sr25519": "dc95571bdb3c4d326ecde045040bb59b0b173fd5a27446a5d9ac61571d86845611473aa9b2ad5d1ebf39293ee64f5dde66001feac311a0d755664573909d7585" + }, + "signedExtensions": { + "CheckMortality": { + "Mortal244": 0 + }, + "CheckNonce": 20274, + "ChargeTransactionPayment": "0" + } + }, + "call": { + "Marketplace": { + "create_listing": { + "make_asset_id": { + "collection_id": "89907", + "token_id": "0" + }, + "take_asset_id": { + "collection_id": "0", + "token_id": "0" + }, + "amount": "1", + "price": "1", + "salt": [ + 115, + 97, + 108, + 116, + 49, + 50, + 51 + ], + "auction_data": { + "None": null + } + } + } + }, + "extrinsic_hash": "0xdc3d7cfb22ebe3a3a9089c33066d763a27c1c9b02cd83cd92a7dc44a620b9c7b" +} +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/Marketplace/FillListing.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/Marketplace/FillListing.php new file mode 100644 index 00000000..88edca22 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/Marketplace/FillListing.php @@ -0,0 +1,59 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +{ + "extrinsic_length": 139, + "version": 4, + "signature": { + "address": { + "Id": "8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48" + }, + "signature": { + "Sr25519": "7e9a8cf663f5b4f6514b82f1139e3a37ae9f2a7d15c1bd600990036b26986c5947f896a9f069bd56034350289662c552734045b7fd1186ac125e64eed5802188" + }, + "signedExtensions": { + "CheckMortality": { + "Mortal36": 0 + }, + "CheckNonce": 265, + "ChargeTransactionPayment": "0" + } + }, + "call": { + "Marketplace": { + "fill_listing": { + "listing_id": "4d4ee03a2d2ccc76d69f7e10d84744c750ef89e1bc1f3b92ffe3ff0035f20962", + "amount": "1" + } + } + }, + "extrinsic_hash": "0xbf37aadc9b0e56df0d0c3986e7a51af03f56d7526ca2f92216d0fe51fec3bd88" +} +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/Marketplace/FinalizeAuction.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/Marketplace/FinalizeAuction.php new file mode 100644 index 00000000..7045cdde --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/Marketplace/FinalizeAuction.php @@ -0,0 +1,58 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +{ + "extrinsic_length": 138, + "version": 4, + "signature": { + "address": { + "Id": "e4569fb538b1cb511472919417e748d96aaab546f15d89f3d387122ab72eef79" + }, + "signature": { + "Sr25519": "a02d7a581fb0adf14b31de4016d0da0b9ccc205f49635eefc56dcc3da7617708bace909fd14177c45c79e41333a0592dfa870c83c604d9e98851b062b4bb1b85" + }, + "signedExtensions": { + "CheckMortality": { + "Mortal116": 0 + }, + "CheckNonce": 79, + "ChargeTransactionPayment": "0" + } + }, + "call": { + "Marketplace": { + "finalize_auction": { + "listing_id": "0aabea26ce1a43e14775b4b2be60e08a1bc3fcbaaeedfba2f3a5ce934d24d73e" + } + } + }, + "extrinsic_hash": "0x0be7f6575781b22339ee08f1812a5db6f2ebf9685378c4cee36004de4297ac69" +} +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/Marketplace/PlaceBid.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/Marketplace/PlaceBid.php new file mode 100644 index 00000000..970075c0 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/Marketplace/PlaceBid.php @@ -0,0 +1,59 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +{ + "extrinsic_length": 147, + "version": 4, + "signature": { + "address": { + "Id": "363cb657ed4ec26798187ed67d90ace3c8d0dcd3804265b1e3f09a564b3c0e00" + }, + "signature": { + "Sr25519": "4e38e0189cc3384a596db67b04a58c105a31dee3ecc09515ab545f1134cd8f15559e882f4aee802a0cdfe5a2fee074dce2ce32d60b8029b3c0275b9253388e8a" + }, + "signedExtensions": { + "CheckMortality": { + "Mortal52": 0 + }, + "CheckNonce": 70, + "ChargeTransactionPayment": "0" + } + }, + "call": { + "Marketplace": { + "place_bid": { + "listing_id": "ac5dce1f4cd914cda85f3c39ef7357cfac7104ff19bbabf0e016d16311da1eac", + "price": "1000000000000000000" + } + } + }, + "extrinsic_hash": "0x030044a57c9d2234102fa0ce0187e9c66fe1c239670d3aa9444409558483ccbe" +} +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/ApproveCollection.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/ApproveCollection.php new file mode 100644 index 00000000..5cc30b6f --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/ApproveCollection.php @@ -0,0 +1,62 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +array:5 [▼ + "extrinsic_length" => 145 + "version" => 4 + "signature" => array:3 [▼ + "address" => array:1 [▼ + "Id" => "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" + ] + "signature" => array:1 [▼ + "Sr25519" => "7433dba41aebafb4ae80d66eeb92c0ac1cda639066e3fd38466451a388a4b24f970b02d7f08a0e33ee7b5d973e58abad66de000fef138f95b5952e1d64b74a83" + ] + "signedExtensions" => array:3 [▼ + "CheckMortality" => array:1 [▼ + "Mortal100" => 0 + ] + "CheckNonce" => 169 + "ChargeTransactionPayment" => "0" + ] + ] + "call" => array:1 [▼ + "MultiTokens" => array:1 [▼ + "approve_collection" => array:3 [▼ + "collection_id" => "13159" + "operator" => "8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48" + "expiration" => array:1 [▼ + "Some" => 300000 + ] + ] + ] + ] + "extrinsic_hash" => "0xcaa235d4d357fd43ecc7c4af235132fced9b5b519374ed3bb1ebc6f72962ff6b" +] +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/ApproveToken.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/ApproveToken.php new file mode 100644 index 00000000..0ee9f3aa --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/ApproveToken.php @@ -0,0 +1,65 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +array:5 [▼ + "extrinsic_length" => 148 + "version" => 4 + "signature" => array:3 [▼ + "address" => array:1 [▼ + "Id" => "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" + ] + "signature" => array:1 [▼ + "Sr25519" => "c842c3b1eca0b0376933d6cc390caae5d3992a6cb7ab7fc032728b66da7e912752d8188bd8df594b63469fe69653e531fcdf37f0e676b083b101c09fef23ea8e" + ] + "signedExtensions" => array:3 [▼ + "CheckMortality" => array:1 [▼ + "Mortal212" => 0 + ] + "CheckNonce" => 173 + "ChargeTransactionPayment" => "0" + ] + ] + "call" => array:1 [▼ + "MultiTokens" => array:1 [▼ + "approve_token" => array:6 [▼ + "collection_id" => "13158" + "token_id" => "1" + "operator" => "90b5ab205c6974c9ea841be688864633dc9ca8a357843eeacf2314649965fe22" + "amount" => "1" + "expiration" => array:1 [▼ + "Some" => 500000 + ] + "current_amount" => "0" + ] + ] + ] + "extrinsic_hash" => "0xfbdf788d8c120486e09794af4c6fe6c475b908269cda15f288cb04e09b49dea9" +] +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/BatchMint.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/BatchMint.php new file mode 100644 index 00000000..6f926864 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/BatchMint.php @@ -0,0 +1,31 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/BatchSetAttribute.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/BatchSetAttribute.php new file mode 100644 index 00000000..bc66d778 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/BatchSetAttribute.php @@ -0,0 +1,31 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/BatchTransfer.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/BatchTransfer.php new file mode 100644 index 00000000..4e239760 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/BatchTransfer.php @@ -0,0 +1,31 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/Burn.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/Burn.php new file mode 100644 index 00000000..907b089e --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/Burn.php @@ -0,0 +1,64 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +array:5 [▼ + "extrinsic_length" => 111 + "version" => 4 + "signature" => array:3 [▼ + "address" => array:1 [▼ + "Id" => "8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48" + ] + "signature" => array:1 [▼ + "Sr25519" => "889f31681c922330a80fd6c48b7d9d5a320e97c2adc76ee6b19c74a90e1d6c1a6eb5381e2ea7c1f9d6b11ee97789b2e0bd64f71ba19fba112d132a06ed0f308e" + ] + "signedExtensions" => array:3 [▼ + "CheckMortality" => array:1 [▼ + "Mortal84" => 1 + ] + "CheckNonce" => 45 + "ChargeTransactionPayment" => "0" + ] + ] + "call" => array:1 [▼ + "MultiTokens" => array:1 [▼ + "burn" => array:2 [▼ + "collection_id" => "13158" + "params" => array:4 [▼ + "token_id" => "1" + "amount" => "1" + "keep_alive" => false + "remove_token_storage" => false + ] + ] + ] + ] + "extrinsic_hash" => "0x099d60de818f02c53216188dc795b0af86925b7fd48e90e2387b6aac4c8eb758" +] +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/CreateCollection.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/CreateCollection.php new file mode 100644 index 00000000..9f88819b --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/CreateCollection.php @@ -0,0 +1,100 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +array:5 [▼ + "extrinsic_length" => 184 + "version" => 4 + "signature" => array:3 [▼ + "address" => array:1 [▼ + "Id" => "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" + ] + "signature" => array:1 [▼ + "Sr25519" => "fc8f2dedc041a86c937c9b46adfd58d967beb2fe50f994ac2286eaf277a5720f9796510b992c5124fc0b11bcd76e084622e98989a1f674212f2ce2d352fa1a88" + ] + "signedExtensions" => array:3 [▼ + "CheckMortality" => array:1 [▼ + "Mortal196" => 1 + ] + "CheckNonce" => 166 + "ChargeTransactionPayment" => "0" + ] + ] + "call" => array:1 [▼ + "MultiTokens" => array:1 [▼ + "create_collection" => array:1 [▼ + "descriptor" => array:3 [▼ + "policy" => array:2 [▼ + "mint" => array:3 [▼ + "max_token_count" => array:1 [▼ + "Some" => "100000" + ] + "max_token_supply" => array:1 [▼ + "Some" => "5555555555" + ] + "force_single_mint" => false + ] + "market" => array:1 [▼ + "royalty" => array:1 [▼ + "Some" => array:2 [▼ + "beneficiary" => "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" + "percentage" => 10000000 + ] + ] + ] + ] + "explicit_royalty_currencies" => array:1 [▼ + 0 => array:2 [▼ + "collection_id" => "0" + "token_id" => "0" + ] + ] + "attributes" => array:1 [▼ + 0 => array:2 [▼ + "key" => array:4 [▼ + 0 => 110 + 1 => 97 + 2 => 109 + 3 => 101 + ] + "value" => array:4 [▼ + 0 => 68 + 1 => 101 + 2 => 109 + 3 => 111 + ] + ] + ] + ] + ] + ] + ] + "extrinsic_hash" => "0xeed170dbb4b7704c42242ae01da5712e569e2a7c4965ce3424725ef4b67b290e" +] +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/DestroyCollection.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/DestroyCollection.php new file mode 100644 index 00000000..457ab37e --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/DestroyCollection.php @@ -0,0 +1,31 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/Freeze.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/Freeze.php new file mode 100644 index 00000000..127f6985 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/Freeze.php @@ -0,0 +1,66 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +array:5 [▼ + "extrinsic_length" => 142 + "version" => 4 + "signature" => array:3 [▼ + "address" => array:1 [▼ + "Id" => "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" + ] + "signature" => array:1 [▼ + "Sr25519" => "e481e2903c9ae573bf6de75eddd990039e968068ff71f838307e254a7376467e3ca4c5c0d309b66c0031118b7510b78f6bf40751527c45791de1914fca27fb85" + ] + "signedExtensions" => array:3 [▼ + "CheckMortality" => array:1 [▼ + "Mortal4" => 1 + ] + "CheckNonce" => 175 + "ChargeTransactionPayment" => "0" + ] + ] + "call" => array:1 [▼ + "MultiTokens" => array:1 [▼ + "freeze" => array:1 [▼ + "info" => array:2 [▼ + "collection_id" => "13158" + "freeze_type" => array:1 [▼ + "TokenAccount" => array:2 [▼ + "token_id" => "1" + "account_id" => "8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48" + ] + ] + ] + ] + ] + ] + "extrinsic_hash" => "0x2700c1191f799ff9442984d9be8595462ff03841ac3e6267078028c0dc26cfd9" +] +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/Mint.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/Mint.php new file mode 100644 index 00000000..9ef104b5 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/Mint.php @@ -0,0 +1,82 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +array:5 [▼ + "extrinsic_length" => 195 + "version" => 4 + "signature" => array:3 [▼ + "address" => array:1 [▼ + "Id" => "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" + ] + "signature" => array:1 [▼ + "Sr25519" => "f44af5bfdac54f936c14dad2760813a9f717a2ecb45601bd8ecfc23b98a65b1ad8754ae41f02350e568044adbc2ca72ca45f338e5574fee906a2de9e7086be8c" + ] + "signedExtensions" => array:3 [▼ + "CheckMortality" => array:1 [▼ + "Mortal36" => 0 + ] + "CheckNonce" => 167 + "ChargeTransactionPayment" => "0" + ] + ] + "call" => array:1 [▼ + "MultiTokens" => array:1 [▼ + "mint" => array:3 [▼ + "recipient" => array:1 [▼ + "Id" => "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" + ] + "collection_id" => "13158" + "params" => array:1 [▼ + "CreateToken" => array:6 [▼ + "token_id" => "1" + "initial_supply" => "1" + "unit_price" => "1000000000000000000" + "cap" => array:1 [▼ + "Some" => array:1 [▼ + "Supply" => "10" + ] + ] + "behavior" => array:1 [▼ + "Some" => array:1 [▼ + "HasRoyalty" => array:2 [▼ + "beneficiary" => "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" + "percentage" => 10000000 + ] + ] + ] + "listing_forbidden" => false + ] + ] + ] + ] + ] + "extrinsic_hash" => "0xb5fbf665da9fefea7f63eb30594bdeae473fddbf759ef8a4a8451aa14dc3bfa9" +] +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/MutateCollection.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/MutateCollection.php new file mode 100644 index 00000000..8f7b3dba --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/MutateCollection.php @@ -0,0 +1,31 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/MutateToken.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/MutateToken.php new file mode 100644 index 00000000..f3013c54 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/MutateToken.php @@ -0,0 +1,31 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/RemoveAllAttributes.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/RemoveAllAttributes.php new file mode 100644 index 00000000..a6bacd89 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/RemoveAllAttributes.php @@ -0,0 +1,31 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/RemoveAttribute.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/RemoveAttribute.php new file mode 100644 index 00000000..02a9c2e0 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/RemoveAttribute.php @@ -0,0 +1,65 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +array:5 [▼ + "extrinsic_length" => 130 + "version" => 4 + "signature" => array:3 [▼ + "address" => array:1 [▼ + "Id" => "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" + ] + "signature" => array:1 [▼ + "Sr25519" => "a6f2f05f24fefd1d22e1480e12cdcc96a9afb4084729cbca853039418733e601693ed9b332ca75214bb4168823d0832d4bc847fc9c7abae531be11a98f259e8f" + ] + "signedExtensions" => array:3 [▼ + "CheckMortality" => array:1 [▶] + "CheckNonce" => 170 + "ChargeTransactionPayment" => "0" + ] + ] + "call" => array:1 [▼ + "MultiTokens" => array:1 [▼ + "remove_attribute" => array:3 [▼ + "collection_id" => "13159" + "token_id" => array:1 [▼ + "Some" => "1" + ] + "key" => array:4 [▼ + 0 => 110 + 1 => 97 + 2 => 109 + 3 => 101 + ] + ] + ] + ] + "extrinsic_hash" => "0x048f54cdda10c8f092a73536c02992c673f400e4cc802a8cacdb1e6260b65d7d" +] +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/SetAttribute.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/SetAttribute.php new file mode 100644 index 00000000..05699b1d --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/SetAttribute.php @@ -0,0 +1,79 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +array:5 [▼ + "extrinsic_length" => 141 + "version" => 4 + "signature" => array:3 [▼ + "address" => array:1 [▼ + "Id" => "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" + ] + "signature" => array:1 [▼ + "Sr25519" => "bee511a4415d361f81e89296a1727592f79dea94596cb1aa31e7c198f1d89f53e01a5c44683a4d4e72b33f55ed277e665dcb1f1248fb03b095433a0d54e7778b" + ] + "signedExtensions" => array:3 [▼ + "CheckMortality" => array:1 [▼ + "Mortal68" => 0 + ] + "CheckNonce" => 168 + "ChargeTransactionPayment" => "0" + ] + ] + "call" => array:1 [▼ + "MultiTokens" => array:1 [▼ + "set_attribute" => array:4 [▼ + "collection_id" => "13158" + "token_id" => array:1 [▼ + "Some" => "1" + ] + "key" => array:4 [▼ + 0 => 110 + 1 => 97 + 2 => 109 + 3 => 101 + ] + "value" => array:10 [▼ + 0 => 68 + 1 => 101 + 2 => 109 + 3 => 111 + 4 => 32 + 5 => 84 + 6 => 111 + 7 => 107 + 8 => 101 + 9 => 110 + ] + ] + ] + ] + "extrinsic_hash" => "0x4cc565efa5251da82c9fa73ebeb33599508b18c207b17bccbb51786be2d7c097" +] +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/Thaw.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/Thaw.php new file mode 100644 index 00000000..13bfb087 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/Thaw.php @@ -0,0 +1,66 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +array:5 [▼ + "extrinsic_length" => 142 + "version" => 4 + "signature" => array:3 [▼ + "address" => array:1 [▼ + "Id" => "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" + ] + "signature" => array:1 [▼ + "Sr25519" => "0a45d1a77b6e042294db0517c669537cbf7681b2150d237dbc134fa3d6c16d64acb38e5458211f00f064768cd71dbcf0cf596bbecb8570250367aca5520da683" + ] + "signedExtensions" => array:3 [▼ + "CheckMortality" => array:1 [▼ + "Mortal20" => 1 + ] + "CheckNonce" => 176 + "ChargeTransactionPayment" => "0" + ] + ] + "call" => array:1 [▼ + "MultiTokens" => array:1 [▼ + "thaw" => array:1 [▼ + "info" => array:2 [▼ + "collection_id" => "13158" + "freeze_type" => array:1 [▼ + "TokenAccount" => array:2 [▼ + "token_id" => "1" + "account_id" => "8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48" + ] + ] + ] + ] + ] + ] + "extrinsic_hash" => "0x39575922bb98773bf5ffbeb76950909213e4e150c7a2cb3f4643f0d1c1df89f3" +] +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/Transfer.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/Transfer.php new file mode 100644 index 00000000..5766b444 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/Transfer.php @@ -0,0 +1,68 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +array:5 [▼ + "extrinsic_length" => 145 + "version" => 4 + "signature" => array:3 [▼ + "address" => array:1 [▼ + "Id" => "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" + ] + "signature" => array:1 [▼ + "Sr25519" => "1c5e1da29eb5e8cff9eb3c37e65deb5b83328c85e014c9bcb2d8f6a3f0dc38441307e0573466fa703b737e4c24ac49a674e1d6cecb32f3021cd84bc6bff1f383" + ] + "signedExtensions" => array:3 [▼ + "CheckMortality" => array:1 [▼ + "Mortal244" => 0 + ] + "CheckNonce" => 174 + "ChargeTransactionPayment" => "0" + ] + ] + "call" => array:1 [▼ + "MultiTokens" => array:1 [▼ + "transfer" => array:3 [▼ + "recipient" => array:1 [▼ + "Id" => "8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48" + ] + "collection_id" => "13158" + "params" => array:1 [▼ + "Simple" => array:3 [▼ + "token_id" => "1" + "amount" => "1" + "keep_alive" => false + ] + ] + ] + ] + ] + "extrinsic_hash" => "0x6b35502360dcbcbc70ec52275deec29961274b6c23c77d54f1e6f7f53c442e36" +] +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/UnapproveCollection.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/UnapproveCollection.php new file mode 100644 index 00000000..f1f063ac --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/UnapproveCollection.php @@ -0,0 +1,31 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/UnapproveToken.php b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/UnapproveToken.php new file mode 100644 index 00000000..68f37c23 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/Extrinsics/MultiTokens/UnapproveToken.php @@ -0,0 +1,31 @@ +signer = Arr::get($data, 'signature.address.Id'); + $self->hash = Arr::get($data, 'extrinsic_hash'); + $self->module = array_key_first(Arr::get($data, 'call')); + $self->call = array_key_first(Arr::get($data, 'call.' . $self->module)); + $self->params = Arr::get($data, 'call.' . $self->module . '.' . $self->call); + + return $self; + } +} + +/* +*/ diff --git a/src/Services/Processor/Substrate/Codec/Polkadart/PolkadartEvent.php b/src/Services/Processor/Substrate/Codec/Polkadart/PolkadartEvent.php new file mode 100644 index 00000000..3117aad6 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Polkadart/PolkadartEvent.php @@ -0,0 +1,12 @@ +", + "operator": "AccountId", + "expiration": "Option" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/ApproveToken.json b/src/Services/Processor/Substrate/Codec/Types/ApproveToken.json new file mode 100644 index 00000000..c51c647a --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/ApproveToken.json @@ -0,0 +1,11 @@ +{ + "ApproveToken": { + "callIndex": "(u8, u8)", + "collectionId": "Compact", + "tokenId": "Compact", + "operator": "AccountId", + "amount": "Compact", + "expiration": "Option", + "currentAmount": "Compact" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/AttributeStorage.json b/src/Services/Processor/Substrate/Codec/Types/AttributeStorage.json new file mode 100644 index 00000000..b6458a5f --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/AttributeStorage.json @@ -0,0 +1,12 @@ +{ + "AttributeStorage": { + "pallet": "u128", + "storage": "u128", + "hashedCollection": "u128", + "collectionId": "u128", + "hashedToken": "u128", + "tokenId": "Option", + "hashedAttribute": "u128", + "attribute": "Bytes" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/BatchAddAccount.json b/src/Services/Processor/Substrate/Codec/Types/BatchAddAccount.json new file mode 100644 index 00000000..4f4dbd83 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/BatchAddAccount.json @@ -0,0 +1,7 @@ +{ + "BatchAddAccount": { + "callIndex": "(u8, u8)", + "tankId": "MultiAddress", + "userIds": "Vec" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/BatchMint.json b/src/Services/Processor/Substrate/Codec/Types/BatchMint.json new file mode 100644 index 00000000..59fe0b0d --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/BatchMint.json @@ -0,0 +1,11 @@ +{ + "BatchMint": { + "callIndex": "(u8, u8)", + "collectionId": "Compact", + "recipients": "Vec" + }, + "MintRecipient": { + "accountId": "AccountId", + "params": "MintParamsOf" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/BatchRemoveAccount.json b/src/Services/Processor/Substrate/Codec/Types/BatchRemoveAccount.json new file mode 100644 index 00000000..120cedeb --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/BatchRemoveAccount.json @@ -0,0 +1,7 @@ +{ + "BatchRemoveAccount": { + "callIndex": "(u8, u8)", + "tankId": "MultiAddress", + "userIds": "Vec" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/BatchSetAttribute.json b/src/Services/Processor/Substrate/Codec/Types/BatchSetAttribute.json new file mode 100644 index 00000000..c12df56a --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/BatchSetAttribute.json @@ -0,0 +1,12 @@ +{ + "BatchSetAttribute": { + "callIndex": "(u8, u8)", + "collectionId": "Compact", + "tokenId": "Option", + "attributes": "Vec" + }, + "Attribute": { + "key": "Bytes", + "value": "Bytes" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/BatchTransfer.json b/src/Services/Processor/Substrate/Codec/Types/BatchTransfer.json new file mode 100644 index 00000000..0f9f56e9 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/BatchTransfer.json @@ -0,0 +1,11 @@ +{ + "BatchTransfer": { + "callIndex": "(u8, u8)", + "collectionId": "Compact", + "recipients": "Vec" + }, + "TransferRecipient": { + "accountId": "AccountId", + "params": "TransferParamsOf" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/Burn.json b/src/Services/Processor/Substrate/Codec/Types/Burn.json new file mode 100644 index 00000000..a21325da --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/Burn.json @@ -0,0 +1,13 @@ +{ + "Burn": { + "callIndex": "(u8, u8)", + "collectionId": "Compact", + "params": "BurnParamsOf" + }, + "BurnParamsOf": { + "tokenId": "Compact", + "amount": "Compact", + "keepAlive": "bool", + "removeTokenStorage": "bool" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/CanaryBatchMint.json b/src/Services/Processor/Substrate/Codec/Types/CanaryBatchMint.json new file mode 100644 index 00000000..35f60a3f --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/CanaryBatchMint.json @@ -0,0 +1,11 @@ +{ + "CanaryBatchMint": { + "callIndex": "(u8, u8)", + "collectionId": "Compact", + "recipients": "Vec" + }, + "CanaryMintRecipient": { + "accountId": "AccountId", + "params": "CanaryMintParamsOf" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/CanaryFreeze.json b/src/Services/Processor/Substrate/Codec/Types/CanaryFreeze.json new file mode 100644 index 00000000..df10711f --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/CanaryFreeze.json @@ -0,0 +1,19 @@ +{ + "CanaryFreeze": { + "callIndex": "(u8, u8)", + "collectionId": "Compact", + "freezeType": "CanaryFreezeOf" + }, + "CanaryFreezeOf": { + "_enum": { + "Collection": "null", + "Token": "TokenFreezeState", + "CollectionAccount": "AccountId", + "TokenAccount": "(Compact, AccountId)" + } + }, + "TokenFreezeState": { + "tokenId": "u128", + "freezeState": "Option" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/CanaryMint.json b/src/Services/Processor/Substrate/Codec/Types/CanaryMint.json new file mode 100644 index 00000000..2ba4747a --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/CanaryMint.json @@ -0,0 +1,31 @@ +{ + "CanaryMint": { + "callIndex": "(u8, u8)", + "recipient": "MultiAddress", + "collectionId": "Compact", + "params": "CanaryMintParamsOf" + }, + "CanaryMintParamsOf": { + "_enum": { + "CreateToken": "CanaryCreateTokenParams", + "Mint": "MintParams" + } + }, + "CanaryCreateTokenParams": { + "tokenId": "Compact", + "initialSupply": "Compact", + "sufficiency": "SufficiencyParams", + "cap": "Option", + "behavior": "Option", + "listingForbidden": "bool", + "freezeState": "Option", + "attributes": "Vec", + "foreignParams": "Option" + }, + "SufficiencyParams": { + "_enum": { + "Insufficient": "Option", + "Sufficient": "u128" + } + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/CanaryThaw.json b/src/Services/Processor/Substrate/Codec/Types/CanaryThaw.json new file mode 100644 index 00000000..b8361ae0 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/CanaryThaw.json @@ -0,0 +1,7 @@ +{ + "CanaryThaw": { + "callIndex": "(u8, u8)", + "collectionId": "Compact", + "freezeType": "CanaryFreezeOf" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/CanaryTokenAccountsStorage.json b/src/Services/Processor/Substrate/Codec/Types/CanaryTokenAccountsStorage.json new file mode 100644 index 00000000..bc66f8c0 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/CanaryTokenAccountsStorage.json @@ -0,0 +1,12 @@ +{ + "CanaryTokenAccountsStorageKey": { + "pallet": "u128", + "storage": "u128", + "hashCollectionId": "u128", + "collectionId": "u128", + "hashTokenId": "u128", + "tokenId": "u128", + "hashAccountId": "u128", + "accountId": "AccountId" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/CanaryTokenStorage.json b/src/Services/Processor/Substrate/Codec/Types/CanaryTokenStorage.json new file mode 100644 index 00000000..65a23ff8 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/CanaryTokenStorage.json @@ -0,0 +1,27 @@ +{ + "CanaryTokenStorageData": { + "supply": "Compact", + "cap": "Option", + "freezeState": "Option", + "minimumBalance": "Compact", + "sufficiency": "Sufficiency", + "mintDeposit": "Compact", + "attributeCount": "Compact", + "marketBehavior": "Option", + "listingForbidden": "bool", + "metadata": "TokenMetadata" + }, + "Sufficiency": { + "_enum": { + "Sufficient": "Null", + "Insufficient": "Compact" + } + }, + "FreezeState": { + "_enum": [ + "Permanent", + "Temporary", + "Never" + ] + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/CancelListing.json b/src/Services/Processor/Substrate/Codec/Types/CancelListing.json new file mode 100644 index 00000000..e6ff3ab7 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/CancelListing.json @@ -0,0 +1,6 @@ +{ + "CancelListing": { + "callIndex": "(u8, u8)", + "listingId": "h256" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/CollectionAccountsStorage.json b/src/Services/Processor/Substrate/Codec/Types/CollectionAccountsStorage.json new file mode 100644 index 00000000..a9dad415 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/CollectionAccountsStorage.json @@ -0,0 +1,15 @@ +{ + "CollectionAccountsStorageKey": { + "pallet": "u128", + "storage": "u128", + "hashCollectionId": "u128", + "collectionId": "u128", + "hashAccountId": "u128", + "accountId": "AccountId" + }, + "CollectionAccountsStorageData": { + "isFrozen": "bool", + "approvals": "BTreeMap>", + "accountCount": "Compact" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/CollectionStorage.json b/src/Services/Processor/Substrate/Codec/Types/CollectionStorage.json new file mode 100644 index 00000000..2f2b236a --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/CollectionStorage.json @@ -0,0 +1,34 @@ +{ + "CollectionStorageKey": { + "pallet": "u128", + "storage": "u128", + "hashCollectionId": "u128", + "collectionId": "u128" + }, + "CollectionStorageData": { + "owner": "AccountId", + "policy": "CollectionPolicy", + "tokenCount": "Compact", + "attributeCount": "Compact", + "totalDeposit": "Compact", + "explicitRoyaltyCurrencies": "Vec" + }, + "CollectionPolicy": { + "mint": "MintPolicy", + "burn": "Null", + "transfer": "TransferPolicy", + "attribute": "Null", + "market": "RoyaltyPolicy" + }, + "RoyaltyPolicy": { + "royalty": "Option" + }, + "MintPolicy": { + "maxTokenCount": "Option", + "maxTokenSupply": "Option", + "forceSingleMint": "bool" + }, + "TransferPolicy": { + "isFrozen": "bool" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/CreateCollection.json b/src/Services/Processor/Substrate/Codec/Types/CreateCollection.json new file mode 100644 index 00000000..997d3690 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/CreateCollection.json @@ -0,0 +1,28 @@ +{ + "CreateCollection": { + "callIndex": "(u8, u8)", + "descriptor": "CollectionDescriptor" + }, + "CollectionDescriptor": { + "policy": "CollectionPolicyDescriptor", + "explicitRoyaltyCurrencies": "Vec", + "attributes": "Vec" + }, + "MultiTokensAssetId": { + "collectionId": "Compact", + "tokenId": "Compact" + }, + "CollectionPolicyDescriptor": { + "mint": "MintPolicyDescriptor", + "market": "Option" + }, + "MintPolicyDescriptor": { + "maxTokenCount": "Option", + "maxTokenSupply": "Option", + "forceSingleMint": "bool" + }, + "RoyaltyPolicyDescriptor": { + "beneficiary": "AccountId", + "percentage": "Compact" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/CreateFuelTank.json b/src/Services/Processor/Substrate/Codec/Types/CreateFuelTank.json new file mode 100644 index 00000000..76c1bc50 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/CreateFuelTank.json @@ -0,0 +1,53 @@ +{ + "CreateFuelTank": { + "callIndex": "(u8, u8)", + "descriptor": "FuelTankDescriptor" + }, + "FuelTankDescriptor": { + "name": "Bytes", + "userAccountManagement": "Option", + "ruleSets": "BTreeMap>", + "providesDeposit": "bool", + "accountRules": "Vec" + }, + "UserAccountManagement": { + "tankReservesExistentialDeposit": "bool", + "tankReservesAccountCreationDeposit": "bool" + }, + "AccountRule": { + "_enum": { + "WhitelistedCallers": "Vec", + "RequireToken": "RequireTokenRule" + } + }, + "RequireTokenRule": { + "collectionId": "u128", + "tokenId": "u128" + }, + "DispatchRule": { + "_enum": { + "WhitelistedCallers": "Vec", + "WhitelistedCollections": "Vec", + "MaxFuelBurnPerTransaction": "u128", + "UserFuelBudget": "UserFuelBudgetRule", + "TankFuelBudget": "TankFuelBudgetRule", + "RequireToken": "RequireTokenRule", + "PermittedCalls": "PermittedCallsRule", + "PermittedExtrinsics": "PermittedExtrinsicsRule" + } + }, + "UserFuelBudgetRule": { + "amount": "Compact", + "resetPeriod": "u32" + }, + "TankFuelBudgetRule": { + "amount": "Compact", + "resetPeriod": "u32" + }, + "PermittedCallsRule": { + "calls": "Vec" + }, + "PermittedExtrinsicsRule": { + "extrinsics": "Vec" + } +} diff --git a/src/Services/Processor/Substrate/Codec/Types/CreateListing.json b/src/Services/Processor/Substrate/Codec/Types/CreateListing.json new file mode 100644 index 00000000..ead2be0f --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/CreateListing.json @@ -0,0 +1,15 @@ +{ + "CreateListing": { + "callIndex": "(u8, u8)", + "makeAssetId": "MultiTokensAssetId", + "takeAssetId": "MultiTokensAssetId", + "amount": "Compact", + "price": "Compact", + "salt": "Bytes", + "auctionData": "Option" + }, + "AuctionData": { + "startBlock": "Compact", + "endBlock": "Compact" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/DestroyCollection.json b/src/Services/Processor/Substrate/Codec/Types/DestroyCollection.json new file mode 100644 index 00000000..30b3882e --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/DestroyCollection.json @@ -0,0 +1,6 @@ +{ + "DestroyCollection": { + "callIndex": "(u8, u8)", + "collectionId": "Compact" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/DestroyFuelTank.json b/src/Services/Processor/Substrate/Codec/Types/DestroyFuelTank.json new file mode 100644 index 00000000..fbee3c1d --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/DestroyFuelTank.json @@ -0,0 +1,6 @@ +{ + "DestroyFuelTank": { + "callIndex": "(u8, u8)", + "tankId": "MultiAddress" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/Dispatch.json b/src/Services/Processor/Substrate/Codec/Types/Dispatch.json new file mode 100644 index 00000000..b2315c27 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/Dispatch.json @@ -0,0 +1,7 @@ +{ + "Dispatch": { + "callIndex": "(u8, u8)", + "tankId": "MultiAddress", + "ruleSetId": "u32" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/DispatchAndTouch.json b/src/Services/Processor/Substrate/Codec/Types/DispatchAndTouch.json new file mode 100644 index 00000000..57d28c90 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/DispatchAndTouch.json @@ -0,0 +1,7 @@ +{ + "DispatchAndTouch": { + "callIndex": "(u8, u8)", + "tankId": "MultiAddress", + "ruleSetId": "u32" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/FillListing.json b/src/Services/Processor/Substrate/Codec/Types/FillListing.json new file mode 100644 index 00000000..85df437c --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/FillListing.json @@ -0,0 +1,7 @@ +{ + "FillListing": { + "callIndex": "(u8, u8)", + "listingId": "h256", + "amount": "Compact" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/FinalizeAuction.json b/src/Services/Processor/Substrate/Codec/Types/FinalizeAuction.json new file mode 100644 index 00000000..cf4542d0 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/FinalizeAuction.json @@ -0,0 +1,6 @@ +{ + "FinalizeAuction": { + "callIndex": "(u8, u8)", + "listingId": "h256" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/ForceSetConsumption.json b/src/Services/Processor/Substrate/Codec/Types/ForceSetConsumption.json new file mode 100644 index 00000000..9b3685b4 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/ForceSetConsumption.json @@ -0,0 +1,13 @@ +{ + "ForceSetConsumption": { + "callIndex": "(u8, u8)", + "tankId": "MultiAddress", + "userId": "Option", + "ruleSetId": "u32", + "consumption": "FuelTanksConsumption" + }, + "FuelTanksConsumption": { + "totalConsumed": "Compact", + "lastResetBlock": "Option" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/Freeze.json b/src/Services/Processor/Substrate/Codec/Types/Freeze.json new file mode 100644 index 00000000..3be294c2 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/Freeze.json @@ -0,0 +1,15 @@ +{ + "Freeze": { + "callIndex": "(u8, u8)", + "collectionId": "Compact", + "freezeType": "FreezeOf" + }, + "FreezeOf": { + "_enum": { + "Collection": "null", + "Token": "u128", + "CollectionAccount": "AccountId", + "TokenAccount": "(Compact, AccountId)" + } + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/FuelTankAccountStorage.json b/src/Services/Processor/Substrate/Codec/Types/FuelTankAccountStorage.json new file mode 100644 index 00000000..f0e5e621 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/FuelTankAccountStorage.json @@ -0,0 +1,14 @@ +{ + "FuelTankAccountStorageKey": { + "pallet": "u128", + "storage": "u128", + "hashTankAccount": "u128", + "tankAccount": "AccountId", + "hashAccount": "u128", + "account": "AccountId" + }, + "FuelTankAccountStorageData": { + "tankDeposit": "u128", + "userDeposit": "u128" + } +} diff --git a/src/Services/Processor/Substrate/Codec/Types/InsertRuleSet.json b/src/Services/Processor/Substrate/Codec/Types/InsertRuleSet.json new file mode 100644 index 00000000..3c0d34e0 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/InsertRuleSet.json @@ -0,0 +1,8 @@ +{ + "InsertRuleSet": { + "callIndex": "(u8, u8)", + "tankId": "MultiAddress", + "ruleSetId": "u32", + "rules": "Vec" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/ListingStorage.json b/src/Services/Processor/Substrate/Codec/Types/ListingStorage.json new file mode 100644 index 00000000..c191fef4 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/ListingStorage.json @@ -0,0 +1,50 @@ +{ + "ListingStorageKey": { + "pallet": "u128", + "storage": "u128", + "listingId": "h256" + }, + "ListingStorageData": { + "seller": "AccountId", + "makeAssetId": "MultiTokensAssetId", + "takeAssetId": "MultiTokensAssetId", + "amount": "Compact", + "price": "Compact", + "minTakeValue": "Compact", + "feeSide": "ListingFeeSide", + "creationBlock": "Compact", + "deposit": "Compact", + "salt": "Bytes", + "data": "ListingData", + "state": "ListingState" + }, + "ListingFeeSide": { + "_enum": [ + "NoFee", + "Make", + "Take" + ] + }, + "ListingData": { + "_enum": { + "FixedPrice": "Null", + "Auction": "AuctionData" + } + }, + "ListingState": { + "_enum": { + "FixedPrice": "FixedPriceState", + "Auction": "AuctionState" + } + }, + "FixedPriceState": { + "amountFilled": "Compact" + }, + "AuctionState": { + "highBid": "Option" + }, + "AuctionBid": { + "bidder": "AccountId", + "price": "Compact" + } +} diff --git a/src/Services/Processor/Substrate/Codec/Types/Mint.json b/src/Services/Processor/Substrate/Codec/Types/Mint.json new file mode 100644 index 00000000..463395b1 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/Mint.json @@ -0,0 +1,42 @@ +{ + "Mint": { + "callIndex": "(u8, u8)", + "recipient": "MultiAddress", + "collectionId": "Compact", + "params": "MintParamsOf" + }, + "MintParamsOf": { + "_enum": { + "CreateToken": "CreateTokenParams", + "Mint": "MintParams", + "CreateForeignToken": "CreateForeignTokenParams" + } + }, + "CreateTokenParams": { + "tokenId": "Compact", + "initialSupply": "Compact", + "unitPrice": "Compact", + "cap": "Option", + "behavior": "Option", + "listingForbidden": "bool", + "attributes": "Vec" + }, + "TokenCap": { + "_enum": { + "SingleMint": "Null", + "Supply": "Compact" + } + }, + "MintParams": { + "tokenId": "Compact", + "amount": "Compact", + "unitPrice": "Option" + }, + "CreateForeignTokenParams": { + "tokenId": "Compact", + "behavior": "Option", + "listingForbidden": "bool", + "metadata": "TokenMetadata", + "existentialDeposit": "Compact" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/MutateCollection.json b/src/Services/Processor/Substrate/Codec/Types/MutateCollection.json new file mode 100644 index 00000000..ea8e01a6 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/MutateCollection.json @@ -0,0 +1,18 @@ +{ + "MutateCollection": { + "callIndex": "(u8, u8)", + "collectionId": "Compact", + "mutation": "CollectionMutation" + }, + "CollectionMutation": { + "owner": "Option", + "royalty": "RoyaltyParamOf", + "explicitRoyaltyCurrencies": "Option>" + }, + "RoyaltyParamOf": { + "_enum": { + "NoMutation": "Null", + "SomeMutation": "Option" + } + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/MutateFuelTank.json b/src/Services/Processor/Substrate/Codec/Types/MutateFuelTank.json new file mode 100644 index 00000000..f344d868 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/MutateFuelTank.json @@ -0,0 +1,18 @@ +{ + "MutateFuelTank": { + "callIndex": "(u8, u8)", + "tankId": "MultiAddress", + "mutation": "TankMutation" + }, + "TankMutation": { + "userAccountManagement": "TankShouldMutate", + "providesDeposit": "Option", + "accountRules": "Option>" + }, + "TankShouldMutate": { + "_enum": { + "NoMutation": "Null", + "SomeMutation": "Option" + } + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/MutateToken.json b/src/Services/Processor/Substrate/Codec/Types/MutateToken.json new file mode 100644 index 00000000..7da273ff --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/MutateToken.json @@ -0,0 +1,19 @@ +{ + "MutateToken": { + "callIndex": "(u8, u8)", + "collectionId": "Compact", + "tokenId": "Compact", + "mutation": "TokenMutation" + }, + "TokenMutation": { + "behavior": "BehaviorParamOf", + "listingForbidden": "Option", + "metadata": "Option" + }, + "BehaviorParamOf": { + "_enum": { + "NoMutation": "Null", + "SomeMutation": "Option" + } + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/PlaceBid.json b/src/Services/Processor/Substrate/Codec/Types/PlaceBid.json new file mode 100644 index 00000000..79e6324c --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/PlaceBid.json @@ -0,0 +1,7 @@ +{ + "PlaceBid": { + "callIndex": "(u8, u8)", + "listingId": "h256", + "price": "Compact" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/RemoveAccount.json b/src/Services/Processor/Substrate/Codec/Types/RemoveAccount.json new file mode 100644 index 00000000..1d7bff3a --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/RemoveAccount.json @@ -0,0 +1,7 @@ +{ + "RemoveAccount": { + "callIndex": "(u8, u8)", + "tankId": "MultiAddress", + "userId": "MultiAddress" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/RemoveAccountRuleData.json b/src/Services/Processor/Substrate/Codec/Types/RemoveAccountRuleData.json new file mode 100644 index 00000000..c055f2eb --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/RemoveAccountRuleData.json @@ -0,0 +1,9 @@ +{ + "RemoveAccountRuleData": { + "callIndex": "(u8, u8)", + "tankId": "MultiAddress", + "userId": "MultiAddress", + "ruleSetId": "u32", + "ruleKind": "DispatchRuleKind" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/RemoveAllAttributes.json b/src/Services/Processor/Substrate/Codec/Types/RemoveAllAttributes.json new file mode 100644 index 00000000..a1a851fd --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/RemoveAllAttributes.json @@ -0,0 +1,8 @@ +{ + "RemoveAllAttributes": { + "callIndex": "(u8, u8)", + "collectionId": "Compact", + "tokenId": "Option", + "attributeCount": "u32" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/RemoveAttribute.json b/src/Services/Processor/Substrate/Codec/Types/RemoveAttribute.json new file mode 100644 index 00000000..75f21e6f --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/RemoveAttribute.json @@ -0,0 +1,8 @@ +{ + "RemoveAttribute": { + "callIndex": "(u8, u8)", + "collectionId": "Compact", + "tokenId": "Option", + "key": "Bytes" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/RemoveRuleSet.json b/src/Services/Processor/Substrate/Codec/Types/RemoveRuleSet.json new file mode 100644 index 00000000..0229789a --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/RemoveRuleSet.json @@ -0,0 +1,7 @@ +{ + "RemoveRuleSet": { + "callIndex": "(u8, u8)", + "tankId": "MultiAddress", + "ruleSetId": "u32" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/ScheduleMutateFreezeState.json b/src/Services/Processor/Substrate/Codec/Types/ScheduleMutateFreezeState.json new file mode 100644 index 00000000..e60aa12f --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/ScheduleMutateFreezeState.json @@ -0,0 +1,8 @@ +{ + "ScheduleMutateFreezeState": { + "callIndex": "(u8, u8)", + "tankId": "MultiAddress", + "ruleSetId": "Option", + "isFrozen": "bool" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/SetAttribute.json b/src/Services/Processor/Substrate/Codec/Types/SetAttribute.json new file mode 100644 index 00000000..7a8df22d --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/SetAttribute.json @@ -0,0 +1,9 @@ +{ + "SetAttribute": { + "callIndex": "(u8, u8)", + "collectionId": "Compact", + "tokenId": "Option", + "key": "Bytes", + "value": "Bytes" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/TankStorage.json b/src/Services/Processor/Substrate/Codec/Types/TankStorage.json new file mode 100644 index 00000000..1800c51a --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/TankStorage.json @@ -0,0 +1,89 @@ +{ + "TankStorageKey": { + "pallet": "u128", + "storage": "u128", + "hashTankAccount": "u128", + "tankAccount": "AccountId" + }, + "TankStorageData": { + "owner": "AccountId", + "name": "Bytes", + "ruleSets": "BTreeMap", + "totalReserved": "Compact", + "accountCount": "Compact", + "userAccountManagement": "Option", + "isFrozen": "bool", + "providesDeposit": "bool", + "accountRules": "BTreeMap" + }, + "AccountRuleKind": { + "_enum": [ + "WhitelistedCallers", + "RequireToken" + ] + }, + "AccountRuleStorage": { + "_enum": { + "WhitelistedCallers": "Vec", + "RequireToken": "RequireTokenRuleStorage" + } + }, + "DispatchRuleSet": { + "rules": "BTreeMap", + "isFrozen": "bool" + }, + "DispatchRuleKind": { + "_enum": [ + "WhitelistedCallers", + "WhitelistedCollections", + "MaxFuelBurnPerTransaction", + "UserFuelBudget", + "TankFuelBudget", + "RequireToken", + "PermittedCalls", + "PermittedExtrinsics" + ] + }, + "DispatchRuleStorage": { + "_enum": { + "WhitelistedCallers": "Vec", + "WhitelistedCollections": "Vec", + "MaxFuelBurnPerTransaction": "u128", + "UserFuelBudget": "UserFuelBudgetRuleStorage", + "TankFuelBudget": "TankFuelBudgetRuleStorage", + "RequireToken": "RequireTokenRuleStorage", + "PermittedCalls": "PermittedCallsRuleStorage", + "PermittedExtrinsics": "PermittedExtrinsicsRuleStorage" + } + }, + "RequireTokenRuleStorage": { + "collectionId": "u128", + "tokenId": "u128" + }, + "UserFuelBudgetRuleStorage": { + "budget": "BudgetRuleAccountStorage", + "userCount": "Compact" + }, + "BudgetRuleAccountStorage": { + "amount": "Compact", + "resetPeriod": "u32" + }, + "TankFuelBudgetRuleStorage": { + "budget": "BudgetRuleAccountStorage", + "consumption": "ConsumptionRuleAccountStorage" + }, + "ConsumptionRuleAccountStorage": { + "totalConsumed": "Compact", + "lastResetBlock": "Option" + }, + "PermittedCallsRuleStorage": { + "calls": "Vec" + }, + "PermittedExtrinsicsRuleStorage": { + "extrinsics": "Vec" + }, + "ExtrinsicInfo": { + "palletName": "Bytes", + "extrinsicName": "Bytes" + } +} diff --git a/src/Services/Processor/Substrate/Codec/Types/Thaw.json b/src/Services/Processor/Substrate/Codec/Types/Thaw.json new file mode 100644 index 00000000..44f22f20 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/Thaw.json @@ -0,0 +1,7 @@ +{ + "Thaw": { + "callIndex": "(u8, u8)", + "collectionId": "Compact", + "freezeType": "FreezeOf" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/TokenAccountsStorage.json b/src/Services/Processor/Substrate/Codec/Types/TokenAccountsStorage.json new file mode 100644 index 00000000..4d9b2957 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/TokenAccountsStorage.json @@ -0,0 +1,26 @@ +{ + "TokenAccountsStorageKey": { + "pallet": "u128", + "storage": "u128", + "hashAccountId": "u128", + "accountId": "AccountId", + "hashCollectionId": "u128", + "collectionId": "u128", + "hashTokenId": "u128", + "tokenId": "u128" + }, + "TokenAccountsStorageData": { + "balance": "Compact", + "reservedBalance": "Compact", + "lockedBalance": "Compact", + "namedReserves": "BTreeMap<[u8;8],u128>", + "locks": "BTreeMap<[u8;8],u128>", + "approvals": "BTreeMap", + "isFrozen": "bool" + }, + "TokenApproval": { + "amount": "Compact", + "expiration": "Option" + } +} + diff --git a/src/Services/Processor/Substrate/Codec/Types/TokenStorage.json b/src/Services/Processor/Substrate/Codec/Types/TokenStorage.json new file mode 100644 index 00000000..2283535b --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/TokenStorage.json @@ -0,0 +1,40 @@ +{ + "TokenStorageKey": { + "pallet": "u128", + "storage": "u128", + "hashCollectionId": "u128", + "collectionId": "u128", + "hashTokenId": "u128", + "tokenId": "u128" + }, + "TokenStorageData": { + "supply": "Compact", + "cap": "Option", + "isFrozen": "bool", + "minimumBalance": "Compact", + "unitPrice": "Compact", + "mintDeposit": "Compact", + "attributeCount": "Compact", + "marketBehavior": "Option", + "listingForbidden": "bool", + "metadata": "TokenMetadata" + }, + "TokenMarketBehavior": { + "_enum": { + "HasRoyalty": "RoyaltyPolicyDescriptor", + "IsCurrency": "Null" + } + }, + "TokenMetadata": { + "_enum": { + "Native": "Null", + "Foreign": "ForeignTokenMetadata" + } + }, + "ForeignTokenMetadata": { + "decimalCount": "Compact", + "name": "Bytes", + "symbol": "Bytes", + "location": "Option" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/Transfer.json b/src/Services/Processor/Substrate/Codec/Types/Transfer.json new file mode 100644 index 00000000..6804de01 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/Transfer.json @@ -0,0 +1,25 @@ +{ + "Transfer": { + "callIndex": "(u8, u8)", + "recipient": "MultiAddress", + "collectionId": "Compact", + "params": "TransferParamsOf" + }, + "TransferParamsOf": { + "_enum": { + "Simple": "SimpleTransferParams", + "Operator": "OperatorTransferParams" + } + }, + "SimpleTransferParams": { + "tokenId": "Compact", + "amount": "Compact", + "keepAlive": "bool" + }, + "OperatorTransferParams": { + "tokenId": "Compact", + "source": "AccountId", + "amount": "Compact", + "keepAlive": "bool" + } +} diff --git a/src/Services/Processor/Substrate/Codec/Types/TransferAllBalance.json b/src/Services/Processor/Substrate/Codec/Types/TransferAllBalance.json new file mode 100644 index 00000000..4f80fd77 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/TransferAllBalance.json @@ -0,0 +1,7 @@ +{ + "TransferAllBalance": { + "callIndex": "(u8, u8)", + "dest": "MultiAddress", + "keepAlive": "bool" + } +} diff --git a/src/Services/Processor/Substrate/Codec/Types/TransferBalance.json b/src/Services/Processor/Substrate/Codec/Types/TransferBalance.json new file mode 100644 index 00000000..203f2c8c --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/TransferBalance.json @@ -0,0 +1,7 @@ +{ + "TransferBalance": { + "callIndex": "(u8, u8)", + "dest": "MultiAddress", + "value": "Compact" + } +} diff --git a/src/Services/Processor/Substrate/Codec/Types/TransferBalanceKeepAlive.json b/src/Services/Processor/Substrate/Codec/Types/TransferBalanceKeepAlive.json new file mode 100644 index 00000000..a30bb795 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/TransferBalanceKeepAlive.json @@ -0,0 +1,7 @@ +{ + "TransferBalanceKeepAlive": { + "callIndex": "(u8, u8)", + "dest": "MultiAddress", + "value": "Compact" + } +} diff --git a/src/Services/Processor/Substrate/Codec/Types/UnapproveCollection.json b/src/Services/Processor/Substrate/Codec/Types/UnapproveCollection.json new file mode 100644 index 00000000..3417dd00 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/UnapproveCollection.json @@ -0,0 +1,7 @@ +{ + "UnapproveCollection": { + "callIndex": "(u8, u8)", + "collectionId": "Compact", + "operator": "AccountId" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/Codec/Types/UnapproveToken.json b/src/Services/Processor/Substrate/Codec/Types/UnapproveToken.json new file mode 100644 index 00000000..7b460804 --- /dev/null +++ b/src/Services/Processor/Substrate/Codec/Types/UnapproveToken.json @@ -0,0 +1,8 @@ +{ + "UnapproveToken": { + "callIndex": "(u8, u8)", + "collectionId": "Compact", + "tokenId": "Compact", + "operator": "AccountId" + } +} \ No newline at end of file diff --git a/src/Services/Processor/Substrate/DecoderService.php b/src/Services/Processor/Substrate/DecoderService.php new file mode 100644 index 00000000..3bf16e2b --- /dev/null +++ b/src/Services/Processor/Substrate/DecoderService.php @@ -0,0 +1,88 @@ +client = new DecoderClient(); + $this->host = config('enjin-platform.decoder_container'); + } + + public function decode(string $type, string|array $bytes): ?array + { + try { + $result = $this->client->getClient()->post($this->host, [ + $type === 'Extrinsics' ? 'extrinsics' : 'events' => $bytes, + 'network' => config('enjin-platform.chains.network'), + ]); + + $data = $this->client->getResponse($result); + + if (!$data) { + Log::critical('Container returned empty response'); + + return null; + } + + return $this->polkadartSerialize($type, $data); + } catch (Throwable $e) { + Log::critical('Error when trying to fetch from container: ' . $e->getMessage()); + Log::critical($e->getTraceAsString()); + } + + return null; + } + + protected function polkadartSerialize($type, $data): array + { + if ($type === 'Extrinsics') { + return array_map(fn ($extrinsic) => $this->polkadartExtrinsic($extrinsic), $data); + } + + return array_map(fn ($event) => $this->polkadartEvent($event), $data); + } + + protected function polkadartEvent($event): PolkadartEvent + { + $module = array_key_first(Arr::get($event, 'event')); + $eventId = is_string($eventId = Arr::get($event, 'event.' . $module)) ? $eventId : array_key_first($eventId); + + $class = Package::getClassesThatImplementInterface(PolkadartEvent::class) + ->where(fn ($class) => $eventId == Str::afterLast($class, '\\')) + ->first(); + + return $class ? $class::fromChain($event) : GenericEvent::fromChain($event); + } + + protected function polkadartExtrinsic($extrinsic): PolkadartExtrinsic + { + $module = array_key_first(Arr::get($extrinsic, 'call')); + $call = is_string($callId = Arr::get($extrinsic, 'call.' . $module)) ? $callId : array_key_first($callId); + + if ($module !== 'MultiTokens') { + return GenericExtrinsic::fromChain($extrinsic); + } + + $class = Package::getClassesThatImplementInterface(PolkadartExtrinsic::class) + ->where(fn ($class) => Str::studly($call) == Str::afterLast($class, '\\')) + ->first(); + + return $class ? $class::fromChain($extrinsic) : GenericExtrinsic::fromChain($extrinsic); + } +} diff --git a/src/Services/Processor/Substrate/EventProcessor.php b/src/Services/Processor/Substrate/EventProcessor.php new file mode 100644 index 00000000..cc5a2c64 --- /dev/null +++ b/src/Services/Processor/Substrate/EventProcessor.php @@ -0,0 +1,62 @@ +block = $block; + $this->codec = $codec; + } + + public function run(): void + { + Log::info("Processing Events from block #{$this->block->number}"); + $events = $this->block->events ?? []; + + foreach ($events as $event) { + $this->processEvent($event); + } + } + + protected function processEvent(PolkadartEvent $event): void + { + $pallet = $event->getPallet(); + + if (class_exists($enum = sprintf("\Enjin\Platform\Enums\Substrate\%sEventType", $pallet))) { + $this->callEvent($enum, $event); + } elseif (class_exists($enum = sprintf("\Enjin\Platform\%s\Enums\Substrate\%sEventType", $pallet, $pallet))) { + $this->callEvent($enum, $event); + } + } + + protected function callEvent($enum, $event): void + { + $enum::tryFrom(class_basename($event))?->getProcessor()?->run($event, $this->block, $this->codec); + } + + protected function shouldIndexCollection(?string $collectionId): bool + { + if (!$collectionId) { + return false; + } + + return $this->shouldIndex('collections', $collectionId); + } + + protected function shouldIndex(string $filter, string $value): bool + { + $indexFilters = collect(config("enjin-platform.indexing.filters.{$filter}", [])); + + return $indexFilters->isEmpty() || $indexFilters->contains($value); + } +} diff --git a/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/Approved.php b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/Approved.php new file mode 100644 index 00000000..ddda7c78 --- /dev/null +++ b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/Approved.php @@ -0,0 +1,111 @@ +getCollection( + $collectionId = $event->collectionId, + ); + $operator = WalletService::firstOrStore(['account' => Account::parseAccount($event->operator)]); + $extrinsic = $block->extrinsics[$event->extrinsicIndex]; + $transaction = Transaction::firstWhere(['transaction_chain_hash' => $extrinsic->hash]); + + if ($tokenId = $event->tokenId) { + $token = $this->getToken($collection->id, $tokenId); + $collectionAccount = $this->getTokenAccount( + $collection->id, + $token->id, + WalletService::firstOrStore(['account' => Account::parseAccount($event->owner)])->id, + ); + + TokenAccountApproval::updateOrCreate( + [ + 'token_account_id' => $collectionAccount->id, + 'wallet_id' => $operatorId = $operator->id, + ], + [ + 'amount' => $event->amount, + 'expiration' => $event->expiration, + ] + ); + + Log::info( + sprintf( + 'An approval for "%s" (id %s) was added to TokenAccount %s, %s, %s (id: %s).', + $event->operator, + $operatorId, + $event->owner, + $collectionId, + $tokenId, + $collectionAccount->id, + ) + ); + + TokenApproved::safeBroadcast( + $collectionId, + $tokenId, + $operator->address, + $event->amount, + $event->expiration, + $transaction + ); + } else { + $collectionAccount = $this->getCollectionAccount( + $collection->id, + WalletService::firstOrStore(['account' => Account::parseAccount($event->owner)])->id, + ); + + CollectionAccountApproval::updateOrCreate( + [ + 'collection_account_id' => $collectionAccount->id, + 'wallet_id' => $operatorId = $operator->id, + ], + [ + 'expiration' => $event->expiration, + ] + ); + + Log::info( + sprintf( + 'An approval for "%s" (id %s) was added to CollectionAccount %s, %s (id: %s).', + $event->operator, + $operatorId, + $event->owner, + $collectionId, + $collectionAccount->id, + ) + ); + + CollectionApproved::safeBroadcast( + $collectionId, + $operator->address, + $event->expiration, + $transaction + ); + } + } +} diff --git a/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/AttributeRemoved.php b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/AttributeRemoved.php new file mode 100644 index 00000000..44fc7672 --- /dev/null +++ b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/AttributeRemoved.php @@ -0,0 +1,74 @@ +getCollection( + $collectionId = $event->collectionId, + ); + + $token = !is_null($tokenId = $event->tokenId) + ? $this->getToken($collection->id, $tokenId) + : null; + + $attribute = $this->getAttribute( + $collection->id, + $token?->id, + $key = HexConverter::hexToString($event->key) + ); + $attribute->delete(); + + Log::info( + sprintf( + 'Attribute "%s" (id %s) of Collection #%s (id %s) %s was removed.', + $key, + $attribute->id, + $collectionId, + $collection->id, + !is_null($tokenId) ? sprintf(' and Token #%s (id %s) ', $tokenId, $token->id) : '' + ) + ); + + $extrinsic = $block->extrinsics[$event->extrinsicIndex]; + $transaction = Transaction::firstWhere(['transaction_chain_hash' => $extrinsic->hash]); + + if ($token) { + $token->decrement('attribute_count'); + TokenAttributeRemoved::safeBroadcast( + $token, + $attribute->key, + $attribute->value, + $transaction + ); + } else { + $collection->decrement('attribute_count'); + CollectionAttributeRemoved::safeBroadcast( + $collection, + $attribute->key, + $attribute->value, + $transaction + ); + } + } +} diff --git a/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/AttributeSet.php b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/AttributeSet.php new file mode 100644 index 00000000..3bc9f518 --- /dev/null +++ b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/AttributeSet.php @@ -0,0 +1,81 @@ +getCollection( + $collectionId = $event->collectionId, + ); + + $token = !is_null($tokenId = $event->tokenId) + ? $this->getToken($collection->id, $tokenId) + : null; + + $attribute = Attribute::updateOrCreate( + [ + 'collection_id' => $collection->id, + 'token_id' => $token?->id, + 'key' => $key = Hex::safeConvertToString($event->key), + ], + [ + 'value' => $value = Hex::safeConvertToString($event->value), + ] + ); + + Log::info( + sprintf( + 'Attribute "%s" (id %s) of Collection #%s (id %s) %s was set to "%s".', + $key, + $attribute->id, + $collectionId, + $collection->id, + !is_null($tokenId) ? sprintf('and Token #%s (id %s) ', $tokenId, $token->id) : '', + $value, + ) + ); + + if ($attribute->wasRecentlyCreated) { + $extrinsic = $block->extrinsics[$event->extrinsicIndex]; + $transaction = Transaction::firstWhere(['transaction_chain_hash' => $extrinsic->hash]); + + if ($token) { + $token->increment('attribute_count'); + TokenAttributeSet::safeBroadcast( + $token, + $key, + $value, + $transaction + ); + } else { + $collection->increment('attribute_count'); + CollectionAttributeSet::safeBroadcast( + $collection, + $key, + $value, + $transaction + ); + } + } + } +} diff --git a/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/CollectionAccountCreated.php b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/CollectionAccountCreated.php new file mode 100644 index 00000000..86d79dd4 --- /dev/null +++ b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/CollectionAccountCreated.php @@ -0,0 +1,54 @@ +getCollection($event->collectionId); + $account = WalletService::firstOrStore(['account' => $event->account]); + $collectionAccount = CollectionAccount::create([ + 'wallet_id' => $account->id, + 'collection_id' => $collection->id, + 'is_frozen' => false, + 'account_count' => 0, + ]); + + Log::info( + sprintf( + 'CollectionAccount (id %s) of Collection #%s (id %s) and account %s was created.', + $collectionAccount->id, + $event->collectionId, + $collection->id, + $account->address, + ) + ); + + $extrinsic = $block->extrinsics[$event->extrinsicIndex]; + + CollectionAccountCreatedEvent::safeBroadcast( + $event->collectionId, + $account, + Transaction::firstWhere(['transaction_chain_hash' => $extrinsic->hash]) + ); + } +} diff --git a/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/CollectionAccountDestroyed.php b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/CollectionAccountDestroyed.php new file mode 100644 index 00000000..a21eab1c --- /dev/null +++ b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/CollectionAccountDestroyed.php @@ -0,0 +1,53 @@ +getCollection($event->collectionId); + $account = WalletService::firstOrStore(['account' => $event->account]); + $collectionAccount = CollectionAccount::firstWhere([ + 'wallet_id' => $account->id, + 'collection_id' => $collection->id, + ]); + $collectionAccount->delete(); + + Log::info( + sprintf( + 'CollectionAccount (id %s) of Collection #%s (id %s) and account %s was deleted.', + $collectionAccount->id, + $event->collectionId, + $collection->id, + $account->address + ) + ); + + $extrinsic = $block->extrinsics[$event->extrinsicIndex]; + + CollectionAccountDestroyedEvent::safeBroadcast( + $this->getCollection($event->collectionId), + $account, + Transaction::firstWhere(['transaction_chain_hash' => $extrinsic->hash]) + ); + } +} diff --git a/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/CollectionCreated.php b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/CollectionCreated.php new file mode 100644 index 00000000..9f6cf55e --- /dev/null +++ b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/CollectionCreated.php @@ -0,0 +1,101 @@ +extrinsics[$event->extrinsicIndex]; + $collection = $this->parseCollection($extrinsic, $event); + + $daemonTransaction = Transaction::firstWhere(['transaction_chain_hash' => $extrinsic->hash]); + + if ($daemonTransaction) { + Log::info(sprintf('Collection %s (id: %s) was created from transaction %s (id: %s).', $event->collectionId, $collection->id, $daemonTransaction->transaction_chain_hash, $daemonTransaction->id)); + } else { + Log::info(sprintf('Collection %s (id: %s) was created from an unknown transaction.', $event->collectionId, $collection->id)); + } + + CollectionCreatedEvent::safeBroadcast( + $collection, + $daemonTransaction + ); + } + + protected function parseCollection(CreateCollection|Generic $extrinsic, CollectionCreatedPolkadart $event): Collection + { + $params = $extrinsic->params; + + if ($extrinsic instanceof Generic) { + // TODO: We need to pop the call from the extrinsic + $calls = collect(Arr::get($extrinsic->params, 'calls'))->filter( + fn ($call) => Arr::get($call, 'MultiTokens.create_collection') !== null + )->first(); + + $params = Arr::get($calls, 'MultiTokens.create_collection'); + } + + $owner = WalletService::firstOrStore(['account' => Account::parseAccount($event->owner)]); + + $beneficiary = Arr::get($params, 'descriptor.policy.market.royalty.Some.beneficiary'); + $percentage = Arr::get($params, 'descriptor.policy.market.royalty.Some.percentage'); + + $collection = Collection::create([ + 'collection_chain_id' => $event->collectionId, + 'owner_wallet_id' => $owner->id, + 'max_token_count' => Arr::get($params, 'descriptor.policy.mint.max_token_count.Some'), + 'max_token_supply' => Arr::get($params, 'descriptor.policy.mint.max_token_supply.Some'), + 'force_single_mint' => Arr::get($params, 'descriptor.policy.mint.force_single_mint'), + 'is_frozen' => false, + 'royalty_wallet_id' => $beneficiary ? WalletService::firstOrStore(['account' => Account::parseAccount($beneficiary)])->id : null, + 'royalty_percentage' => $percentage ? $percentage / 10 ** 7 : null, + 'token_count' => '0', + 'attribute_count' => '0', + 'total_deposit' => '25000000000000000000', + 'network' => config('enjin-platform.chains.network'), + ]); + + $this->collectionRoyaltyCurrencies($collection->id, Arr::get($params, 'descriptor.explicit_royalty_currencies')); + + return $collection; + } + + protected function collectionRoyaltyCurrencies(string $collectionId, array $royaltyCurrencies): void + { + foreach ($royaltyCurrencies as $currency) { + CollectionRoyaltyCurrency::updateOrCreate( + [ + 'collection_id' => $collectionId, + 'currency_collection_chain_id' => $currency['collection_id'], + 'currency_token_chain_id' => $currency['token_id'], + ], + [ + 'created_at' => $now = Carbon::now(), + 'updated_at' => $now, + ] + ); + } + } +} diff --git a/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/CollectionDestroyed.php b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/CollectionDestroyed.php new file mode 100644 index 00000000..9612637c --- /dev/null +++ b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/CollectionDestroyed.php @@ -0,0 +1,39 @@ +getCollection( + $collectionId = $event->collectionId + ); + $collection->delete(); + + Log::info("Collection #{$collectionId} (id {$collection->id}) was destroyed."); + + $extrinsic = $block->extrinsics[$event->extrinsicIndex]; + + CollectionDestroyedEvent::safeBroadcast( + $collection, + Transaction::firstWhere(['transaction_chain_hash' => $extrinsic->hash]) + ); + } +} diff --git a/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/CollectionMutated.php b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/CollectionMutated.php new file mode 100644 index 00000000..14878b9c --- /dev/null +++ b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/CollectionMutated.php @@ -0,0 +1,69 @@ +getCollection($event->collectionId); + $attributes = []; + $royalties = []; + + if ($owner = $event->owner) { + $attributes['owner_wallet_id'] = WalletService::firstOrStore(['account' => Account::parseAccount($owner)])->id; + } + + if ($event->royalty === 'SomeMutation') { + if ($beneficiary = $event->beneficiary) { + $attributes['royalty_wallet_id'] = WalletService::firstOrStore(['account' => Account::parseAccount($beneficiary)])->id; + $attributes['royalty_percentage'] = number_format($event->percentage / 1000000000, 9); + } else { + $attributes['royalty_wallet_id'] = null; + $attributes['royalty_percentage'] = null; + } + } + + if (!is_null($currencies = $event->explicitRoyaltyCurrencies)) { + foreach ($currencies as $currency) { + $royalties[] = new CollectionRoyaltyCurrency([ + 'currency_collection_chain_id' => $currency['collection_id'], + 'currency_token_chain_id' => $currency['token_id'], + ]); + } + + $collection->royaltyCurrencies()->delete(); + $collection->royaltyCurrencies()->saveMany($royalties); + } + + $collection->fill($attributes)->save(); + Log::info("Collection #{$event->collectionId} (id {$collection->id}) was updated."); + + $extrinsic = $block->extrinsics[$event->extrinsicIndex]; + + CollectionMutatedEvent::safeBroadcast( + $collection, + $event->getParams(), + Transaction::firstWhere(['transaction_chain_hash' => $extrinsic->hash]) + ); + } +} diff --git a/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/Freeze.php b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/Freeze.php new file mode 100644 index 00000000..5c391793 --- /dev/null +++ b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/Freeze.php @@ -0,0 +1,166 @@ +extrinsics[$event->extrinsicIndex]; + $transaction = Transaction::firstWhere(['transaction_chain_hash' => $extrinsic->hash]); + + $collection = $this->getCollection($event->collectionId); + match (FreezeType::from($event->freezeType)) { + FreezeType::COLLECTION => $this->freezeCollection($collection, $transaction), + FreezeType::TOKEN => $this->freezeToken($collection, $event->tokenId, $transaction), + FreezeType::COLLECTION_ACCOUNT => $this->freezeCollectionAccount($collection, Account::parseAccount($event->account), $transaction), + FreezeType::TOKEN_ACCOUNT => $this->freezeTokenAccount($collection, $event->tokenId, Account::parseAccount($event->account), $transaction), + }; + } + + /** + * Freeze collection. + * + * @param mixed $collection + * + * @return void + */ + protected function freezeCollection($collection, ?Model $transaction = null): void + { + $collection->is_frozen = true; + $collection->save(); + + Log::info( + sprintf( + 'Collection #%s (id %s) was frozen.', + $collection->collection_chain_id, + $collection->id, + ) + ); + + CollectionFrozen::safeBroadcast( + $collection, + $transaction + ); + } + + /** + * Freeze token. + * + * @param mixed $collection + * @param string $tokenChainId + * + * @return void + */ + protected function freezeToken($collection, string $tokenChainId, ?Model $transaction = null): void + { + $tokenStored = $this->getToken($collection->id, $tokenChainId); + $tokenStored->is_frozen = true; + $tokenStored->save(); + + Log::info( + sprintf( + 'Token #%s (id %s) of Collection #%s (id %s) was frozen.', + $tokenChainId, + $tokenStored->id, + $collection->collection_chain_id, + $collection->id, + ) + ); + + TokenFrozen::safeBroadcast( + $tokenStored, + $transaction + ); + } + + /** + * Freeze collection account. + * + * @param mixed $collection + * @param string $wallet + * + * @return void + */ + protected function freezeCollectionAccount($collection, string $wallet, ?Model $transaction = null): void + { + $walletStored = $this->getWallet($wallet); + $collectionAccountStored = $this->getCollectionAccount($collection->id, $walletStored->id); + $collectionAccountStored->is_frozen = true; + $collectionAccountStored->save(); + + Log::info( + sprintf( + 'CollectionAccount (id %s) of Collection #%s (id %s) and account %s (id %s) was frozen.', + $collectionAccountStored->id, + $collection->collection_chain_id, + $collection->id, + $wallet, + $walletStored->id, + ) + ); + + CollectionAccountFrozen::safeBroadcast( + $collectionAccountStored, + $transaction + ); + } + + /** + * Freeze token account. + * + * @param mixed $collection + * @param string $tokenChainId + * @param string $wallet + * + * @return void + */ + protected function freezeTokenAccount($collection, string $tokenChainId, string $wallet, ?Model $transaction = null): void + { + $walletStored = $this->getWallet($wallet); + $tokenStored = $this->getToken($collection->id, $tokenChainId); + $tokenAccountStored = $this->getTokenAccount($collection->id, $tokenStored->id, $walletStored->id); + $tokenAccountStored->is_frozen = true; + $tokenAccountStored->save(); + + Log::info( + sprintf( + 'TokenAccount (id %s) of Collection #%s (id %s), Token #%s (id %s) and account %s (id %s) was frozen.', + $tokenAccountStored->id, + $collection->collection_chain_id, + $collection->id, + $tokenChainId, + $tokenStored->id, + $wallet, + $walletStored->id, + ) + ); + + TokenAccountFrozen::safeBroadcast( + $tokenAccountStored, + $transaction + ); + } +} diff --git a/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/Minted.php b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/Minted.php new file mode 100644 index 00000000..04bd2b22 --- /dev/null +++ b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/Minted.php @@ -0,0 +1,63 @@ +extrinsics[$event->extrinsicIndex]; + $recipient = WalletService::firstOrStore(['account' => Account::parseAccount($event->recipient)]); + $collection = $this->getCollection($event->collectionId); + $token = $this->getToken($collection->id, $event->tokenId); + $token->update([ + 'supply', $finalSupply = $token->supply + $event->amount, + 'mint_deposit' => $token->unit_price * $finalSupply, + ]); + + $tokenAccount = TokenAccount::firstWhere([ + 'wallet_id' => $recipient->id, + 'collection_id' => $collection->id, + 'token_id' => $token->id, + ]); + $tokenAccount->increment('balance', $event->amount); + + Log::info(sprintf( + 'Minted %s units of Collection #%s (id: %s), Token #%s (id: %s) to %s (id: %s).', + $event->amount, + $event->collectionId, + $collection->id, + $event->tokenId, + $token->id, + $recipient->address, + $recipient->id, + )); + + TokenMinted::safeBroadcast( + $token, + WalletService::firstOrStore(['account' => Account::parseAccount($event->issuer)]), + $recipient, + $event->amount, + Transaction::firstWhere(['transaction_chain_hash' => $extrinsic->hash]) + ); + } +} diff --git a/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/Thawed.php b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/Thawed.php new file mode 100644 index 00000000..d9abf1cf --- /dev/null +++ b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/Thawed.php @@ -0,0 +1,166 @@ +extrinsics[$event->extrinsicIndex]; + $transaction = Transaction::firstWhere(['transaction_chain_hash' => $extrinsic->hash]); + + $collection = $this->getCollection($event->collectionId); + match (FreezeType::from($event->freezeType)) { + FreezeType::COLLECTION => $this->thawCollection($collection, $transaction), + FreezeType::TOKEN => $this->thawToken($collection, $event->tokenId, $transaction), + FreezeType::COLLECTION_ACCOUNT => $this->thawCollectionAccount($collection, Account::parseAccount($event->account), $transaction), + FreezeType::TOKEN_ACCOUNT => $this->thawTokenAccount($collection, $event->tokenId, Account::parseAccount($event->account), $transaction), + }; + } + + /** + * Thaw collection. + * + * @param mixed $collection + * + * @return void + */ + protected function thawCollection($collection, $transaciton = null): void + { + $collection->is_frozen = false; + $collection->save(); + + Log::info( + sprintf( + 'Collection #%s (id %s) was thawed.', + $collection->collection_chain_id, + $collection->id, + ) + ); + + CollectionThawed::safeBroadcast( + $collection, + $transaciton + ); + } + + /** + * Thaw token. + * + * @param mixed $collection + * @param string $tokenChainId + * + * @return void + */ + protected function thawToken($collection, string $tokenChainId, ?Model $transaction = null): void + { + $tokenStored = $this->getToken($collection->id, $tokenChainId); + $tokenStored->is_frozen = false; + $tokenStored->save(); + + Log::info( + sprintf( + 'Token #%s (id %s) of Collection #%s (id %s) was thawed.', + $tokenChainId, + $tokenStored->id, + $collection->collection_chain_id, + $collection->id, + ) + ); + + TokenThawed::safeBroadcast( + $tokenStored, + $transaction + ); + } + + /** + * Thaw collection account. + * + * @param mixed $collection + * @param string $wallet + * + * @return void + */ + protected function thawCollectionAccount($collection, string $wallet, ?Model $transaction = null): void + { + $walletStored = $this->getWallet($wallet); + $collectionAccountStored = $this->getCollectionAccount($collection->id, $walletStored->id); + $collectionAccountStored->is_frozen = false; + $collectionAccountStored->save(); + + Log::info( + sprintf( + 'CollectionAccount (id %s) of Collection #%s (id %s) and account %s (id %s) was thawed.', + $collectionAccountStored->id, + $collection->collection_chain_id, + $collection->id, + $wallet, + $walletStored->id, + ) + ); + + CollectionAccountThawed::safeBroadcast( + $collectionAccountStored, + $transaction + ); + } + + /** + * Thaw token account. + * + * @param mixed $collection + * @param string $tokenChainId + * @param string $wallet + * + * @return void + */ + protected function thawTokenAccount($collection, string $tokenChainId, string $wallet, ?Model $transaction = null): void + { + $walletStored = $this->getWallet($wallet); + $tokenStored = $this->getToken($collection->id, $tokenChainId); + $tokenAccountStored = $this->getTokenAccount($collection->id, $tokenStored->id, $walletStored->id); + $tokenAccountStored->is_frozen = false; + $tokenAccountStored->save(); + + Log::info( + sprintf( + 'TokenAccount (id %s) of Collection #%s (id %s), Token #%s (id %s) and account %s (id %s) was thawed.', + $tokenAccountStored->id, + $collection->collection_chain_id, + $collection->id, + $tokenChainId, + $tokenStored->id, + $wallet, + $walletStored->id, + ) + ); + + TokenAccountThawed::safeBroadcast( + $tokenAccountStored, + $transaction + ); + } +} diff --git a/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/TokenAccountCreated.php b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/TokenAccountCreated.php new file mode 100644 index 00000000..4d5272d3 --- /dev/null +++ b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/TokenAccountCreated.php @@ -0,0 +1,62 @@ + $event->account]); + $collection = $this->getCollection($event->collectionId); + $token = $this->getToken($collection->id, $event->tokenId); + $collectionAccount = $this->getCollectionAccount($collection->id, $account->id); + $collectionAccount->increment('account_count'); + $tokenAccount = TokenAccount::create([ + 'wallet_id' => $account->id, + 'collection_id' => $collection->id, + 'token_id' => $token->id, + 'balance' => 0, // The balances are updated on Mint event + 'reserved_balance' => 0, + 'is_frozen' => false, + ]); + + Log::info( + sprintf( + 'TokenAccount (id %s) of Collection #%s (id %s), Token #%s (id %s) and account %s was created.', + $tokenAccount->id, + $event->collectionId, + $collection->id, + $token->token_chain_id, + $token->id, + $account->address, + ) + ); + + $extrinsic = $block->extrinsics[$event->extrinsicIndex]; + + TokenAccountCreatedEvent::safeBroadcast( + $collection, + $token, + $account, + Transaction::firstWhere(['transaction_chain_hash' => $extrinsic->hash]) + ); + } +} diff --git a/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/TokenAccountDestroyed.php b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/TokenAccountDestroyed.php new file mode 100644 index 00000000..9ec442ce --- /dev/null +++ b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/TokenAccountDestroyed.php @@ -0,0 +1,59 @@ +getCollection($event->collectionId); + $token = $this->getToken($collection->id, $event->tokenId); + $account = WalletService::firstOrStore(['account' => $event->account]); + $collectionAccount = $this->getCollectionAccount($collection->id, $account->id); + $collectionAccount->decrement('account_count'); + $tokenAccount = TokenAccount::firstWhere([ + 'wallet_id' => $account->id, + 'collection_id' => $collection->id, + 'token_id' => $token->id, + ]); + $tokenAccount->delete(); + + Log::info( + sprintf( + 'TokenAccount of Collection #%s (id %s), Token #%s (id %s) and account %s was deleted.', + $event->collectionId, + $collection->id, + $event->tokenId, + $token->id, + $account->address, + ) + ); + + $extrinsic = $block->extrinsics[$event->extrinsicIndex]; + + TokenAccountDestroyedEvent::safeBroadcast( + $collection, + $token, + $account, + Transaction::firstWhere(['transaction_chain_hash' => $extrinsic->hash]) + ); + } +} diff --git a/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/TokenBurned.php b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/TokenBurned.php new file mode 100644 index 00000000..c0bf7bb2 --- /dev/null +++ b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/TokenBurned.php @@ -0,0 +1,60 @@ + $event->account]); + $collection = $this->getCollection($event->collectionId); + $token = $this->getToken($collection->id, $event->tokenId); + $token->decrement('supply', $event->amount); + + $tokenAccount = TokenAccount::firstWhere([ + 'wallet_id' => $account->id, + 'collection_id' => $collection->id, + 'token_id' => $token->id, + ]); + $tokenAccount->decrement('balance', $event->amount); + + Log::info(sprintf( + 'Burned %s units of Collection #%s (id: %s), Token #%s (id: %s) from %s (id: %s).', + $event->amount, + $event->tokenId, + $token->id, + $event->collectionId, + $collection->id, + $account->address, + $account->id + )); + + $extrinsic = $block->extrinsics[$event->extrinsicIndex]; + + TokenBurnedEvent::safeBroadcast( + $this->getCollection($event->collectionId), + $event->tokenId, + $event->account, + $event->amount, + Transaction::firstWhere(['transaction_chain_hash' => $extrinsic->hash]) + ); + } +} diff --git a/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/TokenCreated.php b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/TokenCreated.php new file mode 100644 index 00000000..f7f6f454 --- /dev/null +++ b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/TokenCreated.php @@ -0,0 +1,103 @@ +extrinsics[$event->extrinsicIndex]; + $token = $this->parseToken($extrinsic, $event); + + TokenCreatedEvent::safeBroadcast( + $token, + WalletService::firstOrStore(['account' => Account::parseAccount($event->issuer)]), + Transaction::firstWhere(['transaction_chain_hash' => $extrinsic->hash]) + ); + } + + public function parseToken(Mint|BatchMint|Generic $extrinsic, TokenCreatedPolkadart $event): mixed + { + $params = Arr::get($extrinsic->params, 'params.CreateToken'); + + if ($extrinsic instanceof BatchMint) { + $recipient = collect(Arr::get($extrinsic->params, 'recipients'))->firstWhere('params.CreateToken.token_id', $event->tokenId); + $params = Arr::get($recipient, 'params.CreateToken'); + } + + if ($extrinsic instanceof Generic) { + $calls = collect(Arr::get($extrinsic->params, 'calls')) + ->filter( + fn ($call) => Arr::get($call, 'MultiTokens.mint.collection_id') === $event->collectionId + && Arr::get($call, 'MultiTokens.mint.params.CreateToken.token_id') === $event->tokenId + )->first(); + + $params = Arr::get($calls, 'MultiTokens.mint.params.CreateToken'); + } + + $collection = $this->getCollection($event->collectionId); + $isSingleMint = Arr::get($params, 'cap.Some') === 'SingleMint'; + $capSupply = Arr::get($params, 'cap.Some.Supply'); + $cap = TokenMintCapType::INFINITE; + + if ($capSupply !== null) { + $cap = TokenMintCapType::SUPPLY; + } elseif ($isSingleMint) { + $cap = TokenMintCapType::SINGLE_MINT; + } + + $beneficiary = Arr::get($params, 'behavior.Some.HasRoyalty.beneficiary'); + $percentage = Arr::get($params, 'behavior.Some.HasRoyalty.percentage'); + $isCurrency = Arr::get($params, 'behavior.Some') === 'IsCurrency'; + + $unitPrice = gmp_init(Arr::get($params, 'unit_price') ?? Arr::get($params, 'sufficiency.Insufficient.unit_price.Some') ?? 10 ** 16); + $minBalance = Arr::get($params, 'sufficiency.Sufficient.minimum_balance'); + + if (!$minBalance) { + $minBalance = gmp_div(gmp_pow(10, 16), $unitPrice, GMP_ROUND_PLUSINF); + $minBalance = gmp_cmp(1, $minBalance) > 0 ? '1' : gmp_strval($minBalance); + } + + $isFrozen = in_array(Arr::get($params, 'freeze_state.Some'), ['Permanent', 'Temporary']); + + return Token::create([ + 'collection_id' => $collection->id, + 'token_chain_id' => $event->tokenId, + 'supply' => $initialSupply = Arr::get($params, 'initial_supply'), + 'cap' => $cap->name, + 'cap_supply' => $capSupply, + 'is_frozen' => $isFrozen, + 'royalty_wallet_id' => $beneficiary ? WalletService::firstOrStore(['account' => Account::parseAccount($beneficiary)])->id : null, + 'royalty_percentage' => $percentage ? $percentage / 10 ** 7 : null, + 'is_currency' => $isCurrency, + 'listing_forbidden' => Arr::get($params, 'listing_forbidden'), + 'unit_price' => gmp_strval($unitPrice), + 'minimum_balance' => $minBalance, + 'mint_deposit' => $initialSupply * $unitPrice, + 'attribute_count' => 0, + ]); + } +} diff --git a/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/TokenDestroyed.php b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/TokenDestroyed.php new file mode 100644 index 00000000..1e415f24 --- /dev/null +++ b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/TokenDestroyed.php @@ -0,0 +1,40 @@ +getCollection($collectionId = $event->collectionId); + $token = $this->getToken($collection->id, $tokenId = $event->tokenId); + $token->delete(); + + Log::info("Token #{$tokenId} in Collection ID {$collectionId} was destroyed."); + + $extrinsic = $block->extrinsics[$event->extrinsicIndex]; + + TokenDestroyedEvent::safeBroadcast( + $token, + WalletService::firstOrStore(['account' => $event->caller]), + Transaction::firstWhere(['transaction_chain_hash' => $extrinsic->hash]) + ); + } +} diff --git a/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/TokenMutated.php b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/TokenMutated.php new file mode 100644 index 00000000..36c14a57 --- /dev/null +++ b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/TokenMutated.php @@ -0,0 +1,57 @@ +getCollection($event->collectionId); + $token = $this->getToken($collection->id, $event->tokenId); + + $attributes = []; + if ($listingForbidden = $event->listingForbidden) { + $attributes['listing_forbidden'] = $listingForbidden; + } + + if ($event->behaviorMutation === 'SomeMutation') { + $attributes['is_currency'] = $event->isCurrency; + $attributes['royalty_wallet_id'] = null; + $attributes['royalty_percentage'] = null; + + if ($event->beneficiary) { + $attributes['royalty_wallet_id'] = WalletService::firstOrStore(['account' => Account::parseAccount($event->beneficiary)])->id; + $attributes['royalty_percentage'] = number_format($event->percentage / 1000000000, 9); + } + } + + $token->fill($attributes)->save(); + Log::info("Token #{$token->token_chain_id} (id {$token->id}) of Collection #{$collection->collection_chain_id} (id {$collection->id}) was updated."); + + $extrinsic = $block->extrinsics[$event->extrinsicIndex]; + + TokenMutatedEvent::safeBroadcast( + $token, + $event->getParams(), + Transaction::firstWhere(['transaction_chain_hash' => $extrinsic->hash]) + ); + } +} diff --git a/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/Transferred.php b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/Transferred.php new file mode 100644 index 00000000..4c321a63 --- /dev/null +++ b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/Transferred.php @@ -0,0 +1,65 @@ +getCollection($event->collectionId); + $token = $this->getToken($collection->id, $event->tokenId); + $fromAccount = WalletService::firstOrStore(['account' => $event->from]); + TokenAccount::firstWhere([ + 'wallet_id' => $fromAccount->id, + 'collection_id' => $collection->id, + 'token_id' => $token->id, + ])?->decrement('balance', $event->amount); + + $toAccount = WalletService::firstOrStore(['account' => $event->to]); + $toTokenAccount = TokenAccount::firstWhere([ + 'wallet_id' => $toAccount->id, + 'collection_id' => $collection->id, + 'token_id' => $token->id, + ]); + $toTokenAccount->increment('balance', $event->amount); + + Log::info(sprintf( + 'Transferred %s units of token #%s (id: %s) in collection #%s (id: %s) to %s (id: %s).', + $event->amount, + $event->tokenId, + $token->id, + $event->collectionId, + $collection->id, + $toAccount->address, + $toAccount->id, + )); + + $extrinsic = $block->extrinsics[$event->extrinsicIndex]; + + TokenTransferred::safeBroadcast( + $token, + $fromAccount, + $toAccount, + $event->amount, + Transaction::firstWhere(['transaction_chain_hash' => $extrinsic->hash]) + ); + } +} diff --git a/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/Unapproved.php b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/Unapproved.php new file mode 100644 index 00000000..aee9301a --- /dev/null +++ b/src/Services/Processor/Substrate/Events/Implementations/MultiTokens/Unapproved.php @@ -0,0 +1,74 @@ +getCollection( + $collectionId = $event->collectionId + ); + $operator = WalletService::firstOrStore(['account' => Account::parseAccount($event->operator)]); + + $extrinsic = $block->extrinsics[$event->extrinsicIndex]; + $transaction = Transaction::firstWhere(['transaction_chain_hash' => $extrinsic->hash]); + + if ($tokenId = $event->tokenId) { + $token = $this->getToken($collection->id, $tokenId); + $collectionAccount = $this->getTokenAccount( + $collection->id, + $token->id, + WalletService::firstOrStore(['account' => Account::parseAccount($event->owner)])->id + ); + + TokenAccountApproval::where([ + 'token_account_id' => $collectionAccount->id, + 'wallet_id' => $operator->id, + ])?->delete(); + + TokenUnapproved::safeBroadcast( + $collectionId, + $tokenId, + $operator->address, + $transaction + ); + } else { + $collectionAccount = $this->getCollectionAccount( + $collection->id, + WalletService::firstOrStore(['account' => Account::parseAccount($event->owner)])->id + ); + + CollectionAccountApproval::where([ + 'collection_account_id' => $collectionAccount->id, + 'wallet_id' => $operator->id, + ])?->delete(); + + CollectionUnapproved::safeBroadcast( + $collectionId, + $operator->address, + $transaction + ); + } + } +} diff --git a/src/Services/Processor/Substrate/Events/Implementations/Traits/QueryDataOrFail.php b/src/Services/Processor/Substrate/Events/Implementations/Traits/QueryDataOrFail.php new file mode 100644 index 00000000..ca3df369 --- /dev/null +++ b/src/Services/Processor/Substrate/Events/Implementations/Traits/QueryDataOrFail.php @@ -0,0 +1,89 @@ +first()) { + throw new PlatformException(__('enjin-platform::traits.query_data_or_fail.unable_to_find_collection', ['class' => __CLASS__, 'collectionChainId' => $collectionChainId])); + } + + return $collection; + } + + protected function getToken(int $collectionId, string $tokenChainId): Token + { + if (!$token = Token::where(['collection_id' => $collectionId, 'token_chain_id' => $tokenChainId])->first()) { + throw new PlatformException(__('enjin-platform::traits.query_data_or_fail.unable_to_find_token', ['class' => __CLASS__, 'tokenChainId' => $tokenChainId, 'collectionId' => $collectionId])); + } + + return $token; + } + + protected function getAttribute(int $collectionId, ?int $tokenId, string $key): Attribute + { + if (!$attribute = Attribute::where([ + 'collection_id' => $collectionId, + 'token_id' => $tokenId, + 'key' => $key, + ])->first()) { + throw new PlatformException(__('enjin-platform::traits.query_data_or_fail.unable_to_find_attribute', ['class' => __CLASS__, 'tokenId' => $tokenId, 'collectionId' => $collectionId, 'key' => $key])); + } + + return $attribute; + } + + protected function getCollectionAccount(int $collectionId, int $walletId): CollectionAccount + { + if (!$collectionAccount = CollectionAccount::where([ + 'collection_id' => $collectionId, + 'wallet_id' => $walletId, + ])->first()) { + Log::error(__('enjin-platform::traits.query_data_or_fail.unable_to_find_collection_account', ['class' => __CLASS__, 'walletId' => $walletId, 'collectionId' => $collectionId])); + + return CollectionAccount::create([ + 'collection_id' => $collectionId, + 'wallet_id' => $walletId, + ]); + + // We will not throw an exception here until we can make sure this never happens + // throw new PlatformException(__('enjin-platform::traits.query_data_or_fail.unable_to_find_collection_account', ['class' => __CLASS__, 'walletId' => $walletId, 'collectionId' => $collectionId])); + } + + return $collectionAccount; + } + + protected function getTokenAccount(int $collectionId, int $tokenId, int $walletId): TokenAccount + { + if (!$tokenAccount = TokenAccount::where([ + 'wallet_id' => $walletId, + 'collection_id' => $collectionId, + 'token_id' => $tokenId, + ])->first()) { + throw new PlatformException(__('enjin-platform::traits.query_data_or_fail.unable_to_find_token_account', ['class' => __CLASS__, 'walletId' => $walletId, 'collectionId' => $collectionId, 'tokenId' => $tokenId])); + } + + return $tokenAccount; + } + + protected function getWallet(string $publicKey): Wallet + { + if (!$wallet = Wallet::where(['public_key' => SS58Address::getPublicKey($publicKey)])->first()) { + throw new PlatformException(__('enjin-platform::traits.query_data_or_fail.unable_to_find_wallet_account', ['class' => __CLASS__, 'publicKey' => $publicKey])); + } + + return $wallet; + } +} diff --git a/src/Services/Processor/Substrate/Events/SubstrateEvent.php b/src/Services/Processor/Substrate/Events/SubstrateEvent.php new file mode 100644 index 00000000..95ae829c --- /dev/null +++ b/src/Services/Processor/Substrate/Events/SubstrateEvent.php @@ -0,0 +1,12 @@ +block = $block; + $this->codec = $codec; + } + + public function run() + { + Log::info("Processing Extrinsics from block #{$this->block->number}"); + $extrinsics = $this->block->extrinsics ?? []; + + foreach ($extrinsics as $index => $extrinsic) { + $this->processExtrinsic($extrinsic, $index); + } + } + + protected function processExtrinsic(PolkadartExtrinsic $extrinsic, int $index) + { + if ($extrinsic->signer !== SS58Address::getDaemonAccount()) { + return; + } + + $transaction = Transaction::where([ + 'transaction_chain_hash' => $extrinsic->hash, + 'wallet_public_key' => SS58Address::getDaemonAccount(prefixed: true), + ])->orderBy('created_at', 'desc')->first(); + + if ($transaction) { + if ($this->block->events === null) { + Log::info('Fetching events for block #' . $this->block->number); + $rpc = new SubstrateWebsocket(); + $blockHash = $this->block->hash; + + if ($blockHash === null) { + $blockHash = $rpc->send('chain_getBlockHash', [$this->block->number]); + } + + if ($events = $rpc->send('state_getStorage', [StorageKey::EVENTS->value, $blockHash])) { + $this->block->events = State::eventsForBlock(['number' => $this->block->number, 'events' => $events]) ?? []; + } + } + + $this->updateTransaction($transaction, $index); + $this->saveExtrinsicEvents($transaction, $index); + Log::info( + sprintf( + 'Updated transaction %s with extrinsic id: %s', + $transaction->transaction_chain_hash, + $transaction->transaction_chain_id, + ) + ); + } + } + + protected function updateTransaction($transaction, int $index): void + { + $extrinsicId = "{$this->block->number}-{$index}"; + $resultEvent = collect($this->block->events)->firstWhere( + fn ($event) => (($event instanceof ExtrinsicSuccess) || ($event instanceof ExtrinsicFailed)) + && $event->extrinsicIndex == $index + ); + + TransactionService::update($transaction, [ + 'transaction_chain_id' => $extrinsicId, + 'state' => TransactionState::FINALIZED->name, + 'result' => SystemEventType::tryFrom(class_basename($resultEvent))?->name, + ]); + } + + protected function saveExtrinsicEvents($transaction, int $index): void + { + Event::where('transaction_id', $transaction->id)->delete(); + + $eventsWithTransaction = collect($this->block->events)->filter(fn ($event) => $event->extrinsicIndex == $index) + ->map(fn ($event) => [ + 'transaction_id' => $transaction->id, + 'phase' => '2', + 'look_up' => 'unknown', + 'module_id' => $event->module, + 'event_id' => $event->name, + 'params' => json_encode($event->getParams()), + ])->toArray(); + + Event::insert($eventsWithTransaction); + } +} diff --git a/src/Services/Processor/Substrate/Parser.php b/src/Services/Processor/Substrate/Parser.php new file mode 100644 index 00000000..b7a30ba4 --- /dev/null +++ b/src/Services/Processor/Substrate/Parser.php @@ -0,0 +1,669 @@ +serializationService = $serializationService ?? new Substrate(); + $this->walletService = new WalletService(); + $this->collectionService = new CollectionService($this->walletService); + $this->tokenService = new TokenService($this->walletService); + } + + /** + * Store collections. + */ + public function collectionsStorages(array $data): void + { + $insertData = []; + $insertRoyaltyCurrencies = []; + + foreach ($data as [$key, $collection]) { + $collectionKey = $this->serializationService->decode('collectionStorageKey', $key); + $collectionData = $this->serializationService->decode('collectionStorageData', $collection); + $ownerWallet = $this->getCachedWallet( + $collectionData['owner'], + fn () => $this->walletService->firstOrStore(['account' => $collectionData['owner']]) + ); + $royaltyWallet = ($beneficiary = $collectionData['royaltyBeneficiary']) + ? $this->getCachedWallet( + $beneficiary, + fn () => $this->walletService->firstOrStore(['account' => $beneficiary]) + ) + : null; + + if (!empty($royaltyCurrencies = $collectionData['explicitRoyaltyCurrencies'])) { + $insertRoyaltyCurrencies[] = [ + 'collectionId' => $collectionKey['collectionId'], + 'currencies' => $royaltyCurrencies, + ]; + } + + $insertData[] = [ + 'collection_chain_id' => $collectionKey['collectionId'], + 'owner_wallet_id' => $ownerWallet->id, + 'max_token_count' => $collectionData['maxTokenCount'], + 'max_token_supply' => $collectionData['maxTokenSupply'], + 'force_single_mint' => $collectionData['forceSingleMint'], + 'is_frozen' => $collectionData['isFrozen'], + 'royalty_wallet_id' => $royaltyWallet?->id, + 'royalty_percentage' => $collectionData['royaltyPercentage'], + 'token_count' => $collectionData['tokenCount'], + 'attribute_count' => $collectionData['attributeCount'], + 'total_deposit' => $collectionData['totalDeposit'], + 'network' => config('enjin-platform.chains.network'), + 'created_at' => $now = Carbon::now(), + 'updated_at' => $now, + ]; + } + + $this->collectionService->insert($insertData); + + $this->collectionsRoyaltyCurrencies($insertRoyaltyCurrencies); + } + + /** + * Store collection. + */ + public function collectionStorage(string $key, string $data): mixed + { + $collectionKey = $this->serializationService->decode('collectionStorageKey', $key); + $collectionData = $this->serializationService->decode('collectionStorageData', $data); + $ownerWallet = $this->getCachedWallet( + $collectionData['owner'], + fn () => $this->walletService->firstOrStore(['account' => $collectionData['owner']]) + ); + $royaltyWallet = ($beneficiary = $collectionData['royaltyBeneficiary']) + ? $this->getCachedWallet($beneficiary, fn () => $this->walletService->firstOrStore(['account' => $beneficiary])) + : null; + + $collection = $this->collectionService->store([ + 'collection_chain_id' => $collectionKey['collectionId'], + 'owner_wallet_id' => $ownerWallet->id, + 'max_token_count' => $collectionData['maxTokenCount'], + 'max_token_supply' => $collectionData['maxTokenSupply'], + 'force_single_mint' => $collectionData['forceSingleMint'], + 'is_frozen' => $collectionData['isFrozen'], + 'royalty_wallet_id' => $royaltyWallet?->id, + 'royalty_percentage' => $collectionData['royaltyPercentage'], + 'token_count' => $collectionData['tokenCount'], + 'attribute_count' => $collectionData['attributeCount'], + 'total_deposit' => $collectionData['totalDeposit'], + 'network' => config('enjin-platform.chains.network'), + 'created_at' => $now = Carbon::now(), + 'updated_at' => $now, + ]); + + $this->collectionRoyaltyCurrencies($collection->id, $collectionData['explicitRoyaltyCurrencies']); + + return $collection; + } + + /** + * Store tokens. + */ + public function tokensStorages(array $data): void + { + $insertData = []; + + foreach ($data as [$key, $token]) { + $tokenKey = $this->serializationService->decode('tokenStorageKey', $key); + $tokenData = $this->serializationService->decode('tokenStorageData', $token); + $collection = $this->getCachedCollection( + $tokenKey['collectionId'], + fn () => Collection::where('collection_chain_id', $tokenKey['collectionId'])->firstOrFail() + ); + $royaltyWallet = ($beneficiary = $tokenData['royaltyBeneficiary']) + ? $this->getCachedWallet( + $beneficiary, + fn () => $this->walletService->firstOrStore(['account' => $beneficiary]) + ) + : null; + + $insertData[] = [ + 'token_chain_id' => $tokenKey['tokenId'], + 'collection_id' => $collection->id, + 'supply' => $tokenData['supply'], + 'cap' => $tokenData['cap']->name, + 'cap_supply' => $tokenData['capSupply'], + 'is_frozen' => $tokenData['isFrozen'], + 'royalty_wallet_id' => $royaltyWallet?->id, + 'royalty_percentage' => $tokenData['royaltyPercentage'], + 'is_currency' => $tokenData['isCurrency'], + 'listing_forbidden' => $tokenData['listingForbidden'], + 'minimum_balance' => $tokenData['minimumBalance'], + 'unit_price' => $tokenData['unitPrice'], + 'mint_deposit' => $tokenData['mintDeposit'], + 'attribute_count' => $tokenData['attributeCount'], + 'created_at' => $now = Carbon::now(), + 'updated_at' => $now, + ]; + } + + $this->tokenService->insert($insertData); + } + + /** + * Store token. + */ + public function tokenStorage(string $key, string $data): mixed + { + $tokenKey = $this->serializationService->decode('tokenStorageKey', $key); + $tokenData = $this->serializationService->decode('tokenStorageData', $data); + + $collectionStored = $this->getCachedCollection( + $tokenKey['collectionId'], + fn () => Collection::where('collection_chain_id', $tokenKey['collectionId'])->firstOrFail() + ); + $royaltyWallet = ($beneficiary = $tokenData['royaltyBeneficiary']) + ? $this->getCachedWallet($beneficiary, fn () => $this->walletService->firstOrStore(['account' => $beneficiary])) + : null; + + return $this->tokenService->updateOrStore( + [ + 'collection_id' => $collectionStored->id, + 'token_chain_id' => $tokenKey['tokenId'], + ], + [ + 'supply' => $tokenData['supply'], + 'cap' => $tokenData['cap']->name, + 'cap_supply' => $tokenData['capSupply'], + 'is_frozen' => $tokenData['isFrozen'], + 'royalty_wallet_id' => $royaltyWallet?->id, + 'royalty_percentage' => $tokenData['royaltyPercentage'], + 'is_currency' => $tokenData['isCurrency'], + 'listing_forbidden' => $tokenData['listingForbidden'], + 'minimum_balance' => $tokenData['minimumBalance'], + 'unit_price' => $tokenData['unitPrice'], + 'mint_deposit' => $tokenData['mintDeposit'], + 'attribute_count' => $tokenData['attributeCount'], + ] + ); + } + + /** + * Store collection accounts. + */ + public function collectionsAccountsStorages(array $data): void + { + $insertData = []; + $insertApprovals = []; + + foreach ($data as [$key, $collectionAccount]) { + $collectionAccountKey = $this->serializationService->decode('collectionAccountStorageKey', $key); + $collectionAccountData = $this->serializationService->decode('collectionAccountStorageData', $collectionAccount); + $wallet = $this->getCachedWallet( + $collectionAccountKey['accountId'], + fn () => $this->walletService->firstOrStore(['account' => $collectionAccountKey['accountId']]) + ); + + $collection = $this->getCachedCollection( + $collectionAccountKey['collectionId'], + fn () => Collection::where('collection_chain_id', $collectionAccountKey['collectionId'])->firstOrFail() + ); + + if (!empty($approvals = $collectionAccountData['approvals'])) { + $insertApprovals[] = [ + 'walletId' => $wallet->id, + 'collectionId' => $collection->id, + 'approvals' => $approvals, + ]; + } + + $insertData[] = [ + 'collection_id' => $collection->id, + 'wallet_id' => $wallet->id, + 'is_frozen' => $collectionAccountData['isFrozen'], + 'account_count' => $collectionAccountData['accountCount'], + 'created_at' => $now = Carbon::now(), + 'updated_at' => $now, + ]; + } + + CollectionAccount::insert($insertData); + + $this->collectionsAccountsApprovals($insertApprovals); + } + + /** + * Store collection account. + */ + public function collectionAccountStorage(string $key, string $data): mixed + { + $collectionAccountKey = $this->serializationService->decode('collectionAccountStorageKey', $key); + $collectionAccountData = $this->serializationService->decode('collectionAccountStorageData', $data); + + $walletStored = $this->getCachedWallet( + $collectionAccountKey['accountId'], + fn () => $this->walletService->firstOrStore(['account' => $collectionAccountKey['accountId']]) + ); + $collectionStored = $this->getCachedCollection( + $collectionAccountKey['collectionId'], + fn () => Collection::where('collection_chain_id', $collectionAccountKey['collectionId'])->firstOrFail() + ); + + $collectionAccount = CollectionAccount::updateOrCreate( + [ + 'collection_id' => $collectionStored->id, + 'wallet_id' => $walletStored->id, + ], + [ + 'is_frozen' => $collectionAccountData['isFrozen'], + 'account_count' => $collectionAccountData['accountCount'], + ] + ); + + $this->collectionAccountApprovals($collectionAccount->id, $collectionAccountData['approvals']); + + return $collectionAccount; + } + + /** + * Store token accounts. + */ + public function tokensAccountsStorages(array $data): void + { + $insertData = []; + $insertApprovals = []; + + foreach ($data as [$key, $tokenAccount]) { + $tokenAccountKey = $this->serializationService->decode('tokenAccountStorageKey', $key); + $tokenAccountData = $this->serializationService->decode('tokenAccountStorageData', $tokenAccount); + $wallet = $this->getCachedWallet( + $tokenAccountKey['accountId'], + fn () => $this->walletService->firstOrStore(['account' => $tokenAccountKey['accountId']]) + ); + $collection = $this->getCachedCollection( + $tokenAccountKey['collectionId'], + fn () => Collection::where('collection_chain_id', $tokenAccountKey['collectionId'])->firstOrFail() + ); + + $token = $this->getCachedToken( + $collection->id . '|' . $tokenAccountKey['tokenId'], + fn () => Token::where(['collection_id' => $collection->id, 'token_chain_id' => $tokenAccountKey['tokenId']])->firstOrFail() + ); + + if (!empty($approvals = $tokenAccountData['approvals'])) { + $insertApprovals[] = [ + 'walletId' => $wallet->id, + 'collectionId' => $collection->id, + 'tokenId' => $token->id, + 'approvals' => $approvals, + ]; + } + + $insertData[] = [ + 'wallet_id' => $wallet->id, + 'collection_id' => $collection->id, + 'token_id' => $token->id, + 'balance' => $tokenAccountData['balance'], + 'reserved_balance' => $tokenAccountData['reservedBalance'], + 'is_frozen' => $tokenAccountData['isFrozen'], + 'created_at' => $now = Carbon::now(), + 'updated_at' => $now, + ]; + } + + TokenAccount::insert($insertData); + + $this->tokensAccountsApprovals($insertApprovals); + } + + /** + * Store token account. + */ + public function tokenAccountStorage(string $key, string $data): mixed + { + $tokenAccountKey = $this->serializationService->decode('tokenAccountStorageKey', $key); + $tokenAccountData = $this->serializationService->decode('tokenAccountStorageData', $data); + + $walletStored = $this->getCachedWallet( + $tokenAccountKey['accountId'], + fn () => $this->walletService->firstOrStore(['account' => $tokenAccountKey['accountId']]) + ); + $collectionStored = $this->getCachedCollection( + $tokenAccountKey['collectionId'], + fn () => Collection::where('collection_chain_id', $tokenAccountKey['collectionId'])->firstOrFail() + ); + $tokenStored = $this->getCachedToken( + $collectionStored->id . '|' . $tokenAccountKey['tokenId'], + fn () => Token::where(['collection_id' => $collectionStored->id, 'token_chain_id' => $tokenAccountKey['tokenId']])->firstOrFail() + ); + + $tokenAccount = TokenAccount::updateOrCreate( + [ + 'wallet_id' => $walletStored->id, + 'collection_id' => $collectionStored->id, + 'token_id' => $tokenStored->id, + ], + [ + 'balance' => $tokenAccountData['balance'], + 'reserved_balance' => $tokenAccountData['reservedBalance'], + 'is_frozen' => $tokenAccountData['isFrozen'], + ] + ); + + $this->tokenAccountApprovals($tokenAccount->id, $tokenAccountData['approvals']); + + return $tokenAccount; + } + + /** + * Store attributes. + */ + public function attributesStorages(array $data): void + { + $insertData = []; + + foreach ($data as [$key, $attribute]) { + $attributeKey = $this->serializationService->decode('attributeStorageKey', $key); + $attributeData = Hex::safeConvertToString($this->serializationService->decode('bytes', $attribute)); + + $collection = $this->getCachedCollection( + $attributeKey['collectionId'], + fn () => Collection::where('collection_chain_id', $attributeKey['collectionId'])->firstOrFail() + ); + $token = $this->getCachedToken( + $collection->id . '|' . $attributeKey['tokenId'], + fn () => Token::where(['collection_id' => $collection->id, 'token_chain_id' => $attributeKey['tokenId']])->first() + ); + + $insertData[] = [ + 'collection_id' => $collection->id, + 'token_id' => optional($token)->id, + 'key' => Hex::safeConvertToString($attributeKey['attribute']), + 'value' => $attributeData, + ]; + } + + Attribute::insert($insertData); + } + + /** + * Store attribute. + */ + public function attributeStorage(string $key, string $data): mixed + { + $attributeKey = $this->serializationService->decode('attributeStorageKey', $key); + $attributeData = HexConverter::hexToString($this->serializationService->decode('bytes', $data)); + + $collectionStored = $this->getCachedCollection( + $attributeKey['collectionId'], + fn () => Collection::where('collection_chain_id', $attributeKey['collectionId'])->firstOrFail() + ); + $tokenStored = $this->getCachedToken( + $collectionStored->id . '|' . $attributeKey['tokenId'], + fn () => Token::where(['collection_id' => $collectionStored->id, 'token_chain_id' => $attributeKey['tokenId']])->first() + ); + + return Attribute::create([ + 'collection_id' => $collectionStored->id, + 'token_id' => optional($tokenStored)->id, + 'key_hex' => $keyHex = $attributeKey['attribute'], + 'key' => HexConverter::hexToString($keyHex), + 'value' => $attributeData, + ]); + } + + /** + * Get cached wallet. + */ + protected function getCachedWallet(string $key, ?Closure $default = null): mixed + { + if (!isset(static::$walletCache[$key])) { + static::$walletCache[$key] = $default(); + } + + return static::$walletCache[$key]; + } + + /** + * Get cached collection. + */ + protected function getCachedCollection(string $key, ?Closure $default = null): mixed + { + if (!isset(static::$collectionCache[$key])) { + static::$collectionCache[$key] = $default(); + } + + return static::$collectionCache[$key]; + } + + /** + * Get cached collection account. + */ + protected function getCachedCollectionAccount(string $key, ?Closure $default = null): mixed + { + if (!isset(static::$collectionAccountCache[$key])) { + static::$collectionAccountCache[$key] = $default(); + } + + return static::$collectionAccountCache[$key]; + } + + /** + * Get cached token. + */ + protected function getCachedToken(string $key, ?Closure $default = null): mixed + { + if (!isset(static::$tokenCache[$key])) { + static::$tokenCache[$key] = $default(); + } + + return static::$tokenCache[$key]; + } + + /** + * Get cached toke account. + */ + protected function getCachedTokenAccount(string $key, ?Closure $default = null): mixed + { + if (!isset(static::$tokenAccountCache[$key])) { + static::$tokenAccountCache[$key] = $default(); + } + + return static::$tokenAccountCache[$key]; + } + + /** + * Store collections royalty currencies. + */ + protected function collectionsRoyaltyCurrencies(array $data): void + { + if (empty($data)) { + return; + } + + $insertData = []; + foreach ($data as $royaltyCurrency) { + $collection = $this->getCachedCollection( + $royaltyCurrency['collectionId'], + fn () => Collection::where(['collection_chain_id' => $royaltyCurrency['collectionId']])->firstOrFail() + ); + + foreach ($royaltyCurrency['currencies'] as $currency) { + $insertData[] = [ + 'collection_id' => $collection->id, + 'currency_collection_chain_id' => $currency['collectionId'], + 'currency_token_chain_id' => $currency['tokenId'], + 'created_at' => $now = Carbon::now(), + 'updated_at' => $now, + ]; + } + } + + CollectionRoyaltyCurrency::insert($insertData); + } + + /** + * Store collection royalty currencies. + */ + protected function collectionRoyaltyCurrencies(string $collectionId, array $royaltyCurrencies): void + { + foreach ($royaltyCurrencies as $currency) { + CollectionRoyaltyCurrency::updateOrCreate( + [ + 'collection_id' => $collectionId, + 'currency_collection_chain_id' => $currency['collectionId'], + 'currency_token_chain_id' => $currency['tokenId'], + ], + [ + 'created_at' => $now = Carbon::now(), + 'updated_at' => $now, + ] + ); + } + } + + /** + * Store token accounts approvals. + */ + protected function tokensAccountsApprovals(array $data): void + { + if (empty($data)) { + return; + } + + $insertData = []; + + foreach ($data as $accountApprovals) { + $tokenAccount = $this->getCachedTokenAccount( + $accountApprovals['walletId'] . '|' . $accountApprovals['collectionId'] . '|' . $accountApprovals['tokenId'], + fn () => TokenAccount::where(['wallet_id' => $accountApprovals['walletId'], 'collection_id' => $accountApprovals['collectionId'], 'token_id' => $accountApprovals['tokenId']])->firstOrFail() + ); + foreach ($accountApprovals['approvals'] as $approval) { + $wallet = $this->getCachedWallet( + $approval['accountId'], + fn () => $this->walletService->firstOrStore(['account' => $approval['accountId']]) + ); + $insertData[] = [ + 'token_account_id' => $tokenAccount->id, + 'wallet_id' => $wallet->id, + 'amount' => $approval['amount'], + 'expiration' => $approval['expiration'], + 'created_at' => $now = Carbon::now(), + 'updated_at' => $now, + ]; + } + } + + TokenAccountApproval::insert($insertData); + } + + /** + * Store token account approvals. + */ + protected function tokenAccountApprovals(string $tokenAccountId, array $approvals): void + { + foreach ($approvals as $approval) { + $wallet = $this->walletService->firstOrStore(['account' => $approval['accountId']]); + + TokenAccountApproval::updateOrCreate( + [ + 'token_account_id' => $tokenAccountId, + 'wallet_id' => $wallet->id, + ], + [ + 'amount' => $approval['amount'], + 'expiration' => $approval['expiration'], + 'created_at' => $now = Carbon::now(), + 'updated_at' => $now, + ] + ); + } + } + + /** + * Store collection accounts approvals. + */ + protected function collectionsAccountsApprovals(array $data): void + { + if (empty($data)) { + return; + } + + $insertData = []; + + foreach ($data as $accountApprovals) { + $tokenAccount = $this->getCachedCollectionAccount( + $accountApprovals['walletId'] . '|' . $accountApprovals['collectionId'], + fn () => CollectionAccount::where(['wallet_id' => $accountApprovals['walletId'], 'collection_id' => $accountApprovals['collectionId']])->firstOrFail() + ); + + foreach ($accountApprovals['approvals'] as $approval) { + $wallet = $this->getCachedWallet( + $approval['accountId'], + fn () => $this->walletService->firstOrStore(['account' => $approval['accountId']]) + ); + + $insertData[] = [ + 'collection_account_id' => $tokenAccount->id, + 'wallet_id' => $wallet->id, + 'expiration' => $approval['expiration'], + 'created_at' => $now = Carbon::now(), + 'updated_at' => $now, + ]; + } + } + + CollectionAccountApproval::insert($insertData); + } + + /** + * Store collection account approvals. + */ + protected function collectionAccountApprovals(string $collectionAccountId, array $approvals): void + { + foreach ($approvals as $approval) { + $wallet = $this->walletService->firstOrStore(['account' => $approval['accountId']]); + + CollectionAccountApproval::updateOrCreate( + [ + 'collection_account_id' => $collectionAccountId, + 'wallet_id' => $wallet->id, + ], + [ + 'expiration' => $approval['expiration'], + 'created_at' => $now = Carbon::now(), + 'updated_at' => $now, + ] + ); + } + } +} diff --git a/src/Services/Processor/Substrate/Processor.php b/src/Services/Processor/Substrate/Processor.php new file mode 100644 index 00000000..3108f371 --- /dev/null +++ b/src/Services/Processor/Substrate/Processor.php @@ -0,0 +1,29 @@ +decoder = new DecoderService(); + } + + public function withMetadata(string $type, string|array $bytes, int $blockNumber): null|array|PolkadartExtrinsic + { + try { + return $this->decoder->decode($type, $bytes); + } catch (Throwable $e) { + Log::error('Failed to process ' . $type . ' on block #' . $blockNumber . ': ' . $bytes); + Log::error("The reason was: {$e->getMessage()}"); + } + + return null; + } +} diff --git a/src/Services/Processor/Substrate/State.php b/src/Services/Processor/Substrate/State.php new file mode 100644 index 00000000..f9050036 --- /dev/null +++ b/src/Services/Processor/Substrate/State.php @@ -0,0 +1,231 @@ +client = new SubstrateWebsocket(); + } + + public function __destruct() + { + $this->client->close(); + } + + public function extrinsicsForBlock(array $block): mixed + { + if (null === $block['extrinsics']) { + return null; + } + + if ($extrinsics = Cache::get($cacheKey = PlatformCache::BLOCK_EXTRINSICS->key($block['number']))) { + return $extrinsics; + } + + $extrinsics = (new Processor())->withMetadata( + 'Extrinsics', + JSON::decode($block['extrinsics']), + $block['number'], + ); + + if (!$extrinsics) { + return null; + } + + return Cache::remember( + $cacheKey, + now()->addMinute(), + fn () => $extrinsics + ); + } + + public function eventsForBlock(array $block): mixed + { + if (null === $block['events']) { + return null; + } + + if ($events = Cache::get($cacheKey = PlatformCache::BLOCK_EVENTS->key($block['number']))) { + return $events; + } + + $events = (new Processor())->withMetadata( + 'Vec', + $block['events'], + $block['number'], + ); + + if (!$events) { + return null; + } + + return Cache::remember( + $cacheKey, + now()->addMinute(), + fn () => $events + ); + } + + public function getStorage(string $key, ?string $at): mixed + { + $data = $this->client->send('state_getStorage', [ + $key, + $at, + ]); + + return $data ?: null; + } + + public function checkCollectionAccount($collection, string $addressId, string $blockHash, ?Codec $codec = null): mixed + { + $codec = $codec ?? new Codec(); + $collectionId = $collection->collection_chain_id; + $collectionAccount = $this->getParsedStorage( + key: $codec->encode()->collectionAccountStorageKey($collectionId, $addressId), + at: $blockHash, + parser: 'collectionAccountStorage', + ); + + if (null === $collectionAccount) { + $wallet = WalletService::firstOrStore(['account' => $address = $addressId]); + + $collectionAccount = CollectionAccount::where([ + 'wallet_id' => $wallet->id, + 'collection_id' => $collection->id, + ])->first(); + if ($collectionAccount) { + $collectionAccount->delete(); + Log::info( + sprintf( + 'CollectionAccount of Collection #%s (id %s) and account %s was deleted.', + $collectionId, + $collection->id, + $address, + ) + ); + } + + return $collectionAccount; + } + + Log::info( + sprintf( + 'CollectionAccount (id %s) of Collection #%s (id %s) and account %s was updated.', + $collectionAccount->id, + $collectionId, + $collection->id, + SS58Address::encode($addressId), + ) + ); + + return $collectionAccount; + } + + public function getParsedStorage(string $key, string $at, string $parser): mixed + { + $data = $this->getStorage($key, $at); + if (null === $data) { + return null; + } + + try { + return Parser::{$parser}($key, $data); + } catch (Throwable $e) { + return null; + } + } + + public function checkTokenAccount($collection, $token, string $tokenId, string $addressId, string $blockHash, ?Codec $codec = null) + { + $codec = $codec ?? new Codec(); + $collectionId = $collection->collection_chain_id; + + $tokenAccount = $this->getParsedStorage( + key: $codec->encode()->tokenAccountStorageKey($addressId, $collectionId, $tokenId), + at: $blockHash, + parser: 'tokenAccountStorage', + ); + + if (null === $tokenAccount) { + $wallet = WalletService::firstOrStore(['account' => $address = $addressId]); + + if (null !== $token) { + TokenAccount::where([ + 'wallet_id' => $wallet->id, + 'collection_id' => $collection->id, + 'token_id' => $token->id, + ])->delete(); + + Log::info( + sprintf( + 'TokenAccount of Collection #%s (id %s), Token #%s (id %s) and account %s was deleted.', + $collectionId, + $collection->id, + $tokenId, + $token->id, + $address, + ) + ); + } else { + Log::info( + sprintf( + 'TokenAccount of Collection #%s (id %s), Token #%s and account %s was deleted.', + $collectionId, + $collection->id, + $tokenId, + $addressId + ) + ); + } + } else { + Log::info( + sprintf( + 'TokenAccount (id %s) of Collection #%s (id %s), Token #%s (id %s) and account %s was updated.', + $tokenAccount->id, + $collectionId, + $collection->id, + $token->token_chain_id, + $token->id, + $addressId, + ) + ); + } + } + + public function checkToken($collection, string $tokenId, string $blockHash, ?Codec $codec = null): mixed + { + $codec = $codec ?? new Codec(); + $collectionId = $collection->collection_chain_id; + + $token = $this->getParsedStorage( + key: $codec->encode()->tokenStorageKey($collectionId, $tokenId), + at: $blockHash, + parser: 'tokenStorage', + ); + + if (!isset($token)) { + return null; + } + + Log::info("Token #{$token->token_chain_id} (id {$token->id}) of Collection #{$collectionId} (id {$collection->id}) was updated."); + + return $token; + } +} diff --git a/src/Services/Serialization/Implementations/Substrate.php b/src/Services/Serialization/Implementations/Substrate.php new file mode 100644 index 00000000..d3843538 --- /dev/null +++ b/src/Services/Serialization/Implementations/Substrate.php @@ -0,0 +1,55 @@ +codec = $codec ?? new Codec(); + } + + /** + * Encode the given data. + */ + public function encode(string $method, array $data, $address = null): string + { + $method = Str::camel($method); + + if (!method_exists($this->codec->encode(), $method)) { + throw new PlatformException(__('enjin-platform::error.serialization.method_does_not_exist', ['method' => $method]), 403); + } + + return $this->codec->encode()->{$method}(...$data); + } + + /** + * Decode the encoded data. + */ + public function decode(string $method, string $data): mixed + { + $method = Str::camel($method); + + if (!method_exists($this->codec->decode(), $method)) { + throw new PlatformException(__('enjin-platform::error.serialization.method_does_not_exist', ['method' => $method]), 403); + } + + return $this->codec->decode()->{$method}($data); + } + + /** + * Get method from encoded data. + */ + public function getMethodFromEncoded(string $data): string + { + return $this->codec->decode()->getMethodFromEncoded($data); + } +} diff --git a/src/Services/Serialization/Interfaces/SerializationServiceInterface.php b/src/Services/Serialization/Interfaces/SerializationServiceInterface.php new file mode 100644 index 00000000..e0f1bb5c --- /dev/null +++ b/src/Services/Serialization/Interfaces/SerializationServiceInterface.php @@ -0,0 +1,21 @@ +data = $data; + } + + /** + * Get the name of the encoder. + */ + public static function getName(): string + { + return Str::camel(class_basename(static::class)); + } + + /** + * Get the rules of the encoder. + */ + public static function getRules(): array + { + return ['filled']; + } + + /** + * Decode a tokenId into an array of data. + */ + public function toEncodable($data = null): array + { + return [ + self::getName() => $data ?? $this->data, + ]; + } +} diff --git a/src/Services/Token/Encoders/Erc1155.php b/src/Services/Token/Encoders/Erc1155.php new file mode 100755 index 00000000..f40e96c5 --- /dev/null +++ b/src/Services/Token/Encoders/Erc1155.php @@ -0,0 +1,76 @@ + [ + ...parent::getRules(), + 'string', + new ValidHex(), + 'size:18', + ], + 'erc1155.index' => [ + ...parent::getRules(), + 'integer', + ], + ]; + } + + /** + * Encode an ERC1155 style token ID into an int token ID. + * Note that the max int value returned for Substrate is 128bits compared to Ethereum's 256bit ids. + * + * @param $data + * + * @throws ValidationException + * @return string + */ + public function encode(mixed $data = null): string + { + return HexConverter::hexToUInt($this->tokenIdAndIndexToHex($data ?? $this->data)); + } + + /** + * Create an integer id from the supplied hex token id and index. + * @throws ValidationException + */ + protected function tokenIdAndIndexToHex(object $data): string + { + $idToEncode = HexConverter::unPrefix($data->tokenId); + + if (isset($data->index)) { + return $idToEncode . HexConverter::padLeft(HexConverter::intToHex($data->index), 16); + } + + return HexConverter::padRight($idToEncode, 32); + } +} diff --git a/src/Services/Token/Encoders/Hash.php b/src/Services/Token/Encoders/Hash.php new file mode 100755 index 00000000..00970430 --- /dev/null +++ b/src/Services/Token/Encoders/Hash.php @@ -0,0 +1,57 @@ + $algo])); + } + } + + /** + * Get the type of the encoder. + */ + public static function getType(): Type + { + return GraphQL::type('Object'); + } + + /** + * Get the description of the encoder. + */ + public static function getDescription(): string + { + return __('enjin-platform::input_type.token_id_encoder.hash.description'); + } + + /** + * Encode an arbitrary object of data into a tokenId. + * The entire object will be serialized to JSON and then hashed. + */ + public function encode(mixed $data = null): string + { + return HexConverter::hexToUInt(Blake2::hash(HexConverter::stringToHex(json_encode($data ?? $this->data)), 128)); + } +} diff --git a/src/Services/Token/Encoders/Integer.php b/src/Services/Token/Encoders/Integer.php new file mode 100644 index 00000000..515f22c1 --- /dev/null +++ b/src/Services/Token/Encoders/Integer.php @@ -0,0 +1,51 @@ + [ + ...parent::getRules(), + 'numeric', + new MinBigInt(0), + new MaxBigInt(Hex::MAX_UINT128), + ], + ]; + } + + /** + * Pass in a native token id. + */ + public function encode(mixed $data = null): string + { + return $data->scalar ?? $data ?? $this->data; + } +} diff --git a/src/Services/Token/Encoders/StringId.php b/src/Services/Token/Encoders/StringId.php new file mode 100644 index 00000000..1bf685f0 --- /dev/null +++ b/src/Services/Token/Encoders/StringId.php @@ -0,0 +1,57 @@ + [ + ...parent::getRules(), + 'string', + ], + ]; + } + + /** + * Encode a string into a token ID via its hex representation. + */ + public function encode(mixed $data = null): string + { + $data = $data->scalar ?? $data ?? $this->data; + + $tokenId = HexConverter::hexToUInt(HexConverter::stringToHex($data)); + + if (bccomp($tokenId, Hex::MAX_UINT128) >= 0) { + throw new PlatformException(__('enjin-platform::error.token_int_too_large'), 400); + } + + return $tokenId; + } +} diff --git a/src/Services/Token/TokenIdManager.php b/src/Services/Token/TokenIdManager.php new file mode 100755 index 00000000..ce2da330 --- /dev/null +++ b/src/Services/Token/TokenIdManager.php @@ -0,0 +1,125 @@ +app = $app; + } + + /** + * Dynamically call the default driver instance. + */ + public function __call(string $method, array $parameters) + { + $parameters = $parameters[0]; + $encodableTokenId = $parameters['tokenId'] ?? null; + + if (!isset($encodableTokenId)) { + return; + } + + $data = Arr::first($encodableTokenId); + $type = array_key_first($encodableTokenId); + + Validator::validate($encodableTokenId, $this->encoder($type)::getRules()); + + return $this->encoder($type)->{$method}((object) $data); + } + + /** + * Get an encoder instance by name. + */ + public function encoder(?string $name = null): Encoder + { + $name = $name ? Str::camel($name) : $this->getDefaultDriver(); + if (!$name) { + throw new InvalidArgumentException(__('enjin-platform::error.token_id_encoder.token_id_encoder_not_defined_in_env')); + } + + return $this->encoders[$name] = $this->get($name); + } + + /** + * Get the default cache driver name. + */ + public function getDefaultDriver(): ?string + { + return $this->app['config']['enjin-platform.token_id_encoder']; + } + + /** + * Set the default cache driver name. + */ + public function setDefaultDriver(string $name): void + { + $this->app['config']['enjin-platform.token_id_encoder'] = $name; + } + + /** + * Unset the given driver instances. + */ + public function forgetDriver(array|string|null $name = null): self + { + $name ??= $this->getDefaultDriver(); + + foreach ((array) $name as $cacheName) { + if (isset($this->encoders[$cacheName])) { + unset($this->encoders[$cacheName]); + } + } + + return $this; + } + + /** + * Attempt to get the store from the local platform. + */ + protected function get(string $name): Encoder + { + return $this->encoders[$name] ?? $this->resolve($name); + } + + /** + * Resolve the given store. + * + * @throws \InvalidArgumentException + */ + protected function resolve(string $name): Encoder + { + $config = $this->getConfig($name) ?? []; + + $driverClass = Package::getClass(Str::studly($name)); + + try { + return new $driverClass($config); + } catch (\Throwable $exception) { + throw new InvalidArgumentException(__('enjin-platform::error.token_id_encoder.encoder_not_supported', ['driverClass' => $driverClass])); + } + } + + /** + * Get the cache connection configuration. + */ + protected function getConfig(string $name): mixed + { + return $this->app['config']["platform.token_id_encoders.{$name}"]; + } +} diff --git a/src/Support/Account.php b/src/Support/Account.php new file mode 100644 index 00000000..1e3c9530 --- /dev/null +++ b/src/Support/Account.php @@ -0,0 +1,54 @@ +firstOrStore( + ['public_key' => SS58Address::getPublicKey(config('enjin-platform.chains.daemon-account'))] + ); + } + + return static::$account; + } + + /** + * Get managed wallets public keys. + */ + public static function managedPublicKeys(): array + { + return Cache::rememberForever( + PlatformCache::MANAGED_ACCOUNTS->key(), + fn () => collect(Wallet::where('managed', '=', true)->get()->pluck('public_key')) + ->filter() + ->add(SS58Address::getPublicKey(config('enjin-platform.chains.daemon-account'))) + ->unique() + ->toArray() + ); + } + + /** + * Parse account to public key. + */ + public static function parseAccount(array|string|null $account): ?string + { + if (isset($account['Signed'])) { + return SS58Address::getPublicKey($account['Signed']); + } + + return SS58Address::getPublicKey($account); + } +} diff --git a/src/Support/BitMask.php b/src/Support/BitMask.php new file mode 100644 index 00000000..73dff4f8 --- /dev/null +++ b/src/Support/BitMask.php @@ -0,0 +1,76 @@ +map(fn ($bit, $index) => self::getBit($index, $mask) ? $index : null) + ->filter(fn ($bit) => isset($bit)) + ->values() + ->toArray(); + } + + /** + * Get bit from mask. + */ + public static function getBit(int $bit, int $mask): bool + { + return ($mask & (1 << $bit)) != 0; + } + + /** + * Set bits in mask. + */ + public static function setBits(array $bits, int $mask = 0): mixed + { + return collect($bits)->reduce( + fn ($newMask, $bit) => self::setBit($bit, $newMask), + $mask + ); + } + + /** + * Unset bits in mask. + */ + public static function unsetBits(array $bits, int $mask = 0): mixed + { + return collect($bits)->reduce( + fn ($newMask, $bit) => self::unsetBit($bit, $newMask), + $mask + ); + } + + /** + * Toggle bits in mask. + */ + public static function toggleBits(array $bits, int $mask = 0): mixed + { + return collect($bits)->reduce( + fn ($newMask, $bit) => self::getBit($bit, $newMask) ? self::unsetBit($bit, $newMask) : self::setBit($bit, $newMask), + $mask + ); + } + + /** + * Set bit in mask. + */ + public static function setBit(int $bit, int $mask): int + { + return $mask | (1 << $bit); + } + + /** + * Unset bit in mask. + */ + public static function unsetBit(int $bit, int $mask): int + { + return $mask & ~(1 << $bit); + } +} diff --git a/src/Support/Blake2.php b/src/Support/Blake2.php new file mode 100644 index 00000000..5661462f --- /dev/null +++ b/src/Support/Blake2.php @@ -0,0 +1,43 @@ + 0, 'items' => new CursorPaginator([], $pageSize)]; + } +} diff --git a/src/Support/SS58Address.php b/src/Support/SS58Address.php new file mode 100644 index 00000000..e410f957 --- /dev/null +++ b/src/Support/SS58Address.php @@ -0,0 +1,192 @@ + 255 || $c < 0) { + throw new PlatformException(__('enjin-platform::ss58_address.error.invalid_uint8array')); + } + } + + return $address; + } + + try { + $base58check = new Base58(['characters' => Base58::BITCOIN]); + $buffer = $base58check->decode($input); + $array = unpack('C*', $buffer); + $bytes = array_values($array); + + [$isValid, $endPos, $ss58Length, $ss58Decoded] = self::checkAddressChecksum($bytes); + + if (!$ignoreChecksum && !$isValid) { + throw new PlatformException(__('enjin-platform::ss58_address.error.invalid_decoded_address_checksum')); + } + + if (!in_array($ss58Format, [-1, $ss58Decoded], true)) { + throw new PlatformException(__('enjin-platform::ss58_address.error.unexpected_format', ['ss58Format' => $ss58Format, 'ss58Decoded' => $ss58Decoded])); + } + + return array_slice($bytes, $ss58Length, $endPos - $ss58Length); + } catch (\Exception $e) { + throw new PlatformException(__('enjin-platform::ss58_address.error.cannot_decode_address', ['address' => $address, 'message' => $e->getMessage()])); + } + } + + /** + * Checks the checksum of the given decoded address. + */ + public static function checkAddressChecksum(array $decoded): array + { + $ss58Length = $decoded[0] & 0b01000000 ? 2 : 1; + + $ss58Decoded = $ss58Length === 1 + ? $decoded[0] + : (($decoded[0] & 0b00111111) << 2) | ($decoded[1] >> 6) | (($decoded[1] & 0b00111111) << 8); + + // 32/33 bytes public + 2 bytes checksum + prefix + $isPublicKey = in_array(count($decoded), [34 + $ss58Length, 35 + $ss58Length]); + + $length = count($decoded) - ($isPublicKey ? 2 : 1); + + // calculates the hash and do the checksum byte checks + $hash = HexConverter::hexToBytes(bin2hex(sodium_crypto_generichash( + hex2bin(self::CONTEXT . HexConverter::bytesToHex(array_slice($decoded, 0, $length))), + null, + 64 + ))); + + $isValid = ($decoded[0] & 0b10000000) === 0 && !in_array($decoded[0], [46, 47], true) && ( + $isPublicKey + ? $decoded[count($decoded) - 2] === $hash[0] && $decoded[count($decoded) - 1] === $hash[1] + : $decoded[count($decoded) - 1] === $hash[0] + ); + + return [$isValid, $length, $ss58Length, $ss58Decoded]; + } + + /** + * Checks if the given address is valid. + */ + public static function isValidAddress(string $address): bool + { + try { + self::encode(self::decode($address)); + + return true; + } catch (\Exception $e) { + return false; + } + } + + /** + * Encodes a given address to a SS58 format. + */ + public static function encode(string|array|null $key, ?int $ss58Format = null): ?string + { + if (empty($key)) { + return null; + } + + if ($ss58Format === null) { + $selectedFormat = sprintf( + 'enjin-platform.chains.supported.substrate.%s.ss58-prefix', + config('enjin-platform.chains.network') + ); + $ss58Format = (int) (config($selectedFormat) ?? self::PREFIX); + } + + if ($ss58Format < 0 || $ss58Format > 16383 || in_array($ss58Format, [46, 47], true)) { + throw new PlatformException(__('enjin-platform::ss58_address.error.format_out_of_range')); + } + + $u8a = self::decode($key); + if (!in_array(count($u8a), self::ALLOWED_DECODED_LENGTHS)) { + throw new PlatformException(__('enjin-platform::ss58_address.error.valid_key_expected', ['length' => implode(', ', self::ALLOWED_DECODED_LENGTHS)])); + } + + $prefixBytes = $ss58Format < 64 + ? [$ss58Format] + : [ + (($ss58Format & 0b0000000011111100) >> 2) | 0b01000000, + ($ss58Format >> 8) | (($ss58Format & 0b0000000000000011) << 6), + ]; + + $input = array_merge($prefixBytes, $u8a); + $hash = HexConverter::hexToBytes(bin2hex(sodium_crypto_generichash( + hex2bin(self::CONTEXT . HexConverter::bytesToHex($input)), + '', + 64 + ))); + + $remove = in_array(count($u8a), [32, 33]) ? 2 : 1; + $subarray = array_slice($hash, 0, $remove); + $final = array_merge($input, $subarray); + + return (new Base58(['characters' => Base58::BITCOIN]))->encode(hex2bin(HexConverter::bytesToHex($final))); + } +} diff --git a/src/Support/Twox.php b/src/Support/Twox.php new file mode 100644 index 00000000..d490c39e --- /dev/null +++ b/src/Support/Twox.php @@ -0,0 +1,21 @@ + $seed])); + } + + return implode('', $hashes); + } +} diff --git a/src/Support/Util.php b/src/Support/Util.php new file mode 100644 index 00000000..35e0de01 --- /dev/null +++ b/src/Support/Util.php @@ -0,0 +1,19 @@ + '2.0', + 'method' => $method, + 'params' => $params, + 'id' => random_int(1, 999999999), + ], JSON_THROW_ON_ERROR); + } +} diff --git a/src/Traits/EnumExtensions.php b/src/Traits/EnumExtensions.php new file mode 100644 index 00000000..adb999b2 --- /dev/null +++ b/src/Traits/EnumExtensions.php @@ -0,0 +1,56 @@ +all(); + } + + /** + * Get enum values as array. + */ + public static function caseValuesAsArray(): array + { + return self::caseValuesAsCollection()->all(); + } + + /** + * Get enum cases as collection. + */ + public static function caseNamesAsCollection() + { + return self::casesAsCollection()->pluck('name'); + } + + /** + * Get enum cases value as collection. + */ + public static function caseValuesAsCollection() + { + return self::casesAsCollection()->pluck('value'); + } + + /** + * Get enum case by value. + */ + public static function getEnumCase(string $caseName) + { + return self::casesAsCollection()->filter(fn ($case) => $case->name == $caseName)->first(); + } +} diff --git a/src/Traits/GraphQlEnumTypeExtensions.php b/src/Traits/GraphQlEnumTypeExtensions.php new file mode 100644 index 00000000..c1a86b45 --- /dev/null +++ b/src/Traits/GraphQlEnumTypeExtensions.php @@ -0,0 +1,24 @@ +attributes()['values']); + } + + /** + * Cases names to array. + */ + public function caseNamesAsArray(): array + { + return $this->caseNames()->all(); + } +} diff --git a/src/Traits/HasFieldPagination.php b/src/Traits/HasFieldPagination.php new file mode 100644 index 00000000..39d5fc1f --- /dev/null +++ b/src/Traits/HasFieldPagination.php @@ -0,0 +1,19 @@ +fields(); + foreach ($keys as $k => $key) { + if (isset($fields[$key]) + && Arr::get($fields[$key], 'selectable', true) + && !Arr::get($fields[$key], 'is_relation', false) + ) { + $keys[$k] = Arr::get($fields[$key], 'alias', $key); + } else { + unset($keys[$k]); + } + } + + return $keys; + } + + /** + * Get model relations actual name. + */ + public static function getRelationFields(array $keys): array + { + $fields = resolve(static::class)->fields(); + foreach ($keys as $k => $key) { + if (isset($fields[$key]) && Arr::get($fields[$key], 'is_relation', false)) { + $keys[$k] = Arr::get($fields[$key], 'alias', $key); + } else { + unset($keys[$k]); + } + } + + return $keys; + } +} diff --git a/src/Traits/InheritsGraphQlFields.php b/src/Traits/InheritsGraphQlFields.php new file mode 100644 index 00000000..4d3b5b4f --- /dev/null +++ b/src/Traits/InheritsGraphQlFields.php @@ -0,0 +1,25 @@ +getFields()); + + return $fields->transform(fn ($field) => [ + 'name' => $field->name, + 'description' => $field->description, + 'defaultValue' => $field->defaultValue ?? null, + 'astNode' => $field->astNode, + 'config' => $field->config, + 'type' => $field->getType(), + ])->all(); + } +} diff --git a/testbench.yaml b/testbench.yaml new file mode 100644 index 00000000..45c0ca1c --- /dev/null +++ b/testbench.yaml @@ -0,0 +1,15 @@ +env: + - APP_ENV="local" + - APP_DEBUG="true" + - DB_CONNECTION="mysql" + - DB_DATABASE="platform" + - DB_USERNAME="root" + - DB_PASSWORD="password" + - CACHE_DRIVER="redis" + - QUEUE_CONNECTION="redis" + - CHAIN="substrate" + - NETWORK="canary" + - DAEMON_ACCOUNT="0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" + +providers: + - Enjin\Platform\CoreServiceProvider \ No newline at end of file diff --git a/tests/Feature/GraphQL/Mutations/AcknowledgeEventsTest.php b/tests/Feature/GraphQL/Mutations/AcknowledgeEventsTest.php new file mode 100644 index 00000000..344f347c --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/AcknowledgeEventsTest.php @@ -0,0 +1,127 @@ +generateEvents(1); + $uuid = PendingEvent::query()->first()?->uuid; + + $response = $this->graphql($this->method, [ + 'uuids' => [$uuid], + ]); + + $this->assertTrue($response); + $this->assertNotContains($uuid, collect(PendingEvent::all('uuid'))->pluck('uuid')->toArray()); + } + + public function test_it_can_acknowledge_multiple_events(): void + { + $this->generateEvents(); + $uuids = collect(PendingEvent::all('uuid')->take(5))->pluck('uuid')->toArray(); + + $response = $this->graphql($this->method, [ + 'uuids' => $uuids, + ]); + + $this->assertTrue($response); + $this->assertNotContains($uuids, collect(PendingEvent::all('uuid'))->pluck('uuid')->toArray()); + } + + public function test_it_will_only_ignore_a_uuid_that_doesnt_exists(): void + { + $response = $this->graphql($this->method, [ + 'uuids' => ['do_not_exists'], + ]); + + $this->assertTrue($response); + } + + public function test_it_will_acknowledge_the_events_that_do_exists(): void + { + $this->generateEvents(1); + $uuid = PendingEvent::query()->first()?->uuid; + + $response = $this->graphql($this->method, [ + 'uuids' => [$uuid, 'do_not_exists'], + ]); + + $this->assertTrue($response); + $this->assertNotContains($uuid, collect(PendingEvent::all('uuid'))->pluck('uuid')->toArray()); + } + + // Exception Path + + public function test_it_will_fail_with_no_uuids(): void + { + $response = $this->graphql($this->method, [], true); + + $this->assertStringContainsString( + 'Variable "$uuids" of required type "[String!]!" was not provided', + $response['error'], + ); + } + + public function test_it_will_fail_with_null_uuids(): void + { + $response = $this->graphql($this->method, [ + 'uuids' => null, + ], true); + + $this->assertStringContainsString( + 'Variable "$uuids" of non-null type "[String!]!" must not be null', + $response['error'], + ); + } + + public function test_it_will_fail_with_empty_uuids(): void + { + $response = $this->graphql($this->method, [ + 'uuids' => [], + ], true); + + $this->assertArraySubset( + ['uuids' => ['The uuids field must have at least 1 items.']], + $response['error'], + ); + } + + public function test_it_will_fail_with_empty_item_in_uuids(): void + { + $response = $this->graphql($this->method, [ + 'uuids' => ['', 'abc'], + ], true); + + $this->assertArraySubset( + ['uuids.0' => ['The uuids.0 field must have a value.']], + $response['error'], + ); + } + + protected function generateEvents(?int $numberOfEvents = 5): void + { + collect(range(0, $numberOfEvents))->each(function () { + $collection = Collection::factory()->create()->load(['owner']); + CollectionCreated::safeBroadcast($collection); + }); + } +} diff --git a/tests/Feature/GraphQL/Mutations/ApproveCollectionTest.php b/tests/Feature/GraphQL/Mutations/ApproveCollectionTest.php new file mode 100644 index 00000000..6d7edba4 --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/ApproveCollectionTest.php @@ -0,0 +1,378 @@ +codec = new Codec(); + $this->defaultAccount = config('enjin-platform.chains.daemon-account'); + $this->collection = Collection::factory()->create(); + Token::factory(fake()->numberBetween(1, 10))->create([ + 'collection_id' => $this->collection->id, + ]); + $this->owner = Wallet::find($this->collection->owner_wallet_id); + } + + // Happy Path + + public function test_it_can_approve_a_collection_with_any_operator(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'operator' => $operator = app(Generator::class)->public_key(), + ]); + + $encodedData = $this->codec->encode()->approveCollection( + $this->collection->collection_chain_id, + $operator, + ); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_approve_a_collection_with_operator_that_exists_locally(): void + { + $operator = Wallet::factory()->create(); + + $this->assertDatabaseHas('wallets', [ + 'public_key' => $operator->public_key, + ]); + + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'operator' => SS58Address::encode($operator->public_key), + ]); + + $encodedData = $this->codec->encode()->approveCollection( + $this->collection->collection_chain_id, + $operator->public_key, + ); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_approve_a_collection_with_operator_that_doesnt_exists_locally_and_creates_it(): void + { + Wallet::where('public_key', '=', $operator = app(Generator::class)->public_key())?->delete(); + + $this->assertDatabaseMissing('wallets', [ + 'public_key' => $operator, + ]); + + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'operator' => SS58Address::encode($operator), + ]); + + $this->assertDatabaseHas('wallets', [ + 'public_key' => $operator, + ]); + + $encodedData = $this->codec->encode()->approveCollection( + $this->collection->collection_chain_id, + $operator, + ); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_approve_a_collection_with_expiration(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'operator' => SS58Address::encode($operator = app(Generator::class)->public_key()), + 'expiration' => $expiration = fake()->numberBetween(1), + ]); + + $encodedData = $this->codec->encode()->approveCollection( + $this->collection->collection_chain_id, + $operator, + $expiration + ); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_approve_a_collection_with_bigint(): void + { + Collection::where('collection_chain_id', '=', Hex::MAX_UINT128)?->delete(); + + $collection = Collection::factory()->create([ + 'collection_chain_id' => Hex::MAX_UINT128, + 'owner_wallet_id' => $this->owner->id, + ]); + Token::factory(fake()->numberBetween(1, 10))->create([ + 'collection_id' => $collection->id, + ]); + + $this->assertDatabaseHas('collections', [ + 'id' => $collection->id, + 'collection_chain_id' => $collection->collection_chain_id, + 'owner_wallet_id' => $this->owner->id, + ]); + + $response = $this->graphql('ApproveCollection', [ + 'collectionId' => $collection->collection_chain_id, + 'operator' => SS58Address::encode($operator = app(Generator::class)->public_key()), + ]); + + $encodedData = $this->codec->encode()->approveCollection( + $collection->collection_chain_id, + $operator, + ); + + $this->assertArraySubset([ + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + Event::assertDispatched(TransactionCreated::class); + } + + // Exception Path + + public function test_it_will_fail_with_empty_tokens(): void + { + $collection = Collection::factory()->create([ + 'collection_chain_id' => fake()->numberBetween(5000, 1000), + 'owner_wallet_id' => $this->owner->id, + ]); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collection->collection_chain_id, + 'operator' => app(Generator::class)->public_key(), + ], true); + + $this->assertArraySubset([ + 'collectionId' => ["The collection doesn't have any tokens."], + ], $response['error']); + } + + public function test_it_fail_with_no_collection_id(): void + { + $response = $this->graphql($this->method, [], true); + + $this->assertEquals( + 'Variable "$collectionId" of required type "BigInt!" was not provided.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_null_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => null, + ], true); + + $this->assertEquals( + 'Variable "$collectionId" of non-null type "BigInt!" must not be null.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_no_operator(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + ], true); + + $this->assertEquals( + 'Variable "$operator" of required type "String!" was not provided.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_null_operator(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'operator' => null, + ], true); + + $this->assertEquals( + 'Variable "$operator" of non-null type "String!" must not be null.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_invalid_operator(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'operator' => 'not_a_substrate_address', + ], true); + + $this->assertArraySubset( + ['operator' => ['The operator is not a valid substrate account.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_invalid_expiration(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'operator' => app(Generator::class)->public_key(), + 'expiration' => 'abc', + ], true); + + $this->assertEquals( + 'Variable "$expiration" got invalid value "abc"; Int cannot represent non-integer value: "abc"', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_negative_expiration(): void + { + Block::truncate(); + $block = Block::factory()->create(); + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'operator' => app(Generator::class)->public_key(), + 'expiration' => -1, + ], true); + + $this->assertArraySubset( + ['expiration' => ["The expiration must be at least {$block->number}."]], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_overlimit_expiration(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'operator' => app(Generator::class)->public_key(), + 'expiration' => Hex::MAX_UINT128, + ], true); + + $this->assertEquals( + 'Variable "$expiration" got invalid value "340282366920938463463374607431768211455"; Int cannot represent non-integer value: "340282366920938463463374607431768211455"', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_collection_id_that_doesnt_exists(): void + { + Collection::where('collection_chain_id', '=', $collectionId = fake()->numberBetween(1))?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'operator' => null, + ], true); + + $this->assertEquals( + 'Variable "$operator" of non-null type "String!" must not be null.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_if_passing_daemon_as_operator(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'operator' => SS58Address::getDaemonAccount(true), + ], true); + + $this->assertArraySubset( + ['operator' => ['The operator cannot be set to the daemon account.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } +} diff --git a/tests/Feature/GraphQL/Mutations/ApproveTokenTest.php b/tests/Feature/GraphQL/Mutations/ApproveTokenTest.php new file mode 100644 index 00000000..82ab54f4 --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/ApproveTokenTest.php @@ -0,0 +1,833 @@ +codec = new Codec(); + $walletService = new WalletService(); + $this->defaultAccount = config('enjin-platform.chains.daemon-account'); + $this->wallet = $walletService->firstOrStore(['public_key' => $this->defaultAccount]); + + $this->tokenAccount = TokenAccount::factory([ + 'wallet_id' => $this->wallet, + ])->create(); + + $this->token = Token::find($this->tokenAccount->token_id); + $this->tokenIdEncoder = new Integer($this->token->token_chain_id); + $this->collection = Collection::find($this->tokenAccount->collection_id); + } + + // Happy Path + /** + * It can approve token using encodeTokenId. + */ + public function test_it_can_approve_a_token_with_any_operator_using_adapter(): void + { + $encodedData = $this->codec->encode()->approveToken( + collectionId: $collectionId = $this->collection->collection_chain_id, + tokenId: $this->tokenIdEncoder->encode(), + operator: $operator = app(Generator::class)->public_key(), + amount: $amount = $this->tokenAccount->balance, + currentAmount: $currentAmount = $this->tokenAccount->balance, + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $amount, + 'currentAmount' => $currentAmount, + 'operator' => SS58Address::encode($operator), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + } + + public function test_it_can_approve_a_token_with_any_operator(): void + { + $encodedData = $this->codec->encode()->approveToken( + collectionId: $collectionId = $this->collection->collection_chain_id, + tokenId: $this->tokenIdEncoder->encode(), + operator: $operator = app(Generator::class)->public_key(), + amount: $amount = $this->tokenAccount->balance, + currentAmount: $currentAmount = $this->tokenAccount->balance, + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $amount, + 'currentAmount' => $currentAmount, + 'operator' => SS58Address::encode($operator), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_approve_a_token_with_operator_doesnt_exist(): void + { + Wallet::where('public_key', '=', $operator = app(Generator::class)->public_key())?->delete(); + + $encodedData = $this->codec->encode()->approveToken( + collectionId: $collectionId = $this->collection->collection_chain_id, + tokenId: $this->tokenIdEncoder->encode(), + operator: $operator, + amount: $amount = $this->tokenAccount->balance, + currentAmount: $currentAmount = $this->tokenAccount->balance, + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $amount, + 'currentAmount' => $currentAmount, + 'operator' => SS58Address::encode($operator), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_approve_a_token_with_operator_does_exist(): void + { + Wallet::factory(['public_key' => $operator = app(Generator::class)->public_key()])->create(); + + $encodedData = $this->codec->encode()->approveToken( + collectionId: $collectionId = $this->collection->collection_chain_id, + tokenId: $this->tokenIdEncoder->encode(), + operator: $operator, + amount: $amount = $this->tokenAccount->balance, + currentAmount: $currentAmount = $this->tokenAccount->balance, + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $amount, + 'currentAmount' => $currentAmount, + 'operator' => SS58Address::encode($operator), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_approve_a_token_with_expiration(): void + { + Block::truncate(); + $block = Block::factory()->create(); + $encodedData = $this->codec->encode()->approveToken( + collectionId: $collectionId = $this->collection->collection_chain_id, + tokenId: $this->tokenIdEncoder->encode(), + operator: $operator = app(Generator::class)->public_key(), + amount: $amount = $this->tokenAccount->balance, + currentAmount: $currentAmount = $this->tokenAccount->balance, + expiration: $expiration = fake()->numberBetween($block->number), + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $amount, + 'currentAmount' => $currentAmount, + 'operator' => SS58Address::encode($operator), + 'expiration' => $expiration, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_approve_a_token_with_big_int_collection_id(): void + { + $collection = Collection::factory([ + 'collection_chain_id' => Hex::MAX_UINT128, + ])->create(); + $token = Token::factory([ + 'collection_id' => $collection, + ])->create(); + $tokenAccount = TokenAccount::factory([ + 'wallet_id' => $this->wallet, + 'collection_id' => $collection, + 'token_id' => $token, + ])->create(); + + $encodedData = $this->codec->encode()->approveToken( + collectionId: $collectionId = $collection->collection_chain_id, + tokenId: $this->tokenIdEncoder->encode($token->token_chain_id), + operator: $operator = app(Generator::class)->public_key(), + amount: $amount = $tokenAccount->balance, + currentAmount: $currentAmount = $tokenAccount->balance, + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable($token->token_chain_id), + 'amount' => $amount, + 'currentAmount' => $currentAmount, + 'operator' => SS58Address::encode($operator), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_approve_a_token_with_big_int_token_id(): void + { + $token = Token::factory([ + 'token_chain_id' => Hex::MAX_UINT128, + ])->create(); + $collection = Collection::find($token->collection_id); + $tokenAccount = TokenAccount::factory([ + 'wallet_id' => $this->wallet, + 'collection_id' => $collection, + 'token_id' => $token, + ])->create(); + + $encodedData = $this->codec->encode()->approveToken( + collectionId: $collectionId = $collection->collection_chain_id, + tokenId: $this->tokenIdEncoder->encode($token->token_chain_id), + operator: $operator = app(Generator::class)->public_key(), + amount: $amount = $tokenAccount->balance, + currentAmount: $currentAmount = $tokenAccount->balance, + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable($token->token_chain_id), + 'amount' => $amount, + 'currentAmount' => $currentAmount, + 'operator' => SS58Address::encode($operator), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_approve_a_token_with_big_int_amount(): void + { + $tokenAccount = TokenAccount::factory([ + 'wallet_id' => $this->wallet, + 'balance' => Hex::MAX_UINT128, + ])->create(); + + $collection = Collection::find($tokenAccount->collection_id); + $token = Token::find($tokenAccount->token_id); + + $encodedData = $this->codec->encode()->approveToken( + collectionId: $collectionId = $collection->collection_chain_id, + tokenId: $this->tokenIdEncoder->encode($token->token_chain_id), + operator: $operator = app(Generator::class)->public_key(), + amount: $amount = $tokenAccount->balance, + currentAmount: $currentAmount = $tokenAccount->balance, + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable($token->token_chain_id), + 'amount' => $amount, + 'currentAmount' => $currentAmount, + 'operator' => SS58Address::encode($operator), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_approve_a_token_with_big_int_current_amount(): void + { + $tokenAccount = TokenAccount::factory([ + 'wallet_id' => $this->wallet, + 'balance' => Hex::MAX_UINT128, + ])->create(); + + $collection = Collection::find($tokenAccount->collection_id); + $token = Token::find($tokenAccount->token_id); + + $encodedData = $this->codec->encode()->approveToken( + collectionId: $collectionId = $collection->collection_chain_id, + tokenId: $this->tokenIdEncoder->encode($token->token_chain_id), + operator: $operator = app(Generator::class)->public_key(), + amount: $amount = fake()->numberBetween(), + currentAmount: $currentAmount = $tokenAccount->balance, + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable($token->token_chain_id), + 'amount' => $amount, + 'currentAmount' => $currentAmount, + 'operator' => SS58Address::encode($operator), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + // Exception Path + + public function test_it_will_fail_with_no_args(): void + { + $response = $this->graphql($this->method, [], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of required type "BigInt!" was not provided.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $this->tokenAccount->balance, + 'currentAmount' => $this->tokenAccount->balance, + 'operator' => app(Generator::class)->public_key(), + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of required type "BigInt!" was not provided.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => null, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $this->tokenAccount->balance, + 'currentAmount' => $this->tokenAccount->balance, + 'operator' => app(Generator::class)->public_key(), + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of non-null type "BigInt!" must not be null.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => 'invalid', + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $this->tokenAccount->balance, + 'currentAmount' => $this->tokenAccount->balance, + 'operator' => app(Generator::class)->public_key(), + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" got invalid value "invalid"; Cannot represent following value as uint256', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_collection_id_non_existent(): void + { + Collection::where('collection_chain_id', '=', $collectionId = fake()->numberBetween(2000))?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $this->tokenAccount->balance, + 'currentAmount' => $this->tokenAccount->balance, + 'operator' => app(Generator::class)->public_key(), + ], true); + + $this->assertArraySubset( + ['collectionId' => ['The selected collection id is invalid.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'amount' => $this->tokenAccount->balance, + 'currentAmount' => $this->tokenAccount->balance, + 'operator' => app(Generator::class)->public_key(), + ], true); + + $this->assertStringContainsString( + 'Variable "$tokenId" of required type "EncodableTokenIdInput!" was not provided.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => ['integer' => null], + 'amount' => $this->tokenAccount->balance, + 'currentAmount' => $this->tokenAccount->balance, + 'operator' => app(Generator::class)->public_key(), + ], true); + + $this->assertStringContainsString( + 'The integer field must have a value.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => ['integer' => 'invalid'], + 'amount' => $this->tokenAccount->balance, + 'currentAmount' => $this->tokenAccount->balance, + 'operator' => app(Generator::class)->public_key(), + ], true); + + $this->assertStringContainsString( + 'Variable "$tokenId" got invalid value "invalid" at "tokenId.integer"; Cannot represent following value as uint256', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_token_id_non_existent(): void + { + Token::where('token_chain_id', '=', $tokenId = fake()->numberBetween())?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => ['integer' => $tokenId], + 'amount' => $this->tokenAccount->balance, + 'currentAmount' => $this->tokenAccount->balance, + 'operator' => app(Generator::class)->public_key(), + ], true); + + $this->assertArraySubset( + ['tokenId' => ['The token id doesn\'t exist.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_amount(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'currentAmount' => $this->tokenAccount->balance, + 'operator' => app(Generator::class)->public_key(), + ], true); + + $this->assertStringContainsString( + 'Variable "$amount" of required type "BigInt!" was not provided.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_amount(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => null, + 'currentAmount' => $this->tokenAccount->balance, + 'operator' => app(Generator::class)->public_key(), + ], true); + + $this->assertStringContainsString( + 'Variable "$amount" of non-null type "BigInt!" must not be null.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_negative_amount(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => -1, + 'currentAmount' => $this->tokenAccount->balance, + 'operator' => app(Generator::class)->public_key(), + ], true); + + $this->assertStringContainsString( + 'Variable "$amount" got invalid value -1; Cannot represent following value as uint256', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_zero_amount(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => 0, + 'currentAmount' => $this->tokenAccount->balance, + 'operator' => app(Generator::class)->public_key(), + ], true); + + $this->assertArraySubset( + ['amount' => ['The amount is too small, the minimum value it can be is 1.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_amount(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => 'invalid', + 'currentAmount' => $this->tokenAccount->balance, + 'operator' => app(Generator::class)->public_key(), + ], true); + + $this->assertStringContainsString( + 'Variable "$amount" got invalid value "invalid"; Cannot represent following value as uint256', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_current_amount(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $this->tokenAccount->balance, + 'operator' => app(Generator::class)->public_key(), + ], true); + + $this->assertStringContainsString( + 'Variable "$currentAmount" of required type "BigInt!" was not provided.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_current_amount(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $this->tokenAccount->balance, + 'currentAmount' => null, + 'operator' => app(Generator::class)->public_key(), + ], true); + + $this->assertStringContainsString( + 'Variable "$currentAmount" of non-null type "BigInt!" must not be null.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_negative_current_amount(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $this->tokenAccount->balance, + 'currentAmount' => -1, + 'operator' => app(Generator::class)->public_key(), + ], true); + + $this->assertStringContainsString( + 'Variable "$currentAmount" got invalid value -1; Cannot represent following value as uint256', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_current_amount(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $this->tokenAccount->balance, + 'currentAmount' => 'invalid', + 'operator' => app(Generator::class)->public_key(), + ], true); + + $this->assertStringContainsString( + 'Variable "$currentAmount" got invalid value "invalid"; Cannot represent following value as uint256', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_negative_expiration(): void + { + Block::truncate(); + $block = Block::factory()->create(); + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $this->tokenAccount->balance, + 'currentAmount' => $this->tokenAccount->balance, + 'operator' => app(Generator::class)->public_key(), + 'expiration' => -1, + ], true); + + $this->assertArraySubset( + ['expiration' => ["The expiration must be at least {$block->number}."]], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_expiration(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $this->tokenAccount->balance, + 'currentAmount' => $this->tokenAccount->balance, + 'operator' => app(Generator::class)->public_key(), + 'expiration' => 'invalid', + ], true); + + $this->assertStringContainsString( + 'Variable "$expiration" got invalid value "invalid"; Int cannot represent non-integer value: "invalid"', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_when_passing_daemon_as_operator(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $this->tokenAccount->balance, + 'currentAmount' => $this->tokenAccount->balance, + 'operator' => SS58Address::getDaemonAccount(true), + ], true); + + $this->assertArraySubset( + ['operator' => ['The operator cannot be set to the daemon account.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } +} diff --git a/tests/Feature/GraphQL/Mutations/BatchMintTest.php b/tests/Feature/GraphQL/Mutations/BatchMintTest.php new file mode 100644 index 00000000..f9cc5d2d --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/BatchMintTest.php @@ -0,0 +1,2742 @@ +codec = new Codec(); + $this->defaultAccount = config('enjin-platform.chains.daemon-account'); + $this->wallet = (new WalletService())->firstOrStore(['public_key' => $this->defaultAccount]); + + $this->recipient = Wallet::factory()->create(); + $this->collection = Collection::factory([ + 'max_token_supply' => null, + 'max_token_count' => 100, + 'force_single_mint' => false, + ])->create(); + $this->collectionAccount = CollectionAccount::factory([ + 'collection_id' => $this->collection, + 'wallet_id' => $this->wallet, + 'account_count' => 1, + ])->create(); + $this->token = Token::factory([ + 'collection_id' => $this->collection, + ])->create(); + $this->tokenIdEncoder = new Integer($this->token->token_chain_id); + $this->tokenAccount = TokenAccount::factory([ + 'collection_id' => $this->collection, + 'token_id' => $this->token, + 'wallet_id' => $this->wallet, + ])->create(); + } + + // Happy Path + public function test_it_can_batch_mint_create_single_token_with_cap_null_using_adapter(): void + { + $encodedData = $this->codec->encode()->batchMint( + $collectionId = $this->collection->collection_chain_id, + [ + [ + 'accountId' => $recipient = $this->recipient->public_key, + 'params' => $createParams = new CreateTokenParams( + tokenId: $this->tokenIdEncoder->encode($tokenId = fake()->unique()->numberBetween()), + initialSupply: $supply = fake()->numberBetween(1), + unitPrice: $this->randomGreaterThanMinUnitPriceFor($supply), + cap: TokenMintCapType::INFINITE, + ), + ], + ] + ); + + $params = $createParams->toArray()['CreateToken']; + $params['tokenId'] = $this->tokenIdEncoder->toEncodable($tokenId); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => [ + [ + 'account' => SS58Address::encode($recipient), + 'createParams' => $params, + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + } + + public function test_it_can_batch_mint_create_single_token_with_cap_null(): void + { + $encodedData = $this->codec->encode()->batchMint( + $collectionId = $this->collection->collection_chain_id, + [ + [ + 'accountId' => $recipient = $this->recipient->public_key, + 'params' => $createParams = new CreateTokenParams( + tokenId: $this->tokenIdEncoder->encode($tokenId = fake()->unique()->numberBetween()), + initialSupply: $supply = fake()->numberBetween(1), + unitPrice: $this->randomGreaterThanMinUnitPriceFor($supply), + cap: TokenMintCapType::INFINITE, + ), + ], + ] + ); + + $params = $createParams->toArray()['CreateToken']; + $params['tokenId'] = $this->tokenIdEncoder->toEncodable($tokenId); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => [ + [ + 'account' => SS58Address::encode($recipient), + 'createParams' => $params, + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_batch_mint_create_single_token_with_single_mint_cap(): void + { + $encodedData = $this->codec->encode()->batchMint( + $collectionId = $this->collection->collection_chain_id, + [ + [ + 'accountId' => $recipient = $this->recipient->public_key, + 'params' => $createParams = new CreateTokenParams( + tokenId: $this->tokenIdEncoder->encode($tokenId = fake()->unique()->numberBetween()), + initialSupply: $supply = fake()->numberBetween(1), + unitPrice: $this->randomGreaterThanMinUnitPriceFor($supply), + cap: TokenMintCapType::SINGLE_MINT, + ), + ], + ] + ); + + $params = $createParams->toArray()['CreateToken']; + $params['tokenId'] = $this->tokenIdEncoder->toEncodable($tokenId); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => [ + [ + 'account' => SS58Address::encode($recipient), + 'createParams' => $params, + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_batch_mint_create_single_token_with_supply_cap(): void + { + $encodedData = $this->codec->encode()->batchMint( + $collectionId = $this->collection->collection_chain_id, + [ + [ + 'accountId' => $recipient = $this->recipient->public_key, + 'params' => $createParams = new CreateTokenParams( + tokenId: $this->tokenIdEncoder->encode($tokenId = fake()->unique()->numberBetween()), + initialSupply: $supply = fake()->numberBetween(1), + unitPrice: $this->randomGreaterThanMinUnitPriceFor($supply), + cap: TokenMintCapType::SUPPLY, + supply: fake()->numberBetween($supply) + ), + ], + ] + ); + + $params = $createParams->toArray()['CreateToken']; + $params['tokenId'] = $this->tokenIdEncoder->toEncodable($tokenId); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => [ + [ + 'account' => SS58Address::encode($recipient), + 'createParams' => $params, + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_batch_mint_create_single_token_with_bigint_token_id(): void + { + Token::where('token_chain_id', '=', $tokenId = Hex::MAX_UINT128)?->delete(); + + $encodedData = $this->codec->encode()->batchMint( + $collectionId = $this->collection->collection_chain_id, + [ + [ + 'accountId' => $recipient = $this->recipient->public_key, + 'params' => $createParams = new CreateTokenParams( + tokenId: $this->tokenIdEncoder->encode($tokenId), + initialSupply: $supply = fake()->numberBetween(1), + unitPrice: $this->randomGreaterThanMinUnitPriceFor($supply), + cap: TokenMintCapType::INFINITE, + ), + ], + ] + ); + + $params = $createParams->toArray()['CreateToken']; + $params['tokenId'] = $this->tokenIdEncoder->toEncodable($tokenId); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => [ + [ + 'account' => SS58Address::encode($recipient), + 'createParams' => $params, + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_batch_mint_create_single_token_with_bigint_collection_id(): void + { + Collection::where('collection_chain_id', '=', $collectionId = Hex::MAX_UINT128)?->delete(); + Collection::factory([ + 'collection_chain_id' => $collectionId, + ])->create(); + + $encodedData = $this->codec->encode()->batchMint( + $collectionId, + [ + [ + 'accountId' => $recipient = $this->recipient->public_key, + 'params' => $createParams = new CreateTokenParams( + tokenId: $this->tokenIdEncoder->encode($tokenId = fake()->unique()->numberBetween()), + initialSupply: $supply = fake()->numberBetween(1), + unitPrice: $this->randomGreaterThanMinUnitPriceFor($supply), + cap: TokenMintCapType::INFINITE, + ), + ], + ] + ); + + $params = $createParams->toArray()['CreateToken']; + $params['tokenId'] = $this->tokenIdEncoder->toEncodable($tokenId); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => [ + [ + 'account' => SS58Address::encode($recipient), + 'createParams' => $params, + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_batch_mint_mint_single_token_without_unit_price(): void + { + $encodedData = $this->codec->encode()->batchMint( + $collectionId = $this->collection->collection_chain_id, + [ + [ + 'accountId' => $recipient = $this->recipient->public_key, + 'params' => $mintParams = new MintParams( + tokenId: $this->tokenIdEncoder->encode(), + amount: fake()->numberBetween(1), + ), + ], + ] + ); + + $params = $mintParams->toArray()['Mint']; + $params['tokenId'] = $this->tokenIdEncoder->toEncodable(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => [ + [ + 'account' => SS58Address::encode($recipient), + 'mintParams' => $params, + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_batch_mint_mint_single_token_with_unit_price(): void + { + // TODO: Need to calculate the unitPrice with the previous minted token. + // Will do that later + + $encodedData = $this->codec->encode()->batchMint( + $collectionId = $this->collection->collection_chain_id, + [ + [ + 'accountId' => $recipient = $this->recipient->public_key, + 'params' => $mintParams = new MintParams( + tokenId: $this->tokenIdEncoder->encode(), + amount: $amount = fake()->numberBetween(1), + unitPrice: $this->randomGreaterThanMinUnitPriceFor($amount), + ), + ], + ] + ); + + $params = $mintParams->toArray()['Mint']; + $params['tokenId'] = $this->tokenIdEncoder->toEncodable(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => [ + [ + 'account' => SS58Address::encode($recipient), + 'mintParams' => $params, + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_batch_mint_mint_single_token_with_big_int_collection_id(): void + { + Collection::where('collection_chain_id', '=', $collectionId = Hex::MAX_UINT128)?->delete(); + $collection = Collection::factory([ + 'collection_chain_id' => $collectionId, + ])->create(); + $token = Token::factory([ + 'collection_id' => $collection, + ])->create(); + + $encodedData = $this->codec->encode()->batchMint( + $collectionId, + [ + [ + 'accountId' => $recipient = $this->recipient->public_key, + 'params' => $mintParams = new MintParams( + tokenId: $this->tokenIdEncoder->encode($tokenId = $token->token_chain_id), + amount: fake()->numberBetween(1), + ), + ], + ] + ); + + $params = $mintParams->toArray()['Mint']; + $params['tokenId'] = $this->tokenIdEncoder->toEncodable($tokenId); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => [ + [ + 'account' => SS58Address::encode($recipient), + 'mintParams' => $params, + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_batch_mint_mint_single_token_with_bigint_token_id(): void + { + Token::where('token_chain_id', '=', $tokenId = Hex::MAX_UINT128)?->delete(); + Token::factory([ + 'collection_id' => $this->collection->id, + 'token_chain_id' => $tokenId, + ])->create(); + + $encodedData = $this->codec->encode()->batchMint( + $collectionId = $this->collection->collection_chain_id, + [ + [ + 'accountId' => $recipient = $this->recipient->public_key, + 'params' => $mintParams = new MintParams( + tokenId: $this->tokenIdEncoder->encode($tokenId), + amount: fake()->numberBetween(1), + ), + ], + ] + ); + + $params = $mintParams->toArray()['Mint']; + $params['tokenId'] = $this->tokenIdEncoder->toEncodable($tokenId); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => [ + [ + 'account' => SS58Address::encode($recipient), + 'mintParams' => $params, + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_batch_mint_create_multiple_tokens(): void + { + Token::where('collection_id', '=', $this->collection->collection_id)?->delete(); + + $tokenId = fake()->numberBetween(); + + $recipients = collect(range(0, 9)) + ->map(fn () => [ + 'accountId' => Wallet::factory()->create()->public_key, + 'params' => new CreateTokenParams( + tokenId: $tokenId = $this->tokenIdEncoder->encode($tokenId), + initialSupply: $supply = fake()->numberBetween(1), + unitPrice: $this->randomGreaterThanMinUnitPriceFor($supply), + cap: TokenMintCapType::INFINITE, + ), + ]); + + $encodedData = $this->codec->encode()->batchMint( + $collectionId = $this->collection->collection_chain_id, + $recipients->toArray() + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => $recipients->map(function ($recipient) use ($tokenId) { + $params = $recipient['params']->toArray()['CreateToken']; + $params['tokenId'] = $this->tokenIdEncoder->toEncodable($tokenId); + + return [ + 'account' => SS58Address::encode($recipient['accountId']), + 'createParams' => $params, + ]; + })->toArray(), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_batch_mint_mint_multiple_tokens(): void + { + Token::where('collection_id', '=', $this->collection->collection_id)?->delete(); + + $tokens = Token::factory([ + 'collection_id' => $this->collection, + 'cap' => TokenMintCapType::INFINITE->name, + ])->count(10)->create(); + + $recipients = $tokens->map(fn ($token) => [ + 'accountId' => Wallet::factory()->create()->public_key, + 'params' => new MintParams( + tokenId: $this->tokenIdEncoder->encode($token->token_chain_id), + amount: fake()->numberBetween(1), + ), + ]); + + $encodedData = $this->codec->encode()->batchMint( + $collectionId = $this->collection->collection_chain_id, + $recipients->toArray() + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => $recipients->map(function ($recipient) { + $params = $recipient['params']->toArray()['Mint']; + $params['tokenId'] = $this->tokenIdEncoder->toEncodable($params['tokenId']); + + return [ + 'account' => SS58Address::encode($recipient['accountId']), + 'mintParams' => $params, + ]; + })->toArray(), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_batch_mint_mixed_tokens(): void + { + Token::where('collection_id', '=', $this->collection->collection_id)?->delete(); + + $recipients = collect(range(0, 8)) + ->map(fn ($x) => [ + 'accountId' => Wallet::factory()->create()->public_key, + 'params' => new CreateTokenParams( + tokenId: $x + 1, + initialSupply: $supply = fake()->numberBetween(1), + unitPrice: $this->randomGreaterThanMinUnitPriceFor($supply), + cap: TokenMintCapType::INFINITE, + ), + ]); + + $tokens = Token::factory()->count(10)->sequence(fn ($s) => [ + 'collection_id' => $this->collection, + 'token_chain_id' => $s->index + 10, + ])->create(); + + $recipients = $recipients->merge( + $tokens->map(fn ($token) => [ + 'accountId' => Wallet::factory()->create()->public_key, + 'params' => new MintParams( + tokenId: $this->tokenIdEncoder->encode($token->token_chain_id), + amount: fake()->numberBetween(1), + ), + ]) + ); + + $encodedData = $this->codec->encode()->batchMint( + $collectionId = $this->collection->collection_chain_id, + $recipients->toArray(), + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => $recipients->map(function ($recipient) { + $createParams = Arr::get($recipient['params']->toArray(), 'CreateToken'); + $mintParams = Arr::get($recipient['params']->toArray(), 'Mint'); + + if (isset($createParams['tokenId'])) { + $createParams['tokenId'] = $this->tokenIdEncoder->toEncodable($createParams['tokenId']); + } + if (isset($mintParams['tokenId'])) { + $mintParams['tokenId'] = $this->tokenIdEncoder->toEncodable($mintParams['tokenId']); + } + + return [ + 'account' => SS58Address::encode($recipient['accountId']), + 'createParams' => $createParams, + 'mintParams' => $mintParams, + ]; + })->toArray(), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_batch_mint_create_single_token_to_recipient_that_doesnt_exists(): void + { + Wallet::where('public_key', '=', $address = app(Generator::class)->public_key())?->delete(); + Token::where('collection_id', '=', $this->collection->collection_id)?->delete(); + + $tokenId = fake()->unique()->numberBetween(); + + $recipient = [ + 'accountId' => $address, + 'params' => new CreateTokenParams( + tokenId: $this->tokenIdEncoder->encode($tokenId), + initialSupply: $supply = fake()->numberBetween(1), + unitPrice: $this->randomGreaterThanMinUnitPriceFor($supply), + cap: TokenMintCapType::INFINITE, + ), + ]; + + $encodedData = $this->codec->encode()->batchMint( + $collectionId = $this->collection->collection_chain_id, + [$recipient] + ); + + $params = Arr::get($recipient['params']->toArray(), 'CreateToken'); + $params['tokenId'] = $this->tokenIdEncoder->toEncodable($tokenId); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => [ + [ + 'account' => SS58Address::encode($recipient['accountId']), + 'createParams' => $params, + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + $this->assertDatabaseHas('wallets', [ + 'public_key' => $address, + ]); + } + + public function test_it_can_batch_mint_with_royalty(): void + { + $tokenId = fake()->unique()->numberBetween(); + + $encodedData = $this->codec->encode()->batchMint( + $collectionId = $this->collection->collection_chain_id, + [ + [ + 'accountId' => $recipient = $this->recipient->public_key, + 'params' => $createParams = new CreateTokenParams( + tokenId: $this->tokenIdEncoder->encode($tokenId), + initialSupply: $supply = fake()->numberBetween(1), + unitPrice: $unitPrice = $this->randomGreaterThanMinUnitPriceFor($supply), + cap: TokenMintCapType::INFINITE, + behavior: new TokenMarketBehaviorParams( + hasRoyalty: new RoyaltyPolicyParams( + beneficiary: $beneficiary = $this->defaultAccount, + percentage: $percentage = fake()->numberBetween(1, 50), + ), + ), + ), + ], + ] + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => [ + [ + 'account' => SS58Address::encode($recipient), + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable($tokenId), + 'initialSupply' => $supply, + 'unitPrice' => $unitPrice, + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + 'behavior' => [ + 'hasRoyalty' => [ + 'beneficiary' => $beneficiary, + 'percentage' => $percentage, + ], + ], + ], + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_batch_mint_with_listing_forbidden(): void + { + $tokenId = fake()->unique()->numberBetween(); + + $encodedData = $this->codec->encode()->batchMint( + $collectionId = $this->collection->collection_chain_id, + [ + [ + 'accountId' => $recipient = $this->recipient->public_key, + 'params' => $createParams = new CreateTokenParams( + tokenId: $this->tokenIdEncoder->encode($tokenId), + initialSupply: $supply = fake()->numberBetween(1), + unitPrice: $this->randomGreaterThanMinUnitPriceFor($supply), + listingForbidden: fake()->boolean(), + cap: TokenMintCapType::INFINITE, + ), + ], + ] + ); + + $params = $createParams->toArray()['CreateToken']; + $params['tokenId'] = $this->tokenIdEncoder->toEncodable($tokenId); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => [ + [ + 'account' => SS58Address::encode($recipient), + 'createParams' => $params, + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + // Exception Path + + public function test_it_will_fail_with_empty_args(): void + { + $response = $this->graphql($this->method, [], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of required type "BigInt!" was not provided', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->unique()->numberBetween()), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($supply), + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of required type "BigInt!" was not provided', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_collection_id_equals_null(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => null, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->unique()->numberBetween()), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($supply), + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of non-null type "BigInt!" must not be null', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => 'invalid', + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->unique()->numberBetween()), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($supply), + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" got invalid value "invalid"; Cannot represent following value as uint256', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_collection_id_that_doesnt_exists(): void + { + Collection::where('collection_chain_id', '=', $collectionId = fake()->numberBetween(2000))?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->unique()->numberBetween()), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($supply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + ], + ], + ], + ], true); + + $this->assertArraySubset( + ['collectionId' => ['The selected collection id is invalid.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_recipients(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + ], true); + + $this->assertStringContainsString( + 'Variable "$recipients" of required type "[MintRecipient!]!" was not provided', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_recipients(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => null, + ], true); + + $this->assertStringContainsString( + 'Variable "$recipients" of non-null type "[MintRecipient!]!" must not be null', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_recipients(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => 'invalid', + ], true); + + $this->assertStringContainsString( + 'Variable "$recipients" got invalid value "invalid"; Expected type "MintRecipient" to be an object', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_empty_list_of_recipients(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [], + ], true); + + $this->assertArraySubset( + ['recipients' => ['The recipients field must have at least 1 items.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_create_params_and_mint_params_missing(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + ], + ], + ], true); + + $this->assertStringContainsString( + 'You need to set either create params or mint params for every recipient', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_create_params_equals_null(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => null, + ], + ], + ], true); + + $this->assertStringContainsString( + 'You need to set either create params or mint params for every recipient', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_mint_params_equals_null(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'mintParams' => null, + ], + ], + ], true); + + $this->assertStringContainsString( + 'You need to set either create params or mint params for every recipient', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_create_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => 'invalid', + ], + ], + ], true); + + $this->assertStringContainsString( + 'Expected type "CreateTokenParams" to be an object', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_empty_create_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Field "tokenId" of required type "EncodableTokenIdInput!" was not provided', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_mint_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'mintParams' => 'invalid', + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$recipients" got invalid value "invalid" at "recipients[0].mintParams"; Expected type "MintTokenParams" to be an object', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_empty_mint_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'mintParams' => [], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Field "tokenId" of required type "EncodableTokenIdInput!" was not provided', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_address_missing(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->unique()->numberBetween()), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($supply), + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Field "account" of required type "String!" was not provided', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_address_null(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => null, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->unique()->numberBetween()), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($supply), + ], + ], + ], + ], true); + + $this->assertStringContainsString( + '"recipients[0].account"; Expected non-nullable type "String!" not to be null', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_address(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => 'invalid', + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->unique()->numberBetween()), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($supply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + ], + ], + ], + ], true); + + $this->assertArraySubset( + ['recipients.0.account' => ['The recipients.0.account is not a valid substrate account.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_token_id_missing_in_create_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($supply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Field "tokenId" of required type "EncodableTokenIdInput!" was not provided', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_token_id_null_in_create_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => null, + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($supply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$recipients" got invalid value null at "recipients[0].createParams.tokenId"; Expected non-nullable type "EncodableTokenIdInput!" not to be null', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_token_id_in_create_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable('invalid'), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($supply), + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$recipients" got invalid value "invalid" at "recipients[0].createParams.tokenId.integer"; Cannot represent following value as uint256', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_overflow_token_id_in_create_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(Hex::MAX_UINT256), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($supply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + ], + ], + ], + ], true); + + $this->assertArraySubset( + ['integer' => ['The integer is too large, the maximum value it can be is 340282366920938463463374607431768211455.']], + $response['errors'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_negative_token_id_in_create_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(-1), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($supply), + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$recipients" got invalid value -1 at "recipients[0].createParams.tokenId.integer"; Cannot represent following value as uint256', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_negative_initial_supply_in_create_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->unique()->numberBetween()), + 'initialSupply' => -1, + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor(1), + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$recipients" got invalid value -1 at "recipients[0].createParams.initialSupply"; Cannot represent following value as uint256', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_zero_initial_supply_in_create_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->unique()->numberBetween()), + 'initialSupply' => 0, + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor(1), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + ], + ], + ], + ], true); + + $this->assertArraySubset( + ['recipients.0.createParams.initialSupply' => ['The recipients.0.create params.initial supply is too small, the minimum value it can be is 1.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_overflow_initial_supply_in_create_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->unique()->numberBetween()), + 'initialSupply' => Hex::MAX_UINT256, + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor(1), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + ], + ], + ], + ], true); + + $this->assertArraySubset( + ['recipients.0.createParams.initialSupply' => ['The recipients.0.create params.initial supply is too large, the maximum value it can be is 340282366920938463463374607431768211455.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_unit_price_in_create_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->unique()->numberBetween()), + 'initialSupply' => fake()->numberBetween(1), + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Field "unitPrice" of required type "BigInt!" was not provided', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_negative_unit_price_in_create_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->unique()->numberBetween()), + 'initialSupply' => fake()->numberBetween(1), + 'unitPrice' => -1, + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$recipients" got invalid value -1 at "recipients[0].createParams.unitPrice"; Cannot represent following value as uint256', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_zero_unit_price_in_create_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->unique()->numberBetween()), + 'initialSupply' => fake()->numberBetween(1), + 'unitPrice' => 0, + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + ], + ], + ], + ], true); + + $this->assertArraySubset( + ['recipients.0.createParams.unitPrice' => ['The recipients.0.create params.unit price is too small, the min token deposit is 0.01 EFI thus initialSupply * unitPrice must be greater than 10^16.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_unit_price_in_create_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->unique()->numberBetween()), + 'initialSupply' => fake()->numberBetween(1), + 'unitPrice' => 'invalid', + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$recipients" got invalid value "invalid" at "recipients[0].createParams.unitPrice"; Cannot represent following value as uint256', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_unit_price_less_than_the_minimum_amount_in_create_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->unique()->numberBetween()), + 'initialSupply' => fake()->numberBetween(1, 10 ** 5), + 'unitPrice' => 1, + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + ], + ], + ], + ], true); + + $this->assertArraySubset( + ['recipients.0.createParams.unitPrice' => ['The recipients.0.create params.unit price is too small, the min token deposit is 0.01 EFI thus initialSupply * unitPrice must be greater than 10^16.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_cap_in_create_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->unique()->numberBetween()), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($supply), + 'cap' => 'invalid', + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$recipients" got invalid value "invalid" at "recipients[0].createParams.cap"; Expected type "TokenMintCap" to be an object', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_cap_type_in_create_token_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->unique()->numberBetween()), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($supply), + 'cap' => [ + 'type' => 'invalid', + ], + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Value "invalid" does not exist in "TokenMintCapType" enum', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_missing_cap_type_in_create_token_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->unique()->numberBetween()), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($supply), + 'cap' => [ + 'amount' => $supply, + ], + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Field "type" of required type "TokenMintCapType!" was not provided', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_cap_type_equals_null_in_create_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->unique()->numberBetween()), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($supply), + 'cap' => [ + 'type' => null, + 'amount' => $supply, + ], + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$recipients" got invalid value null at "recipients[0].createParams.cap.type"; Expected non-nullable type "TokenMintCapType!" not to be null', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_amount_missing_on_cap_equals_supply_in_create_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->unique()->numberBetween()), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($supply), + 'cap' => [ + 'type' => TokenMintCapType::SUPPLY->name, + ], + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Supply CAP amount must be set when using Supply CAP', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_cap_amount_lower_than_initial_supply_in_create_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->unique()->numberBetween()), + 'initialSupply' => $supply = 2, + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($supply), + 'cap' => [ + 'type' => TokenMintCapType::SUPPLY->name, + 'amount' => $supply - 1, + ], + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Supply CAP amount must be greater than or equal to initial supply', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_token_id_in_mint_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'mintParams' => [ + 'amount' => fake()->numberBetween(1), + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Field "tokenId" of required type "EncodableTokenIdInput!" was not provided', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_negative_token_id_in_mint_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'mintParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(-1), + 'amount' => fake()->numberBetween(1), + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$recipients" got invalid value -1 at "recipients[0].mintParams.tokenId.integer"; Cannot represent following value as uint256', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_overflow_token_id_in_mint_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'mintParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(Hex::MAX_UINT256), + 'amount' => fake()->numberBetween(1), + ], + ], + ], + ], true); + + $this->assertArraySubset( + ['integer' => ['The integer is too large, the maximum value it can be is 340282366920938463463374607431768211455.']], + $response['errors'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_token_id_in_mint_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'mintParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable('invalid'), + 'amount' => fake()->numberBetween(1), + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$recipients" got invalid value "invalid" at "recipients[0].mintParams.tokenId.integer"; Cannot represent following value as uint256', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_token_id_in_mint_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'mintParams' => [ + 'tokenId' => null, + 'amount' => fake()->numberBetween(1), + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Expected non-nullable type "EncodableTokenIdInput!" not to be null', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_a_token_id_that_doesnt_exists_in_mint_params(): void + { + Token::where('token_chain_id', '=', $tokenId = fake()->numberBetween())?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'mintParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable($tokenId), + 'amount' => fake()->numberBetween(1), + ], + ], + ], + ], true); + + + $this->assertArraySubset( + ['recipients.0.mintParams.tokenId' => ['The recipients.0.mintParams.tokenId does not exist in the specified collection.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_negative_amount_in_mint_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'mintParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => -1, + ], + ], + ], + ], true); + + $this->assertStringContainsString( + '"recipients[0].mintParams.amount"; Cannot represent following value as uint256', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_zero_amount_in_mint_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'mintParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => 0, + ], + ], + ], + ], true); + + $this->assertArraySubset( + ['recipients.0.mintParams.amount' => ['The recipients.0.mint params.amount is too small, the minimum value it can be is 1.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_amount_in_mint_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'mintParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => 'invalid', + ], + ], + ], + ], true); + + $this->assertStringContainsString( + '"recipients[0].mintParams.amount"; Cannot represent following value as uint256', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_amount_in_mint_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'mintParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Field "amount" of required type "BigInt!" was not provided', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_amount_in_mint_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'mintParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => null, + ], + ], + ], + ], true); + + $this->assertStringContainsString( + '"recipients[0].mintParams.amount"; Expected non-nullable type "BigInt!" not to be null', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_overflow_amount_in_mint_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'mintParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => Hex::MAX_UINT256, + ], + ], + ], + ], true); + + $this->assertArraySubset( + ['recipients.0.mintParams.amount' => ['The recipients.0.mint params.amount is too large, the maximum value it can be is 340282366920938463463374607431768211455.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_unit_price_in_mint_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'mintParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => fake()->numberBetween(1), + 'unitPrice' => 'invalid', + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'got invalid value "invalid" at "recipients[0].mintParams.unitPrice"; Cannot represent following value as uint256', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_zero_unit_price_in_mint_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'mintParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => fake()->numberBetween(1), + 'unitPrice' => 0, + ], + ], + ], + ], true); + + $this->assertArraySubset( + ['recipients.0.mintParams.unitPrice' => ['The recipients.0.mint params.unit price is too small, the minimum value it can be is 1.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_overflow_unit_price_in_mint_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'mintParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => fake()->numberBetween(1), + 'unitPrice' => Hex::MAX_UINT256, + ], + ], + ], + ], true); + + $this->assertArraySubset( + ['recipients.0.mintParams.unitPrice' => ['The recipients.0.mint params.unit price is too large, the maximum value it can be is 340282366920938463463374607431768211455.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_if_providing_mint_and_create_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'mintParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => fake()->numberBetween(1), + ], + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($supply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Cannot set create params and mint params for the same recipient', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_over_250_recipients(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => collect(range(0, 250))->map( + fn () => [ + 'account' => $this->recipient->public_key, + 'mintParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => fake()->numberBetween(1), + ], + ] + )->toArray(), + ], true); + + $this->assertArraySubset( + ['recipients' => ['The recipients field must not have more than 250 items.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_royalty_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->minUnitPriceFor($supply), + 'behavior' => ['hasRoyalty' => 'invalid'], + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + ], + ], + ], + ], true); + + $this->assertStringContainsString( + '"recipients[0].createParams.behavior.hasRoyalty"; Expected type "RoyaltyInput" to be an object', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_empty_array_royalty(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->minUnitPriceFor($supply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + 'behavior' => [ + 'hasRoyalty' => [], + ], + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Field "beneficiary" of required type "String!" was not provided', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_missing_beneficiary_in_royalty(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->minUnitPriceFor($supply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + 'behavior' => ['hasRoyalty' => [ + 'percentage' => fake()->numberBetween(1, 50), + ]], + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Field "beneficiary" of required type "String!" was not provided', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_beneficiary_in_royalty(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->minUnitPriceFor($supply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + 'behavior' => ['hasRoyalty' => [ + 'beneficiary' => null, + 'percentage' => fake()->numberBetween(1, 50), + ]], + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$recipients" got invalid value null at "recipients[0].createParams.behavior.hasRoyalty.beneficiary"; Expected non-nullable type "String!" not to be null', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_beneficiary_in_royalty(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->minUnitPriceFor($supply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + 'behavior' => ['hasRoyalty' => [ + 'beneficiary' => 'invalid', + 'percentage' => fake()->numberBetween(1, 50), + ]], + ], + ], + ], + ], true); + + $this->assertArraySubset( + [ + 'recipients.0.createParams.behavior.hasRoyalty.beneficiary' => [ + 0 => 'The recipients.0.create params.behavior.has royalty.beneficiary is not a valid substrate account.', + ], + ], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_missing_percentage_in_royalty(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->minUnitPriceFor($supply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + 'behavior' => ['hasRoyalty' => [ + 'beneficiary' => $this->recipient->public_key, + ]], + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Field "percentage" of required type "Float!" was not provided', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_percentage_in_royalty(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->minUnitPriceFor($supply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + 'behavior' => ['hasRoyalty' => [ + 'beneficiary' => $this->recipient->public_key, + 'percentage' => null, + ]], + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'got invalid value null at "recipients[0].createParams.behavior.hasRoyalty.percentage"; Expected non-nullable type "Float!" not to be null', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_percentage_in_royalty(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->minUnitPriceFor($supply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + 'behavior' => ['hasRoyalty' => [ + 'beneficiary' => $this->recipient->public_key, + 'percentage' => 'invalid', + ]], + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Float cannot represent non numeric value', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_negative_percentage_in_royalty(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->minUnitPriceFor($supply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + 'behavior' => ['hasRoyalty' => [ + 'beneficiary' => $this->recipient->public_key, + 'percentage' => -1, + ]], + ], + ], + ], + ], true); + + $this->assertArraySubset( + [ + 'recipients.0.createParams.behavior.hasRoyalty.percentage' => [ + 0 => 'The recipients.0.create params.behavior.has royalty.percentage valid for a royalty is in the range of 0.1% to 50% and a maximum of 7 decimal places.', + ], + ], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_zero_percentage_in_royalty(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->minUnitPriceFor($supply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + 'behavior' => ['hasRoyalty' => [ + 'beneficiary' => $this->recipient->public_key, + 'percentage' => 0, + ]], + ], + ], + ], + ], true); + + $this->assertArraySubset( + [ + 'recipients.0.createParams.behavior.hasRoyalty.percentage' => [ + 0 => 'The recipients.0.create params.behavior.has royalty.percentage valid for a royalty is in the range of 0.1% to 50% and a maximum of 7 decimal places.', + ], + ], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_less_than_min_percentage_in_royalty(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->minUnitPriceFor($supply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + 'behavior' => ['hasRoyalty' => [ + 'beneficiary' => $this->recipient->public_key, + 'percentage' => 0.09, + ]], + ], + ], + ], + ], true); + + $this->assertArraySubset( + [ + 'recipients.0.createParams.behavior.hasRoyalty.percentage' => [ + 0 => 'The recipients.0.create params.behavior.has royalty.percentage valid for a royalty is in the range of 0.1% to 50% and a maximum of 7 decimal places.', + ], + ], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_more_than_max_percentage_in_royalty(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->minUnitPriceFor($supply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + 'behavior' => ['hasRoyalty' => [ + 'beneficiary' => $this->recipient->public_key, + 'percentage' => 50.1, + ]], + ], + ], + ], + ], true); + + $this->assertArraySubset( + [ + 'recipients.0.createParams.behavior.hasRoyalty.percentage' => [ + 0 => 'The recipients.0.create params.behavior.has royalty.percentage valid for a royalty is in the range of 0.1% to 50% and a maximum of 7 decimal places.', + ], + ], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_percentage_with_more_than_seven_decimal_places_in_royalty(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->minUnitPriceFor($supply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + 'behavior' => ['hasRoyalty' => [ + 'beneficiary' => $this->recipient->public_key, + 'percentage' => 10.000000001, + ]], + ], + ], + ], + ], true); + + $this->assertArraySubset( + [ + 'recipients.0.createParams.behavior.hasRoyalty.percentage' => [ + 0 => 'The recipients.0.create params.behavior.has royalty.percentage valid for a royalty is in the range of 0.1% to 50% and a maximum of 7 decimal places.', + ], + ], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + // Helpers + + public function test_it_will_fail_with_invalid_listing_forbidden(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->minUnitPriceFor($supply), + 'listingForbidden' => 'invalid', + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$recipients" got invalid value "invalid" at "recipients[0].createParams.listingForbidden"; Boolean cannot represent a non boolean value', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_if_exceed_max_token_count_in_collection(): void + { + $this->collection->forceFill(['max_token_count' => 0])->save(); + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'createParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $supply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($supply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + ], + ], + ], + ], true); + + $this->assertArraySubset( + ['collectionId' => ['The overall token count 2 have exceeded the maximum cap of 0 tokens.']], + $response['error'], + ); + } + + protected function randomGreaterThanMinUnitPriceFor(string $initialSupply): string + { + $min = $this->minUnitPriceFor($initialSupply); + + return gmp_strval(gmp_random_range($min, Hex::MAX_UINT128)); + } + + protected function minUnitPriceFor(string $initialSupply): string + { + return gmp_strval(gmp_div(gmp_pow(10, 16), gmp_init($initialSupply), GMP_ROUND_PLUSINF)); + } +} diff --git a/tests/Feature/GraphQL/Mutations/BatchSetAttributeTest.php b/tests/Feature/GraphQL/Mutations/BatchSetAttributeTest.php new file mode 100644 index 00000000..95254a8f --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/BatchSetAttributeTest.php @@ -0,0 +1,573 @@ +codec = new Codec(); + $walletService = new WalletService(); + + $this->defaultAccount = config('enjin-platform.chains.daemon-account'); + $owner = $walletService->firstOrStore(['public_key' => $this->defaultAccount]); + $this->collection = Collection::factory()->create([ + 'owner_wallet_id' => $owner->id, + ]); + $this->token = Token::factory([ + 'collection_id' => $this->collection->id, + ])->create(); + $this->tokenIdEncoder = new Integer($this->token->token_chain_id); + } + + // Happy Path + public function test_it_can_batch_set_attribute_on_token(): void + { + $encodedData = $this->codec->encode()->batchSetAttribute( + $collectionId = $this->collection->collection_chain_id, + $tokenId = $this->tokenIdEncoder->encode(), + $attributes = $this->randomAttributes(), + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'attributes' => $attributes, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + } + + public function test_it_can_batch_set_attribute_on_collection(): void + { + $encodedData = $this->codec->encode()->batchSetAttribute( + $collectionId = $this->collection->collection_chain_id, + $tokenId = null, + $attributes = $this->randomAttributes(), + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'attributes' => $attributes, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + } + + public function test_it_can_batch_set_attribute_on_collection_with_continue_on_failure(): void + { + $encodedData = $this->codec->encode()->batchSetAttribute( + $collectionId = $this->collection->collection_chain_id, + $tokenId = null, + $attributes = $this->randomAttributes(), + true, + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'attributes' => $attributes, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + } + + public function test_it_can_batch_set_attribute_on_token_max_amount(): void + { + $encodedData = $this->codec->encode()->batchSetAttribute( + $collectionId = $this->collection->collection_chain_id, + $tokenId = $this->tokenIdEncoder->encode(), + $attributes = $this->randomAttributes(20, 20), + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'attributes' => $attributes, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + } + + public function test_it_can_batch_set_attribute_on_collection_max_amount(): void + { + $encodedData = $this->codec->encode()->batchSetAttribute( + $collectionId = $this->collection->collection_chain_id, + $tokenId = null, + $attributes = $this->randomAttributes(20, 20), + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'attributes' => $attributes, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + } + + public function test_it_can_batch_set_attribute_with_encoded_token(): void + { + $encodedData = $this->codec->encode()->batchSetAttribute( + $collectionId = $this->collection->collection_chain_id, + $this->tokenIdEncoder->encode(), + $attributes = $this->randomAttributes(), + true, + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'attributes' => $attributes, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + } + + // Exception Path + public function test_it_will_fail_with_no_args(): void + { + $response = $this->graphql($this->method, [], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of required type "BigInt!" was not provided', + $response['error'] + ); + } + + public function test_it_will_fail_with_no_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'tokenId' => $this->token->token_chain_id, + 'attributes' => $this->randomAttributes(), + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of required type "BigInt!" was not provided', + $response['error'] + ); + } + + public function test_it_will_fail_with_collection_id_equals_null(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => null, + 'tokenId' => $this->token->token_chain_id, + 'attributes' => $this->randomAttributes(), + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of non-null type "BigInt!" must not be null', + $response['error'] + ); + } + + public function test_it_will_fail_with_collection_that_doesnt_exists(): void + { + Collection::where('collection_chain_id', '=', $collectionId = fake()->randomNumber())?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'attributes' => $this->randomAttributes(), + ], true); + + $this->assertArraySubset( + [ + 'collectionId' => [ + 0 => 'The selected collection id is invalid.', + ], + ], + $response['error'] + ); + } + + public function test_it_will_fail_with_invalid_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => 123, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'attributes' => $this->randomAttributes(), + ], true); + + $this->assertArraySubset( + [ + 'collectionId' => [ + 0 => 'The selected collection id is invalid.', + ], + 'tokenId' => [ + 0 => 'The token id does not exist in the specified collection.', + ], + ], + $response['error'] + ); + } + + public function test_it_will_fail_with_invalid_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(123), + 'attributes' => $this->randomAttributes(), + ], true); + + $this->assertArraySubset( + [ + 'tokenId' => [ + 0 => 'The token id does not exist in the specified collection.', + ], + ], + $response['error'] + ); + } + + public function test_it_will_fail_with_token_that_doesnt_exists(): void + { + Token::where('token_chain_id', '=', $tokenId = fake()->randomNumber())?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable($tokenId), + 'attributes' => $this->randomAttributes(), + ], true); + + $this->assertArraySubset( + [ + 'tokenId' => [ + 0 => 'The token id does not exist in the specified collection.', + ], + ], + $response['error'] + ); + } + + public function test_it_will_fail_with_no_attributes(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + ], true); + + $this->assertStringContainsString( + 'Variable "$attributes" of required type "[AttributeInput!]!" was not provided', + $response['error'] + ); + } + + public function test_it_will_fail_with_null_attributes(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'attributes' => null, + ], true); + + $this->assertStringContainsString( + 'Variable "$attributes" of non-null type "[AttributeInput!]!" must not be null', + $response['error'] + ); + } + + public function test_it_will_fail_with_invalid_attributes(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'attributes' => 'invalid', + ], true); + + $this->assertStringContainsString( + 'Variable "$attributes" got invalid value "invalid"; Expected type "AttributeInput" to be an object', + $response['error'] + ); + } + + public function test_it_will_fail_with_empty_attributes(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'attributes' => [], + ], true); + + $this->assertArraySubset( + [ + 'attributes' => [ + 0 => 'The attributes field must have at least 1 items.', + ], + ], + $response['error'] + ); + } + + public function test_it_will_fail_with_missing_key_in_attributes(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'attributes' => [ + [ + 'value' => 'abc', + ], + ], + ], true); + + $this->assertStringContainsString( + 'abc', + $response['error'] + ); + } + + public function test_it_will_fail_with_missing_value_in_attributes(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'attributes' => [ + [ + 'key' => 'abc', + ], + ], + ], true); + + $this->assertStringContainsString( + 'abc', + $response['error'] + ); + } + + public function test_it_will_fail_with_null_key_in_attributes(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'attributes' => [ + [ + 'key' => null, + 'value' => 'abc', + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$attributes" got invalid value null at "attributes[0].key"; Expected non-nullable type "String!" not to be null', + $response['error'] + ); + } + + public function test_it_will_fail_with_null_value_in_attributes(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'attributes' => [ + [ + 'key' => 'abc', + 'value' => null, + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$attributes" got invalid value null at "attributes[0].value"; Expected non-nullable type "String!" not to be null', + $response['error'] + ); + } + + public function test_it_will_fail_with_invalid_key_in_attributes(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'attributes' => [ + [ + 'key' => 123, + 'value' => 'abc', + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$attributes" got invalid value 123 at "attributes[0].key"; String cannot represent a non string value', + $response['error'] + ); + } + + public function test_it_will_fail_with_invalid_value_in_attributes(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'attributes' => [ + [ + 'key' => 'abc', + 'value' => 123, + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$attributes" got invalid value 123 at "attributes[0].value"; String cannot represent a non string value', + $response['error'] + ); + } + + public function test_it_will_fail_with_more_than_max_attributes(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'attributes' => $this->randomAttributes(21, 21), + ], true); + + $this->assertArraySubset( + [ + 'attributes' => [ + 0 => 'The attributes field must not have more than 20 items.', + ], + ], + $response['error'] + ); + } + + public function test_it_will_fail_with_is_not_the_owner(): void + { + $collection = Collection::factory()->create(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collection->collection_chain_id, + 'attributes' => $this->randomAttributes(), + ], true); + + $this->assertArraySubset( + ['collectionId' => ['The collection id provided is not owned by you.']], + $response['error'] + ); + } + + protected function randomAttributes(?int $min = 1, ?int $max = 10): array + { + return collect(range(1, mt_rand($min, $max)))->map( + fn () => [ + 'key' => fake()->word, + 'value' => fake()->word, + ] + )->toArray(); + } +} diff --git a/tests/Feature/GraphQL/Mutations/BatchTransferTest.php b/tests/Feature/GraphQL/Mutations/BatchTransferTest.php new file mode 100644 index 00000000..a62bb602 --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/BatchTransferTest.php @@ -0,0 +1,2136 @@ +codec = new Codec(); + $walletService = new WalletService(); + $this->defaultAccount = config('enjin-platform.chains.daemon-account'); + $this->wallet = $walletService->firstOrStore(['public_key' => $this->defaultAccount]); + + $this->recipient = Wallet::factory()->create(); + $this->tokenAccount = TokenAccount::factory([ + 'wallet_id' => $this->wallet, + ])->create(); + $this->token = Token::find($this->tokenAccount->token_id); + $this->tokenIdEncoder = new Integer($this->token->token_chain_id); + $this->collection = Collection::find($this->tokenAccount->collection_id); + $this->collectionAccount = CollectionAccount::factory([ + 'collection_id' => $this->collection, + 'wallet_id' => $this->wallet, + 'account_count' => 1, + ])->create(); + } + + // Happy Path + + public function test_it_can_batch_simple_single_transfer_using_adapter(): void + { + $encodedData = $this->codec->encode()->batchTransfer( + $collectionId = $this->collection->collection_chain_id, + [ + [ + 'accountId' => $recipient = $this->recipient->public_key, + 'params' => new SimpleTransferParams( + tokenId: $this->tokenIdEncoder->encode(), + amount: $amount = fake()->numberBetween(1, $this->tokenAccount->balance) + ), + ], + ] + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => [ + [ + 'account' => SS58Address::encode($recipient), + 'simpleParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $amount, + ], + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + } + + public function test_it_can_batch_simple_single_transfer(): void + { + $encodedData = $this->codec->encode()->batchTransfer( + $collectionId = $this->collection->collection_chain_id, + [ + [ + 'accountId' => $recipient = $this->recipient->public_key, + 'params' => new SimpleTransferParams( + tokenId: $this->tokenIdEncoder->encode(), + amount: $amount = fake()->numberBetween(1, $this->tokenAccount->balance) + ), + ], + ] + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => [ + [ + 'account' => SS58Address::encode($recipient), + 'simpleParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $amount, + ], + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_batch_simple_single_transfer_with_keep_alive(): void + { + $encodedData = $this->codec->encode()->batchTransfer( + $collectionId = $this->collection->collection_chain_id, + [ + [ + 'accountId' => $recipient = $this->recipient->public_key, + 'params' => new SimpleTransferParams( + tokenId: $this->tokenIdEncoder->encode(), + amount: $amount = fake()->numberBetween(1, $this->tokenAccount->balance), + keepAlive: $keepAlive = fake()->boolean(), + ), + ], + ] + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => [ + [ + 'account' => SS58Address::encode($recipient), + 'simpleParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $amount, + 'keepAlive' => $keepAlive, + ], + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_batch_simple_single_transfer_with_null_keep_alive(): void + { + $encodedData = $this->codec->encode()->batchTransfer( + $collectionId = $this->collection->collection_chain_id, + [ + [ + 'accountId' => $recipient = $this->recipient->public_key, + 'params' => new SimpleTransferParams( + tokenId: $this->tokenIdEncoder->encode(), + amount: $amount = fake()->numberBetween(1, $this->tokenAccount->balance), + keepAlive: null, + ), + ], + ] + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => [ + [ + 'account' => SS58Address::encode($recipient), + 'simpleParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $amount, + 'keepAlive' => null, + ], + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_batch_operator_single_transfer(): void + { + $encodedData = $this->codec->encode()->batchTransfer( + $collectionId = $this->collection->collection_chain_id, + [ + [ + 'accountId' => $recipient = $this->recipient->public_key, + 'params' => new OperatorTransferParams( + tokenId: $this->tokenIdEncoder->encode(), + source: $source = $this->wallet->public_key, + amount: $amount = fake()->numberBetween(1, $this->tokenAccount->balance) + ), + ], + ] + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => [ + [ + 'account' => SS58Address::encode($recipient), + 'operatorParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => SS58Address::encode($source), + 'amount' => $amount, + ], + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_batch_operator_single_transfer_with_keep_alive(): void + { + $encodedData = $this->codec->encode()->batchTransfer( + $collectionId = $this->collection->collection_chain_id, + [ + [ + 'accountId' => $recipient = $this->recipient->public_key, + 'params' => new OperatorTransferParams( + tokenId: $this->tokenIdEncoder->encode(), + source: $source = $this->wallet->public_key, + amount: $amount = fake()->numberBetween(1, $this->tokenAccount->balance), + keepAlive: $keepAlive = fake()->boolean(), + ), + ], + ] + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => [ + [ + 'account' => SS58Address::encode($recipient), + 'operatorParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => SS58Address::encode($source), + 'amount' => $amount, + 'keepAlive' => $keepAlive, + ], + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_batch_operator_single_transfer_with_null_keep_alive(): void + { + $encodedData = $this->codec->encode()->batchTransfer( + $collectionId = $this->collection->collection_chain_id, + [ + [ + 'accountId' => $recipient = $this->recipient->public_key, + 'params' => new OperatorTransferParams( + tokenId: $this->tokenIdEncoder->encode(), + source: $source = $this->wallet->public_key, + amount: $amount = fake()->numberBetween(1, $this->tokenAccount->balance), + keepAlive: null, + ), + ], + ] + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => [ + [ + 'account' => SS58Address::encode($recipient), + 'operatorParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => SS58Address::encode($source), + 'amount' => $amount, + 'keepAlive' => null, + ], + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_batch_simple_multiple_transfers(): void + { + // TODO: We should validate if the sum of all amount doesn't exceed the total balance + // Will do that later + + $recipients = collect( + range( + 0, + ($value = fake()->randomNumber(1, $this->tokenAccount->balance)) < 10 ? $value : 9 + ) + )->map(fn ($x) => [ + 'accountId' => Wallet::factory()->create()->public_key, + 'params' => new SimpleTransferParams( + tokenId: $this->tokenIdEncoder->encode(), + amount: fake()->numberBetween(1, $this->tokenAccount->balance) + ), + ]); + + $encodedData = $this->codec->encode()->batchTransfer( + $collectionId = $this->collection->collection_chain_id, + $recipients->toArray() + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => $recipients->map(function ($recipient) { + $simpleParams = $recipient['params']->toArray()['Simple']; + $simpleParams['tokenId'] = $this->tokenIdEncoder->toEncodable(); + + return [ + 'account' => SS58Address::encode($recipient['accountId']), + 'simpleParams' => $simpleParams, + ]; + })->toArray(), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_batch_operator_multiple_transfers(): void + { + $recipients = collect( + range( + 0, + ($value = fake()->randomNumber(1, $this->tokenAccount->balance)) < 10 ? $value : 9 + ) + )->map(fn ($x) => [ + 'accountId' => Wallet::factory()->create()->public_key, + 'params' => new OperatorTransferParams( + tokenId: $this->tokenIdEncoder->encode(), + source: $this->wallet->public_key, + amount: fake()->numberBetween(1, $this->tokenAccount->balance) + ), + ]); + + $encodedData = $this->codec->encode()->batchTransfer( + $collectionId = $this->collection->collection_chain_id, + $recipients->toArray() + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => $recipients->map(function ($recipient) { + $operatorParams = $recipient['params']->toArray()['Operator']; + $operatorParams['tokenId'] = $this->tokenIdEncoder->toEncodable(); + + return [ + 'account' => SS58Address::encode($recipient['accountId']), + 'operatorParams' => $operatorParams, + ]; + })->toArray(), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_batch_mixed_multiple_transfers(): void + { + $recipients = collect( + range( + 0, + ($value = fake()->randomNumber(1, $this->tokenAccount->balance)) < 10 ? $value : 9 + ) + )->map(fn ($x) => [ + 'accountId' => fake()->randomElement([ + Wallet::factory()->create()->public_key, app(Generator::class)->public_key(), + ]), + 'params' => fake()->randomElement([ + new SimpleTransferParams( + tokenId: $this->tokenIdEncoder->encode(), + amount: fake()->numberBetween(1, $this->tokenAccount->balance) + ), + new OperatorTransferParams( + tokenId: $this->tokenIdEncoder->encode(), + source: $this->wallet->public_key, + amount: fake()->numberBetween(1, $this->tokenAccount->balance) + ), + ]), + ]); + + $encodedData = $this->codec->encode()->batchTransfer( + $collectionId = $this->collection->collection_chain_id, + $recipients->toArray() + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => $recipients->map(function ($recipient) { + $simpleParams = Arr::get($recipient['params']->toArray(), 'Simple'); + if (isset($simpleParams)) { + $simpleParams['tokenId'] = $this->tokenIdEncoder->toEncodable(); + } + $operatorParams = Arr::get($recipient['params']->toArray(), 'Operator'); + if (isset($operatorParams)) { + $operatorParams['tokenId'] = $this->tokenIdEncoder->toEncodable(); + } + + return [ + 'account' => SS58Address::encode($recipient['accountId']), + 'simpleParams' => $simpleParams, + 'operatorParams' => $operatorParams, + ]; + })->toArray(), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_batch_transfer_with_signing_wallet_simple_transfer(): void + { + $signingWallet = Wallet::factory([ + 'managed' => true, + ])->create(); + + Collection::where('collection_chain_id', Hex::MAX_UINT128)->update(['collection_chain_id' => random_int(1, 1000)]); + + $collection = Collection::factory([ + 'collection_chain_id' => Hex::MAX_UINT128, + ])->create(); + CollectionAccount::factory([ + 'collection_id' => $collection, + 'wallet_id' => $signingWallet, + 'account_count' => 1, + ])->create(); + $token = Token::factory([ + 'collection_id' => $collection, + ])->create(); + $tokenAccount = TokenAccount::factory([ + 'collection_id' => $collection, + 'token_id' => $token, + 'wallet_id' => $signingWallet, + ])->create(); + + $recipient = [ + 'accountId' => $this->defaultAccount, + 'params' => new SimpleTransferParams( + tokenId: $this->tokenIdEncoder->encode($token->token_chain_id), + amount: fake()->numberBetween(1, $tokenAccount->balance) + ), + ]; + + $encodedData = $this->codec->encode()->batchTransfer( + $collectionId = $collection->collection_chain_id, + [$recipient] + ); + + $simpleParams = Arr::get($recipient['params']->toArray(), 'Simple'); + $simpleParams['tokenId'] = $this->tokenIdEncoder->toEncodable($token->token_chain_id); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => [ + [ + 'account' => SS58Address::encode($recipient['accountId']), + 'simpleParams' => $simpleParams, + ], + ], + 'signingAccount' => SS58Address::encode($signingWallet->public_key), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $signingWallet->public_key, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_batch_transfer_with_signing_wallet_operator_transfer(): void + { + $signingWallet = Wallet::factory([ + 'managed' => true, + ])->create(); + + Collection::where('collection_chain_id', Hex::MAX_UINT128)->update(['collection_chain_id' => random_int(1, 1000)]); + $collection = Collection::factory([ + 'collection_chain_id' => Hex::MAX_UINT128, + ])->create(); + CollectionAccount::factory([ + 'collection_id' => $collection, + 'wallet_id' => $this->wallet, + 'account_count' => 1, + ])->create(); + $token = Token::factory([ + 'collection_id' => $collection, + ])->create(); + $tokenAccount = TokenAccount::factory([ + 'collection_id' => $collection, + 'token_id' => $token, + 'wallet_id' => $this->wallet, + ])->create(); + + $recipient = [ + 'accountId' => $this->recipient->public_key, + 'params' => new OperatorTransferParams( + tokenId: $this->tokenIdEncoder->encode($token->token_chain_id), + source: $this->wallet->public_key, + amount: fake()->numberBetween(1, $tokenAccount->balance) + ), + ]; + + $encodedData = $this->codec->encode()->batchTransfer( + $collectionId = $collection->collection_chain_id, + [$recipient] + ); + + $operatorParams = Arr::get($recipient['params']->toArray(), 'Operator'); + $operatorParams['tokenId'] = $this->tokenIdEncoder->toEncodable($token->token_chain_id); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => [ + [ + 'account' => SS58Address::encode($recipient['accountId']), + 'operatorParams' => $operatorParams, + ], + ], + 'signingAccount' => SS58Address::encode($signingWallet->public_key), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $signingWallet->public_key, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_batch_transfer_with_null_signing_wallet(): void + { + $recipient = [ + 'accountId' => Wallet::factory()->create()->public_key, + 'params' => fake()->randomElement([ + new SimpleTransferParams( + tokenId: $this->tokenIdEncoder->encode(), + amount: fake()->numberBetween(1, $this->tokenAccount->balance) + ), + new OperatorTransferParams( + tokenId: $this->tokenIdEncoder->encode(), + source: $this->wallet->public_key, + amount: fake()->numberBetween(1, $this->tokenAccount->balance) + ), + ]), + ]; + + $encodedData = $this->codec->encode()->batchTransfer( + $collectionId = $this->collection->collection_chain_id, + [$recipient] + ); + + $simpleParams = Arr::get($recipient['params']->toArray(), 'Simple'); + if (isset($simpleParams)) { + $simpleParams['tokenId'] = $this->tokenIdEncoder->toEncodable(); + } + $operatorParams = Arr::get($recipient['params']->toArray(), 'Operator'); + if (isset($operatorParams)) { + $operatorParams['tokenId'] = $this->tokenIdEncoder->toEncodable(); + } + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => [ + [ + 'account' => SS58Address::encode($recipient['accountId']), + 'simpleParams' => $simpleParams, + 'operatorParams' => $operatorParams, + ], + ], + 'signingAccount' => null, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_batch_transfer_with_big_int_collection_id(): void + { + Collection::where('collection_chain_id', Hex::MAX_UINT128)->update(['collection_chain_id' => random_int(1, 1000)]); + $collection = Collection::factory([ + 'collection_chain_id' => Hex::MAX_UINT128, + ])->create(); + CollectionAccount::factory([ + 'collection_id' => $collection, + 'wallet_id' => $this->wallet, + 'account_count' => 1, + ])->create(); + $token = Token::factory([ + 'collection_id' => $collection, + ])->create(); + $tokenAccount = TokenAccount::factory([ + 'collection_id' => $collection, + 'token_id' => $token, + 'wallet_id' => $this->wallet, + ])->create(); + + $recipient = [ + 'accountId' => Wallet::factory()->create()->public_key, + 'params' => fake()->randomElement([ + new SimpleTransferParams( + tokenId: $this->tokenIdEncoder->encode($token->token_chain_id), + amount: fake()->numberBetween(1, $tokenAccount->balance) + ), + new OperatorTransferParams( + tokenId: $this->tokenIdEncoder->encode($token->token_chain_id), + source: $this->wallet->public_key, + amount: fake()->numberBetween(1, $tokenAccount->balance) + ), + ]), + ]; + + $encodedData = $this->codec->encode()->batchTransfer( + $collectionId = $collection->collection_chain_id, + [$recipient] + ); + + $simpleParams = Arr::get($recipient['params']->toArray(), 'Simple'); + if (isset($simpleParams)) { + $simpleParams['tokenId'] = $this->tokenIdEncoder->toEncodable($token->token_chain_id); + } + $operatorParams = Arr::get($recipient['params']->toArray(), 'Operator'); + if (isset($operatorParams)) { + $operatorParams['tokenId'] = $this->tokenIdEncoder->toEncodable($token->token_chain_id); + } + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => [ + [ + 'account' => SS58Address::encode($recipient['accountId']), + 'simpleParams' => $simpleParams, + 'operatorParams' => $operatorParams, + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_batch_transfer_with_big_int_token_id(): void + { + $collection = Collection::factory()->create(); + CollectionAccount::factory([ + 'collection_id' => $collection, + 'wallet_id' => $this->wallet, + 'account_count' => 1, + ])->create(); + + Token::where('token_chain_id', Hex::MAX_UINT128)->update(['token_chain_id' => random_int(1, 1000)]); + $token = Token::factory([ + 'collection_id' => $collection, + 'token_chain_id' => Hex::MAX_UINT128, + ])->create(); + $tokenAccount = TokenAccount::factory([ + 'collection_id' => $collection, + 'token_id' => $token, + 'wallet_id' => $this->wallet, + ])->create(); + + $recipient = [ + 'accountId' => Wallet::factory()->create()->public_key, + 'params' => fake()->randomElement([ + new SimpleTransferParams( + tokenId: $this->tokenIdEncoder->encode($token->token_chain_id), + amount: fake()->numberBetween(1, $tokenAccount->balance) + ), + new OperatorTransferParams( + tokenId: $this->tokenIdEncoder->encode($token->token_chain_id), + source: $this->wallet->public_key, + amount: fake()->numberBetween(1, $tokenAccount->balance) + ), + ]), + ]; + + $encodedData = $this->codec->encode()->batchTransfer( + $collectionId = $collection->collection_chain_id, + [$recipient] + ); + + $simpleParams = Arr::get($recipient['params']->toArray(), 'Simple'); + if (isset($simpleParams)) { + $simpleParams['tokenId'] = $this->tokenIdEncoder->toEncodable($token->token_chain_id); + } + $operatorParams = Arr::get($recipient['params']->toArray(), 'Operator'); + if (isset($operatorParams)) { + $operatorParams['tokenId'] = $this->tokenIdEncoder->toEncodable($token->token_chain_id); + } + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => [ + [ + 'account' => SS58Address::encode($recipient['accountId']), + 'simpleParams' => $simpleParams, + 'operatorParams' => $operatorParams, + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_batch_transfer_with_recipient_that_doesnt_exists(): void + { + Wallet::where('public_key', '=', $address = app(Generator::class)->public_key())?->delete(); + + $recipient = [ + 'accountId' => $address, + 'params' => fake()->randomElement([ + new SimpleTransferParams( + tokenId: $this->tokenIdEncoder->encode(), + amount: fake()->numberBetween(1, $this->tokenAccount->balance) + ), + new OperatorTransferParams( + tokenId: $this->tokenIdEncoder->encode(), + source: $this->wallet->public_key, + amount: fake()->numberBetween(1, $this->tokenAccount->balance) + ), + ]), + ]; + + $encodedData = $this->codec->encode()->batchTransfer( + $collectionId = $this->collection->collection_chain_id, + [$recipient] + ); + + $simpleParams = Arr::get($recipient['params']->toArray(), 'Simple') ?? null; + if (!empty($simpleParams)) { + $simpleParams['tokenId'] = $this->tokenIdEncoder->toEncodable(); + } + $operatorParams = Arr::get($recipient['params']->toArray(), 'Operator') ?? null; + if (!empty($operatorParams)) { + $operatorParams['tokenId'] = $this->tokenIdEncoder->toEncodable(); + } + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => [ + [ + 'account' => SS58Address::encode($recipient['accountId']), + 'simpleParams' => $simpleParams, + 'operatorParams' => $operatorParams, + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + // Exception Path + + public function test_it_fail_with_no_args(): void + { + $response = $this->graphql($this->method, [], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of required type "BigInt!" was not provided.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_no_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'simpleParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $this->tokenAccount->balance, + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of required type "BigInt!" was not provided.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_null_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => null, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'simpleParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $this->tokenAccount->balance, + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of non-null type "BigInt!" must not be null.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_invalid_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => 'invalid', + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'simpleParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $this->tokenAccount->balance, + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" got invalid value "invalid"; Cannot represent following value as uint256', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_collection_id_non_existent(): void + { + Collection::where('collection_chain_id', '=', $collectionId = fake()->numberBetween(2000))?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipients' => [ + [ + 'account' => SS58Address::encode($this->recipient->public_key), + 'simpleParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $this->tokenAccount->balance, + ], + ], + ], + ], true); + + $this->assertArraySubset( + ['collectionId' => ['The selected collection id is invalid.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_no_recipients(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + ], true); + + $this->assertStringContainsString( + 'Variable "$recipients" of required type "[TransferRecipient!]!" was not provided.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_empty_recipients(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [], + ], true); + + $this->assertArraySubset( + ['recipients' => ['The recipients field must have at least 1 items.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_null_recipient(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => null, + ], true); + + $this->assertStringContainsString( + 'Variable "$recipients" of non-null type "[TransferRecipient!]!" must not be null.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_invalid_recipient(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => 'invalid', + ], true); + + $this->assertStringContainsString( + 'Variable "$recipients" got invalid value "invalid"; Expected type "TransferRecipient" to be an object', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_missing_address_in_recipient(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'simpleParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $this->tokenAccount->balance, + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Field "account" of required type "String!" was not provided', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_null_address_in_recipient(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => null, + 'simpleParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $this->tokenAccount->balance, + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'value null at "recipients[0].account"; Expected non-nullable type "String!" not to be null', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_invalid_address_in_recipient(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => 'invalid', + 'simpleParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $this->tokenAccount->balance, + ], + ], + ], + ], true); + + $this->assertArraySubset( + ['recipients.0.account' => ['The recipients.0.account is not a valid substrate account.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_missing_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => SS58Address::encode($this->recipient->public_key), + ], + ], + ], true); + + $this->assertStringContainsString( + 'You need to set either simple params or operator params for every recipient.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_null_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => SS58Address::encode($this->recipient->public_key), + 'simpleParams' => null, + ], + ], + ], true); + + $this->assertStringContainsString( + 'You need to set either simple params or operator params for every recipient.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_empty_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => SS58Address::encode($this->recipient->public_key), + 'simpleParams' => [], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Field "tokenId" of required type "EncodableTokenIdInput!" was not provided', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_invalid_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'simpleParams' => 'invalid', + ], + ], + ], true); + + $this->assertStringContainsString( + 'got invalid value "invalid" at "recipients[0].simpleParams"; Expected type "SimpleTransferParams" to be an object', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_missing_token_id_in_recipient(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'simpleParams' => [ + 'amount' => $this->tokenAccount->balance, + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Field "tokenId" of required type "EncodableTokenIdInput!" was not provided', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_null_token_id_in_recipient(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'simpleParams' => [ + 'tokenId' => null, + 'amount' => $this->tokenAccount->balance, + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$recipients" got invalid value null at "recipients[0].simpleParams.tokenId"; Expected non-nullable type "EncodableTokenIdInput!" not to be null', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_invalid_token_id_in_recipient(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'simpleParams' => [ + 'tokenId' => ['integer' => 'invalid'], + 'amount' => $this->tokenAccount->balance, + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$recipients" got invalid value "invalid" at "recipients[0].simpleParams.tokenId.integer"; Cannot represent following value as uint256', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_token_id_non_existent_in_recipient(): void + { + $tokenIdEncoder = new Integer(fake()->numberBetween()); + + Token::where('token_chain_id', '=', $tokenIdEncoder->encode())?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => SS58Address::encode($this->recipient->public_key), + 'simpleParams' => [ + 'tokenId' => $tokenIdEncoder->toEncodable(), + 'amount' => $this->tokenAccount->balance, + ], + ], + ], + ], true); + + $this->assertArraySubset( + ['recipients.0.simpleParams.tokenId' => ['The recipients.0.simpleParams.tokenId does not exist in the specified collection.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_no_amount(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'simpleParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Field "amount" of required type "BigInt!" was not provided', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_null_amount(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'simpleParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => null, + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$recipients" got invalid value null at "recipients[0].simpleParams.amount"; Expected non-nullable type "BigInt!" not to be null', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_zero_amount(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'simpleParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => 0, + ], + ], + ], + ], true); + + $this->assertArraySubset( + ['recipients.0.simpleParams.amount' => ['The recipients.0.simple params.amount is too small, the minimum value it can be is 1.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_negative_amount(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'simpleParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => -1, + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$recipients" got invalid value -1 at "recipients[0].simpleParams.amount"; Cannot represent following value as uint256', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_invalid_amount(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'simpleParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => 'invalid', + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'got invalid value "invalid" at "recipients[0].simpleParams.amount"; Cannot represent following value as uint256', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_amount_greater_than_balance(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'simpleParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => fake()->numberBetween($this->tokenAccount->balance + 1), + ], + ], + ], + ], true); + + $this->assertArraySubset( + ['recipients.0.simpleParams.amount' => ['The recipients.0.simple params.amount is invalid, the amount provided is bigger than the token account balance.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_invalid_keep_alive(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'simpleParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => fake()->numberBetween(1, $this->tokenAccount->balance), + 'keepAlive' => 'invalid', + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$recipients" got invalid value "invalid" at "recipients[0].simpleParams.keepAlive"; Boolean cannot represent a non boolean value', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_both_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => SS58Address::encode($this->recipient->public_key), + 'simpleParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => fake()->numberBetween(1, $this->tokenAccount->balance), + ], + 'operatorParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => SS58Address::encode($this->wallet->public_key), + 'amount' => fake()->numberBetween(1, $this->tokenAccount->balance), + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Cannot set simple params and operator params for the same recipient.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_over_two_hundred_fifty_items(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => collect(range(0, 251))->map(fn () => [ + 'account' => SS58Address::encode(app(Generator::class)->public_key()), + ...fake()->randomElement([ + [ + 'simpleParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => fake()->numberBetween(1, $this->tokenAccount->balance), + ], + ], + [ + 'operatorParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => SS58Address::encode($this->wallet->public_key), + 'amount' => fake()->numberBetween(1, $this->tokenAccount->balance), + ], + ], + ]), + ], )->toArray(), + ], true); + + $this->assertArraySubset( + ['recipients' => ['The recipients field must not have more than 250 items.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_null_operator(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'operatorParams' => null, + ], + ], + ], true); + + $this->assertStringContainsString( + 'You need to set either simple params or operator params for every recipient.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_empty_operator(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'operatorParams' => [], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Field "tokenId" of required type "EncodableTokenIdInput!" was not provided', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_invalid_operator(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'operatorParams' => 'invalid', + ], + ], + ], true); + + $this->assertStringContainsString( + 'got invalid value "invalid" at "recipients[0].operatorParams"; Expected type "OperatorTransferParams" to be an object', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_missing_token_id_in_operator(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'operatorParams' => [ + 'source' => $this->wallet->public_key, + 'amount' => fake()->numberBetween(1, $this->tokenAccount->balance), + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Field "tokenId" of required type "EncodableTokenIdInput!" was not provided', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_null_token_id_in_operator(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'operatorParams' => [ + 'tokenId' => null, + 'source' => $this->wallet->public_key, + 'amount' => fake()->numberBetween(1, $this->tokenAccount->balance), + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$recipients" got invalid value null at "recipients[0].operatorParams.tokenId"; Expected non-nullable type "EncodableTokenIdInput!" not to be null', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_invalid_token_id_in_operator(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'operatorParams' => [ + 'tokenId' => 'invalid', + 'source' => $this->wallet->public_key, + 'amount' => fake()->numberBetween(1, $this->tokenAccount->balance), + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$recipients" got invalid value "invalid" at "recipients[0].operatorParams.tokenId"; Expected type "EncodableTokenIdInput" to be an object', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_token_id_in_operator_non_existent(): void + { + Token::where('token_chain_id', '=', $tokenId = fake()->numberBetween())?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => SS58Address::encode($this->recipient->public_key), + 'operatorParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable($tokenId), + 'source' => SS58Address::encode($this->wallet->public_key), + 'amount' => fake()->numberBetween(1, $this->tokenAccount->balance), + ], + ], + ], + ], true); + + $this->assertArraySubset( + ['recipients.0.operatorParams.tokenId' => ['The recipients.0.operatorParams.tokenId does not exist in the specified collection.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_missing_source_in_operator(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'operatorParams' => [ + 'tokenId' => (new Integer($this->token->token_chain_id))->toEncodable(), + 'amount' => fake()->numberBetween(1, $this->tokenAccount->balance), + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Field "source" of required type "String!" was not provided', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_null_source_in_operator(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => $this->recipient->public_key, + 'operatorParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => null, + 'amount' => fake()->numberBetween(1, $this->tokenAccount->balance), + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$recipients" got invalid value null at "recipients[0].operatorParams.source"; Expected non-nullable type "String!" not to be null', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_invalid_source_in_operator(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => SS58Address::encode($this->recipient->public_key), + 'operatorParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => 'invalid', + 'amount' => fake()->numberBetween(1, $this->tokenAccount->balance), + ], + ], + ], + ], true); + + $this->assertArraySubset( + ['recipients.0.operatorParams.source' => ['The recipients.0.operator params.source is not a valid substrate account.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_source_doesnt_exists_in_operator(): void + { + Wallet::where('public_key', '=', $source = app(Generator::class)->public_key())?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => SS58Address::encode($this->recipient->public_key), + 'operatorParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => SS58Address::encode($source), + 'amount' => fake()->numberBetween(1, $this->tokenAccount->balance), + ], + ], + ], + ], true); + + $this->assertArraySubset( + ['recipients.0.operatorParams.amount' => ['The recipients.0.operator params.amount is invalid, the amount provided is bigger than the token account balance.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_missing_amount_in_operator(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => SS58Address::encode($this->recipient->public_key), + 'operatorParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => SS58Address::encode($this->wallet->public_key), + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Field "amount" of required type "BigInt!" was not provided', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_zero_amount_in_operator(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => SS58Address::encode($this->recipient->public_key), + 'operatorParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => SS58Address::encode($this->wallet->public_key), + 'amount' => 0, + ], + ], + ], + ], true); + + $this->assertArraySubset( + ['recipients.0.operatorParams.amount' => ['The recipients.0.operator params.amount is too small, the minimum value it can be is 1.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_negative_amount_in_operator(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => SS58Address::encode($this->recipient->public_key), + 'operatorParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => SS58Address::encode($this->wallet->public_key), + 'amount' => -1, + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'got invalid value -1 at "recipients[0].operatorParams.amount"; Cannot represent following value as uint256', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_invalid_amount_in_operator(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => SS58Address::encode($this->recipient->public_key), + 'operatorParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => SS58Address::encode($this->wallet->public_key), + 'amount' => 'invalid', + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$recipients" got invalid value "invalid" at "recipients[0].operatorParams.amount', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_amount_greater_than_token_balance_in_operator(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => SS58Address::encode($this->recipient->public_key), + 'operatorParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => SS58Address::encode($this->wallet->public_key), + 'amount' => fake()->numberBetween($this->tokenAccount->balance + 1), + ], + ], + ], + ], true); + + $this->assertArraySubset( + ['recipients.0.operatorParams.amount' => ['The recipients.0.operator params.amount is invalid, the amount provided is bigger than the token account balance.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_invalid_keep_alive_in_operator(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => SS58Address::encode($this->recipient->public_key), + 'operatorParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => SS58Address::encode($this->wallet->public_key), + 'amount' => fake()->numberBetween(1, $this->tokenAccount->balance), + 'keepAlive' => 'invalid', + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'got invalid value "invalid" at "recipients[0].operatorParams.keepAlive"; Boolean cannot represent a non boolean value', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_invalid_signing_wallet(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => SS58Address::encode($this->wallet->public_key), + 'simpleParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => fake()->numberBetween(1, $this->tokenAccount->balance), + ], + ], + ], + 'signingAccount' => 'invalid', + ], true); + + $this->assertArraySubset( + ['signingAccount' => ['The signing account is not a valid substrate account.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_not_managed_signing_wallet(): void + { + $signingWallet = Wallet::factory([ + 'managed' => false, + ])->create(); + Collection::where('collection_chain_id', Hex::MAX_UINT128)->update(['collection_chain_id' => random_int(1, 1000)]); + $collection = Collection::factory([ + 'collection_chain_id' => Hex::MAX_UINT128, + ])->create(); + CollectionAccount::factory([ + 'collection_id' => $collection, + 'wallet_id' => $signingWallet, + 'account_count' => 1, + ])->create(); + $token = Token::factory([ + 'collection_id' => $collection, + ])->create(); + $tokenAccount = TokenAccount::factory([ + 'collection_id' => $collection, + 'token_id' => $token, + 'wallet_id' => $signingWallet, + ])->create(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collection->collection_chain_id, + 'recipients' => [ + [ + 'account' => SS58Address::encode($this->recipient->public_key), + 'simpleParams' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable($token->token_chain_id), + 'amount' => fake()->numberBetween(1, $tokenAccount->balance), + ], + ], + ], + 'signingAccount' => SS58Address::encode($signingWallet->public_key), + ], true); + + $this->assertArraySubset( + ['signingAccount' => ['The signing account is not a wallet managed by this platform.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } +} diff --git a/tests/Feature/GraphQL/Mutations/BurnTest.php b/tests/Feature/GraphQL/Mutations/BurnTest.php new file mode 100644 index 00000000..6494e38f --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/BurnTest.php @@ -0,0 +1,633 @@ +codec = new Codec(); + $walletService = new WalletService(); + $this->defaultAccount = config('enjin-platform.chains.daemon-account'); + $this->wallet = $walletService->firstOrStore(['public_key' => $this->defaultAccount]); + + $this->tokenAccount = TokenAccount::factory([ + 'wallet_id' => $this->wallet, + ])->create(); + + $this->token = Token::find($this->tokenAccount->token_id); + $this->tokenIdEncoder = new Integer($this->token->token_chain_id); + $this->collection = Collection::find($this->tokenAccount->collection_id); + } + + // Happy Path + + public function test_can_burn_a_token_with_default_values_using_adapter(): void + { + $encodedData = $this->codec->encode()->burn( + $collectionId = $this->collection->collection_chain_id, + new BurnParams( + tokenId: $this->tokenIdEncoder->encode(), + amount: $amount = fake()->numberBetween(0, $this->tokenAccount->balance), + ), + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $amount, + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + } + + public function test_can_burn_a_token_with_default_values(): void + { + $encodedData = $this->codec->encode()->burn( + $collectionId = $this->collection->collection_chain_id, + new BurnParams( + tokenId: $this->tokenIdEncoder->encode(), + amount: $amount = fake()->numberBetween(0, $this->tokenAccount->balance), + ), + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $amount, + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_can_burn_a_token_with_keepalive(): void + { + $encodedData = $this->codec->encode()->burn( + $collectionId = $this->collection->collection_chain_id, + new BurnParams( + tokenId: $this->tokenIdEncoder->encode(), + amount: $amount = fake()->numberBetween(0, $this->tokenAccount->balance), + keepAlive: $keepAlive = fake()->boolean(), + ), + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $amount, + 'keepAlive' => $keepAlive, + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_can_burn_a_token_with_remove_token_storage(): void + { + $encodedData = $this->codec->encode()->burn( + $collectionId = $this->collection->collection_chain_id, + new BurnParams( + tokenId: $this->tokenIdEncoder->encode(), + amount: $amount = fake()->numberBetween(0, $this->tokenAccount->balance), + removeTokenStorage: $removeTokenStorage = fake()->boolean(), + ), + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => $amount, + 'removeTokenStorage' => $removeTokenStorage, + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_can_burn_a_token_with_all_args(): void + { + $encodedData = $this->codec->encode()->burn( + $collectionId = $this->collection->collection_chain_id, + $params = new BurnParams( + tokenId: $this->tokenIdEncoder->encode(), + amount: fake()->numberBetween(0, $this->tokenAccount->balance), + keepAlive: fake()->boolean(), + removeTokenStorage: fake()->boolean(), + ), + ); + + $params = $params->toArray(); + $params['tokenId'] = $this->tokenIdEncoder->toEncodable(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'params' => $params, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_can_burn_a_token_with_bigint_tokenid(): void + { + $collection = Collection::factory([ + 'collection_chain_id' => fake()->numberBetween(2000), + ])->create(); + + $token = Token::factory([ + 'collection_id' => $collection, + 'token_chain_id' => Hex::MAX_UINT128, + ])->create(); + + $tokenAccount = TokenAccount::factory([ + 'collection_id' => $collection, + 'token_id' => $token, + 'wallet_id' => $this->wallet, + ])->create(); + + $encodedData = $this->codec->encode()->burn( + $collectionId = $collection->collection_chain_id, + $params = new BurnParams( + tokenId: $this->tokenIdEncoder->encode($token->token_chain_id), + amount: fake()->numberBetween(0, $tokenAccount->balance), + ), + ); + + $params = $params->toArray(); + $params['tokenId'] = $this->tokenIdEncoder->toEncodable($token->token_chain_id); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'params' => $params, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_can_burn_a_token_with_bigint_amount(): void + { + $collection = Collection::factory([ + 'collection_chain_id' => fake()->numberBetween(2000), + ])->create(); + + $token = Token::factory([ + 'collection_id' => $collection, + 'token_chain_id' => Hex::MAX_UINT128, + ])->create(); + + TokenAccount::factory([ + 'collection_id' => $collection, + 'token_id' => $token, + 'wallet_id' => $this->wallet, + 'balance' => $balance = Hex::MAX_UINT128, + ])->create(); + + $encodedData = $this->codec->encode()->burn( + $collectionId = $collection->collection_chain_id, + $params = new BurnParams( + tokenId: $token->token_chain_id, + amount: $balance, + ), + ); + + $params = $params->toArray(); + $params['tokenId'] = $this->tokenIdEncoder->toEncodable($token->token_chain_id); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'params' => $params, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + // Exception Path + + public function test_it_will_fail_collection_id_that_doesnt_exists(): void + { + Collection::where('collection_chain_id', '=', $collectionId = fake()->numberBetween(2000))?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertArraySubset( + ['collectionId' => ['The selected collection id is invalid.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_token_id_that_doesnt_exists(): void + { + Token::where('token_chain_id', '=', $tokenId = fake()->numberBetween())?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable($tokenId), + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertArraySubset( + ['params.tokenId' => ['The params.token id does not exist in the specified collection.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_invalid_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => 'not_valid', + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" got invalid value "not_valid"; Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_invalid_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => 'not_valid', + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$params" got invalid value "not_valid" at "params.tokenId"; Expected type "EncodableTokenIdInput" to be an object', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_negative_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => -1, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" got invalid value -1; Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_negative_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => -1, + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$params" got invalid value -1 at "params.tokenId"; Expected type "EncodableTokenIdInput" to be an object', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_negative_amount(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => -1, + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$params" got invalid value -1 at "params.amount"; Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_zero_amount(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => 0, + ], + ], true); + + $this->assertArraySubset( + ['params.amount' => ['The params.amount is too small, the minimum value it can be is 1.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_no_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of required type "BigInt!" was not provided.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_no_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertStringContainsString( + 'Field "tokenId" of required type "EncodableTokenIdInput!" was not provided', + $response['errors'][0]['message'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_no_amount(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + ], + ], true); + + $this->assertStringContainsString( + 'Field "amount" of required type "BigInt!" was not provided', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_invalid_keepalive(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + 'keepAlive' => 'invalid', + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$params" got invalid value "invalid" at "params.keepAlive"; Boolean cannot represent a non boolean value', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_invalid_removetokenstorage(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + 'removeTokenStorage' => 'invalid', + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$params" got invalid value "invalid" at "params.removeTokenStorage"; Boolean cannot represent a non boolean value', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_empty_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [], + ], true); + + $this->assertStringContainsString( + 'Variable "$params" got invalid value []; Field "tokenId" of required type "EncodableTokenIdInput!" was not provide', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_when_trying_to_burn_more_than_balance(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => fake()->numberBetween($this->tokenAccount->balance), + ], + ], true); + + $this->assertArraySubset( + ['params.amount' => ['The params.amount is invalid, the amount provided is bigger than the token account balance.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } +} diff --git a/tests/Feature/GraphQL/Mutations/CreateCollectionTest.php b/tests/Feature/GraphQL/Mutations/CreateCollectionTest.php new file mode 100644 index 00000000..ca00e80f --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/CreateCollectionTest.php @@ -0,0 +1,1233 @@ +codec = new Codec(); + $this->defaultAccount = config('enjin-platform.chains.daemon-account'); + $this->tokenIdEncoder = new Integer(); + } + + // Happy Path + + public function test_it_will_fail_with_duplicate_names(): void + { + self::$queries['CreateCollectionDuplicateFieldName'] = ' + mutation CreateCollection { + CreateCollection( + mintPolicy: { + maxTokenCount: 100000 + maxTokenSupply: 10 + forceSingleMint: true + } + marketPolicy: { + royalty: { + beneficiary: "rf8YmxhSe9WGJZvCH8wtzAndweEmz6dTV6DjmSHgHvPEFNLAJ", + percentage: 5 + percentage: 50 + } + } + + ) { + id + encodedData + state + } + }'; + $response = $this->graphql('CreateCollectionDuplicateFieldName', [], true, ['operationName' => $this->method]); + $this->assertArraySubset( + ['percentage' => ['message' => 'There can be only one input field named "percentage".']], + $response['errors'] + ); + } + + public function test_create_collection_single_mint(): void + { + $encodedData = $this->codec->encode()->createCollection( + $policy = new MintPolicyParams( + forceSingleMint: fake()->boolean(), + ) + ); + + $response = $this->graphql($this->method, [ + 'mintPolicy' => $policy->toArray(), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_one_create_collection_transaction_is_created_using_idempotency(): void + { + $encodedData = $this->codec->encode()->createCollection( + $policy = new MintPolicyParams( + forceSingleMint: fake()->boolean(), + ) + ); + + $idempotencyKey = fake()->uuid(); + + $expectedResponse = [ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + 'idempotencyKey' => $idempotencyKey, + ]; + + // First run + $response = $this->graphql($this->method, [ + 'mintPolicy' => $policy->toArray(), + 'idempotencyKey' => $idempotencyKey, + ]); + + $this->assertArraySubset($expectedResponse, $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $responseId = $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + // Second run, should return the same data as the first run, but without dispatching a new event. + $response = $this->graphql($this->method, [ + 'mintPolicy' => $policy->toArray(), + 'idempotencyKey' => $idempotencyKey, + ]); + + $this->assertArraySubset($expectedResponse, $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $responseId, + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatchedTimes(TransactionCreated::class, 1); + } + + public function test_create_collection_with_max_token_count(): void + { + $encodedData = $this->codec->encode()->createCollection( + $policy = new MintPolicyParams( + forceSingleMint: fake()->boolean(), + maxTokenCount: fake()->numberBetween(1, Hex::MAX_UINT64), + ) + ); + + $response = $this->graphql($this->method, [ + 'mintPolicy' => $policy->toArray(), + ]); + + $this->assertArraySubset([ + 'state' => TransactionState::PENDING->name, + 'method' => $this->method, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_create_collection_with_max_token_supply(): void + { + $encodedData = $this->codec->encode()->createCollection( + $policy = new MintPolicyParams( + forceSingleMint: fake()->boolean(), + maxTokenSupply: fake()->numberBetween(1, Hex::MAX_UINT64), + ) + ); + + $response = $this->graphql($this->method, [ + 'mintPolicy' => $policy->toArray(), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_create_collection_with_all_mint_args(): void + { + $encodedData = $this->codec->encode()->createCollection( + $policy = new MintPolicyParams( + forceSingleMint: fake()->boolean(), + maxTokenCount: fake()->numberBetween(1, Hex::MAX_UINT64), + maxTokenSupply: fake()->numberBetween(1, Hex::MAX_UINT64), + ) + ); + + $response = $this->graphql($this->method, [ + 'mintPolicy' => $policy->toArray(), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_create_collection_works_with_big_int_max_token_count(): void + { + $encodedData = $this->codec->encode()->createCollection( + $policy = new MintPolicyParams( + forceSingleMint: fake()->boolean(), + maxTokenCount: Hex::MAX_UINT64, + ) + ); + + $response = $this->graphql($this->method, [ + 'mintPolicy' => $policy->toArray(), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_create_collection_works_with_big_int_max_token_supply(): void + { + $encodedData = $this->codec->encode()->createCollection( + $policy = new MintPolicyParams( + forceSingleMint: fake()->boolean(), + maxTokenSupply: Hex::MAX_UINT128, + ) + ); + + $response = $this->graphql($this->method, [ + 'mintPolicy' => $policy->toArray(), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_works_with_royalty(): void + { + $encodedData = $this->codec->encode()->createCollection( + $mintPolicy = new MintPolicyParams( + forceSingleMint: fake()->boolean(), + ), + $marketPolicy = new RoyaltyPolicyParams( + beneficiary: app(Generator::class)->public_key(), + percentage: fake()->numberBetween(1, 50) + ), + ); + + $response = $this->graphql($this->method, [ + 'mintPolicy' => $mintPolicy->toArray(), + 'marketPolicy' => [ + 'royalty' => $marketPolicy->toArray(), + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_works_with_explicit_royalty_currencies(): void + { + $encodedData = $this->codec->encode()->createCollection( + $mintPolicy = new MintPolicyParams( + forceSingleMint: fake()->boolean(), + ), + explicitRoyaltyCurrencies: $currencies = $this->generateCurrencies(fake()->numberBetween(1, 9)), + ); + + $response = $this->graphql($this->method, [ + 'mintPolicy' => $mintPolicy->toArray(), + 'explicitRoyaltyCurrencies' => $this->generateEncodeableTokenIds($currencies), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_works_with_empty_array_of_currencies(): void + { + $encodedData = $this->codec->encode()->createCollection( + $mintPolicy = new MintPolicyParams( + forceSingleMint: fake()->boolean(), + ), + explicitRoyaltyCurrencies: [], + ); + + $response = $this->graphql($this->method, [ + 'mintPolicy' => $mintPolicy->toArray(), + 'explicitRoyaltyCurrencies' => [], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_works_with_all_args(): void + { + $encodedData = $this->codec->encode()->createCollection( + $mintPolicy = new MintPolicyParams( + forceSingleMint: fake()->boolean(), + maxTokenCount: fake()->numberBetween(), + maxTokenSupply: fake()->numberBetween(), + ), + $marketPolicy = new RoyaltyPolicyParams( + beneficiary: app(Generator::class)->public_key(), + percentage: fake()->numberBetween(1, 50) + ), + $currencies = $this->generateCurrencies(fake()->numberBetween(1, 9)), + ); + + $response = $this->graphql($this->method, [ + 'mintPolicy' => $mintPolicy->toArray(), + 'marketPolicy' => [ + 'royalty' => $marketPolicy->toArray(), + ], + 'explicitRoyaltyCurrencies' => $this->generateEncodeableTokenIds($currencies), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + // Exception paths + public function test_it_will_fail_with_empty_idempotency_key(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => false, + ], + 'idempotencyKey' => '', + ], true); + + $this->assertArraySubset( + ['idempotencyKey' => ['The idempotency key field must have a value.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_short_idempotency_key(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => false, + ], + 'idempotencyKey' => fake()->text(28), + ], true); + + $this->assertArraySubset( + ['idempotencyKey' => ['The idempotency key field must be at least 36 characters.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_long_idempotency_key(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => false, + ], + 'idempotencyKey' => fake()->realTextBetween(256, 300), + ], true); + + $this->assertArraySubset( + ['idempotencyKey' => ['The idempotency key field must not be greater than 255 characters.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_args(): void + { + $response = $this->graphql($this->method, [], true); + + $this->assertStringContainsString( + 'Variable "$mintPolicy" of required type "MintPolicy!" was not provided.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_mint_policy(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => null, + ], true); + + $this->assertStringContainsString( + 'Variable "$mintPolicy" of non-null type "MintPolicy!" must not be null.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_mint_policy(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => 'invalid', + ], true); + + $this->assertStringContainsString( + 'Variable "$mintPolicy" got invalid value "invalid"; Expected type "MintPolicy" to be an object', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_force_single_mint(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [], + ], true); + + $this->assertStringContainsString( + 'Variable "$mintPolicy" got invalid value []; Field "forceSingleMint" of required type "Boolean!" was not provided', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_force_single_mint(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => null, + ], + ], true); + + $this->assertStringContainsString( + 'value null at "mintPolicy.forceSingleMint"; Expected non-nullable type "Boolean!" not to be null', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_force_single_mint(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => 'invalid', + ], + ], true); + + $this->assertStringContainsString( + 'Boolean cannot represent a non boolean value', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_negative_token_count(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => fake()->boolean(), + 'maxTokenCount' => -1, + ], + ], true); + + $this->assertStringContainsString( + 'Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_zero_token_count(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => fake()->boolean(), + 'maxTokenCount' => 0, + ], + ], true); + + $this->assertArraySubset( + ['mintPolicy.maxTokenCount' => ['The mint policy.max token count is too small, the minimum value it can be is 1.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_token_count(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => fake()->boolean(), + 'maxTokenCount' => 'invalid', + ], + ], true); + + $this->assertStringContainsString( + 'Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_overflow_token_count(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => fake()->boolean(), + 'maxTokenCount' => Hex::MAX_UINT128, + ], + ], true); + + $this->assertArraySubset( + ['mintPolicy.maxTokenCount' => ['The mint policy.max token count is too large, the maximum value it can be is 18446744073709551615.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_negative_token_supply(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => fake()->boolean(), + 'maxTokenSupply' => -1, + ], + ], true); + + $this->assertStringContainsString( + 'Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_zero_token_supply(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => fake()->boolean(), + 'maxTokenSupply' => 0, + ], + ], true); + + $this->assertArraySubset( + ['mintPolicy.maxTokenSupply' => ['The mint policy.max token supply is too small, the minimum value it can be is 1.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_token_supply(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => fake()->boolean(), + 'maxTokenSupply' => 'invalid', + ], + ], true); + + $this->assertStringContainsString( + 'Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_overflow_token_supply(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => fake()->boolean(), + 'maxTokenSupply' => Hex::MAX_UINT256, + ], + ], true); + + $this->assertArraySubset( + ['mintPolicy.maxTokenSupply' => ['The mint policy.max token supply is too large, the maximum value it can be is 340282366920938463463374607431768211455.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_market_policy(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => fake()->boolean(), + ], + 'marketPolicy' => 'invalid', + ], true); + + $this->assertStringContainsString( + 'Variable "$marketPolicy" got invalid value "invalid"; Expected type "MarketPolicy" to be an object', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_empty_market_policy(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => fake()->boolean(), + ], + 'marketPolicy' => [], + ], true); + + $this->assertStringContainsString( + 'Variable "$marketPolicy" got invalid value []', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_royalty_policy(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => fake()->boolean(), + ], + 'marketPolicy' => [ + 'royalty' => 'invalid', + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$marketPolicy" got invalid value "invalid" at "marketPolicy.royalty"; Expected type "RoyaltyInput" to be an object', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_royalty(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => fake()->boolean(), + ], + 'marketPolicy' => [ + 'royalty' => null, + ], + ], true); + + $this->assertStringContainsString( + 'value null at "marketPolicy.royalty"; Expected non-nullable type "RoyaltyInput!" not to be null', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_beneficiary(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => fake()->boolean(), + ], + 'marketPolicy' => [ + 'royalty' => [ + 'beneficiary' => 'invalid', + 'percentage' => fake()->numberBetween(1, 50), + ], + ], + ], true); + + $this->assertArraySubset( + ['marketPolicy.royalty.beneficiary' => ['The market policy.royalty.beneficiary is not a valid substrate account.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_beneficiary(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => fake()->boolean(), + ], + 'marketPolicy' => [ + 'royalty' => [ + 'percentage' => fake()->numberBetween(1, 50), + ], + ], + ], true); + + $this->assertStringContainsString( + 'Field "beneficiary" of required type "String!" was not provided', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_beneficiary(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => fake()->boolean(), + ], + 'marketPolicy' => [ + 'royalty' => [ + 'beneficiary' => null, + 'percentage' => fake()->numberBetween(1, 50), + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$marketPolicy" got invalid value null at "marketPolicy.royalty.beneficiary"; Expected non-nullable type "String!" not to be null', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_percentage(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => fake()->boolean(), + ], + 'marketPolicy' => [ + 'royalty' => [ + 'beneficiary' => app(Generator::class)->public_key(), + ], + ], + ], true); + + $this->assertStringContainsString( + 'Field "percentage" of required type "Float!" was not provided', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_percentage(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => fake()->boolean(), + ], + 'marketPolicy' => [ + 'royalty' => [ + 'beneficiary' => app(Generator::class)->public_key(), + 'percentage' => null, + ], + ], + ], true); + + $this->assertStringContainsString( + '"marketPolicy.royalty.percentage"; Expected non-nullable type "Float!" not to be null', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_percentage(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => fake()->boolean(), + ], + 'marketPolicy' => [ + 'royalty' => [ + 'beneficiary' => app(Generator::class)->public_key(), + 'percentage' => 'invalid', + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$marketPolicy" got invalid value "invalid" at "marketPolicy.royalty.percentage"; Float cannot represent non numeric value', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_negative_percentage(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => fake()->boolean(), + ], + 'marketPolicy' => [ + 'royalty' => [ + 'beneficiary' => app(Generator::class)->public_key(), + 'percentage' => -1, + ], + ], + ], true); + + $this->assertArraySubset( + ['marketPolicy.royalty.percentage' => ['The market policy.royalty.percentage valid for a royalty is in the range of 0.1% to 50% and a maximum of 7 decimal places.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_zero_percentage(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => fake()->boolean(), + ], + 'marketPolicy' => [ + 'royalty' => [ + 'beneficiary' => app(Generator::class)->public_key(), + 'percentage' => 0, + ], + ], + ], true); + + $this->assertArraySubset( + ['marketPolicy.royalty.percentage' => ['The market policy.royalty.percentage valid for a royalty is in the range of 0.1% to 50% and a maximum of 7 decimal places.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_more_than_max_percentage(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => fake()->boolean(), + ], + 'marketPolicy' => [ + 'royalty' => [ + 'beneficiary' => app(Generator::class)->public_key(), + 'percentage' => 50.1, + ], + ], + ], true); + + $this->assertArraySubset( + ['marketPolicy.royalty.percentage' => ['The market policy.royalty.percentage valid for a royalty is in the range of 0.1% to 50% and a maximum of 7 decimal places.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_explicit_royalty_currencies(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => fake()->boolean(), + ], + 'explicitRoyaltyCurrencies' => 'invalid', + ], true); + + $this->assertStringContainsString( + 'Variable "$explicitRoyaltyCurrencies" got invalid value "invalid"; Expected type "MultiTokenIdInput" to be an object', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_currency_with_missing_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => fake()->boolean(), + ], + 'explicitRoyaltyCurrencies' => $this->generateEncodeableTokenIds(array_merge($this->generateCurrencies(), [ + [ + 'tokenId' => fake()->numberBetween(), + ], + ])), + ], true); + + $this->assertStringContainsString( + 'Field "collectionId" of required type "BigInt!" was not provided', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_currency_with_null_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => fake()->boolean(), + ], + 'explicitRoyaltyCurrencies' => $this->generateEncodeableTokenIds(array_merge($this->generateCurrencies(), [ + [ + 'collectionId' => null, + 'tokenId' => fake()->numberBetween(), + ], + ])), + ], true); + + $this->assertStringContainsString( + 'Variable "$explicitRoyaltyCurrencies" got invalid value null at "explicitRoyaltyCurrencies[6].collectionId"; Expected non-nullable type "BigInt!" not to be null', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_currency_with_invalid_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => fake()->boolean(), + ], + 'explicitRoyaltyCurrencies' => $this->generateEncodeableTokenIds(array_merge($this->generateCurrencies(), [ + [ + 'collectionId' => 'invalid', + 'tokenId' => fake()->numberBetween(), + ], + ])), + ], true); + + $this->assertStringContainsString( + 'Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_currency_with_missing_token_id(): void + { + $collection = Collection::factory()->create(); + + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => fake()->boolean(), + ], + 'explicitRoyaltyCurrencies' => array_merge($this->generateCurrencies(), [ + [ + 'collectionId' => $collection->collection_chain_id, + ], + ]), + ], true); + + $this->assertStringContainsString( + '"explicitRoyaltyCurrencies[0].tokenId"; Expected type "EncodableTokenIdInput" to be an object', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_currency_with_null_token_id(): void + { + $collection = Collection::factory()->create(); + + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => fake()->boolean(), + ], + 'explicitRoyaltyCurrencies' => array_merge($this->generateCurrencies(), [ + [ + 'collectionId' => $collection->collection_chain_id, + 'tokenId' => null, + ], + ]), + ], true); + + $this->assertStringContainsString( + '"explicitRoyaltyCurrencies[0].tokenId"; Expected type "EncodableTokenIdInput" to be an object', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_currency_with_invalid_token_id(): void + { + $collection = Collection::factory()->create(); + + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => fake()->boolean(), + ], + 'explicitRoyaltyCurrencies' => array_merge($this->generateCurrencies(), [ + [ + 'collectionId' => $collection->collection_chain_id, + 'tokenId' => 'invalid', + ], + ]), + ], true); + + $this->assertStringContainsString( + '"explicitRoyaltyCurrencies[0].tokenId"; Expected type "EncodableTokenIdInput" to be an object', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_duplicated_currency(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => fake()->boolean(), + ], + 'explicitRoyaltyCurrencies' => $this->generateEncodeableTokenIds(array_merge($currencies = $this->generateCurrencies(), [$currencies[0]])), + ], true); + + $this->assertArraySubset( + ['explicitRoyaltyCurrencies' => ['The explicit royalty currencies must be an array of distinct multi assets.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_more_than_ten_currencies(): void + { + $response = $this->graphql($this->method, [ + 'mintPolicy' => [ + 'forceSingleMint' => fake()->boolean(), + ], + 'explicitRoyaltyCurrencies' => $this->generateEncodeableTokenIds($this->generateCurrencies(11)), + ], true); + + $this->assertArraySubset( + ['explicitRoyaltyCurrencies' => ['The explicit royalty currencies field must not have more than 10 items.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + protected function generateCurrencies(?int $total = 5): array + { + return array_map( + fn () => [ + 'tokenId' => ($token = Token::factory()->create())->token_chain_id, + 'collectionId' => Collection::find($token->collection_id)->collection_chain_id, + ], + range(0, $total), + ); + } + + protected function generateEncodeableTokenIds($items) + { + return collect($items)->transform(function ($item) { + $item['tokenId'] = $this->tokenIdEncoder->toEncodable($item['tokenId']); + + return $item; + })->all(); + } +} diff --git a/tests/Feature/GraphQL/Mutations/CreateTokenTest.php b/tests/Feature/GraphQL/Mutations/CreateTokenTest.php new file mode 100644 index 00000000..2e3d1460 --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/CreateTokenTest.php @@ -0,0 +1,1424 @@ +codec = new Codec(); + $this->collection = Collection::factory()->create(); + $this->recipient = Wallet::factory()->create(); + $this->defaultAccount = config('enjin-platform.chains.daemon-account'); + $this->tokenIdEncoder = new Integer(fake()->unique()->numberBetween()); + } + + public function test_can_create_a_token_with_cap_equals_null_using_adapter(): void + { + $encodedData = $this->codec->encode()->mint( + $recipient = $this->recipient->public_key, + $collectionId = $this->collection->collection_chain_id, + $params = new CreateTokenParams( + tokenId: $this->tokenIdEncoder->encode(), + initialSupply: $initialSupply = fake()->numberBetween(1), + unitPrice: $this->randomGreaterThanMinUnitPriceFor($initialSupply), + cap: TokenMintCapType::INFINITE + ), + ); + + $params = $params->toArray()['CreateToken']; + $params['tokenId'] = $this->tokenIdEncoder->toEncodable(); + + $response = $this->graphql($this->method, [ + 'recipient' => $recipient, + 'collectionId' => $collectionId, + 'params' => $params, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + } + // Happy Path + + public function test_can_create_a_token_with_cap_equals_null(): void + { + $encodedData = $this->codec->encode()->mint( + $recipient = $this->recipient->public_key, + $collectionId = $this->collection->collection_chain_id, + $params = new CreateTokenParams( + tokenId: $this->tokenIdEncoder->encode($tokenId = fake()->numberBetween()), + initialSupply: $initialSupply = fake()->numberBetween(1), + unitPrice: $this->randomGreaterThanMinUnitPriceFor($initialSupply), + cap: TokenMintCapType::INFINITE + ), + ); + + $params = $params->toArray()['CreateToken']; + $params['tokenId'] = $this->tokenIdEncoder->toEncodable($tokenId); + + $response = $this->graphql($this->method, [ + 'recipient' => $recipient, + 'collectionId' => $collectionId, + 'params' => $params, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_can_create_a_token_with_single_mint(): void + { + $encodedData = $this->codec->encode()->mint( + $recipient = $this->recipient->public_key, + $collectionId = $this->collection->collection_chain_id, + new CreateTokenParams( + tokenId: $this->tokenIdEncoder->encode($tokenId = fake()->numberBetween()), + initialSupply: $initialSupply = fake()->numberBetween(1), + unitPrice: $unitPrice = $this->randomGreaterThanMinUnitPriceFor($initialSupply), + cap: $capType = TokenMintCapType::SINGLE_MINT + ), + ); + + $response = $this->graphql($this->method, [ + 'recipient' => $recipient, + 'collectionId' => $collectionId, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable($tokenId), + 'initialSupply' => $initialSupply, + 'unitPrice' => $unitPrice, + 'cap' => [ + 'type' => $capType->name, + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_can_create_a_token_with_supply_cap(): void + { + $encodedData = $this->codec->encode()->mint( + $recipient = $this->recipient->public_key, + $collectionId = $this->collection->collection_chain_id, + new CreateTokenParams( + tokenId: $this->tokenIdEncoder->encode($tokenId = fake()->numberBetween()), + initialSupply: $initialSupply = fake()->numberBetween(1), + unitPrice: $unitPrice = $this->randomGreaterThanMinUnitPriceFor($initialSupply), + cap: $capType = TokenMintCapType::SUPPLY, + supply: $capSupply = fake()->numberBetween($initialSupply) + ), + ); + + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($recipient), + 'collectionId' => $collectionId, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable($tokenId), + 'initialSupply' => $initialSupply, + 'unitPrice' => $unitPrice, + 'cap' => [ + 'type' => $capType->name, + 'amount' => $capSupply, + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_can_create_a_token_with_royalty_equals_null(): void + { + $encodedData = $this->codec->encode()->mint( + $recipient = $this->recipient->public_key, + $collectionId = $this->collection->collection_chain_id, + new CreateTokenParams( + tokenId: $this->tokenIdEncoder->encode($tokenId = fake()->numberBetween()), + initialSupply: $initialSupply = fake()->numberBetween(1), + unitPrice: $unitPrice = $this->randomGreaterThanMinUnitPriceFor($initialSupply), + behavior: null, + cap: TokenMintCapType::INFINITE, + ), + ); + + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($recipient), + 'collectionId' => $collectionId, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable($tokenId), + 'initialSupply' => $initialSupply, + 'unitPrice' => $unitPrice, + 'behavior' => null, + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_can_create_a_token_with_royalty(): void + { + $encodedData = $this->codec->encode()->mint( + $recipient = $this->recipient->public_key, + $collectionId = $this->collection->collection_chain_id, + new CreateTokenParams( + tokenId: $this->tokenIdEncoder->encode($tokenId = fake()->numberBetween()), + initialSupply: $initialSupply = fake()->numberBetween(1), + unitPrice: $unitPrice = $this->randomGreaterThanMinUnitPriceFor($initialSupply), + cap: TokenMintCapType::INFINITE, + behavior: new TokenMarketBehaviorParams( + hasRoyalty: new RoyaltyPolicyParams( + beneficiary: $beneficiary = $this->defaultAccount, + percentage: $percentage = fake()->numberBetween(1, 50) + ), + ), + ), + ); + + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($recipient), + 'collectionId' => $collectionId, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable($tokenId), + 'initialSupply' => $initialSupply, + 'unitPrice' => $unitPrice, + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + 'behavior' => [ + 'hasRoyalty' => [ + 'beneficiary' => SS58Address::encode($beneficiary), + 'percentage' => $percentage, + ], + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_can_create_a_token_with_listing_forbidden_equals_null(): void + { + $encodedData = $this->codec->encode()->mint( + $recipient = $this->recipient->public_key, + $collectionId = $this->collection->collection_chain_id, + new CreateTokenParams( + tokenId: $this->tokenIdEncoder->encode($tokenId = fake()->numberBetween()), + initialSupply: $initialSupply = fake()->numberBetween(1), + unitPrice: $unitPrice = $this->randomGreaterThanMinUnitPriceFor($initialSupply), + cap: TokenMintCapType::INFINITE, + listingForbidden: null, + ), + ); + + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($recipient), + 'collectionId' => $collectionId, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable($tokenId), + 'initialSupply' => $initialSupply, + 'unitPrice' => $unitPrice, + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + 'listingForbidden' => null, + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_can_create_a_token_with_listing_forbidden(): void + { + $encodedData = $this->codec->encode()->mint( + $recipient = $this->recipient->public_key, + $collectionId = $this->collection->collection_chain_id, + new CreateTokenParams( + tokenId: $this->tokenIdEncoder->encode($tokenId = fake()->numberBetween()), + initialSupply: $initialSupply = fake()->numberBetween(1), + unitPrice: $unitPrice = $this->randomGreaterThanMinUnitPriceFor($initialSupply), + listingForbidden: $listingForbidden = fake()->boolean(), + cap: TokenMintCapType::INFINITE, + ), + ); + + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($recipient), + 'collectionId' => $collectionId, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable($tokenId), + 'initialSupply' => $initialSupply, + 'unitPrice' => $unitPrice, + 'listingForbidden' => $listingForbidden, + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_can_create_a_token_with_different_types_for_numbers(): void + { + $encodedData = $this->codec->encode()->mint( + $recipient = $this->recipient->public_key, + $collectionId = $this->collection->collection_chain_id, + new CreateTokenParams( + tokenId: $this->tokenIdEncoder->encode($tokenId = fake()->numberBetween()), + initialSupply: $initialSupply = fake()->numberBetween(1), + unitPrice: $unitPrice = $this->randomGreaterThanMinUnitPriceFor($initialSupply), + cap: TokenMintCapType::INFINITE, + ), + ); + + $response = $this->graphql($this->method, [ + 'recipient' => $recipient, + 'collectionId' => (int) $collectionId, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable((string) $tokenId), + 'initialSupply' => (string) $initialSupply, + 'unitPrice' => $unitPrice, + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_can_create_a_token_with_bigint_collection_id(): void + { + $collection = Collection::factory([ + 'collection_chain_id' => Hex::MAX_UINT128, + ])->create(); + + $encodedData = $this->codec->encode()->mint( + $recipient = $this->recipient->public_key, + $collectionId = $collection->collection_chain_id, + $params = new CreateTokenParams( + tokenId: $this->tokenIdEncoder->encode($tokenId = fake()->numberBetween()), + initialSupply: $initialSupply = fake()->numberBetween(1), + unitPrice: $this->randomGreaterThanMinUnitPriceFor($initialSupply), + cap: TokenMintCapType::INFINITE, + ), + ); + + $params = $params->toArray()['CreateToken']; + $params['tokenId'] = $this->tokenIdEncoder->toEncodable($tokenId); + + $response = $this->graphql('CreateToken', [ + 'recipient' => SS58Address::encode($recipient), + 'collectionId' => $collectionId, + 'params' => $params, + ]); + + $this->assertArraySubset([ + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_can_create_a_token_with_bigint_token_id(): void + { + $collection = Collection::factory()->create(); + + $encodedData = $this->codec->encode()->mint( + $recipient = $this->recipient->public_key, + $collectionId = $collection->collection_chain_id, + $params = new CreateTokenParams( + tokenId: $this->tokenIdEncoder->encode(Hex::MAX_UINT128), + initialSupply: $initialSupply = fake()->numberBetween(1), + unitPrice: $this->randomGreaterThanMinUnitPriceFor($initialSupply), + cap: TokenMintCapType::INFINITE, + ), + ); + + $params = $params->toArray()['CreateToken']; + $params['tokenId'] = $this->tokenIdEncoder->toEncodable(Hex::MAX_UINT128); + + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($recipient), + 'collectionId' => $collectionId, + 'params' => $params, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_can_create_a_token_with_not_existent_recipient_and_creates_it(): void + { + Wallet::where('public_key', '=', $recipient = app(Generator::class)->public_key())?->delete(); + + $collection = Collection::factory([ + 'collection_chain_id' => Hex::MAX_UINT128, + ])->create(); + + $encodedData = $this->codec->encode()->mint( + $recipient, + $collectionId = $collection->collection_chain_id, + $params = new CreateTokenParams( + tokenId: $this->tokenIdEncoder->encode($tokenId = fake()->numberBetween()), + initialSupply: $initialSupply = fake()->numberBetween(1), + unitPrice: $this->randomGreaterThanMinUnitPriceFor($initialSupply), + cap: TokenMintCapType::INFINITE, + ), + ); + + $params = $params->toArray()['CreateToken']; + $params['tokenId'] = $this->tokenIdEncoder->toEncodable($tokenId); + + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($recipient), + 'collectionId' => $collectionId, + 'params' => $params, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + $this > $this->assertDatabaseHas('wallets', [ + 'public_key' => $recipient, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + // Exceptions Path + + public function test_it_will_fail_trying_to_create_a_token_with_an_id_that_already_exists(): void + { + Token::where('token_chain_id', '=', $tokenId = fake()->numberBetween())?->delete(); + Token::factory([ + 'collection_id' => $this->collection->id, + 'token_chain_id' => $tokenId, + ])->create(); + + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable($tokenId), + 'initialSupply' => $initialSupply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($initialSupply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + ], + ], true); + + $this->assertArraySubset( + ['params.tokenId' => ['The params.token id already exists in the specified collection.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_recipient(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => 'not_substrate_address', + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $initialSupply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($initialSupply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + ], + ], true); + + $this->assertStringContainsString( + 'The recipient is not a valid substrate account.', + $response['error']['recipient'][0] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_collection_id_doesnt_exists(): void + { + Collection::where('collection_chain_id', '=', $collectionId = fake()->numberBetween(2000))?->delete(); + + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'collectionId' => $collectionId, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $initialSupply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($initialSupply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + ], + ], true); + + $this->assertArraySubset( + ['collectionId' => ['The selected collection id is invalid.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_negative_token_id(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(-1), + 'initialSupply' => $initialSupply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($initialSupply), + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$params" got invalid value -1 at "params.tokenId.integer"; Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_overflow_token_id(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(Hex::MAX_UINT256), + 'initialSupply' => $initialSupply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($initialSupply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + ], + ], true); + + $this->assertArraySubset( + ['integer' => ['The integer is too large, the maximum value it can be is 340282366920938463463374607431768211455.']], + $response['errors'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_negative_supply(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => -1, + 'unitPrice' => gmp_strval(gmp_pow(10, 17)), + ], + ], true); + + $this->assertStringContainsString( + '"params.initialSupply"; Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_supply_zero(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => 0, + 'unitPrice' => gmp_strval(gmp_pow(10, 17)), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + ], + ], true); + + $this->assertStringContainsString( + 'The params.initial supply is too small, the minimum value it can be is 1.', + $response['error']['params.initialSupply'][0] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_supply_overflow(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => Hex::MAX_UINT256, + 'unitPrice' => gmp_strval(gmp_pow(10, 17)), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + ], + ], true); + + $this->assertStringContainsString( + 'The params.initial supply is too large, the maximum value it can be is', + $response['error']['params.initialSupply'][0] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_unit_price(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $initialSupply = fake()->numberBetween(1), + 'unitPrice' => $this->minUnitPriceFor($initialSupply) - 1, + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + ], + ], true); + + $this->assertArraySubset( + ['params.unitPrice' => ['The params.unit price is too small, the min token deposit is 0.01 EFI thus initialSupply * unitPrice must be greater than 10^16.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_cap(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $initialSupply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($initialSupply), + 'cap' => 'invalid', + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$params" got invalid value "invalid" at "params.cap"; Expected type "TokenMintCap" to be an object', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_empty_cap(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $initialSupply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($initialSupply), + 'cap' => [], + ], + ], true); + + $this->assertStringContainsString( + 'Field "type" of required type "TokenMintCapType!" was not provided', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_cap_single_mint_and_supply(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $initialSupply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($initialSupply), + 'cap' => [ + 'type' => TokenMintCapType::SINGLE_MINT->name, + 'amount' => fake()->numberBetween($initialSupply), + ], + ], + ], true); + + $this->assertArraySubset( + ['params.cap.amount' => ['The params.cap.amount field is prohibited when params.cap.type is SINGLE_MINT.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_cap_supply_less_than_initial_supply(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $initialSupply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($initialSupply), + 'cap' => [ + 'type' => TokenMintCapType::SUPPLY->name, + 'amount' => fake()->numberBetween(0, $initialSupply - 1), + ], + ], + ], true); + + $this->assertStringContainsString( + 'The params.cap.amount is too small, the minimum value it can be is', + $response['error']['params.cap.amount'][0] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_cap_supply_amount_zero(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $initialSupply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($initialSupply), + 'cap' => [ + 'type' => TokenMintCapType::SUPPLY->name, + 'amount' => 0, + ], + ], + ], true); + + $this->assertStringContainsString( + 'The params.cap.amount is too small, the minimum value it can be is', + $response['error']['params.cap.amount'][0] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_cap_supply_amount_negative(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $initialSupply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($initialSupply), + 'cap' => [ + 'type' => TokenMintCapType::SUPPLY->name, + 'supply' => -1, + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$params" got invalid value {"type":"SUPPLY","supply":-1} at "params.cap"', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_cap_supply_amount_null(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $initialSupply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($initialSupply), + 'cap' => [ + 'type' => TokenMintCapType::SUPPLY->name, + 'amount' => null, + ], + ], + ], true); + + $this->assertArraySubset( + ['params.cap.amount' => ['The params.cap.amount field is required when params.cap.type is SUPPLY.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_without_cap_type(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $initialSupply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($initialSupply), + 'cap' => [ + 'amount' => fake()->numberBetween($initialSupply), + ], + ], + ], true); + + $this->assertStringContainsString( + '"params.cap"; Field "type" of required type "TokenMintCapType!" was not provided', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_royalty(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $initialSupply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($initialSupply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + 'behavior' => 'invalid', + ], + ], true); + + $this->assertStringContainsString( + 'got invalid value "invalid" at "params.behavior"; Expected type "TokenMarketBehaviorInput" to be an object', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_empty_royalty(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $initialSupply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($initialSupply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + 'behavior' => [ + 'hasRoyalty' => [], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Field "beneficiary" of required type "String!" was not provided', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_missing_beneficiary_on_royalty(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $initialSupply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($initialSupply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + 'behavior' => [ + 'hasRoyalty' => [ + 'percentage' => fake()->numberBetween(1, 50), + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Field "beneficiary" of required type "String!" was not provided', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_beneficiary_on_royalty(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $initialSupply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($initialSupply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + 'behavior' => ['hasRoyalty' => [ + 'beneficiary' => null, + 'percentage' => fake()->numberBetween(1, 50), + ]], + ], + ], true); + + $this->assertStringContainsString( + '"params.behavior.hasRoyalty.beneficiary"; Expected non-nullable type "String!" not to be null', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_beneficiary_on_royalty(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $initialSupply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($initialSupply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + 'behavior' => ['hasRoyalty' => [ + 'beneficiary' => 'invalid', + 'percentage' => fake()->numberBetween(1, 50), + ]], + ], + ], true); + + $this->assertArraySubset( + [ + 'params.behavior.hasRoyalty.beneficiary' => [ + 0 => 'The params.behavior.has royalty.beneficiary is not a valid substrate account.', + ], + ], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_percentage_on_royalty(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $initialSupply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($initialSupply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + 'behavior' => ['hasRoyalty' => [ + 'beneficiary' => SS58Address::encode($this->recipient->public_key), + ]], + ], + ], true); + + $this->assertStringContainsString( + 'Field "percentage" of required type "Float!" was not provided', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_percentage_on_royalty(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $initialSupply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($initialSupply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + 'behavior' => [ + 'hasRoyalty' => [ + 'beneficiary' => SS58Address::encode($this->recipient->public_key), + 'percentage' => null, + ], ], + ], + ], true); + + $this->assertStringContainsString( + 'Expected non-nullable type "Float!" not to be null', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_percentage_on_royalty(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $initialSupply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($initialSupply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + 'behavior' => ['hasRoyalty' => [ + 'beneficiary' => SS58Address::encode($this->recipient->public_key), + 'percentage' => 'invalid', + ]], + ], + ], true); + + $this->assertStringContainsString( + 'Float cannot represent non numeric value', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_negative_percentage_on_royalty(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $initialSupply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($initialSupply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + 'behavior' => ['hasRoyalty' => [ + 'beneficiary' => SS58Address::encode($this->recipient->public_key), + 'percentage' => -0.1, + ]], + ], + ], true); + + $this->assertArraySubset( + [ + 'params.behavior.hasRoyalty.percentage' => [ + 0 => 'The params.behavior.has royalty.percentage valid for a royalty is in the range of 0.1% to 50% and a maximum of 7 decimal places.', + ], + ], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_zero_percentage_on_royalty(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $initialSupply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($initialSupply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + 'behavior' => [ + 'hasRoyalty' => [ + 'beneficiary' => SS58Address::encode($this->recipient->public_key), + 'percentage' => 0, + ], + ], + ], + ], true); + + $this->assertArraySubset( + [ + 'params.behavior.hasRoyalty.percentage' => [ + 0 => 'The params.behavior.has royalty.percentage valid for a royalty is in the range of 0.1% to 50% and a maximum of 7 decimal places.', + ], + ], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_less_than_the_minimum_percentage_on_royalty(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $initialSupply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($initialSupply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + 'behavior' => [ + 'hasRoyalty' => [ + 'beneficiary' => SS58Address::encode($this->recipient->public_key), + 'percentage' => 0.09, + ], + ], + ], + ], true); + + $this->assertArraySubset( + [ + 'params.behavior.hasRoyalty.percentage' => [ + 0 => 'The params.behavior.has royalty.percentage valid for a royalty is in the range of 0.1% to 50% and a maximum of 7 decimal places.', + ], + ], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_more_than_the_max_percentage_on_royalty(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $initialSupply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($initialSupply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + 'behavior' => [ + 'hasRoyalty' => [ + 'beneficiary' => SS58Address::encode($this->recipient->public_key), + 'percentage' => 50.1, + ], + ], + ], + ], true); + + $this->assertArraySubset( + [ + 'params.behavior.hasRoyalty.percentage' => [ + 0 => 'The params.behavior.has royalty.percentage valid for a royalty is in the range of 0.1% to 50% and a maximum of 7 decimal places.', + ], + ], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_listing_forbidden(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $initialSupply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($initialSupply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + 'listingForbidden' => 'invalid', + ], + ], true); + + $this->assertStringContainsString( + '"params.listingForbidden"; Boolean cannot represent a non boolean value', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_if_exceed_max_token_count_in_collection(): void + { + $this->collection->forceFill(['max_token_count' => 0])->save(); + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(fake()->numberBetween()), + 'initialSupply' => $initialSupply = fake()->numberBetween(1), + 'unitPrice' => $this->randomGreaterThanMinUnitPriceFor($initialSupply), + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + ], + ], + ], true); + + $this->assertArraySubset( + ['collectionId' => ['The overall token count 1 have exceeded the maximum cap of 0 tokens.']], + $response['error'], + ); + } + + protected function randomGreaterThanMinUnitPriceFor(string $initialSupply): string + { + $min = $this->minUnitPriceFor($initialSupply); + + return gmp_strval(gmp_random_range($min, Hex::MAX_UINT128)); + } + + protected function minUnitPriceFor(string $initialSupply): string + { + return gmp_strval(gmp_div(gmp_pow(10, 16), gmp_init($initialSupply), GMP_ROUND_PLUSINF)); + } +} diff --git a/tests/Feature/GraphQL/Mutations/CreateWalletTest.php b/tests/Feature/GraphQL/Mutations/CreateWalletTest.php new file mode 100644 index 00000000..5f18a369 --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/CreateWalletTest.php @@ -0,0 +1,102 @@ +uuid())?->delete(); + + $response = $this->graphql('CreateWallet', [ + 'externalId' => $externalId, + ]); + + $this->assertTrue($response); + $this->assertDatabaseHas('wallets', [ + 'public_key' => null, + 'external_id' => $externalId, + 'managed' => true, + ]); + } + + public function test_create_ask_to_create_a_wallet_with_multiple_words(): void + { + Wallet::where('external_id', '=', $externalId = fake()->uuid())?->delete(); + + $response = $this->graphql('CreateWallet', [ + 'externalId' => $externalId, + ]); + + $this->assertTrue($response); + $this->assertDatabaseHas('wallets', [ + 'public_key' => null, + 'external_id' => $externalId, + 'managed' => true, + ]); + } + + public function test_create_ask_to_create_a_wallet_with_ascii(): void + { + Wallet::where('external_id', '=', $externalId = fake()->uuid())?->delete(); + + $response = $this->graphql('CreateWallet', [ + 'externalId' => $externalId, + ]); + + $this->assertTrue($response); + $this->assertDatabaseHas('wallets', [ + 'public_key' => null, + 'external_id' => $externalId, + 'managed' => true, + ]); + } + + // Exception Path + + public function test_it_fail_if_external_id_is_not_unique(): void + { + Wallet::factory([ + 'external_id' => $externalId = fake()->uuid(), + ])->create(); + + $response = $this->graphql('CreateWallet', [ + 'externalId' => $externalId, + ], true); + + $this->assertArraySubset( + ['externalId' => ['The external id has already been taken.']], + $response['error'], + ); + } + + public function test_it_fail_no_external_id(): void + { + $response = $this->graphql('CreateWallet', [], true); + + $this->assertStringContainsString( + 'Variable "$externalId" of required type "String!" was not provided.', + $response['error'], + ); + } + + public function test_it_fail_null_external_id(): void + { + $response = $this->graphql('CreateWallet', [ + 'externalId' => null, + ], true); + + $this->assertStringContainsString( + 'Variable "$externalId" of non-null type "String!" must not be null.', + $response['error'], + ); + } +} diff --git a/tests/Feature/GraphQL/Mutations/DestroyCollectionTest.php b/tests/Feature/GraphQL/Mutations/DestroyCollectionTest.php new file mode 100644 index 00000000..38af210f --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/DestroyCollectionTest.php @@ -0,0 +1,232 @@ +codec = new Codec(); + $walletService = new WalletService(); + + $this->defaultAccount = config('enjin-platform.chains.daemon-account'); + $this->owner = $walletService->firstOrStore(['public_key' => $this->defaultAccount]); + $this->collection = Collection::factory()->create([ + 'owner_wallet_id' => $this->owner->id, + ]); + } + + // Happy Path + public function test_it_can_destroy_a_collection(): void + { + $encodedData = $this->codec->encode()->destroyCollection($this->collection->collection_chain_id); + + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_destroy_a_collection_with_bigint(): void + { + $encodedData = $this->codec->encode()->destroyCollection($this->collection->collection_chain_id); + + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + // Exception Path + public function test_it_will_fail_with_no_args(): void + { + $response = $this->graphql($this->method, [], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of required type "BigInt!" was not provided.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_collection_id_null(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => null, + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of non-null type "BigInt!" must not be null', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => 'invalid', + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" got invalid value "invalid"; Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_negative_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => '-1', + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" got invalid value "-1"; Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_collection_id_less_than_two_thousand(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => fake()->numberBetween(0, 1999), + ], true); + + $this->assertArraySubset( + ['collectionId' => ['The collection id is too small, the minimum value it can be is 2000.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_overflow_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => Hex::MAX_UINT256, + ], true); + + $this->assertArraySubset( + ['collectionId' => ['The collection id is too large, the maximum value it can be is 340282366920938463463374607431768211455.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_if_collection_id_doesnt_exists(): void + { + Collection::where('collection_chain_id', '=', $collectionId = fake()->numberBetween(1))?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + ], true); + + $this->assertArraySubset( + ['collectionId' => ['The selected collection id is invalid.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_if_destroy_a_collection_that_has_tokens(): void + { + $collection = Collection::factory(['owner_wallet_id' => $this->owner->id])->create(); + Token::factory(['collection_id' => $collection])->create(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collection->collection_chain_id, + ], true); + + $this->assertArraySubset( + ['collectionId' => ['The collection id must not have any existing tokens.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_trying_to_destroy_a_collection_owned_by_another_person(): void + { + $collection = Collection::factory()->create(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collection->collection_chain_id, + ], true); + + $this->assertArraySubset( + ['collectionId' => ['The collection id provided is not owned by you.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } +} diff --git a/tests/Feature/GraphQL/Mutations/FreezeTest.php b/tests/Feature/GraphQL/Mutations/FreezeTest.php new file mode 100644 index 00000000..391f0ed9 --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/FreezeTest.php @@ -0,0 +1,751 @@ +codec = new Codec(); + $walletService = new WalletService(); + $this->defaultAccount = config('enjin-platform.chains.daemon-account'); + $this->wallet = $walletService->firstOrStore(['public_key' => $this->defaultAccount]); + + $this->tokenAccount = TokenAccount::factory([ + 'wallet_id' => $this->wallet, + ])->create(); + $this->collection = Collection::find($collectionId = $this->tokenAccount->collection_id); + $this->token = Token::find($this->tokenAccount->token_id); + $this->tokenIdEncoder = new Integer($this->token->token_chain_id); + $this->collectionAccount = CollectionAccount::factory([ + 'collection_id' => $collectionId, + 'wallet_id' => $this->wallet, + 'account_count' => 1, + ])->create(); + } + + // Happy Path + + public function test_can_freeze_a_collection(): void + { + $encodedData = $this->codec->encode()->freeze( + $collectionId = $this->collection->collection_chain_id, + new FreezeTypeParams( + type: $freezeType = FreezeType::COLLECTION + ), + ); + + $response = $this->graphql($this->method, [ + 'freezeType' => $freezeType->name, + 'collectionId' => $collectionId, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_can_freeze_a_big_int_collection(): void + { + $collection = Collection::factory([ + 'collection_chain_id' => $collectionId = Hex::MAX_UINT128, + ])->create(); + CollectionAccount::factory([ + 'collection_id' => $collection, + 'wallet_id' => $this->wallet, + 'account_count' => 1, + ])->create(); + + $encodedData = $this->codec->encode()->freeze( + $collectionId, + new FreezeTypeParams( + type: $freezeType = FreezeType::COLLECTION + ), + ); + + $response = $this->graphql($this->method, [ + 'freezeType' => $freezeType->name, + 'collectionId' => $collectionId, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_can_freeze_a_collection_account(): void + { + $encodedData = $this->codec->encode()->freeze( + $collectionId = $this->collection->collection_chain_id, + new FreezeTypeParams( + type: $freezeType = FreezeType::COLLECTION_ACCOUNT, + account: $account = $this->wallet->public_key, + ), + ); + + $response = $this->graphql($this->method, [ + 'freezeType' => $freezeType->name, + 'collectionId' => $collectionId, + 'collectionAccount' => SS58Address::encode($account), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_can_freeze_a_token_using_adapter(): void + { + $encodedData = $this->codec->encode()->freeze( + $collectionId = $this->collection->collection_chain_id, + new FreezeTypeParams( + type: $freezeType = FreezeType::TOKEN, + token: $this->tokenIdEncoder->encode(), + ), + ); + + $response = $this->graphql($this->method, [ + 'freezeType' => $freezeType->name, + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + } + + public function test_can_freeze_a_token(): void + { + $encodedData = $this->codec->encode()->freeze( + $collectionId = $this->collection->collection_chain_id, + new FreezeTypeParams( + type: $freezeType = FreezeType::TOKEN, + token: $this->tokenIdEncoder->encode(), + ), + ); + + $response = $this->graphql($this->method, [ + 'freezeType' => $freezeType->name, + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_can_freeze_a_big_int_token(): void + { + $collection = Collection::factory()->create(); + + Token::factory([ + 'collection_id' => $collection, + 'token_chain_id' => $tokenId = Hex::MAX_UINT128, + ])->create(); + + CollectionAccount::factory([ + 'collection_id' => $collection, + 'wallet_id' => $this->wallet, + 'account_count' => 1, + ])->create(); + + $encodedData = $this->codec->encode()->freeze( + $collectionId = $collection->collection_chain_id, + new FreezeTypeParams( + type: $freezeType = FreezeType::TOKEN, + token: $this->tokenIdEncoder->encode($tokenId), + ), + ); + + $response = $this->graphql($this->method, [ + 'freezeType' => $freezeType->name, + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable($tokenId), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_can_freeze_a_token_account(): void + { + $encodedData = $this->codec->encode()->freeze( + $collectionId = $this->collection->collection_chain_id, + new FreezeTypeParams( + type: $freezeType = FreezeType::TOKEN_ACCOUNT, + token: $this->tokenIdEncoder->encode(), + account: $account = $this->wallet->public_key, + ), + ); + + $response = $this->graphql($this->method, [ + 'freezeType' => $freezeType->name, + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'tokenAccount' => SS58Address::encode($account), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + // Exception Path + + public function test_it_will_fail_with_freeze_type_non_existent(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => 'ASSET', + 'collectionId' => $this->collection->collection_chain_id, + ], true); + + $this->assertStringContainsString( + 'got invalid value "ASSET"; Value "ASSET" does not exist in "FreezeType" enum', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_collection_id_non_existent(): void + { + Collection::where('collection_chain_id', '=', $collectionId = fake()->numberBetween(2000))?->delete(); + + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::COLLECTION->name, + 'collectionId' => $collectionId, + ], true); + + $this->assertArraySubset( + ['collectionId' => ['The selected collection id is invalid.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::COLLECTION->name, + 'collectionId' => 'invalid', + ], true); + + $this->assertStringContainsString( + 'got invalid value "invalid"; Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::COLLECTION->name, + 'collectionId' => null, + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of non-null type "BigInt!" must not be null.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::COLLECTION->name, + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of required type "BigInt!" was not provided.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_token_id(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::TOKEN->name, + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => null, + ], true); + + $this->assertArraySubset( + ['tokenId' => ['The token id field is required.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_token_id(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::TOKEN->name, + 'collectionId' => $this->collection->collection_chain_id, + ], true); + + $this->assertArraySubset( + ['tokenId' => ['The token id field is required.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_token_id(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::TOKEN->name, + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable('invalid'), + ], true); + + $this->assertStringContainsString( + 'value "invalid" at "tokenId.integer"; Cannot represent following value as uint256: "invalid"', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_token_id_non_existent(): void + { + Token::where('token_chain_id', '=', $tokenId = fake()->numberBetween())?->delete(); + + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::TOKEN->name, + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable($tokenId), + ], true); + + $this->assertArraySubset( + ['tokenId' => ['The token id does not exist in the specified collection.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_collection_account(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::COLLECTION_ACCOUNT->name, + 'collectionId' => $this->collection->collection_chain_id, + 'collectionAccount' => null, + ], true); + + $this->assertArraySubset( + ['collectionAccount' => ['The collection account field is required.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_collection_account(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::COLLECTION_ACCOUNT->name, + 'collectionId' => $this->collection->collection_chain_id, + ], true); + + $this->assertArraySubset( + ['collectionAccount' => ['The collection account field is required.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_collection_account(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::COLLECTION_ACCOUNT->name, + 'collectionId' => $this->collection->collection_chain_id, + 'collectionAccount' => 'invalid', + ], true); + + $this->assertArraySubset( + ['collectionAccount' => ['The collection account is not a valid substrate account.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_collection_account_non_existent(): void + { + Wallet::where('public_key', '=', $address = app(Generator::class)->public_key())?->delete(); + + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::COLLECTION_ACCOUNT->name, + 'collectionId' => $collectionId = $this->collection->collection_chain_id, + 'collectionAccount' => $address, + ], true); + + $this->assertArraySubset( + ['collectionAccount' => ["Could not find a collection account for {$address} at collection {$collectionId}."]], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_token_account_non_existent(): void + { + Wallet::where('public_key', '=', $publicKey = app(Generator::class)->public_key())?->delete(); + + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::TOKEN_ACCOUNT->name, + 'collectionId' => $collectionId = $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable($tokenId = $this->token->token_chain_id), + 'tokenAccount' => $tokenAccount = SS58Address::encode($publicKey), + ], true); + + $this->assertArraySubset( + ['tokenAccount' => ["Could not find a token account for {$tokenAccount} at collection {$collectionId} and token {$tokenId}."]], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_token_account(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::TOKEN_ACCOUNT->name, + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'tokenAccount' => null, + ], true); + + $this->assertArraySubset( + ['tokenAccount' => ['The token account field is required.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_token_account(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::TOKEN_ACCOUNT->name, + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + ], true); + + $this->assertArraySubset( + ['tokenAccount' => ['The token account field is required.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_token_account(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::TOKEN_ACCOUNT->name, + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'tokenAccount' => 'invalid', + ], true); + + $this->assertArraySubset( + ['tokenAccount' => ['The token account is not a valid substrate account.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_cant_pass_token_id_when_freezing_collection(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::COLLECTION->name, + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + ], true); + + $this->assertArraySubset( + ['tokenId' => ['The token id field is prohibited.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_cant_pass_collection_account_when_freezing_collection(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::COLLECTION->name, + 'collectionId' => $this->collection->collection_chain_id, + 'collectionAccount' => $this->wallet->public_key, + ], true); + + $this->assertArraySubset( + ['collectionAccount' => ['The collection account field is prohibited.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_cant_pass_token_account_when_freezing_collection(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::COLLECTION->name, + 'collectionId' => $this->collection->collection_chain_id, + 'tokenAccount' => $this->wallet->public_key, + ], true); + + $this->assertArraySubset( + ['tokenAccount' => ['The token account field is prohibited.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_cant_pass_token_account_when_freezing_collection_account(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::COLLECTION_ACCOUNT->name, + 'collectionId' => $this->collection->collection_chain_id, + 'collectionAccount' => $this->wallet->public_key, + 'tokenAccount' => $this->wallet->public_key, + ], true); + + $this->assertArraySubset( + ['tokenAccount' => ['The token account field is prohibited.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_cant_pass_token_id_when_freezing_collection_account(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::COLLECTION_ACCOUNT->name, + 'collectionId' => $this->collection->collection_chain_id, + 'collectionAccount' => $this->wallet->public_key, + 'tokenId' => $this->tokenIdEncoder->toEncodable($this->token->token_chain_address), + ], true); + + $this->assertArraySubset( + ['tokenId' => ['The token id field is prohibited.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_cant_pass_token_account_when_freezing_token(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::TOKEN->name, + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'tokenAccount' => $this->wallet->public_key, + ], true); + + $this->assertArraySubset( + ['tokenAccount' => ['The token account field is prohibited.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_cant_pass_collection_account_when_freezing_token(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::TOKEN->name, + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'collectionAccount' => $this->wallet->public_key, + ], true); + + $this->assertArraySubset( + ['collectionAccount' => ['The collection account field is prohibited.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_cant_pass_collection_account_when_freezing_token_account(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::TOKEN_ACCOUNT->name, + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'collectionAccount' => SS58Address::encode($this->wallet->public_key), + 'tokenAccount' => SS58Address::encode($this->wallet->public_key), + ], true); + + $this->assertArraySubset( + ['collectionAccount' => ['The collection account field is prohibited.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } +} diff --git a/tests/Feature/GraphQL/Mutations/MarkAndListPendingTransactionsTest.php b/tests/Feature/GraphQL/Mutations/MarkAndListPendingTransactionsTest.php new file mode 100644 index 00000000..3f7f3810 --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/MarkAndListPendingTransactionsTest.php @@ -0,0 +1,226 @@ +transactions = $this->generateTransactions(); + } + + protected function tearDown(): void + { + Transaction::destroy($this->transactions); + + parent::tearDown(); + } + + // Exception Path + public function test_it_can_fetch_with_no_args_without_auth(): void + { + $response = $this->httpGraphql($this->method); + $this->assertTrue(count($response['edges']) > 0); + } + + public function test_it_can_fetch_with_no_args(): void + { + $response = $this->graphql($this->method); + + $this->assertTrue(count($response['edges']) > 0); + } + + public function test_it_can_fetch_with_empty_args(): void + { + $response = $this->graphql($this->method, []); + + $this->assertTrue(count($response['edges']) > 0); + } + + public function test_it_can_fetch_with_null_wallet_addresses(): void + { + $response = $this->graphql($this->method, [ + 'accounts' => null, + ]); + + $this->assertTrue(count($response['edges']) > 0); + } + + // Happy Path + + public function test_it_can_fetch_with_empty_wallet_addresses(): void + { + $response = $this->graphql($this->method, [ + 'accounts' => [], + ]); + + $this->assertTrue(count($response['edges']) > 0); + } + + public function test_it_can_fetch_with_null_mark_as_processing(): void + { + $response = $this->graphql($this->method, [ + 'markAsProcessing' => null, + ]); + + $totalCount = $response['totalCount']; + + $response = $this->graphql($this->method, [ + 'markAsProcessing' => true, + ]); + + $this->assertTrue( + $response['totalCount'] > 0 + && $totalCount > 0 + && $response['totalCount'] < $totalCount + ); + } + + public function test_it_can_fetch_with_false_mark_as_processing(): void + { + $response = $this->graphql($this->method, [ + 'markAsProcessing' => false, + ]); + + $totalCount = $response['totalCount']; + + $response = $this->graphql($this->method, [ + 'markAsProcessing' => false, + ]); + + $this->assertTrue($totalCount > 0); + $this->assertEquals($totalCount, $response['totalCount']); + } + + public function test_it_can_fetch_with_null_after(): void + { + $response = $this->graphql($this->method, [ + 'after' => null, + ]); + + $this->assertTrue(count($response['edges']) > 0); + $this->assertFalse($response['pageInfo']['hasPreviousPage']); + } + + public function test_it_can_fetch_with_null_first(): void + { + $response = $this->graphql($this->method, [ + 'first' => null, + 'markAsProcessing' => false, + ]); + + $this->assertTrue(($totalItems = count($response['edges'])) > 0); + + $response = $this->graphql($this->method, [ + 'markAsProcessing' => false, + ]); + + $this->assertTrue($totalItems === count($response['edges'])); + } + + public function test_it_fetches_managed_wallets_tx_without_passing_their_address(): void + { + Wallet::factory([ + 'public_key' => $publicKey = app(Generator::class)->public_key(), + 'managed' => true, + ])->create(); + + Transaction::factory([ + 'wallet_public_key' => $publicKey, + 'transaction_chain_id' => null, + 'transaction_chain_hash' => null, + ])->create(); + + $response = $this->graphql($this->method); + + $this->assertEquals( + $publicKey, + $response['edges'][0]['node']['wallet']['account']['publicKey'] + ); + } + + public function test_it_can_filter_transactions_by_address(): void + { + Wallet::factory([ + 'public_key' => $publicKey = app(Generator::class)->public_key(), + 'managed' => true, + ])->create(); + + Transaction::factory([ + 'wallet_public_key' => $publicKey, + 'transaction_chain_id' => null, + 'transaction_chain_hash' => null, + ])->create(); + + $response = $this->graphql('MarkAndListPendingTransactions', [ + 'accounts' => [SS58Address::encode($publicKey)], + ]); + + $this->assertTrue(1 === $response['totalCount']); + $this->assertEquals( + $publicKey, + $response['edges'][0]['node']['wallet']['account']['publicKey'] + ); + } + + public function test_it_not_txs_will_appear_with_address_that_has_no_tx(): void + { + Wallet::where('public_key', '=', $publicKey = app(Generator::class)->public_key())?->delete(); + + $response = $this->graphql('MarkAndListPendingTransactions', [ + 'accounts' => [SS58Address::encode($publicKey)], + ]); + + $this->assertTrue(0 === $response['totalCount']); + } + + public function test_it_will_fail_with_invalid_mark_as_processing(): void + { + $response = $this->graphql($this->method, [ + 'markAsProcessing' => 'invalid', + ], true); + + $this->assertStringContainsString( + 'Variable "$markAsProcessing" got invalid value "invalid"; Boolean cannot represent a non boolean value', + $response['error'] + ); + } + + public function test_it_will_fail_with_invalid_substrate_address(): void + { + $response = $this->graphql($this->method, [ + 'accounts' => ['not_valid_address'], + ], true); + + $this->assertArraySubset( + ['accounts.0' => ['The accounts.0 is not a valid substrate account.']], + $response['error'] + ); + } + + protected function generateTransactions(?int $numberOfTransactions = 40): Collection + { + return collect(range(0, $numberOfTransactions)) + ->map(fn () => Transaction::factory([ + 'transaction_chain_id' => null, + 'transaction_chain_hash' => null, + ])->create()); + } +} diff --git a/tests/Feature/GraphQL/Mutations/MintTokenTest.php b/tests/Feature/GraphQL/Mutations/MintTokenTest.php new file mode 100644 index 00000000..95a074d6 --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/MintTokenTest.php @@ -0,0 +1,362 @@ +codec = new Codec(); + $this->token = Token::factory()->create(); + $this->tokenIdEncoder = new Integer($this->token->token_chain_id); + $this->collection = Collection::find($this->token->collection_id); + $this->recipient = Wallet::factory()->create(); + $this->defaultAccount = config('enjin-platform.chains.daemon-account'); + } + + // Happy Path + + public function test_can_mint_a_token_without_unit_price(): void + { + $encodedData = $this->codec->encode()->mint( + $recipient = $this->recipient->public_key, + $collectionId = $this->collection->collection_chain_id, + $params = new MintParams( + tokenId: $this->tokenIdEncoder->encode(), + amount: fake()->numberBetween(), + ), + ); + + $params = $params->toArray()['Mint']; + $params['tokenId'] = $this->tokenIdEncoder->toEncodable(); + + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($recipient), + 'collectionId' => $collectionId, + 'params' => $params, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_can_mint_a_token_with_different_types(): void + { + $encodedData = $this->codec->encode()->mint( + $recipient = $this->recipient->public_key, + $collectionId = $this->collection->collection_chain_id, + new MintParams( + tokenId: $this->tokenIdEncoder->encode(), + amount: $amount = fake()->numberBetween(), + ), + ); + + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($recipient), + 'collectionId' => (int) $collectionId, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => (string) $amount, + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_can_mint_a_token_with_bigint_collection_id_and_token_id(): void + { + $collection = Collection::factory([ + 'collection_chain_id' => Hex::MAX_UINT128, + ])->create(); + + $token = Token::factory([ + 'collection_id' => $collection->id, + 'token_chain_id' => Hex::MAX_UINT128, + ])->create(); + + $encodedData = $this->codec->encode()->mint( + $recipient = $this->recipient->public_key, + $collectionId = $collection->collection_chain_id, + $params = new MintParams( + tokenId: $this->tokenIdEncoder->encode($token->token_chain_id), + amount: fake()->numberBetween(), + ), + ); + + $params = $params->toArray()['Mint']; + $params['tokenId'] = $this->tokenIdEncoder->toEncodable($token->token_chain_id); + + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($recipient), + 'collectionId' => $collectionId, + 'params' => $params, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_can_mint_a_token_with_not_existent_recipient_and_creates_it(): void + { + Wallet::where('public_key', '=', $recipient = app(Generator::class)->public_key())?->delete(); + + $encodedData = $this->codec->encode()->mint( + $recipient, + $collectionId = $this->collection->collection_chain_id, + $params = new MintParams( + tokenId: $this->tokenIdEncoder->encode(), + amount: fake()->numberBetween(), + ), + ); + + $params = $params->toArray()['Mint']; + $params['tokenId'] = $this->tokenIdEncoder->toEncodable(); + + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($recipient), + 'collectionId' => $collectionId, + 'params' => $params, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + $this > $this->assertDatabaseHas('wallets', [ + 'public_key' => $recipient, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + // Exceptions Path + + public function test_it_will_fail_with_invalid_recipient(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => 'not_substrate_address', + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => fake()->numberBetween(), + ], + ], true); + + $this->assertArraySubset( + ['recipient' => ['The recipient is not a valid substrate account.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_collection_id_doesnt_exists(): void + { + Collection::where('collection_chain_id', '=', $collectionId = fake()->numberBetween(2000))?->delete(); + + $response = $this->graphql($this->method, [ + 'recipient' => $this->recipient->public_key, + 'collectionId' => $collectionId, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => fake()->numberBetween(), + ], + ], true); + + $this->assertStringContainsString( + 'The selected collection id is invalid.', + $response['error']['collectionId'][0] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_negative_token_id(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => $this->recipient->public_key, + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => -1, + 'amount' => fake()->numberBetween(), + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$params" got invalid value -1 at "params.tokenId"; Expected type "EncodableTokenIdInput" to be an object', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_overflow_token_id(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => $this->recipient->public_key, + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(Hex::MAX_UINT256), + 'amount' => fake()->numberBetween(1), + ], + ], true); + + $this->assertStringContainsString( + 'The integer is too large, the maximum value it can be is', + $response['errors']['integer'][0] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_negative_amount(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => $this->recipient->public_key, + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => -1, + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$params" got invalid value -1 at "params.amount"; Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_zero_amount(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => $this->recipient->public_key, + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => 0, + ], + ], true); + + $this->assertArraySubset( + ['params.amount' => ['The params.amount is too small, the minimum value it can be is 1.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_overflow_amount(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => $this->recipient->public_key, + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => Hex::MAX_UINT256, + ], + ], true); + + $this->assertStringContainsString( + 'The params.amount is too large, the maximum value it can be is 340282366920938463463374607431768211455.', + $response['error']['params.amount'][0] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } +} diff --git a/tests/Feature/GraphQL/Mutations/MutateCollectionTest.php b/tests/Feature/GraphQL/Mutations/MutateCollectionTest.php new file mode 100644 index 00000000..adeb4941 --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/MutateCollectionTest.php @@ -0,0 +1,782 @@ +codec = new Codec(); + $this->defaultAccount = config('enjin-platform.chains.daemon-account'); + $this->collection = Collection::factory()->create(); + $this->tokenIdEncoder = new Integer(); + } + + // Happy Path + + public function test_it_will_fail_with_duplicate_field_names(): void + { + self::$queries['MutateCollectionDuplicateFieldName'] = ' + mutation MutateCollection{ + MutateCollection( + collectionId: 2750, + mutation: { + owner:"rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe", + explicitRoyaltyCurrencies:[ + { + collectionId:12, + collectionId:10, + collectionId:15, + tokenId: 1, + tokenId: 2 + } + ] + } + ) + }'; + $response = $this->graphql('MutateCollectionDuplicateFieldName', [], true, ['operationName' => $this->method]); + + $this->assertArraySubset( + ['collectionId' => ['message' => 'There can be only one input field named "collectionId".']], + $response['errors'] + ); + $this->assertArraySubset( + ['tokenId' => ['message' => 'There can be only one input field named "tokenId".']], + $response['errors'] + ); + } + + public function test_it_can_mutate_a_collection_with_owner(): void + { + $encodedData = $this->codec->encode()->mutateCollection( + collectionId: $collectionId = $this->collection->collection_chain_id, + owner: $owner = $this->defaultAccount, + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'mutation' => [ + 'owner' => SS58Address::encode($owner), + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_mutate_a_collection_with_explicit_royalty_currencies(): void + { + $encodedData = $this->codec->encode()->mutateCollection( + collectionId: $collectionId = $this->collection->collection_chain_id, + explicitRoyaltyCurrencies: $currencies = $this->generateCurrencies(fake()->numberBetween(1, 9)) + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'mutation' => [ + 'explicitRoyaltyCurrencies' => $this->generateEncodeableTokenIds($currencies), + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_mutate_a_collection_with_owner_and_currencies(): void + { + $encodedData = $this->codec->encode()->mutateCollection( + collectionId: $collectionId = $this->collection->collection_chain_id, + owner: $owner = $this->defaultAccount, + explicitRoyaltyCurrencies: $currencies = $this->generateCurrencies(fake()->numberBetween(1, 9)) + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'mutation' => [ + 'owner' => SS58Address::encode($owner), + 'explicitRoyaltyCurrencies' => $this->generateEncodeableTokenIds($currencies), + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_mutate_a_collection_with_big_int_collection_id(): void + { + $collection = Collection::factory([ + 'collection_chain_id' => Hex::MAX_UINT128, + ])->create(); + + $encodedData = $this->codec->encode()->mutateCollection( + collectionId: $collectionId = $collection->collection_chain_id, + owner: $owner = $this->defaultAccount, + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'mutation' => [ + 'owner' => SS58Address::encode($owner), + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_mutate_a_collection_new_owner_doesnt_exists_locally_and_save(): void + { + Wallet::where('public_key', '=', $owner = app(Generator::class)->public_key())?->delete(); + + $encodedData = $this->codec->encode()->mutateCollection( + collectionId: $collectionId = $this->collection->collection_chain_id, + owner: $owner + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'mutation' => [ + 'owner' => SS58Address::encode($owner), + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + $this->assertDatabaseHas('wallets', [ + 'public_key' => $owner, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_mutate_a_collection_new_owner_that_exists_locally(): void + { + $owner = Wallet::factory()->create(); + + $encodedData = $this->codec->encode()->mutateCollection( + collectionId: $collectionId = $this->collection->collection_chain_id, + owner: $owner->public_key + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'mutation' => [ + 'owner' => SS58Address::encode($owner->public_key), + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_mutate_a_collection_with_an_empty_list_of_currencies(): void + { + $encodedData = $this->codec->encode()->mutateCollection( + collectionId: $collectionId = $this->collection->collection_chain_id, + explicitRoyaltyCurrencies: [], + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'mutation' => [ + 'explicitRoyaltyCurrencies' => [], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + // Exception Paths + + public function test_it_will_fail_with_no_args(): void + { + $response = $this->graphql($this->method, [], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of required type "BigInt!" was not provided', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => null, + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of non-null type "BigInt!" must not be null', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'mutation' => [ + 'owner' => $this->defaultAccount, + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of required type "BigInt!" was not provided', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => 'invalid', + 'mutation' => [ + 'owner' => $this->defaultAccount, + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" got invalid value "invalid"; Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_collection_id_that_doesnt_exists(): void + { + Collection::where('collection_chain_id', '=', $collectionId = fake()->numberBetween(2000))?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'mutation' => [ + 'owner' => $this->defaultAccount, + ], + ], true); + + $this->assertArraySubset( + ['collectionId' => ['The selected collection id is invalid.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_mutation(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'mutation' => 'invalid', + ], true); + + $this->assertStringContainsString( + 'Variable "$mutation" got invalid value "invalid"; Expected type "CollectionMutationInput" to be an object', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_mutation(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'mutation' => null, + ], true); + + $this->assertStringContainsString( + 'Variable "$mutation" of non-null type "CollectionMutationInput!" must not be null', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_mutation(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + ], true); + + $this->assertStringContainsString( + 'Variable "$mutation" of required type "CollectionMutationInput!" was not provided', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_negative_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => -1, + 'mutation' => [ + 'owner' => $this->defaultAccount, + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" got invalid value -1; Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_owner_invalid(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'mutation' => [ + 'owner' => 'not_substrate_address', + ], + ], true); + + $this->assertArraySubset( + ['mutation.owner' => ['The mutation.owner is not a valid substrate account.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_currencies(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'mutation' => [ + 'explicitRoyaltyCurrencies' => 'invalid', + ], + ], true); + + $this->assertStringContainsString( + 'got invalid value "invalid"; Expected type "MultiTokenIdInput" to be an object', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_more_than_ten_currencies(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'mutation' => [ + 'explicitRoyaltyCurrencies' => $this->generateEncodeableTokenIds($this->generateCurrencies(11)), + ], + ], true); + + $this->assertArraySubset( + ['mutation.explicitRoyaltyCurrencies' => ['The mutation.explicit royalty currencies field must not have more than 10 items.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_missing_collection_id_in_currency(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'mutation' => [ + 'explicitRoyaltyCurrencies' => $this->generateEncodeableTokenIds(array_merge($this->generateCurrencies(), [ + [ + 'tokenId' => fake()->numberBetween(), + ], + ])), + ], + ], true); + + $this->assertStringContainsString( + 'Field "collectionId" of required type "BigInt!" was not provided', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_missing_token_id_in_currency(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'mutation' => [ + 'explicitRoyaltyCurrencies' => $this->generateEncodeableTokenIds(array_merge($this->generateCurrencies(), [ + [ + 'collectionId' => $this->collection->collection_chain_id, + ], + ])), + ], + ], true); + + $this->assertStringContainsString( + 'Field "tokenId" of required type "EncodableTokenIdInput!" was not provided', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_collection_id_in_currency(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'mutation' => [ + 'explicitRoyaltyCurrencies' => $this->generateEncodeableTokenIds(array_merge($this->generateCurrencies(), [ + [ + 'collectionId' => null, + 'tokenId' => fake()->numberBetween(), + ], + ])), + ], + ], true); + + $this->assertStringContainsString( + 'value null at "mutation.explicitRoyaltyCurrencies[6].collectionId"; Expected non-nullable type "BigInt!" not to be null', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_token_id_in_currency(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'mutation' => [ + 'explicitRoyaltyCurrencies' => $this->generateEncodeableTokenIds(array_merge($this->generateCurrencies(), [ + [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => null, + ], + ])), + ], + ], true); + + $this->assertStringContainsString( + 'Expected non-nullable type "EncodableTokenIdInput!" not to be null', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_duplicated_currency(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'mutation' => [ + 'explicitRoyaltyCurrencies' => $this->generateEncodeableTokenIds(array_merge($currencies = $this->generateCurrencies(), [$currencies[0]])), + ], + ], true); + + $this->assertArraySubset( + ['mutation.explicitRoyaltyCurrencies' => ['The mutation.explicit royalty currencies must be an array of distinct multi assets.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_collection_id_in_currency(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'mutation' => [ + 'explicitRoyaltyCurrencies' => $this->generateEncodeableTokenIds(array_merge($this->generateCurrencies(), [ + [ + 'collectionId' => 'invalid', + 'tokenId' => fake()->numberBetween(), + ], + ])), + ], + ], true); + + $this->assertStringContainsString( + 'Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_token_id_in_currency(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'mutation' => [ + 'explicitRoyaltyCurrencies' => $this->generateEncodeableTokenIds(array_merge($this->generateCurrencies(), [ + [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => 'invalid', + ], + ])), + ], + ], true); + + $this->assertStringContainsString( + 'Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_negative_collection_id_in_currency(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'mutation' => [ + 'explicitRoyaltyCurrencies' => $this->generateEncodeableTokenIds(array_merge($this->generateCurrencies(), [ + [ + 'collectionId' => -1, + 'tokenId' => fake()->numberBetween(), + ], + ])), + ], + ], true); + + $this->assertStringContainsString( + 'Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_negative_token_id_in_currency(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'mutation' => [ + 'explicitRoyaltyCurrencies' => $this->generateEncodeableTokenIds(array_merge($this->generateCurrencies(), [ + [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => -1, + ], + ])), + ], + ], true); + + $this->assertStringContainsString( + 'got invalid value -1 at "mutation.explicitRoyaltyCurrencies[6].tokenId.integer"; Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_overflow_collection_id_in_currency(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'mutation' => [ + 'explicitRoyaltyCurrencies' => $this->generateEncodeableTokenIds(array_merge($this->generateCurrencies(), [ + [ + 'collectionId' => Hex::MAX_UINT256, + 'tokenId' => fake()->numberBetween(), + ], + ])), + ], + ], true); + + $this->assertArraySubset( + ['mutation.explicitRoyaltyCurrencies.6.collectionId' => ['The mutation.explicit royalty currencies.6.collection id is too large, the maximum value it can be is 340282366920938463463374607431768211455.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_overflow_token_id_in_currency(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'mutation' => [ + 'explicitRoyaltyCurrencies' => $this->generateEncodeableTokenIds(array_merge($this->generateCurrencies(), [ + [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => Hex::MAX_UINT256, + ], + ])), + ], + ], true); + + $this->assertArraySubset( + ['mutation.explicitRoyaltyCurrencies.6.tokenId.integer' => ['The mutation.explicitRoyaltyCurrencies.6.tokenId.integer is too large, the maximum value it can be is 340282366920938463463374607431768211455.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + protected function generateCurrencies(?int $total = 5): array + { + return array_map( + fn () => [ + 'tokenId' => ($token = Token::factory()->create())->token_chain_id, + 'collectionId' => Collection::find($token->collection_id)->collection_chain_id, + ], + range(0, $total), + ); + } + + protected function generateEncodeableTokenIds($items): array + { + return collect($items)->transform(function ($item) { + if (isset($item['tokenId'])) { + $item['tokenId'] = $this->tokenIdEncoder->toEncodable($item['tokenId']); + } + + return $item; + })->all(); + } +} diff --git a/tests/Feature/GraphQL/Mutations/MutateTokenTest.php b/tests/Feature/GraphQL/Mutations/MutateTokenTest.php new file mode 100644 index 00000000..d1637010 --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/MutateTokenTest.php @@ -0,0 +1,844 @@ +codec = new Codec(); + $this->defaultAccount = config('enjin-platform.chains.daemon-account'); + + $this->collection = Collection::factory()->create(); + $this->token = Token::factory([ + 'collection_id' => $this->collection, + ])->create(); + $this->tokenIdEncoder = new Integer($this->token->token_chain_id); + } + + // Happy Path + public function test_it_can_mutate_a_token_with_listing_forbidden_using_adapter(): void + { + $encodedData = $this->codec->encode()->mutateToken( + $collectionId = $this->collection->collection_chain_id, + $this->tokenIdEncoder->encode(), + listingForbidden: $listingForbidden = fake()->boolean(), + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'mutation' => [ + 'listingForbidden' => $listingForbidden, + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + } + + public function test_it_can_mutate_a_token_with_listing_forbidden(): void + { + $encodedData = $this->codec->encode()->mutateToken( + $collectionId = $this->collection->collection_chain_id, + $this->tokenIdEncoder->encode(), + listingForbidden: $listingForbidden = fake()->boolean(), + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'mutation' => [ + 'listingForbidden' => $listingForbidden, + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_mutate_a_token_with_empty_behavior(): void + { + $encodedData = $this->codec->encode()->mutateToken( + $collectionId = $this->collection->collection_chain_id, + $this->tokenIdEncoder->encode(), + behavior: [], + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'mutation' => [ + 'behavior' => [], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_mutate_a_token_with_behavior_is_currency(): void + { + $encodedData = $this->codec->encode()->mutateToken( + $collectionId = $this->collection->collection_chain_id, + $this->tokenIdEncoder->encode(), + behavior: new TokenMarketBehaviorParams(isCurrency: true), + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'mutation' => [ + 'behavior' => [ + 'isCurrency' => true, + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_mutate_a_token_with_behavior_has_royalty(): void + { + $encodedData = $this->codec->encode()->mutateToken( + $collectionId = $this->collection->collection_chain_id, + $this->tokenIdEncoder->encode(), + behavior: new TokenMarketBehaviorParams(hasRoyalty: new RoyaltyPolicyParams( + beneficiary: $beneficiary = app(Generator::class)->public_key(), + percentage: $percentage = fake()->numberBetween(1, 40), + )), + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'mutation' => [ + 'behavior' => [ + 'hasRoyalty' => [ + 'beneficiary' => $beneficiary, + 'percentage' => $percentage, + ], + ], + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_mutate_a_token_with_all_fields(): void + { + $behavior = fake()->randomElement([ + [], + new TokenMarketBehaviorParams(isCurrency: true), + new TokenMarketBehaviorParams(hasRoyalty: new RoyaltyPolicyParams( + beneficiary: app(Generator::class)->chain_address(), + percentage: fake()->numberBetween(1, 40), + )), + ]); + + $encodedData = $this->codec->encode()->mutateToken( + $collectionId = $this->collection->collection_chain_id, + $this->tokenIdEncoder->encode(), + behavior: $behavior, + listingForbidden: $listingForbidden = fake()->boolean(), + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'mutation' => [ + 'behavior' => is_array($behavior) ? [] : $behavior->toArray(), + 'listingForbidden' => $listingForbidden, + ], + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + // Exception Paths + public function test_it_will_fail_with_no_args(): void + { + $response = $this->graphql($this->method, [], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of required type "BigInt!" was not provided', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'mutation' => [ + 'listingForbidden' => fake()->boolean(), + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of required type "BigInt!" was not provided', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => null, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'mutation' => [ + 'listingForbidden' => fake()->boolean(), + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of non-null type "BigInt!" must not be null', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => 'invalid', + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'mutation' => [ + 'listingForbidden' => fake()->boolean(), + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" got invalid value "invalid"; Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_collection_id_that_doesnt_exists(): void + { + Collection::where('collection_chain_id', '=', $collectionId = fake()->numberBetween())?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'mutation' => [ + 'listingForbidden' => fake()->boolean(), + ], + ], true); + + $this->assertArraySubset( + ['collectionId' => ['The selected collection id is invalid.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'mutation' => [ + 'listingForbidden' => fake()->boolean(), + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$tokenId" of required type "EncodableTokenIdInput!" was not provided.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => null, + 'mutation' => [ + 'listingForbidden' => fake()->boolean(), + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$tokenId" of non-null type "EncodableTokenIdInput!" must not be null.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => ['integer' => 'invalid'], + 'mutation' => [ + 'listingForbidden' => fake()->boolean(), + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$tokenId" got invalid value "invalid" at "tokenId.integer"; Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_token_id_that_doesnt_exists(): void + { + Token::where('token_chain_id', '=', $tokenId = fake()->numberBetween())?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => ['integer' => $tokenId], + 'mutation' => [ + 'listingForbidden' => fake()->boolean(), + ], + ], true); + + $this->assertArraySubset( + ['tokenId' => ['The token id does not exist in the specified collection.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_mutation(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + ], true); + + $this->assertStringContainsString( + 'Variable "$mutation" of required type "TokenMutationInput!" was not provided', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_mutation(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'mutation' => null, + ], true); + + $this->assertStringContainsString( + 'Variable "$mutation" of non-null type "TokenMutationInput!" must not be null', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_mutation(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'mutation' => 'invalid', + ], true); + + $this->assertStringContainsString( + 'Variable "$mutation" got invalid value', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_empty_mutation(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'mutation' => [], + ], true); + + $this->assertArraySubset( + ['mutation.behavior' => ['The mutation.behavior field is required when mutation.listing forbidden is not present.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_only_listing_forbidden_equals_null(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'mutation' => [ + 'listingForbidden' => null, + ], + ], true); + + $this->assertArraySubset( + ['mutation.behavior' => ['The mutation.behavior field is required when mutation.listing forbidden is not present.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_listing_forbidden(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'mutation' => [ + 'listingForbidden' => 'invalid', + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$mutation" got invalid value "invalid" at "mutation.listingForbidden"; Boolean cannot represent a non boolean value', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_only_behavior_equals_null(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'mutation' => [ + 'behavior' => null, + ], + ], true); + + $this->assertArraySubset( + ['mutation.behavior' => ['The mutation.behavior field is required when mutation.listing forbidden is not present.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_behavior(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'mutation' => [ + 'behavior' => 'invalid', + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$mutation" got invalid value "invalid" at "mutation.behavior"; Expected type "TokenMarketBehaviorInput" to be an object', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_beneficiary_in_has_royalty(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'mutation' => [ + 'behavior' => [ + 'hasRoyalty' => [ + 'percentage' => 20, + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Field "beneficiary" of required type "String!" was not provided', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_beneficiary_in_has_royalty(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'mutation' => [ + 'behavior' => [ + 'hasRoyalty' => [ + 'percentage' => 20, + 'beneficiary' => 'invalid', + ], + ], + ], + ], true); + + $this->assertArraySubset( + ['mutation.behavior.hasRoyalty.beneficiary' => ['The mutation.behavior.has royalty.beneficiary is not a valid substrate account.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_beneficiary_in_has_royalty(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'mutation' => [ + 'behavior' => [ + 'hasRoyalty' => [ + 'percentage' => 20, + 'beneficiary' => null, + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$mutation" got invalid value null at "mutation.behavior.hasRoyalty.beneficiary"; Expected non-nullable type "String!" not to be null', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_percentage_in_has_royalty(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'mutation' => [ + 'behavior' => [ + 'hasRoyalty' => [ + 'beneficiary' => 'invalid', + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Field "percentage" of required type "Float!" was not provided', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_percentage_in_has_royalty(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'mutation' => [ + 'behavior' => [ + 'hasRoyalty' => [ + 'beneficiary' => $this->defaultAccount, + 'percentage' => null, + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$mutation" got invalid value null at "mutation.behavior.hasRoyalty.percentage"; Expected non-nullable type "Float!" not to be null', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_negative_percentage_in_has_royalty(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'mutation' => [ + 'behavior' => [ + 'hasRoyalty' => [ + 'beneficiary' => $this->defaultAccount, + 'percentage' => -1, + ], + ], + ], + ], true); + + $this->assertArraySubset( + ['mutation.behavior.hasRoyalty.percentage' => ['The mutation.behavior.has royalty.percentage valid for a royalty is in the range of 0.1% to 50% and a maximum of 7 decimal places.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_zero_percentage_in_has_royalty(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'mutation' => [ + 'behavior' => [ + 'hasRoyalty' => [ + 'beneficiary' => $this->defaultAccount, + 'percentage' => 0, + ], + ], + ], + ], true); + + $this->assertArraySubset( + ['mutation.behavior.hasRoyalty.percentage' => ['The mutation.behavior.has royalty.percentage valid for a royalty is in the range of 0.1% to 50% and a maximum of 7 decimal places.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_percentage_greater_than_max_in_has_royalty(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'mutation' => [ + 'behavior' => [ + 'hasRoyalty' => [ + 'beneficiary' => $this->defaultAccount, + 'percentage' => 51, + ], + ], + ], + ], true); + + $this->assertArraySubset( + ['mutation.behavior.hasRoyalty.percentage' => ['The mutation.behavior.has royalty.percentage valid for a royalty is in the range of 0.1% to 50% and a maximum of 7 decimal places.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_percentage_in_has_royalty(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' =>$this->tokenIdEncoder->toEncodable(), + 'mutation' => [ + 'behavior' => [ + 'hasRoyalty' => [ + 'beneficiary' => $this->defaultAccount, + 'percentage' => 'invalid', + ], + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$mutation" got invalid value "invalid" at "mutation.behavior.hasRoyalty.percentage"; Float cannot represent non numeric value', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_only_is_currency_null_in_has_royalty(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'mutation' => [ + 'behavior' => [ + 'isCurrency' => null, + ], + ], + ], true); + + $this->assertArraySubset( + ['mutation.behavior.isCurrency' => ['The isCurrency parameter only accepts true. If you don\'t want it to be a currency, don\'t pass it.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_is_currency_equals_to_false(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'mutation' => [ + 'behavior' => [ + 'isCurrency' => false, + ], + ], + ], true); + + $this->assertArraySubset( + ['mutation.behavior.isCurrency' => ['The isCurrency parameter only accepts true. If you don\'t want it to be a currency, don\'t pass it.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_is_currency(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'mutation' => [ + 'behavior' => [ + 'isCurrency' => 'invalid', + ], + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$mutation" got invalid value "invalid" at "mutation.behavior.isCurrency"; Boolean cannot represent a non boolean value', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } +} diff --git a/tests/Feature/GraphQL/Mutations/OperatorTransferTokenTest.php b/tests/Feature/GraphQL/Mutations/OperatorTransferTokenTest.php new file mode 100644 index 00000000..2f6ab51b --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/OperatorTransferTokenTest.php @@ -0,0 +1,984 @@ +codec = new Codec(); + $this->defaultAccount = config('enjin-platform.chains.daemon-account'); + + $this->wallet = Wallet::factory()->create(); + $this->recipient = Wallet::factory()->create(); + $this->tokenAccount = TokenAccount::factory([ + 'wallet_id' => $this->wallet, + ])->create(); + $this->token = Token::find($this->tokenAccount->token_id); + $this->tokenIdEncoder = new Integer($this->token->token_chain_id); + $this->collection = Collection::find($this->tokenAccount->collection_id); + $this->collectionAccount = CollectionAccount::factory([ + 'collection_id' => $this->collection, + 'wallet_id' => $this->wallet, + 'account_count' => 1, + ])->create(); + } + + public function test_it_can_transfer_token_using_adapter(): void + { + $encodedData = $this->codec->encode()->transferToken( + $recipient = $this->recipient->public_key, + $collectionId = $this->collection->collection_chain_id, + $params = new OperatorTransferParams( + tokenId: $this->tokenIdEncoder->encode(), + source: $this->wallet->public_key, + amount: fake()->numberBetween(0, $this->tokenAccount->balance), + keepAlive: fake()->boolean(), + ), + ); + + $params = $params->toArray()['Operator']; + $params['tokenId'] = $this->tokenIdEncoder->toEncodable(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipient' => SS58Address::encode($recipient), + 'params' => $params, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + } + // Happy Path + + public function test_it_can_transfer_token(): void + { + $encodedData = $this->codec->encode()->transferToken( + $recipient = $this->recipient->public_key, + $collectionId = $this->collection->collection_chain_id, + $params = new OperatorTransferParams( + tokenId: $this->tokenIdEncoder->encode(), + source: $this->wallet->public_key, + amount: fake()->numberBetween(0, $this->tokenAccount->balance), + keepAlive: fake()->boolean(), + ), + ); + + $params = $params->toArray()['Operator']; + $params['tokenId'] = $this->tokenIdEncoder->toEncodable(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipient' => SS58Address::encode($recipient), + 'params' => $params, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_transfer_token_without_pass_keep_alive(): void + { + $encodedData = $this->codec->encode()->transferToken( + $recipient = $this->recipient->public_key, + $collectionId = $this->collection->collection_chain_id, + $params = new OperatorTransferParams( + tokenId: $this->tokenIdEncoder->encode(), + source: $this->wallet->public_key, + amount: fake()->numberBetween(0, $this->tokenAccount->balance), + ), + ); + + $params = $params->toArray()['Operator']; + $params['tokenId'] = $this->tokenIdEncoder->toEncodable(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipient' => SS58Address::encode($recipient), + 'params' => $params, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_transfer_token_with_signing_wallet(): void + { + $signingWallet = Wallet::factory([ + 'managed' => true, + ])->create(); + + $encodedData = $this->codec->encode()->transferToken( + $recipient = $this->recipient->public_key, + $collectionId = $this->collection->collection_chain_id, + $params = new OperatorTransferParams( + tokenId: $this->tokenIdEncoder->encode(), + source: $this->wallet->public_key, + amount: fake()->numberBetween(0, $this->tokenAccount->balance), + keepAlive: fake()->boolean(), + ), + ); + + $params = $params->toArray()['Operator']; + $params['tokenId'] = $this->tokenIdEncoder->toEncodable(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipient' => SS58Address::encode($recipient), + 'params' => $params, + 'signingAccount' => SS58Address::encode($signingWallet->public_key), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $signingWallet->public_key, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_transfer_token_with_null_signing_wallet(): void + { + $encodedData = $this->codec->encode()->transferToken( + $recipient = $this->recipient->public_key, + $collectionId = $this->collection->collection_chain_id, + $params = new OperatorTransferParams( + tokenId: $this->tokenIdEncoder->encode(), + source: $this->wallet->public_key, + amount: fake()->numberBetween(0, $this->tokenAccount->balance), + keepAlive: fake()->boolean(), + ), + ); + + $params = $params->toArray()['Operator']; + $params['tokenId'] = $this->tokenIdEncoder->toEncodable(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipient' => SS58Address::encode($recipient), + 'params' => $params, + 'signingAccount' => null, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_transfer_token_with_recipient_that_doesnt_exists(): void + { + Wallet::where('public_key', '=', $address = app(Generator::class)->public_key())?->delete(); + + $encodedData = $this->codec->encode()->transferToken( + $recipient = $address, + $collectionId = $this->collection->collection_chain_id, + $params = new OperatorTransferParams( + tokenId: $this->tokenIdEncoder->encode(), + source: $this->wallet->public_key, + amount: fake()->numberBetween(0, $this->tokenAccount->balance), + keepAlive: fake()->boolean(), + ), + ); + + $params = $params->toArray()['Operator']; + $params['tokenId'] = $this->tokenIdEncoder->toEncodable(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipient' => SS58Address::encode($recipient), + 'params' => $params, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + $this->assertDatabaseHas('wallets', [ + 'public_key' => $address, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_transfer_token_with_bigint_collection_id(): void + { + Collection::where('collection_chain_id', Hex::MAX_UINT128)->update(['collection_chain_id' => random_int(1, 1000)]); + $collection = Collection::factory([ + 'collection_chain_id' => Hex::MAX_UINT128, + ])->create(); + CollectionAccount::factory([ + 'collection_id' => $collection, + 'wallet_id' => $this->wallet, + 'account_count' => 1, + ])->create(); + $token = Token::factory([ + 'collection_id' => $collection, + ])->create(); + $tokenAccount = TokenAccount::factory([ + 'collection_id' => $collection, + 'token_id' => $token, + 'wallet_id' => $this->wallet, + ])->create(); + + $encodedData = $this->codec->encode()->transferToken( + $recipient = $this->recipient->public_key, + $collectionId = $collection->collection_chain_id, + $params = new OperatorTransferParams( + tokenId: $this->tokenIdEncoder->encode($token->token_chain_id), + source: $this->wallet->public_key, + amount: fake()->numberBetween(0, $tokenAccount->balance), + keepAlive: fake()->boolean(), + ), + ); + + $params = $params->toArray()['Operator']; + $params['tokenId'] = $this->tokenIdEncoder->toEncodable($token->token_chain_id); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipient' => SS58Address::encode($recipient), + 'params' => $params, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_transfer_token_with_bigint_token_id(): void + { + $collection = Collection::factory()->create(); + CollectionAccount::factory([ + 'collection_id' => $collection, + 'wallet_id' => $this->wallet, + 'account_count' => 1, + ])->create(); + Token::where('token_chain_id', Hex::MAX_UINT128)->update(['token_chain_id' => random_int(1, 1000)]); + $token = Token::factory([ + 'collection_id' => $collection, + 'token_chain_id' => Hex::MAX_UINT128, + ])->create(); + $tokenAccount = TokenAccount::factory([ + 'collection_id' => $collection, + 'token_id' => $token, + 'wallet_id' => $this->wallet, + ])->create(); + + $encodedData = $this->codec->encode()->transferToken( + $recipient = $this->recipient->public_key, + $collectionId = $collection->collection_chain_id, + $params = new OperatorTransferParams( + tokenId: $this->tokenIdEncoder->encode($token->token_chain_id), + source: $this->wallet->public_key, + amount: fake()->numberBetween(0, $tokenAccount->balance), + keepAlive: fake()->boolean(), + ), + ); + + $params = $params->toArray()['Operator']; + $params['tokenId'] = $this->tokenIdEncoder->toEncodable($token->token_chain_id); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipient' => SS58Address::encode($recipient), + 'params' => $params, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + // Exception Path + + public function test_it_will_fail_collection_id_doesnt_exists(): void + { + Collection::where('collection_chain_id', '=', $collectionId = fake()->numberBetween(2000))?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => SS58Address::encode($this->wallet->public_key), + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertArraySubset( + ['collectionId' => ['The selected collection id is invalid.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_invalid_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => 'not_valid', + 'recipient' => $this->recipient->public_key, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => $this->wallet->public_key, + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" got invalid value "not_valid"; Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_null_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => null, + 'recipient' => $this->recipient->public_key, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => $this->wallet->public_key, + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of non-null type "BigInt!" must not be null.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_no_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => $this->recipient->public_key, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => $this->wallet->public_key, + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of required type "BigInt!" was not provided.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_invalid_recipient(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => 'not_valid', + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => SS58Address::encode($this->wallet->public_key), + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertArraySubset( + ['recipient' => ['The recipient is not a valid substrate account.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_null_recipient(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => null, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => $this->wallet->public_key, + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$recipient" of non-null type "String!" must not be null.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_no_recipient(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => $this->wallet->public_key, + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$recipient" of required type "String!" was not provided.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_token_doesnt_exists(): void + { + Token::where('token_chain_id', '=', $tokenId = fake()->numberBetween())?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable($tokenId), + 'source' => $this->wallet->public_key, + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertArraySubset( + ['params.tokenId' => ['The params.token id does not exist in the specified collection.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_invalid_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => $this->recipient->public_key, + 'params' => [ + 'tokenId' => 'not_valid', + 'source' => $this->wallet->public_key, + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$params" got invalid value "not_valid" at "params.tokenId"; Expected type "EncodableTokenIdInput" to be an object', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_null_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => $this->recipient->public_key, + 'params' => [ + 'tokenId' => null, + 'source' => $this->wallet->public_key, + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$params" got invalid value null at "params.tokenId"; Expected non-nullable type "EncodableTokenIdInput!" not to be null', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_no_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => $this->recipient->public_key, + 'params' => [ + 'source' => $this->wallet->public_key, + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertStringContainsString( + 'Field "tokenId" of required type "EncodableTokenIdInput!" was not provided', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_source(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => 'invalid', + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertArraySubset( + ['params.source' => ['The params.source is not a valid substrate account.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_source(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertStringContainsString( + 'Field "source" of required type "String!" was not provided', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_source(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => null, + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$params" got invalid value null at "params.source"; Expected non-nullable type "String!" not to be null', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_invalid_amount(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => $this->wallet->public_key, + 'amount' => 'not_valid', + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$params" got invalid value "not_valid" at "params.amount"; Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_null_amount(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => SS58Address::encode($this->wallet->public_key), + 'amount' => null, + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$params" got invalid value null at "params.amount"; Expected non-nullable type "BigInt!" not to be null', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_no_amount(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => SS58Address::encode($this->wallet->public_key), + ], + ], true); + + $this->assertStringContainsString( + 'Field "amount" of required type "BigInt!" was not provided', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_negative_amount(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => SS58Address::encode($this->wallet->public_key), + 'amount' => -1, + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$params" got invalid value -1 at "params.amount"; Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_zero_amount(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => SS58Address::encode($this->wallet->public_key), + 'amount' => 0, + ], + ], true); + + $this->assertArraySubset( + ['params.amount' => ['The params.amount is too small, the minimum value it can be is 1.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_greater_than_balance(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => SS58Address::encode($this->wallet->public_key), + 'amount' => fake()->numberBetween($this->tokenAccount->balance), + ], + ], true); + + $this->assertArraySubset( + ['params.amount' => ['The params.amount is invalid, the amount provided is bigger than the token account balance.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_invalid_keep_alive(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => $this->recipient->public_key, + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => $this->wallet->public_key, + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + 'keepAlive' => 'invalid', + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$params" got invalid value "invalid" at "params.keepAlive"; Boolean cannot represent a non boolean value', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_invalid_signing_wallet(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => SS58Address::encode($this->wallet->public_key), + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + 'signingAccount' => 'invalid', + ], true); + + $this->assertArraySubset( + ['signingAccount' => ['The signing account is not a valid substrate account.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_empty_string_signing_wallet(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => SS58Address::encode($this->wallet->public_key), + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + 'signingAccount' => '', + ], true); + + $this->assertArraySubset( + ['signingAccount' => ['The signing account field must have a value.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_not_managed_signing_wallet(): void + { + $signingWallet = Wallet::factory([ + 'managed' => false, + ])->create(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'params' => [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'source' => SS58Address::encode($this->wallet->public_key), + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + 'signingAccount' => SS58Address::encode($signingWallet->public_key), + ], true); + + $this->assertArraySubset( + ['signingAccount' => ['The signing account is not a wallet managed by this platform.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_no_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => $this->recipient->public_key, + ], true); + + $this->assertStringContainsString( + 'Variable "$params" of required type "OperatorTransferParams!" was not provided.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_null_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => $this->recipient->public_key, + 'params' => null, + ], true); + + $this->assertStringContainsString( + 'Variable "$params" of non-null type "OperatorTransferParams!" must not be null.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_empty_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => $this->recipient->public_key, + 'params' => [], + ], true); + + $this->assertStringContainsString( + 'Field "tokenId" of required type "EncodableTokenIdInput!" was not provided', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } +} diff --git a/tests/Feature/GraphQL/Mutations/RemoveAllAttributesTest.php b/tests/Feature/GraphQL/Mutations/RemoveAllAttributesTest.php new file mode 100644 index 00000000..4af49d34 --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/RemoveAllAttributesTest.php @@ -0,0 +1,334 @@ +codec = new Codec(); + $this->defaultAccount = config('enjin-platform.chains.daemon-account'); + $this->wallet = (new WalletService())->firstOrStore(['public_key' => $this->defaultAccount]); + + $this->attribute = Attribute::factory()->create(); + $this->collection = Collection::find($this->attribute->collection_id); + $this->collection->update(['owner_wallet_id' => $this->wallet->id]); + $this->token = Token::find($this->attribute->token_id); + $this->token->update(['collection_id' => $this->collection->id]); + $this->tokenIdEncoder = new Integer($this->token->token_chain_id); + } + + public function test_it_can_remove_an_attribute(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId = $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'attributeCount' => $attributeCount = 1, + ]); + + $encodedData = $this->codec->encode()->removeAllAttributes( + $collectionId, + $this->tokenIdEncoder->encode(), + $attributeCount, + ); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_remove_an_attribute_with_empty_attribute_count(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId = $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'attributeCount' => null, + ]); + + $encodedData = $this->codec->encode()->removeAllAttributes( + $collectionId, + $this->tokenIdEncoder->encode(), + 1, + ); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_remove_an_attribute_with_no_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId = $this->collection->collection_chain_id, + 'attributeCount' => 1, + ]); + + $encodedData = $this->codec->encode()->removeAllAttributes( + $collectionId, + null, + 1, + ); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_remove_an_attribute_with_bigint_collection_id(): void + { + Collection::where('collection_chain_id', Hex::MAX_UINT128)->update(['collection_chain_id' => random_int(1, 1000)]); + $collection = Collection::factory(['collection_chain_id' => Hex::MAX_UINT128, 'owner_wallet_id'=>$this->wallet->id])->create(); + $collectionId = $collection->collection_chain_id; + + $token = Token::factory(['collection_id' => $collection])->create(); + Attribute::factory(['collection_id' => $collection, 'token_id' => $token])->create(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable($token->token_chain_id), + 'attributeCount' => $attributeCount = 1, + ]); + + $encodedData = $this->codec->encode()->removeAllAttributes( + $collectionId, + $this->tokenIdEncoder->encode($token->token_chain_id), + $attributeCount, + ); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_remove_an_attribute_with_bigint_token_id(): void + { + $collection = Collection::factory(['owner_wallet_id'=>$this->wallet->id, 'collection_chain_id' => $collectionId = fake()->numberBetween(2000)])->create(); + Token::where('token_chain_id', Hex::MAX_UINT128)->update(['token_chain_id' => random_int(1, 1000)]); + $token = Token::factory(['collection_id' => $collection, 'token_chain_id' => $tokenId = Hex::MAX_UINT128])->create(); + Attribute::factory(['collection_id' => $collection, 'token_id' => $token])->create(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId = $collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable($token->token_chain_id), + 'attributeCount' => $attributeCount = 1, + ]); + + $encodedData = $this->codec->encode()->removeAllAttributes( + $collectionId, + $this->tokenIdEncoder->encode($token->token_chain_id), + $attributeCount, + ); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + // Exception Path + public function test_it_fail_with_for_collection_that_doesnt_exists(): void + { + Collection::where('collection_chain_id', '=', $collectionId = '123456')?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'attributeCount' => 1, + ], true); + + $this->assertArraySubset( + ['collectionId' => ['The selected collection id is invalid.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_for_token_that_doesnt_exists(): void + { + Token::where('token_chain_id', '=', $tokenId = fake()->numberBetween())?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable($tokenId), + 'attributeCount' => 1, + ], true); + + $this->assertArraySubset( + ['tokenId' => ['The token id does not exist in the specified collection.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_invalid_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => 'not_valid', + 'tokenId' => $this->token->token_chain_id, + 'attributeCount' => 1, + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" got invalid value "not_valid"; Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_invalid_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => 'not_valid', + 'attributeCount' => 1, + ], true); + + $this->assertStringContainsString( + 'Variable "$tokenId" got invalid value "not_valid"; Expected type "EncodableTokenIdInput" to be an object', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_no_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'tokenId' => $this->token->token_chain_id, + 'attributeCount' => 1, + ], true); + + $this->assertEquals( + 'Variable "$collectionId" of required type "BigInt!" was not provided.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_null_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => null, + 'tokenId' => $this->token->token_chain_id, + 'attributeCount' => 1, + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of non-null type "BigInt!" must not be null.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } +} diff --git a/tests/Feature/GraphQL/Mutations/RemoveCollectionAttributeTest.php b/tests/Feature/GraphQL/Mutations/RemoveCollectionAttributeTest.php new file mode 100644 index 00000000..2fc0382d --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/RemoveCollectionAttributeTest.php @@ -0,0 +1,278 @@ +codec = new Codec(); + $this->defaultAccount = config('enjin-platform.chains.daemon-account'); + + $this->attribute = Attribute::factory([ + 'token_id' => null, + ])->create(); + + $this->collection = Collection::find($this->attribute->collection_id); + } + + // Happy Path + + public function test_it_can_remove_an_attribute(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId = $this->collection->collection_chain_id, + 'key' => $key = $this->attribute->key, + ]); + + $encodedData = $this->codec->encode()->removeAttribute( + $collectionId, + null, + $key, + ); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_remove_an_attribute_with_null_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId = $this->collection->collection_chain_id, + 'tokenId' => null, + 'key' => $key = $this->attribute->key, + ]); + + $encodedData = $this->codec->encode()->removeAttribute( + $collectionId, + null, + $key, + ); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_remove_an_attribute_with_bigint_collection_id(): void + { + $collection = Collection::factory([ + 'collection_chain_id' => $collectionId = Hex::MAX_UINT128, + ])->create(); + + $attribute = Attribute::factory([ + 'collection_id' => $collection, + 'token_id' => null, + ])->create(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => null, + 'key' => $key = $attribute->key, + ]); + + $encodedData = $this->codec->encode()->removeAttribute( + $collectionId, + null, + $key, + ); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + // Exception Path + + public function test_it_fail_with_for_collection_that_doesnt_exists(): void + { + Collection::where('collection_chain_id', '=', $collectionId = fake()->numberBetween(2000))?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'key' => $this->attribute->key, + ], true); + + $this->assertArraySubset( + ['collectionId' => ['The selected collection id is invalid.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_invalid_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => 'not_valid', + 'key' => $this->attribute->key, + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" got invalid value "not_valid"; Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_no_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'key' => $this->attribute->key, + ], true); + + $this->assertEquals( + 'Variable "$collectionId" of required type "BigInt!" was not provided.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_null_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => null, + 'key' => $this->attribute->key, + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of non-null type "BigInt!" must not be null.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_no_key(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + ], true); + + $this->assertStringContainsString( + 'Variable "$key" of required type "String!" was not provided.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_null_key(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'key' => null, + ], true); + + $this->assertStringContainsString( + 'Variable "$key" of non-null type "String!" must not be null.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_empty_key(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'key' => '', + ], true); + + $this->assertArraySubset( + ['key' => ['The key field must have a value.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_key_doesnt_exists(): void + { + Attribute::where('key', '=', $key = fake()->word())?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'key' => $key, + ], true); + + $this->assertArraySubset( + ['key' => ['The key does not exist in the specified collection.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } +} diff --git a/tests/Feature/GraphQL/Mutations/RemoveTokenAttributeTest.php b/tests/Feature/GraphQL/Mutations/RemoveTokenAttributeTest.php new file mode 100644 index 00000000..c9edbbd3 --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/RemoveTokenAttributeTest.php @@ -0,0 +1,378 @@ +codec = new Codec(); + $this->defaultAccount = config('enjin-platform.chains.daemon-account'); + + $this->attribute = Attribute::factory()->create(); + $this->collection = Collection::find($this->attribute->collection_id); + $this->token = Token::find($this->attribute->token_id); + $this->token->update(['collection_id' => $this->collection->id]); + $this->tokenIdEncoder = new Integer($this->token->token_chain_id); + } + + // Happy Path + + public function test_it_can_remove_an_attribute(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId = $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'key' => $key = $this->attribute->key, + ]); + + $encodedData = $this->codec->encode()->removeAttribute( + $collectionId, + $this->tokenIdEncoder->encode(), + $key, + ); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_remove_an_attribute_with_bigint_collection_id(): void + { + Collection::where('collection_chain_id', Hex::MAX_UINT128)->update(['collection_chain_id' => random_int(1, 1000)]); + $collection = Collection::factory([ + 'collection_chain_id' => Hex::MAX_UINT128, + ])->create(); + $collectionId = $collection->collection_chain_id; + + $token = Token::factory([ + 'collection_id' => $collection, + ])->create(); + + $attribute = Attribute::factory([ + 'collection_id' => $collection, + 'token_id' => $token, + ])->create(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable($token->token_chain_id), + 'key' => $key = $attribute->key, + ]); + + $encodedData = $this->codec->encode()->removeAttribute( + $collectionId, + $this->tokenIdEncoder->encode($token->token_chain_id), + $key, + ); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_remove_an_attribute_with_bigint_token_id(): void + { + $collection = Collection::factory([ + 'collection_chain_id' => $collectionId = fake()->numberBetween(2000), + ])->create(); + + Token::where('token_chain_id', Hex::MAX_UINT128)->update(['token_chain_id' => random_int(1, 1000)]); + $token = Token::factory([ + 'collection_id' => $collection, + 'token_chain_id' => $tokenId = Hex::MAX_UINT128, + ])->create(); + + $attribute = Attribute::factory([ + 'collection_id' => $collection, + 'token_id' => $token, + ])->create(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable($tokenId), + 'key' => $key = $attribute->key, + ]); + + $encodedData = $this->codec->encode()->removeAttribute( + $collectionId, + $this->tokenIdEncoder->encode($tokenId), + $key, + ); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + // Exception Path + + public function test_it_fail_with_for_collection_that_doesnt_exists(): void + { + Collection::where('collection_chain_id', '=', $collectionId = fake()->numberBetween(2000))?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable($this->token->token_chain_id), + 'key' => $this->attribute->key, + ], true); + + $this->assertArraySubset( + ['collectionId' => ['The selected collection id is invalid.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_for_token_that_doesnt_exists(): void + { + Token::where('token_chain_id', '=', $tokenId = fake()->numberBetween())?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable($tokenId), + 'key' => $this->attribute->key, + ], true); + + $this->assertArraySubset( + ['tokenId' => ['The token id doesn\'t exist.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_invalid_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => 'not_valid', + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'key' => $this->attribute->key, + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" got invalid value "not_valid"; Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_invalid_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => 'not_valid', + 'key' => $this->attribute->key, + ], true); + + $this->assertStringContainsString( + 'Variable "$tokenId" got invalid value "not_valid"; Expected type "EncodableTokenIdInput" to be an object', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_no_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'key' => $this->attribute->key, + ], true); + + $this->assertEquals( + 'Variable "$collectionId" of required type "BigInt!" was not provided.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_no_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'key' => $this->attribute->key, + ], true); + + $this->assertArraySubset( + ['message' => 'Variable "$tokenId" of required type "EncodableTokenIdInput!" was not provided.'], + $response['errors'][0] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_null_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => null, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'key' => $this->attribute->key, + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of non-null type "BigInt!" must not be null.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_null_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => null, + 'key' => $this->attribute->key, + ], true); + + $this->assertArraySubset( + ['message' => 'Variable "$tokenId" of non-null type "EncodableTokenIdInput!" must not be null.'], + $response['errors'][0] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_no_key(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + ], true); + + $this->assertStringContainsString( + 'Variable "$key" of required type "String!" was not provided.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_null_key(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'key' => null, + ], true); + + $this->assertStringContainsString( + 'Variable "$key" of non-null type "String!" must not be null.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_empty_key(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'key' => '', + ], true); + + $this->assertArraySubset( + ['key' => ['The key field must have a value.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_key_doesnt_exists(): void + { + Attribute::where('key', '=', $key = fake()->word())?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'key' => $key, + ], true); + + $this->assertArraySubset( + ['key' => ['The key does not exist in the specified token.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } +} diff --git a/tests/Feature/GraphQL/Mutations/RetryTransactionsTest.php b/tests/Feature/GraphQL/Mutations/RetryTransactionsTest.php new file mode 100644 index 00000000..f4d6d448 --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/RetryTransactionsTest.php @@ -0,0 +1,125 @@ +create(); + $response = $this->graphql($this->method, [ + 'ids' => [$transaction->id], + ]); + $this->assertTrue($response); + $transaction->refresh(); + $this->assertEquals($transaction->state, TransactionState::PENDING->name); + $this->assertNull($transaction->transaction_chain_hash); + + + $transaction = Transaction::factory()->create(); + $response = $this->graphql($this->method, [ + 'idempotencyKeys' => [$transaction->idempotency_key], + ]); + $this->assertTrue($response); + $transaction->refresh(); + $this->assertEquals($transaction->state, TransactionState::PENDING->name); + $this->assertNull($transaction->transaction_chain_hash); + } + + public function test_it_will_fail_with_invalid_parameter_ids(): void + { + $response = $this->graphql($this->method, ['ids' => 'Invalid'], true); + $this->assertStringContainsString( + 'Variable "$ids" got invalid value "Invalid"; Cannot represent following value as uint256', + $response['error'] + ); + + $response = $this->graphql($this->method, ['ids' => null], true); + $this->assertArraySubset( + [ + 'ids' => ['The ids field is required when idempotency keys is not present.'], + 'idempotencyKeys' => ['The idempotency keys field is required when ids is not present.'], + ], + $response['error'] + ); + + $response = $this->graphql($this->method, ['ids' => []], true); + $this->assertArraySubset( + [ + 'ids' => ['The ids field is required when idempotency keys is not present.'], + 'idempotencyKeys' => ['The idempotency keys field is required when ids is not present.'], + ], + $response['error'] + ); + + $response = $this->graphql($this->method, ['ids' => [12345678910]], true); + $this->assertArraySubset( + ['ids' => ['The selected ids is invalid.']], + $response['error'] + ); + + $response = $this->graphql($this->method, ['ids' => [1], 'idempotencyKeys' => ['asd']], true); + $this->assertArraySubset( + [ + 'ids' => ['The ids field prohibits idempotency keys from being present.'], + 'idempotencyKeys' => ['The idempotency keys field prohibits ids from being present.'], + ], + $response['error'] + ); + + $response = $this->graphql($this->method, ['ids' => [Hex::MAX_UINT256 + 1]], true); + $this->assertStringContainsString( + 'Cannot represent following value as uint256', + $response['error'] + ); + + $response = $this->graphql($this->method, ['ids' => [1, 1]], true); + $this->assertArraySubset( + ['ids.0' => ['The ids.0 field has a duplicate value.']], + $response['error'] + ); + } + + public function test_it_will_fail_with_invalid_parameter_idempotency_keys(): void + { + $response = $this->graphql($this->method, ['idempotencyKeys' => null], true); + $this->assertArraySubset( + [ + 'ids' => ['The ids field is required when idempotency keys is not present.'], + 'idempotencyKeys' => ['The idempotency keys field is required when ids is not present.'], + ], + $response['error'] + ); + + $response = $this->graphql($this->method, ['idempotencyKeys' => []], true); + $this->assertArraySubset( + [ + 'ids' => ['The ids field is required when idempotency keys is not present.'], + 'idempotencyKeys' => ['The idempotency keys field is required when ids is not present.'], + ], + $response['error'] + ); + + $response = $this->graphql($this->method, ['idempotencyKeys' => [fake()->uuid()]], true); + $this->assertArraySubset( + ['idempotencyKeys' => ['The selected idempotency keys is invalid.']], + $response['error'] + ); + + $response = $this->graphql($this->method, ['idempotencyKeys' => ['a', 'a']], true); + $this->assertArraySubset( + ['idempotencyKeys.0' => ['The idempotencyKeys.0 field has a duplicate value.']], + $response['error'] + ); + } +} diff --git a/tests/Feature/GraphQL/Mutations/SetCollectionAttributeTest.php b/tests/Feature/GraphQL/Mutations/SetCollectionAttributeTest.php new file mode 100644 index 00000000..e1ea9f72 --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/SetCollectionAttributeTest.php @@ -0,0 +1,191 @@ +codec = new Codec(); + $this->defaultAccount = config('enjin-platform.chains.daemon-account'); + $this->collection = Collection::factory()->create(); + } + + // Happy Path + + public function test_it_can_create_an_attribute(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'key' => $key = fake()->word(), + 'value' => $value = fake()->realText(), + ]); + + $encodedData = $this->codec->encode()->setAttribute( + $this->collection->collection_chain_id, + null, + $key, + $value + ); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + Event::assertDispatched(TransactionCreated::class); + } + + // Exception Path + + public function test_it_fail_with_for_collection_that_doesnt_exists(): void + { + Collection::where('collection_chain_id', '=', $collectionId = fake()->numberBetween(2000))?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'key' => fake()->word(), + 'value' => fake()->realText(), + ], true); + + $this->assertArraySubset( + ['collectionId' => ['The selected collection id is invalid.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_invalid_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => 'not_valid', + 'key' => fake()->word(), + 'value' => fake()->realText(), + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" got invalid value "not_valid"; Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_no_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'key' => fake()->word(), + 'value' => fake()->realText(), + ], true); + + $this->assertEquals( + 'Variable "$collectionId" of required type "BigInt!" was not provided.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_null_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => null, + 'key' => fake()->word(), + 'value' => fake()->realText(), + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of non-null type "BigInt!" must not be null.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_no_key(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'value' => fake()->realText(), + ], true); + + $this->assertStringContainsString( + 'Variable "$key" of required type "String!" was not provided.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_null_key(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'key' => null, + 'value' => fake()->realText(), + ], true); + + $this->assertStringContainsString( + 'Variable "$key" of non-null type "String!" must not be null.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_no_value(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'key' => fake()->word, + ], true); + + $this->assertStringContainsString( + 'Variable "$value" of required type "String!" was not provided.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_null_value(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'key' => fake()->word, + 'value' => null, + ], true); + + $this->assertStringContainsString( + 'Variable "$value" of non-null type "String!" must not be null.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } +} diff --git a/tests/Feature/GraphQL/Mutations/SetTokenAttributeTest.php b/tests/Feature/GraphQL/Mutations/SetTokenAttributeTest.php new file mode 100644 index 00000000..1105c506 --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/SetTokenAttributeTest.php @@ -0,0 +1,381 @@ +codec = new Codec(); + $this->defaultAccount = config('enjin-platform.chains.daemon-account'); + $this->token = Token::factory()->create(); + $this->collection = Collection::find($this->token->collection_id); + $this->tokenIdEncoder = new Integer($this->token->token_chain_id); + } + + public function test_it_can_create_an_attribute_using_adapter(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId = $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'key' => $key = fake()->word(), + 'value' => $value = fake()->realText(), + ]); + + $encodedData = $this->codec->encode()->setAttribute( + $collectionId, + $this->tokenIdEncoder->encode(), + $key, + $value + ); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + } + // Happy Path + + public function test_it_can_create_an_attribute(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId = $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'key' => $key = fake()->word(), + 'value' => $value = fake()->realText(), + ]); + + $encodedData = $this->codec->encode()->setAttribute( + $collectionId, + $this->tokenIdEncoder->encode(), + $key, + $value + ); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_create_an_attribute_with_bigint_collection_id(): void + { + $collection = Collection::factory([ + 'collection_chain_id' => $collectionId = Hex::MAX_UINT128, + ])->create(); + + $token = Token::factory([ + 'collection_id' => $collection, + ])->create(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable($token->token_chain_id), + 'key' => $key = fake()->word(), + 'value' => $value = fake()->realText(), + ]); + + $encodedData = $this->codec->encode()->setAttribute( + $collectionId, + $this->tokenIdEncoder->encode($token->token_chain_id), + $key, + $value + ); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_create_an_attribute_with_bigint_token_id(): void + { + $collection = Collection::factory([ + 'collection_chain_id' => $collectionId = fake()->numberBetween(2000), + ])->create(); + + Token::factory([ + 'collection_id' => $collection, + 'token_chain_id' => $tokenId = Hex::MAX_UINT128, + ])->create(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable($tokenId), + 'key' => $key = fake()->word(), + 'value' => $value = fake()->realText(), + ]); + + $encodedData = $this->codec->encode()->setAttribute( + $collectionId, + $this->tokenIdEncoder->encode($tokenId), + $key, + $value + ); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + Event::assertDispatched(TransactionCreated::class); + } + + // Exception Path + + public function test_it_fail_with_for_collection_that_doesnt_exists(): void + { + Collection::where('collection_chain_id', '=', $collectionId = fake()->numberBetween(2000))?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'key' => fake()->word(), + 'value' => fake()->realText(), + ], true); + + $this->assertArraySubset( + ['collectionId' => ['The selected collection id is invalid.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_invalid_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => 'not_valid', + 'tokenId' => $this->token->token_chain_id, + 'key' => fake()->word(), + 'value' => fake()->realText(), + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" got invalid value "not_valid"; Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_no_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'tokenId' => $this->token->token_chain_id, + 'key' => fake()->word(), + 'value' => fake()->realText(), + ], true); + + $this->assertEquals( + 'Variable "$collectionId" of required type "BigInt!" was not provided.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_null_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => null, + 'tokenId' => $this->token->token_chain_id, + 'key' => fake()->word(), + 'value' => fake()->realText(), + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of non-null type "BigInt!" must not be null.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_for_token_that_doesnt_exists(): void + { + Token::where('token_chain_id', '=', $tokenId = fake()->numberBetween())?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable($tokenId), + 'key' => fake()->word(), + 'value' => fake()->realText(), + ], true); + + $this->assertArraySubset( + ['tokenId' => ['The token id does not exist in the specified collection.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_invalid_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => ['integer' => 'not_valid'], + 'key' => fake()->word(), + 'value' => fake()->realText(), + ], true); + + $this->assertStringContainsString( + 'Variable "$tokenId" got invalid value "not_valid" at "tokenId.integer"; Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_no_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'key' => fake()->word(), + 'value' => fake()->realText(), + ], true); + + $this->assertEquals( + 'Variable "$tokenId" of required type "EncodableTokenIdInput!" was not provided.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_null_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => ['integer' => null], + 'key' => fake()->word(), + 'value' => fake()->realText(), + ], true); + + $this->assertEquals( + 'The integer field must have a value.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_no_key(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'value' => fake()->realText(), + ], true); + + $this->assertStringContainsString( + 'Variable "$key" of required type "String!" was not provided.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_null_key(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'key' => null, + 'value' => fake()->realText(), + ], true); + + $this->assertStringContainsString( + 'Variable "$key" of non-null type "String!" must not be null.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_no_value(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'key' => fake()->word, + ], true); + + $this->assertStringContainsString( + 'Variable "$value" of required type "String!" was not provided.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_null_value(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'key' => fake()->word, + 'value' => null, + ], true); + + $this->assertStringContainsString( + 'Variable "$value" of non-null type "String!" must not be null.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } +} diff --git a/tests/Feature/GraphQL/Mutations/SetWalletAccountTest.php b/tests/Feature/GraphQL/Mutations/SetWalletAccountTest.php new file mode 100644 index 00000000..ab64e627 --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/SetWalletAccountTest.php @@ -0,0 +1,158 @@ +wallet = Wallet::factory([ + 'external_id' => fake()->uuid(), + 'public_key' => null, + 'managed' => true, + ])->create(); + } + + // Happy Path + public function test_it_can_update_wallet_with_id_without_auth(): void + { + Wallet::where('public_key', '=', $publicKey = app(Generator::class)->public_key)?->delete(); + + $response = $this->httpGraphql( + $this->method, + ['variables' => ['id' => $this->wallet->id, 'account' => $publicKey]] + ); + $this->assertTrue($response); + $this->assertDatabaseHas('wallets', [ + 'id' => $this->wallet->id, + 'external_id' => $this->wallet->external_id, + 'public_key' => $publicKey, + 'managed' => true, + ]); + } + + public function test_it_can_update_wallet_with_id(): void + { + Wallet::where('public_key', '=', $publicKey = app(Generator::class)->public_key())?->delete(); + + $response = $this->graphql($this->method, [ + 'id' => $this->wallet->id, + 'account' => SS58Address::encode($publicKey), + ]); + + $this->assertTrue($response); + $this->assertDatabaseHas('wallets', [ + 'id' => $this->wallet->id, + 'external_id' => $this->wallet->external_id, + 'public_key' => $publicKey, + 'managed' => true, + ]); + } + + public function test_it_can_update_wallet_with_external_id(): void + { + Wallet::where('public_key', '=', $publicKey = app(Generator::class)->public_key())?->delete(); + + $response = $this->graphql($this->method, [ + 'externalId' => $this->wallet->external_id, + 'account' => SS58Address::encode($publicKey), + ]); + + $this->assertTrue($response); + $this->assertDatabaseHas('wallets', [ + 'id' => $this->wallet->id, + 'external_id' => $this->wallet->external_id, + 'public_key' => $publicKey, + 'managed' => true, + ]); + } + + // Exception Path + + public function test_it_will_fail_with_no_address(): void + { + $response = $this->graphql($this->method, [ + 'id' => $this->wallet->id, + ], true); + + $this->assertStringContainsString( + 'Variable "$account" of required type "String!" was not provided.', + $response['error'] + ); + } + + public function test_it_will_fail_with_null_address(): void + { + $response = $this->graphql($this->method, [ + 'id' => $this->wallet->id, + 'account' => null, + ], true); + + $this->assertStringContainsString( + 'Variable "$account" of non-null type "String!" must not be null.', + $response['error'] + ); + } + + public function test_it_will_fail_with_invalid_address(): void + { + $response = $this->graphql($this->method, [ + 'id' => $this->wallet->id, + 'account' => 'invalid_address', + ], true); + + $this->assertArraySubset( + ['account' => ['The account is not a valid substrate account.']], + $response['error'] + ); + } + + public function test_it_will_fail_with_duplicated_address(): void + { + Wallet::factory([ + 'public_key' => $publicKey = app(Generator::class)->public_key(), + ])->create(); + + $response = $this->graphql($this->method, [ + 'id' => $this->wallet->id, + 'account' => SS58Address::encode($publicKey), + ], true); + + $this->assertStringContainsString( + 'The account has already been taken.', + $response['error'] + ); + } + + public function test_it_will_fail_if_another_address_has_been_set(): void + { + $wallet = Wallet::factory([ + 'public_key' => app(Generator::class)->unique()->public_key(), + ])->create(); + + $response = $this->graphql($this->method, [ + 'id' => $wallet->id, + 'account' => SS58Address::encode(app(Generator::class)->unique()->public_key()), + ], true); + + $this->assertStringContainsString( + 'The wallet account is immutable once set.', + $response['error'] + ); + } +} diff --git a/tests/Feature/GraphQL/Mutations/SimpleTransferTokenTest.php b/tests/Feature/GraphQL/Mutations/SimpleTransferTokenTest.php new file mode 100644 index 00000000..fcc53805 --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/SimpleTransferTokenTest.php @@ -0,0 +1,932 @@ +codec = new Codec(); + $walletService = new WalletService(); + $this->defaultAccount = config('enjin-platform.chains.daemon-account'); + + $this->wallet = $walletService->firstOrStore(['public_key' => $this->defaultAccount]); + + $this->recipient = Wallet::factory()->create(); + $this->tokenAccount = TokenAccount::factory([ + 'wallet_id' => $this->wallet, + ])->create(); + $this->token = Token::find($this->tokenAccount->token_id); + $this->tokenIdInput = new Integer($this->token->token_chain_id); + $this->collection = Collection::find($this->tokenAccount->collection_id); + $this->collectionAccount = CollectionAccount::factory([ + 'collection_id' => $this->collection, + 'wallet_id' => $this->wallet, + 'account_count' => 1, + ])->create(); + } + + public function test_it_can_transfer_token_using_adapter(): void + { + $encodedData = $this->codec->encode()->transferToken( + $recipient = $this->recipient->public_key, + $collectionId = $this->collection->collection_chain_id, + $params = new SimpleTransferParams( + tokenId: $this->tokenIdInput->encode(), + amount: fake()->numberBetween(0, $this->tokenAccount->balance), + keepAlive: fake()->boolean(), + ), + ); + + $params = $params->toArray()['Simple']; + $params['tokenId'] = $this->tokenIdInput->toEncodable(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipient' => SS58Address::encode($recipient), + 'params' => $params, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + } + // Happy Path + + public function test_it_can_transfer_token(): void + { + $encodedData = $this->codec->encode()->transferToken( + $recipient = $this->recipient->public_key, + $collectionId = $this->collection->collection_chain_id, + $params = new SimpleTransferParams( + tokenId: $this->tokenIdInput->encode(), + amount: fake()->numberBetween(0, $this->tokenAccount->balance), + keepAlive: fake()->boolean(), + ), + ); + + $params = $params->toArray()['Simple']; + $params['tokenId'] = $this->tokenIdInput->toEncodable(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipient' => SS58Address::encode($recipient), + 'params' => $params, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_transfer_token_without_pass_keep_alive(): void + { + $encodedData = $this->codec->encode()->transferToken( + $recipient = $this->recipient->public_key, + $collectionId = $this->collection->collection_chain_id, + $params = new SimpleTransferParams( + tokenId: $this->tokenIdInput->encode(), + amount: fake()->numberBetween(0, $this->tokenAccount->balance), + ), + ); + + $params = $params->toArray()['Simple']; + $params['tokenId'] = $this->tokenIdInput->toEncodable(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipient' => SS58Address::encode($recipient), + 'params' => $params, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_transfer_token_with_signing_wallet(): void + { + $signingWallet = Wallet::factory([ + 'managed' => true, + ])->create(); + $tokenAccount = TokenAccount::factory([ + 'wallet_id' => $signingWallet, + ])->create(); + $token = Token::find($tokenAccount->token_id); + $collection = Collection::find($tokenAccount->collection_id); + CollectionAccount::factory([ + 'collection_id' => $collection, + 'wallet_id' => $signingWallet, + 'account_count' => 1, + ])->create(); + + $encodedData = $this->codec->encode()->transferToken( + $recipient = $this->recipient->public_key, + $collectionId = $collection->collection_chain_id, + $params = new SimpleTransferParams( + tokenId: $this->tokenIdInput->encode($token->token_chain_id), + amount: fake()->numberBetween(0, $tokenAccount->balance), + keepAlive: fake()->boolean(), + ), + ); + + $params = $params->toArray()['Simple']; + $params['tokenId'] = $this->tokenIdInput->toEncodable($token->token_chain_id); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipient' => SS58Address::encode($recipient), + 'params' => $params, + 'signingAccount' => SS58Address::encode($signingWallet->public_key), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $signingWallet->public_key, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_transfer_token_with_null_signing_wallet(): void + { + $encodedData = $this->codec->encode()->transferToken( + $recipient = $this->recipient->public_key, + $collectionId = $this->collection->collection_chain_id, + $params = new SimpleTransferParams( + tokenId: $this->tokenIdInput->encode(), + amount: fake()->numberBetween(0, $this->tokenAccount->balance), + keepAlive: fake()->boolean(), + ), + ); + + $params = $params->toArray()['Simple']; + $params['tokenId'] = $this->tokenIdInput->toEncodable(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipient' => SS58Address::encode($recipient), + 'params' => $params, + 'signingAccount' => null, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_transfer_token_with_recipient_that_doesnt_exists(): void + { + Wallet::where('public_key', '=', $publicKey = app(Generator::class)->public_key())?->delete(); + + $encodedData = $this->codec->encode()->transferToken( + $recipient = $publicKey, + $collectionId = $this->collection->collection_chain_id, + $params = new SimpleTransferParams( + tokenId: $this->tokenIdInput->encode(), + amount: fake()->numberBetween(0, $this->tokenAccount->balance), + keepAlive: fake()->boolean(), + ), + ); + + $params = $params->toArray()['Simple']; + $params['tokenId'] = $this->tokenIdInput->toEncodable(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipient' => SS58Address::encode($recipient), + 'params' => $params, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + $this->assertDatabaseHas('wallets', [ + 'public_key' => $publicKey, + ]); + } + + public function test_it_can_transfer_token_with_bigint_collection_id(): void + { + Collection::where('collection_chain_id', Hex::MAX_UINT128)->update(['collection_chain_id' => random_int(1, 1000)]); + $collection = Collection::factory([ + 'collection_chain_id' => Hex::MAX_UINT128, + ])->create(); + CollectionAccount::factory([ + 'collection_id' => $collection, + 'wallet_id' => $this->wallet, + 'account_count' => 1, + ])->create(); + $token = Token::factory([ + 'collection_id' => $collection, + ])->create(); + $tokenAccount = TokenAccount::factory([ + 'collection_id' => $collection, + 'token_id' => $token, + 'wallet_id' => $this->wallet, + ])->create(); + + $encodedData = $this->codec->encode()->transferToken( + $recipient = $this->recipient->public_key, + $collectionId = $collection->collection_chain_id, + $params = new SimpleTransferParams( + tokenId: $this->tokenIdInput->encode($token->token_chain_id), + amount: fake()->numberBetween(0, $tokenAccount->balance), + keepAlive: fake()->boolean(), + ), + ); + + $params = $params->toArray()['Simple']; + $params['tokenId'] = $this->tokenIdInput->toEncodable($token->token_chain_id); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipient' => SS58Address::encode($recipient), + 'params' => $params, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_transfer_token_with_bigint_token_id(): void + { + $collection = Collection::factory()->create(); + CollectionAccount::factory([ + 'collection_id' => $collection, + 'wallet_id' => $this->wallet, + 'account_count' => 1, + ])->create(); + Token::where('token_chain_id', Hex::MAX_UINT128)->update(['token_chain_id' => random_int(1, 1000)]); + $token = Token::factory([ + 'collection_id' => $collection, + 'token_chain_id' => Hex::MAX_UINT128, + ])->create(); + $tokenAccount = TokenAccount::factory([ + 'collection_id' => $collection, + 'token_id' => $token, + 'wallet_id' => $this->wallet, + ])->create(); + + $encodedData = $this->codec->encode()->transferToken( + $recipient = $this->recipient->public_key, + $collectionId = $collection->collection_chain_id, + $params = new SimpleTransferParams( + tokenId: $this->tokenIdInput->encode($token->token_chain_id), + amount: fake()->numberBetween(0, $tokenAccount->balance), + keepAlive: fake()->boolean(), + ), + ); + + $params = $params->toArray()['Simple']; + $params['tokenId'] = $this->tokenIdInput->toEncodable($token->token_chain_id); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipient' => SS58Address::encode($recipient), + 'params' => $params, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + // Exception Path + + public function test_it_will_fail_collection_id_doesnt_exists(): void + { + Collection::where('collection_chain_id', '=', $collectionId = fake()->numberBetween(2000))?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'params' => [ + 'tokenId' => $this->tokenIdInput->toEncodable(), + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertArraySubset( + ['collectionId' => ['The selected collection id is invalid.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_invalid_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => 'not_valid', + 'recipient' => $this->recipient->public_key, + 'params' => [ + 'tokenId' => $this->tokenIdInput->toEncodable(), + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" got invalid value "not_valid"; Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_null_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => null, + 'recipient' => $this->recipient->public_key, + 'params' => [ + 'tokenId' => $this->tokenIdInput->toEncodable(), + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of non-null type "BigInt!" must not be null.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_no_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => $this->recipient->public_key, + 'params' => [ + 'tokenId' => $this->tokenIdInput->toEncodable(), + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of required type "BigInt!" was not provided.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_invalid_recipient(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => 'not_valid', + 'params' => [ + 'tokenId' => $this->tokenIdInput->toEncodable(), + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertArraySubset( + ['recipient' => ['The recipient is not a valid substrate account.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_null_recipient(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => null, + 'params' => [ + 'tokenId' => $this->tokenIdInput->toEncodable(), + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$recipient" of non-null type "String!" must not be null.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_no_recipient(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'params' => [ + 'tokenId' => $this->tokenIdInput->toEncodable(), + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$recipient" of required type "String!" was not provided.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_token_doesnt_exists(): void + { + Token::where('token_chain_id', '=', $tokenId = fake()->numberBetween())?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'params' => [ + 'tokenId' => $this->tokenIdInput->toEncodable($tokenId), + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertArraySubset( + ['params.tokenId' => ['The params.token id does not exist in the specified collection.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_invalid_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => $this->recipient->public_key, + 'params' => [ + 'tokenId' => 'not_valid', + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$params" got invalid value "not_valid" at "params.tokenId"; Expected type "EncodableTokenIdInput" to be an object', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_null_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => $this->recipient->public_key, + 'params' => [ + 'tokenId' => null, + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$params" got invalid value null at "params.tokenId"; Expected non-nullable type "EncodableTokenIdInput!', + $response['errors'][0]['message'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_no_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => $this->recipient->public_key, + 'params' => [ + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + ], true); + + $this->assertStringContainsString( + 'Field "tokenId" of required type "EncodableTokenIdInput!" was not provided', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_invalid_amount(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => $this->recipient->public_key, + 'params' => [ + 'tokenId' => $this->tokenIdInput->toEncodable(), + 'amount' => 'not_valid', + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$params" got invalid value "not_valid" at "params.amount"; Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_null_amount(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => $this->recipient->public_key, + 'params' => [ + 'tokenId' => $this->tokenIdInput->toEncodable(), + 'amount' => null, + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$params" got invalid value null at "params.amount"; Expected non-nullable type "BigInt!" not to be null', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_no_amount(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => $this->recipient->public_key, + 'params' => [ + 'tokenId' => $this->tokenIdInput->toEncodable(), + ], + ], true); + + $this->assertStringContainsString( + 'Field "amount" of required type "BigInt!" was not provided', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_negative_amount(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => $this->recipient->public_key, + 'params' => [ + 'tokenId' => $this->tokenIdInput->toEncodable(), + 'amount' => -1, + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$params" got invalid value -1 at "params.amount"; Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_zero_amount(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'params' => [ + 'tokenId' => $this->tokenIdInput->toEncodable(), + 'amount' => 0, + ], + ], true); + + $this->assertArraySubset( + ['params.amount' => ['The params.amount is too small, the minimum value it can be is 1.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_greater_than_balance(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'params' => [ + 'tokenId' => $this->tokenIdInput->toEncodable(), + 'amount' => fake()->numberBetween($this->tokenAccount->balance), + ], + ], true); + + $this->assertArraySubset( + ['params.amount' => ['The params.amount is invalid, the amount provided is bigger than the token account balance.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_greater_than_balance_from_signing_wallet(): void + { + $signingWallet = Wallet::factory([ + 'public_key' => app(Generator::class)->public_key(), + 'managed' => true, + ])->create(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'params' => [ + 'tokenId' => $this->tokenIdInput->toEncodable(), + 'amount' => fake()->numberBetween($this->tokenAccount->balance), + ], + 'signingAccount' => SS58Address::encode($signingWallet->public_key), + ], true); + + $this->assertArraySubset( + ['params.amount' => ['The params.amount is invalid, the amount provided is bigger than the token account balance.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_invalid_keep_alive(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => $this->recipient->public_key, + 'params' => [ + 'tokenId' => $this->tokenIdInput->toEncodable(), + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + 'keepAlive' => 'invalid', + ], + ], true); + + $this->assertStringContainsString( + 'Variable "$params" got invalid value "invalid" at "params.keepAlive"; Boolean cannot represent a non boolean value', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_invalid_signing_wallet(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'params' => [ + 'tokenId' => $this->tokenIdInput->toEncodable(), + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + 'signingAccount' => 'invalid', + ], true); + + $this->assertArraySubset( + ['signingAccount' => ['The signing account is not a valid substrate account.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_empty_string_signing_wallet(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'params' => [ + 'tokenId' => $this->tokenIdInput->toEncodable(), + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + 'signingAccount' => '', + ], true); + + $this->assertArraySubset( + ['signingAccount' => ['The signing account field must have a value.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_not_managed_signing_wallet(): void + { + $signingWallet = Wallet::factory([ + 'managed' => false, + ])->create(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => SS58Address::encode($this->recipient->public_key), + 'params' => [ + 'tokenId' => $this->tokenIdInput->toEncodable(), + 'amount' => fake()->numberBetween(0, $this->tokenAccount->balance), + ], + 'signingAccount' => SS58Address::encode($signingWallet->public_key), + ], true); + + $this->assertArraySubset( + ['signingAccount' => ['The signing account is not a wallet managed by this platform.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_no_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => $this->recipient->public_key, + ], true); + + $this->assertStringContainsString( + 'Variable "$params" of required type "SimpleTransferParams!" was not provided.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_null_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => $this->recipient->public_key, + 'params' => null, + ], true); + + $this->assertStringContainsString( + 'Variable "$params" of non-null type "SimpleTransferParams!" must not be null.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_empty_params(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'recipient' => $this->recipient->public_key, + 'params' => [], + ], true); + + $this->assertStringContainsString( + 'Variable "$params" got invalid value []; Field "tokenId" of required type "EncodableTokenIdInput!', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } +} diff --git a/tests/Feature/GraphQL/Mutations/ThawTest.php b/tests/Feature/GraphQL/Mutations/ThawTest.php new file mode 100644 index 00000000..028a211b --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/ThawTest.php @@ -0,0 +1,750 @@ +codec = new Codec(); + $walletService = new WalletService(); + $this->defaultAccount = config('enjin-platform.chains.daemon-account'); + $this->wallet = $walletService->firstOrStore(['public_key' => $this->defaultAccount]); + + $this->tokenAccount = TokenAccount::factory([ + 'wallet_id' => $this->wallet, + ])->create(); + $this->collection = Collection::find($collectionId = $this->tokenAccount->collection_id); + $this->token = Token::find($this->tokenAccount->token_id); + $this->tokenIdEncoder = new Integer($this->token->token_chain_id); + $this->collectionAccount = CollectionAccount::factory([ + 'collection_id' => $collectionId, + 'wallet_id' => $this->wallet, + 'account_count' => 1, + ])->create(); + } + + // Happy Path + + public function test_can_thaw_a_collection(): void + { + $encodedData = $this->codec->encode()->thaw( + $collectionId = $this->collection->collection_chain_id, + new FreezeTypeParams( + type: $freezeType = FreezeType::COLLECTION + ), + ); + + $response = $this->graphql($this->method, [ + 'freezeType' => $freezeType->name, + 'collectionId' => $collectionId, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_can_thaw_a_big_int_collection(): void + { + $collection = Collection::factory([ + 'collection_chain_id' => $collectionId = Hex::MAX_UINT128, + ])->create(); + CollectionAccount::factory([ + 'collection_id' => $collection, + 'wallet_id' => $this->wallet, + 'account_count' => 1, + ])->create(); + + $encodedData = $this->codec->encode()->thaw( + $collectionId, + new FreezeTypeParams( + type: $freezeType = FreezeType::COLLECTION + ), + ); + + $response = $this->graphql($this->method, [ + 'freezeType' => $freezeType->name, + 'collectionId' => $collectionId, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_can_thaw_a_collection_account(): void + { + $encodedData = $this->codec->encode()->thaw( + $collectionId = $this->collection->collection_chain_id, + new FreezeTypeParams( + type: $freezeType = FreezeType::COLLECTION_ACCOUNT, + account: $account = $this->wallet->public_key, + ), + ); + + $response = $this->graphql($this->method, [ + 'freezeType' => $freezeType->name, + 'collectionId' => $collectionId, + 'collectionAccount' => SS58Address::encode($account), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_can_thaw_a_token_using_adapter(): void + { + $encodedData = $this->codec->encode()->thaw( + $collectionId = $this->collection->collection_chain_id, + new FreezeTypeParams( + type: $freezeType = FreezeType::TOKEN, + token: $this->tokenIdEncoder->encode(), + ), + ); + + $response = $this->graphql($this->method, [ + 'freezeType' => $freezeType->name, + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + } + + public function test_can_thaw_a_token(): void + { + $encodedData = $this->codec->encode()->thaw( + $collectionId = $this->collection->collection_chain_id, + new FreezeTypeParams( + type: $freezeType = FreezeType::TOKEN, + token: $this->tokenIdEncoder->encode(), + ), + ); + + $response = $this->graphql($this->method, [ + 'freezeType' => $freezeType->name, + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_can_thaw_a_big_int_token(): void + { + $collection = Collection::factory()->create(); + + Token::factory([ + 'collection_id' => $collection, + 'token_chain_id' => $tokenId = Hex::MAX_UINT128, + ])->create(); + + CollectionAccount::factory([ + 'collection_id' => $collection, + 'wallet_id' => $this->wallet, + 'account_count' => 1, + ])->create(); + + $encodedData = $this->codec->encode()->thaw( + $collectionId = $collection->collection_chain_id, + new FreezeTypeParams( + type: $freezeType = FreezeType::TOKEN, + token: $this->tokenIdEncoder->encode($tokenId), + ), + ); + + $response = $this->graphql($this->method, [ + 'freezeType' => $freezeType->name, + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable($tokenId), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_can_thaw_a_token_account(): void + { + $encodedData = $this->codec->encode()->thaw( + $collectionId = $this->collection->collection_chain_id, + new FreezeTypeParams( + type: $freezeType = FreezeType::TOKEN_ACCOUNT, + token: $this->tokenIdEncoder->encode(), + account: $account = $this->wallet->public_key, + ), + ); + + $response = $this->graphql($this->method, [ + 'freezeType' => $freezeType->name, + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'tokenAccount' => SS58Address::encode($account), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + // Exception Path + + public function test_it_will_fail_with_thaw_type_non_existent(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => 'ASSET', + 'collectionId' => $this->collection->collection_chain_id, + ], true); + + $this->assertStringContainsString( + 'Variable "$freezeType" got invalid value "ASSET"; Value "ASSET" does not exist in "FreezeType" enum', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_collection_id_non_existent(): void + { + Collection::where('collection_chain_id', '=', $collectionId = fake()->numberBetween(2000))?->delete(); + + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::COLLECTION->name, + 'collectionId' => $collectionId, + ], true); + + $this->assertArraySubset( + ['collectionId' => ['The selected collection id is invalid.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::COLLECTION->name, + 'collectionId' => 'invalid', + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" got invalid value "invalid"; Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::COLLECTION->name, + 'collectionId' => null, + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of non-null type "BigInt!" must not be null.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::COLLECTION->name, + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of required type "BigInt!" was not provided.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_token_id(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::TOKEN->name, + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => null, + ], true); + + $this->assertArraySubset( + ['tokenId' => ['The token id field is required.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_token_id(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::TOKEN->name, + 'collectionId' => $this->collection->collection_chain_id, + ], true); + + $this->assertArraySubset( + ['tokenId' => ['The token id field is required.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_token_id(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::TOKEN->name, + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => 'invalid', + ], true); + + $this->assertStringContainsString( + 'Variable "$tokenId" got invalid value "invalid"; Expected type "EncodableTokenIdInput" to be an object', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_token_id_non_existent(): void + { + Token::where('token_chain_id', '=', $tokenId = fake()->numberBetween())?->delete(); + + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::TOKEN->name, + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable($tokenId), + ], true); + + $this->assertArraySubset( + ['tokenId' => ['The token id does not exist in the specified collection.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_collection_account(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::COLLECTION_ACCOUNT->name, + 'collectionId' => $this->collection->collection_chain_id, + 'collectionAccount' => null, + ], true); + + $this->assertArraySubset( + ['collectionAccount' => ['The collection account field is required.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_collection_account(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::COLLECTION_ACCOUNT->name, + 'collectionId' => $this->collection->collection_chain_id, + ], true); + + $this->assertArraySubset( + ['collectionAccount' => ['The collection account field is required.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_collection_account(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::COLLECTION_ACCOUNT->name, + 'collectionId' => $this->collection->collection_chain_id, + 'collectionAccount' => 'invalid', + ], true); + + $this->assertArraySubset( + ['collectionAccount' => ['The collection account is not a valid substrate account.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_collection_account_non_existent(): void + { + Wallet::where('public_key', '=', $address = app(Generator::class)->public_key())?->delete(); + + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::COLLECTION_ACCOUNT->name, + 'collectionId' => $collectionId = $this->collection->collection_chain_id, + 'collectionAccount' => $address, + ], true); + + $this->assertArraySubset( + ['collectionAccount' => ["Could not find a collection account for {$address} at collection {$collectionId}."]], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_token_account_non_existent(): void + { + Wallet::where('public_key', '=', $address = app(Generator::class)->public_key())?->delete(); + + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::TOKEN_ACCOUNT->name, + 'collectionId' => $collectionId = $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'tokenAccount' => $address, + ], true); + + $this->assertArraySubset( + ['tokenAccount' => ["Could not find a token account for {$address} at collection {$collectionId} and token {$this->token->token_chain_id}."]], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_token_account(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::TOKEN_ACCOUNT->name, + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'tokenAccount' => null, + ], true); + + $this->assertArraySubset( + ['tokenAccount' => ['The token account field is required.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_token_account(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::TOKEN_ACCOUNT->name, + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + ], true); + + $this->assertArraySubset( + ['tokenAccount' => ['The token account field is required.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_token_account(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::TOKEN_ACCOUNT->name, + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'tokenAccount' => 'invalid', + ], true); + + $this->assertArraySubset( + ['tokenAccount' => ['The token account is not a valid substrate account.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_cant_pass_token_id_when_freezing_collection(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::COLLECTION->name, + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + ], true); + + $this->assertArraySubset( + ['tokenId' => ['The token id field is prohibited.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_cant_pass_collection_account_when_freezing_collection(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::COLLECTION->name, + 'collectionId' => $this->collection->collection_chain_id, + 'collectionAccount' => $this->wallet->public_key, + ], true); + + $this->assertArraySubset( + ['collectionAccount' => ['The collection account field is prohibited.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_cant_pass_token_account_when_freezing_collection(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::COLLECTION->name, + 'collectionId' => $this->collection->collection_chain_id, + 'tokenAccount' => $this->wallet->public_key, + ], true); + + $this->assertArraySubset( + ['tokenAccount' => ['The token account field is prohibited.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_cant_pass_token_account_when_freezing_collection_account(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::COLLECTION_ACCOUNT->name, + 'collectionId' => $this->collection->collection_chain_id, + 'collectionAccount' => $this->wallet->public_key, + 'tokenAccount' => $this->wallet->public_key, + ], true); + + $this->assertArraySubset( + ['tokenAccount' => ['The token account field is prohibited.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_cant_pass_token_id_when_freezing_collection_account(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::COLLECTION_ACCOUNT->name, + 'collectionId' => $this->collection->collection_chain_id, + 'collectionAccount' => $this->wallet->public_key, + 'tokenId' => $this->tokenIdEncoder->toEncodable($this->token->token_chain_address), + ], true); + + $this->assertArraySubset( + ['tokenId' => ['The token id field is prohibited.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_cant_pass_token_account_when_freezing_token(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::TOKEN->name, + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'tokenAccount' => $this->wallet->public_key, + ], true); + + $this->assertArraySubset( + ['tokenAccount' => ['The token account field is prohibited.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_cant_pass_collection_account_when_freezing_token(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::TOKEN->name, + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'collectionAccount' => $this->wallet->public_key, + ], true); + + $this->assertArraySubset( + ['collectionAccount' => ['The collection account field is prohibited.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_cant_pass_collection_account_when_freezing_token_account(): void + { + $response = $this->graphql($this->method, [ + 'freezeType' => FreezeType::TOKEN_ACCOUNT->name, + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'collectionAccount' => $this->wallet->public_key, + 'tokenAccount' => $this->wallet->public_key, + ], true); + + $this->assertArraySubset( + ['collectionAccount' => ['The collection account field is prohibited.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } +} diff --git a/tests/Feature/GraphQL/Mutations/TransferAllBalanceTest.php b/tests/Feature/GraphQL/Mutations/TransferAllBalanceTest.php new file mode 100644 index 00000000..a5cc9af9 --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/TransferAllBalanceTest.php @@ -0,0 +1,414 @@ +codec = new Codec(); + $this->defaultAccount = config('enjin-platform.chains.daemon-account'); + } + + // Happy Path + + public function test_it_can_transfer_all_balance(): void + { + $encodedData = $this->codec->encode()->transferAllBalance( + $address = app(Generator::class)->public_key(), + $keepAlive = fake()->boolean(), + ); + + $response = $this->graphql($this->method, [ + 'recipient' => $address, + 'keepAlive' => $keepAlive, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_transfer_all_with_missing_keep_alive(): void + { + $encodedData = $this->codec->encode()->transferAllBalance( + $address = app(Generator::class)->public_key(), + ); + + $response = $this->graphql($this->method, [ + 'recipient' => $address, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_transfer_all_with_null_keep_alive(): void + { + $encodedData = $this->codec->encode()->transferAllBalance( + $address = app(Generator::class)->public_key(), + ); + + $response = $this->graphql($this->method, [ + 'recipient' => $address, + 'keepAlive' => null, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_transfer_all_to_a_wallet_that_doesnt_exists(): void + { + Wallet::where('public_key', '=', $address = app(Generator::class)->public_key())?->delete(); + + $encodedData = $this->codec->encode()->transferAllBalance( + $address, + $keepAlive = fake()->boolean(), + ); + + $response = $this->graphql($this->method, [ + 'recipient' => $address, + 'keepAlive' => $keepAlive, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + $this->assertDatabaseHas('wallets', [ + 'public_key' => $address, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_transfer_all_to_a_wallet_that_exists(): void + { + Wallet::factory([ + 'public_key' => $publicKey = app(Generator::class)->public_key(), + ])->create(); + + $encodedData = $this->codec->encode()->transferAllBalance( + $publicKey, + $keepAlive = fake()->boolean(), + ); + + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($publicKey), + 'keepAlive' => $keepAlive, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_transfer_all_with_another_signing_wallet(): void + { + Wallet::factory([ + 'public_key' => $publicKey = app(Generator::class)->public_key(), + 'managed' => true, + ])->create(); + + $encodedData = $this->codec->encode()->transferAllBalance( + $this->defaultAccount, + $keepAlive = fake()->boolean(), + ); + + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->defaultAccount), + 'keepAlive' => $keepAlive, + 'signingAccount' => SS58Address::encode($publicKey), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $publicKey, + ], + ], + ], $response); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_transfer_all_passing_the_default_wallet_on_signing_wallet(): void + { + $encodedData = $this->codec->encode()->transferAllBalance( + $this->defaultAccount, + $keepAlive = fake()->boolean(), + ); + + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->defaultAccount), + 'keepAlive' => $keepAlive, + 'signingAccount' => SS58Address::encode($this->defaultAccount), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_transfer_all_with_signing_wallet_null(): void + { + $encodedData = $this->codec->encode()->transferAllBalance( + $publicKey = app(Generator::class)->public_key(), + $keepAlive = fake()->boolean(), + ); + + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($publicKey), + 'keepAlive' => $keepAlive, + 'signingAccount' => null, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + Event::assertDispatched(TransactionCreated::class); + } + + // Exception Path + + public function test_it_will_fail_with_no_recipient(): void + { + $response = $this->graphql($this->method, [], true); + + $this->assertStringContainsString( + 'Variable "$recipient" of required type "String!" was not provided.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_recipient(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => null, + ], true); + + $this->assertStringContainsString( + 'Variable "$recipient" of non-null type "String!" must not be null.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_recipient(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => 'not_valid', + ], true); + + $this->assertArraySubset( + ['recipient' => ['The recipient is not a valid substrate account.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_keepalive(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => app(Generator::class)->public_key(), + 'keepAlive' => 'not_valid', + ], true); + + $this->assertStringContainsString( + 'Variable "$keepAlive" got invalid value "not_valid"; Boolean cannot represent a non boolean value', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_signing_wallet(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => app(Generator::class)->public_key(), + 'signingAccount' => 'not_valid', + ], true); + + $this->assertArraySubset( + ['signingAccount' => ['The signing account is not a valid substrate account.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_empty_string_signing_wallet(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => app(Generator::class)->public_key(), + 'signingAccount' => '', + ], true); + + $this->assertArraySubset( + ['signingAccount' => ['The signing account field must have a value.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_signing_wallet_not_saved(): void + { + Wallet::where('public_key', '=', $publicKey = app(Generator::class)->public_key())?->delete(); + + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->defaultAccount), + 'signingAccount' => SS58Address::encode($publicKey), + ], true); + + $this->assertArraySubset( + ['signingAccount' => ['The signing account is not a wallet managed by this platform.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_signing_wallet_that_is_not_managed(): void + { + Wallet::factory([ + 'public_key' => $publicKey = app(Generator::class)->public_key(), + 'managed' => false, + ])->create(); + + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->defaultAccount), + 'signingAccount' => SS58Address::encode($publicKey), + ], true); + + $this->assertArraySubset( + ['signingAccount' => ['The signing account is not a wallet managed by this platform.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } +} diff --git a/tests/Feature/GraphQL/Mutations/TransferBalanceTest.php b/tests/Feature/GraphQL/Mutations/TransferBalanceTest.php new file mode 100644 index 00000000..52a4feb0 --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/TransferBalanceTest.php @@ -0,0 +1,556 @@ +codec = new Codec(); + $this->defaultAccount = config('enjin-platform.chains.daemon-account'); + } + + // Happy Path + + public function test_it_can_transfer_balance_without_keep_alive(): void + { + $encodedData = $this->codec->encode()->TransferBalance( + $publicKey = app(Generator::class)->public_key(), + $amount = fake()->numberBetween(), + ); + + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($publicKey), + 'amount' => $amount, + 'keepAlive' => false, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_transfer_balance_with_bigint_amount(): void + { + $encodedData = $this->codec->encode()->TransferBalance( + $publicKey = app(Generator::class)->public_key(), + $amount = Hex::MAX_UINT128, + ); + + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($publicKey), + 'amount' => $amount, + 'keepAlive' => false, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_transfer_balance_with_keep_alive(): void + { + $encodedData = $this->codec->encode()->TransferBalanceKeepAlive( + $publicKey = app(Generator::class)->public_key(), + $amount = fake()->numberBetween(), + ); + + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($publicKey), + 'amount' => $amount, + 'keepAlive' => true, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_transfer_with_missing_keep_alive(): void + { + $encodedData = $this->codec->encode()->TransferBalance( + $publicKey = app(Generator::class)->public_key(), + $amount = fake()->numberBetween(), + ); + + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($publicKey), + 'amount' => $amount, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_transfer_with_null_keep_alive(): void + { + $encodedData = $this->codec->encode()->TransferBalance( + $publicKey = app(Generator::class)->public_key(), + $amount = fake()->numberBetween(), + ); + + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($publicKey), + 'amount' => $amount, + 'keepAlive' => null, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_transfer_to_a_wallet_that_doesnt_exists(): void + { + Wallet::where('public_key', '=', $publicKey = app(Generator::class)->public_key())?->delete(); + + $encodedData = $this->codec->encode()->TransferBalance( + $publicKey, + $amount = fake()->numberBetween(), + ); + + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($publicKey), + 'amount' => $amount, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + $this->assertDatabaseHas('wallets', [ + 'public_key' => $publicKey, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_transfer_to_a_wallet_that_exists(): void + { + Wallet::factory([ + 'public_key' => $publicKey = app(Generator::class)->public_key(), + ])->create(); + + $encodedData = $this->codec->encode()->TransferBalance( + $publicKey, + $amount = fake()->numberBetween(), + ); + + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($publicKey), + 'amount' => $amount, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_transfer_with_another_signing_wallet(): void + { + Wallet::factory([ + 'public_key' => $publicKey = app(Generator::class)->public_key(), + 'managed' => true, + ])->create(); + + $encodedData = $this->codec->encode()->TransferBalance( + $this->defaultAccount, + $amount = fake()->numberBetween(), + ); + + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->defaultAccount), + 'amount' => $amount, + 'signingAccount' => SS58Address::encode($publicKey), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $publicKey, + ], + ], + ], $response); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_transfer_with_signing_wallet_null(): void + { + $encodedData = $this->codec->encode()->TransferBalance( + $publicKey = app(Generator::class)->public_key(), + $amount = fake()->numberBetween(), + ); + + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($publicKey), + 'amount' => $amount, + 'signingAccount' => null, + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + Event::assertDispatched(TransactionCreated::class); + } + + // Exception Path + + public function test_it_will_fail_with_no_args(): void + { + $response = $this->graphql($this->method, [], true); + + $this->assertStringContainsString( + 'Variable "$recipient" of required type "String!" was not provided.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_recipient(): void + { + $response = $this->graphql($this->method, [ + 'amount' => fake()->numberBetween(), + ], true); + + $this->assertStringContainsString( + 'Variable "$recipient" of required type "String!" was not provided.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_amount(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode(app(Generator::class)->public_key()), + ], true); + + $this->assertStringContainsString( + 'Variable "$amount" of required type "BigInt!" was not provided.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_recipient(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => null, + 'amount' => fake()->numberBetween(), + ], true); + + $this->assertStringContainsString( + 'Variable "$recipient" of non-null type "String!" must not be null.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_amount(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode(app(Generator::class)->public_key()), + 'amount' => null, + ], true); + + $this->assertStringContainsString( + 'Variable "$amount" of non-null type "BigInt!" must not be null.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_recipient(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => 'not_valid', + 'amount' => fake()->numberBetween(), + ], true); + + $this->assertArraySubset( + ['recipient' => ['The recipient is not a valid substrate account.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_amount(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode(app(Generator::class)->public_key()), + 'amount' => 'not_valid', + ], true); + + $this->assertStringContainsString( + 'Variable "$amount" got invalid value "not_valid"; Cannot represent following value as uint256', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_negative_amount(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode(app(Generator::class)->public_key()), + 'amount' => -1, + ], true); + + $this->assertStringContainsString( + 'Variable "$amount" got invalid value -1; Cannot represent following value as uint256', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_zero_amount(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode(app(Generator::class)->public_key()), + 'amount' => 0, + ], true); + + $this->assertArraySubset( + ['amount' => ['The amount is too small, the minimum value it can be is 1.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_keepalive(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode(app(Generator::class)->public_key()), + 'amount' => fake()->numberBetween(), + 'keepAlive' => 'not_valid', + ], true); + + $this->assertStringContainsString( + 'Variable "$keepAlive" got invalid value "not_valid"; Boolean cannot represent a non boolean value', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_signing_wallet(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode(app(Generator::class)->public_key()), + 'amount' => fake()->numberBetween(), + 'signingAccount' => 'not_valid', + ], true); + + $this->assertArraySubset( + ['signingAccount' => ['The signing account is not a valid substrate account.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_empty_string_signing_wallet(): void + { + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode(app(Generator::class)->public_key()), + 'amount' => fake()->numberBetween(), + 'signingAccount' => '', + ], true); + + $this->assertArraySubset( + ['signingAccount' => ['The signing account field must have a value.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_signing_wallet_not_saved(): void + { + Wallet::where('public_key', '=', $publicKey = app(Generator::class)->public_key())?->delete(); + + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->defaultAccount), + 'amount' => fake()->numberBetween(), + 'signingAccount' => SS58Address::encode($publicKey), + ], true); + + $this->assertArraySubset( + ['signingAccount' => ['The signing account is not a wallet managed by this platform.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_signing_wallet_that_is_not_managed(): void + { + Wallet::factory([ + 'public_key' => $publicKey = app(Generator::class)->public_key(), + 'managed' => false, + ])->create(); + + $response = $this->graphql($this->method, [ + 'recipient' => SS58Address::encode($this->defaultAccount), + 'amount' => fake()->numberBetween(), + 'signingAccount' => SS58Address::encode($publicKey), + ], true); + + $this->assertArraySubset( + ['signingAccount' => ['The signing account is not a wallet managed by this platform.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } +} diff --git a/tests/Feature/GraphQL/Mutations/UnapproveCollectionTest.php b/tests/Feature/GraphQL/Mutations/UnapproveCollectionTest.php new file mode 100644 index 00000000..b85c3a5f --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/UnapproveCollectionTest.php @@ -0,0 +1,283 @@ +codec = new Codec(); + $walletService = new WalletService(); + $this->defaultAccount = config('enjin-platform.chains.daemon-account'); + $this->owner = $walletService->firstOrStore(['public_key' => $this->defaultAccount]); + + $this->collection = Collection::factory()->create([ + 'owner_wallet_id' => $this->owner->id, + ]); + + $this->collectionAccount = CollectionAccount::factory()->create([ + 'collection_id' => $this->collection->id, + 'wallet_id' => $this->owner->id, + ]); + + $this->collectionAccountApproval = CollectionAccountApproval::factory()->create([ + 'collection_account_id' => $this->collectionAccount->id, + ]); + + $this->operator = Wallet::find($this->collectionAccountApproval->wallet_id); + } + + // Happy Path + + public function test_it_can_unapprove_a_collection_with_string(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'operator' => SS58Address::encode($this->operator->public_key), + ]); + + $encodedData = $this->codec->encode()->unapproveCollection( + $this->collection->collection_chain_id, + $this->operator->public_key, + ); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_unapprove_a_collection_with_int(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => (int) $this->collection->collection_chain_id, + 'operator' => SS58Address::encode($this->operator->public_key), + ]); + + $encodedData = $this->codec->encode()->unapproveCollection( + $this->collection->collection_chain_id, + $this->operator->public_key, + ); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_unapprove_a_collection_with_bigint(): void + { + $collection = Collection::factory()->create([ + 'collection_chain_id' => Hex::MAX_UINT128, + 'owner_wallet_id' => $this->owner->id, + ]); + + $collectionAccount = CollectionAccount::find($this->collectionAccount->id); + $collectionAccount->collection_id = $collection->id; + $collectionAccount->save(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collection->collection_chain_id, + 'operator' => SS58Address::encode($this->operator->public_key), + ]); + + $encodedData = $this->codec->encode()->unapproveCollection( + $collection->collection_chain_id, + $this->operator->public_key, + ); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + Event::assertDispatched(TransactionCreated::class); + } + + // Exception Path + + public function test_it_fail_with_no_collection_id(): void + { + $response = $this->graphql($this->method, [], true); + + $this->assertEquals( + 'Variable "$collectionId" of required type "BigInt!" was not provided.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_collection_id_null(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => null, + ], true); + + $this->assertEquals( + 'Variable "$collectionId" of non-null type "BigInt!" must not be null.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_no_operator(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => fake()->numberBetween(1), + ], true); + + $this->assertEquals( + 'Variable "$operator" of required type "String!" was not provided.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_operator_null(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => fake()->numberBetween(1), + 'operator' => null, + ], true); + + $this->assertEquals( + 'Variable "$operator" of non-null type "String!" must not be null.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_invalid_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => 'abc', + 'operator' => SS58Address::encode($this->operator->public_key), + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" got invalid value "abc"; Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_negative_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => -1, + 'operator' => SS58Address::encode($this->operator->public_key), + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" got invalid value -1; Cannot represent following value as uint256', + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_invalid_operator(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'operator' => 'not_a_substrate_address', + ], true); + + $this->assertArraySubset( + ['operator' => ['The operator is not a valid substrate account.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_collection_id_doesnt_exists(): void + { + Collection::where('collection_chain_id', '=', $collectionId = fake()->numberBetween(1))?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'operator' => SS58Address::encode($this->operator->public_key), + ], true); + + $this->assertArraySubset( + ['collectionId' => ['The selected collection id is invalid.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_fail_with_operator_doesnt_exists(): void + { + Wallet::where('public_key', '=', $operator = app(Generator::class)->public_key())?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId = $this->collection->collection_chain_id, + 'operator' => SS58Address::encode($operator), + ], true); + + $this->assertStringContainsString( + 'Could not find an approval for', + $response['error']['operator'][0] + ); + + Event::assertNotDispatched(TransactionCreated::class); + } +} diff --git a/tests/Feature/GraphQL/Mutations/UnapproveTokenTest.php b/tests/Feature/GraphQL/Mutations/UnapproveTokenTest.php new file mode 100644 index 00000000..33eab70e --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/UnapproveTokenTest.php @@ -0,0 +1,433 @@ +codec = new Codec(); + $walletService = new WalletService(); + $this->defaultAccount = config('enjin-platform.chains.daemon-account'); + $this->wallet = $walletService->firstOrStore(['public_key' => $this->defaultAccount]); + + $this->collection = Collection::factory()->create(); + $this->collectionAccount = CollectionAccount::factory([ + 'collection_id' => $this->collection, + 'wallet_id' => $this->wallet, + 'account_count' => 1, + ])->create(); + $this->token = Token::factory([ + 'collection_id' => $this->collection, + ])->create(); + $this->tokenIdEncoder = new Integer($this->token->token_chain_id); + $this->tokenAccount = TokenAccount::factory([ + 'wallet_id' => $this->wallet, + 'collection_id' => $this->collection, + 'token_id' => $this->token, + ])->create(); + $this->tokenAccountApproval = TokenAccountApproval::factory([ + 'token_account_id' => $this->tokenAccount, + ])->create(); + $this->operator = Wallet::find($this->tokenAccountApproval->wallet_id); + } + + // Happy Path + + public function test_it_can_unapprove_a_token(): void + { + $encodedData = $this->codec->encode()->unapproveToken( + collectionId: $collectionId = $this->collection->collection_chain_id, + tokenId: $this->tokenIdEncoder->encode(), + operator: $operator = $this->operator->public_key, + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'operator' => SS58Address::encode($operator), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_unapprove_a_token_with_big_int_collection_id(): void + { + Collection::where('collection_chain_id', Hex::MAX_UINT128)->update(['collection_chain_id' => random_int(1, 1000)]); + $collection = Collection::factory([ + 'collection_chain_id' => Hex::MAX_UINT128, + ])->create(); + CollectionAccount::factory([ + 'collection_id' => $collection, + 'wallet_id' => $this->wallet, + 'account_count' => 1, + ])->create(); + $token = Token::factory([ + 'collection_id' => $collection, + ])->create(); + $tokenAccount = TokenAccount::factory([ + 'wallet_id' => $this->wallet, + 'collection_id' => $collection, + 'token_id' => $token, + ])->create(); + $operator = Wallet::factory()->create(); + TokenAccountApproval::factory([ + 'token_account_id' => $tokenAccount, + 'wallet_id' => $operator, + ])->create(); + + $encodedData = $this->codec->encode()->unapproveToken( + collectionId: $collectionId = $collection->collection_chain_id, + tokenId: $this->tokenIdEncoder->encode($token->token_chain_id), + operator: $operator = $operator->public_key, + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable($token->token_chain_id), + 'operator' => SS58Address::encode($operator), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + public function test_it_can_unapprove_a_token_with_big_int_token_id(): void + { + $collection = Collection::factory()->create(); + CollectionAccount::factory([ + 'collection_id' => $collection, + 'wallet_id' => $this->wallet, + 'account_count' => 1, + ])->create(); + + Token::where('token_chain_id', Hex::MAX_UINT128)->update(['token_chain_id' => random_int(1, 1000)]); + + $token = Token::factory([ + 'collection_id' => $collection, + 'token_chain_id' => Hex::MAX_UINT128, + ])->create(); + $tokenAccount = TokenAccount::factory([ + 'wallet_id' => $this->wallet, + 'collection_id' => $collection, + 'token_id' => $token, + ])->create(); + + $operator = Wallet::factory()->create(); + TokenAccountApproval::factory([ + 'token_account_id' => $tokenAccount, + 'wallet_id' => $operator, + ])->create(); + + $encodedData = $this->codec->encode()->unapproveToken( + collectionId: $collectionId = $collection->collection_chain_id, + tokenId: $this->tokenIdEncoder->encode($token->token_chain_id), + operator: $operator = $operator->public_key, + ); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable($token->token_chain_id), + 'operator' => SS58Address::encode($operator), + ]); + + $this->assertArraySubset([ + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encodedData' => $encodedData, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + + $this->assertDatabaseHas('transactions', [ + 'id' => $response['id'], + 'method' => $this->method, + 'state' => TransactionState::PENDING->name, + 'encoded_data' => $encodedData, + ]); + + Event::assertDispatched(TransactionCreated::class); + } + + // Exception Path + + public function test_it_will_fail_with_no_args(): void + { + $response = $this->graphql($this->method, [], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of required type "BigInt!" was not provided.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'operator' => $this->operator->public_key, + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of required type "BigInt!" was not provided.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => null, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'operator' => $this->operator->public_key, + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of non-null type "BigInt!" must not be null.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => 'invalid', + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'operator' => $this->operator->public_key, + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" got invalid value "invalid"; Cannot represent following value as uint256', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_collection_id_non_existent(): void + { + Collection::where('collection_chain_id', '=', $collectionId = fake()->numberBetween(2000))?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'operator' => SS58Address::encode($this->operator->public_key), + ], true); + + $this->assertArraySubset( + ['collectionId' => ['The selected collection id is invalid.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'operator' => $this->operator->public_key, + ], true); + + $this->assertStringContainsString( + 'Variable "$tokenId" of required type "EncodableTokenIdInput!" was not provided.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => null, + 'operator' => $this->operator->public_key, + ], true); + + $this->assertStringContainsString( + 'Variable "$tokenId" of non-null type "EncodableTokenIdInput!" must not be null.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => 'invalid', + 'operator' => $this->operator->public_key, + ], true); + + $this->assertStringContainsString( + 'Variable "$tokenId" got invalid value "invalid"; Expected type "EncodableTokenIdInput" to be an object', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_token_id_non_existent(): void + { + Token::where('token_chain_id', '=', $tokenId = fake()->numberBetween())?->delete(); + $operator = SS58Address::encode($this->operator->public_key); + + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => ['integer' => $tokenId], + 'operator' => $operator, + ], true); + + $this->assertArraySubset( + ['operator' => ["Could not find an approval for {$operator} at collection {$this->collection->collection_chain_id} and token {$tokenId}."]], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_no_operator(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + ], true); + + $this->assertStringContainsString( + 'Variable "$operator" of required type "String!" was not provided.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_null_operator(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'operator' => null, + ], true); + + $this->assertStringContainsString( + 'Variable "$operator" of non-null type "String!" must not be null.', + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_invalid_operator(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'operator' => 'invalid', + ], true); + + $this->assertArraySubset( + ['operator' => ['The operator is not a valid substrate account.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } + + public function test_it_will_fail_with_not_found_approval(): void + { + Wallet::where('public_key', '=', $operator = app(Generator::class)->public_key())?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId = $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + 'operator' => $operator = SS58Address::encode($operator), + ], true); + + $this->assertArraySubset( + ['operator' => ["Could not find an approval for {$operator} at collection {$collectionId} and token {$this->token->token_chain_id}."]], + $response['error'], + ); + + Event::assertNotDispatched(TransactionCreated::class); + } +} diff --git a/tests/Feature/GraphQL/Mutations/UpdateTransactionTest.php b/tests/Feature/GraphQL/Mutations/UpdateTransactionTest.php new file mode 100644 index 00000000..0d682ff6 --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/UpdateTransactionTest.php @@ -0,0 +1,337 @@ +transaction = Transaction::factory([ + 'transaction_chain_id' => null, + 'transaction_chain_hash' => null, + 'signed_at_block' => null, + ])->create(); + } + + // Happy Path + public function test_it_can_update_only_transaction_state_without_auth(): void + { + $response = $this->httpGraphql( + $this->method, + [ + 'variables' => [ + 'id' => $this->transaction->id, + 'state' => $state = fake()->randomElement(array_diff( + TransactionState::caseNamesAsArray(), + [TransactionState::PENDING->name], + )), + ], + ] + ); + + $this->assertTrue($response); + $this->assertDatabaseHas('transactions', [ + 'id' => $this->transaction->id, + 'transaction_chain_id' => $this->transaction->transaction_chain_id, + 'transaction_chain_hash' => $this->transaction->transaction_chain_hash, + 'state' => $state, + 'encoded_data' => $this->transaction->encoded_data, + 'signed_at_block' => $this->transaction->signed_at_block, + ]); + + Event::assertDispatched(TransactionUpdated::class); + } + + public function test_it_can_update_only_transaction_state(): void + { + $response = $this->graphql($this->method, [ + 'id' => $this->transaction->id, + 'state' => $state = fake()->randomElement(array_diff( + TransactionState::caseNamesAsArray(), + [TransactionState::PENDING->name], + )), + ]); + + $this->assertTrue($response); + $this->assertDatabaseHas('transactions', [ + 'id' => $this->transaction->id, + 'transaction_chain_id' => $this->transaction->transaction_chain_id, + 'transaction_chain_hash' => $this->transaction->transaction_chain_hash, + 'state' => $state, + 'encoded_data' => $this->transaction->encoded_data, + 'signed_at_block' => $this->transaction->signed_at_block, + ]); + + Event::assertDispatched(TransactionUpdated::class); + } + + public function test_it_can_update_transaction_id(): void + { + Transaction::where('transaction_chain_id', '=', $transactionId = fake()->numerify('######-#'))?->delete(); + + $response = $this->graphql($this->method, [ + 'id' => $this->transaction->id, + 'transactionId' => $transactionId, + ]); + + $this->assertTrue($response); + $this->assertDatabaseHas('transactions', [ + 'id' => $this->transaction->id, + 'transaction_chain_id' => $transactionId, + 'transaction_chain_hash' => $this->transaction->transaction_chain_hash, + 'state' => $this->transaction->state, + 'encoded_data' => $this->transaction->encoded_data, + 'signed_at_block' => $this->transaction->signed_at_block, + ]); + + Event::assertDispatched(TransactionUpdated::class); + } + + public function test_it_can_update_transaction_hash(): void + { + $response = $this->graphql($this->method, [ + 'id' => $this->transaction->id, + 'transactionHash' => $transactionHash = HexConverter::prefix(fake()->sha256()), + ]); + + $this->assertTrue($response); + $this->assertDatabaseHas('transactions', [ + 'id' => $this->transaction->id, + 'transaction_chain_id' => $this->transaction->transaction_chain_id, + 'transaction_chain_hash' => $transactionHash, + 'state' => $this->transaction->state, + 'encoded_data' => $this->transaction->encoded_data, + 'signed_at_block' => $this->transaction->signed_at_block, + ]); + + Event::assertDispatched(TransactionUpdated::class); + } + + public function test_it_can_update_signed_at_block(): void + { + $response = $this->graphql($this->method, [ + 'id' => $this->transaction->id, + 'signedAtBlock' => $signedAtBlock = fake()->numberBetween(), + ]); + + $this->assertTrue($response); + $this->assertDatabaseHas('transactions', [ + 'id' => $this->transaction->id, + 'transaction_chain_id' => $this->transaction->transaction_chain_id, + 'transaction_chain_hash' => $this->transaction->transaction_chain_hash, + 'state' => $this->transaction->state, + 'encoded_data' => $this->transaction->encoded_data, + 'signed_at_block' => $signedAtBlock, + ]); + + Event::assertDispatched(TransactionUpdated::class); + } + + public function test_it_can_update_all_four(): void + { + Transaction::where('transaction_chain_id', '=', $transactionId = fake()->numerify('######-#'))?->delete(); + + $response = $this->graphql($this->method, [ + 'id' => $this->transaction->id, + 'state' => $state = fake()->randomElement(TransactionState::caseNamesAsArray()), + 'transactionId' => $transactionId, + 'transactionHash' => $transactionHash = HexConverter::prefix(fake()->sha256()), + 'signedAtBlock' => $signedAtBlock = fake()->numberBetween(), + ]); + + $this->assertTrue($response); + $this->assertDatabaseHas('transactions', [ + 'id' => $this->transaction->id, + 'transaction_chain_id' => $transactionId, + 'transaction_chain_hash' => $transactionHash, + 'state' => $state, + 'encoded_data' => $this->transaction->encoded_data, + 'signed_at_block' => $signedAtBlock, + ]); + + Event::assertDispatched(TransactionUpdated::class); + } + + // Exception Path + + public function test_it_will_fail_with_id_doesnt_exists(): void + { + Transaction::where('id', '=', $id = fake()->randomDigit())?->delete(); + + $response = $this->graphql($this->method, [ + 'id' => $id, + 'state' => fake()->randomElement(TransactionState::caseNamesAsArray()), + 'transactionId' => fake()->numerify('######-#'), + 'transactionHash' => HexConverter::prefix(fake()->sha256()), + 'signedAtBlock' => fake()->numberBetween(), + ], true); + + $this->assertStringContainsString( + 'Transaction not found.', + $response['error'] + ); + + Event::assertNotDispatched(TransactionUpdated::class); + } + + public function test_it_will_fail_with_invalid_id(): void + { + $response = $this->graphql($this->method, [ + 'id' => 'not_valid', + 'state' => fake()->randomElement(TransactionState::caseNamesAsArray()), + 'transactionId' => fake()->numerify('######-#'), + 'transactionHash' => HexConverter::prefix(fake()->sha256()), + 'signedAtBlock' => fake()->numberBetween(), + ], true); + + $this->assertStringContainsString( + 'Variable "$id" got invalid value "not_valid"; Int cannot represent non-integer value', + $response['error'] + ); + + Event::assertNotDispatched(TransactionUpdated::class); + } + + public function test_it_will_fail_with_invalid_state(): void + { + $response = $this->graphql($this->method, [ + 'id' => $this->transaction->id, + 'state' => 'not_valid', + ], true); + + $this->assertStringContainsString( + 'Variable "$state" got invalid value "not_valid"; Value "not_valid" does not exist in "TransactionState" enum', + $response['error'] + ); + + Event::assertNotDispatched(TransactionUpdated::class); + } + + public function test_it_will_fail_with_invalid_transaction_id(): void + { + $response = $this->graphql($this->method, [ + 'id' => $this->transaction->id, + 'transactionId' => 'not_valid', + ], true); + + $this->assertArraySubset( + ['transactionId' => ['The transaction id has a not valid substrate transaction ID.']], + $response['error'], + ); + + Event::assertNotDispatched(TransactionUpdated::class); + } + + public function test_it_will_fail_with_invalid_transaction_hash(): void + { + $response = $this->graphql($this->method, [ + 'id' => $this->transaction->id, + 'transactionHash' => 'not_valid', + ], true); + + $this->assertArraySubset( + ['transactionHash' => ['The transaction hash has an invalid hex string.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionUpdated::class); + } + + public function test_it_will_fail_with_no_prefix_on_hash(): void + { + $response = $this->graphql($this->method, [ + 'id' => $this->transaction->id, + 'transactionHash' => fake()->sha256(), + ], true); + + $this->assertArraySubset( + ['transactionHash' => ['The transaction hash has an invalid hex string.']], + $response['error'] + ); + + Event::assertNotDispatched(TransactionUpdated::class); + } + + public function test_it_will_fail_with_invalid_signed_at_block(): void + { + $response = $this->graphql($this->method, [ + 'id' => $this->transaction->id, + 'signedAtBlock' => 'not_valid', + ], true); + + $this->assertStringContainsString( + 'Variable "$signedAtBlock" got invalid value "not_valid"; Int cannot represent non-integer value', + $response['error'] + ); + + Event::assertNotDispatched(TransactionUpdated::class); + } + + public function test_it_will_fail_with_no_fields(): void + { + $response = $this->graphql($this->method, [ + 'id' => $this->transaction->id, + ], true); + + $this->assertArraySubset([ + 'state' => ['The state field is required when none of transaction id / transaction hash / signed at block are present.'], + 'transactionId' => ['The transaction id field is required when none of state / transaction hash / signed at block are present.'], + 'transactionHash' => ['The transaction hash field is required when none of state / transaction id / signed at block are present.'], + 'signedAtBlock' => ['The signed at block field is required when none of state / transaction id / transaction hash are present.'], + ], $response['error']); + + Event::assertNotDispatched(TransactionUpdated::class); + } + + public function test_it_will_fail_trying_to_set_a_transaction_id_that_was_already_set(): void + { + $transaction = Transaction::factory()->create(); + + $response = $this->graphql($this->method, [ + 'id' => $transaction->id, + 'transactionId' => $transaction->transaction_chain_id, + ], true); + + $this->assertStringContainsString( + 'The transaction id and hash are immutable once set', + $response['error'] + ); + + Event::assertNotDispatched(TransactionUpdated::class); + } + + public function test_it_will_fail_trying_to_set_a_transaction_hash_that_was_already_set(): void + { + $transaction = Transaction::factory()->create(); + + $response = $this->graphql($this->method, [ + 'id' => $transaction->id, + 'transactionHash' => $transaction->transaction_chain_hash, + ], true); + + $this->assertStringContainsString( + 'The transaction id and hash are immutable once set', + $response['error'] + ); + + Event::assertNotDispatched(TransactionUpdated::class); + } +} diff --git a/tests/Feature/GraphQL/Mutations/UpdateWalletExternalIdTest.php b/tests/Feature/GraphQL/Mutations/UpdateWalletExternalIdTest.php new file mode 100644 index 00000000..4224e1fa --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/UpdateWalletExternalIdTest.php @@ -0,0 +1,242 @@ +wallet = Wallet::factory([ + 'external_id' => fake()->uuid(), + 'public_key' => app(Generator::class)->public_key, + 'managed' => false, + ])->create(); + } + + // Happy Path + + public function test_it_can_update_wallet_with_id(): void + { + $newExternalId = fake()->uuid(); + + $response = $this->graphql($this->method, [ + 'address' => null, + 'externalId' => null, + 'id' => $this->wallet->id, + 'newExternalId' => $newExternalId, + ]); + + $this->assertTrue($response); + $this->assertDatabaseHas('wallets', [ + 'id' => $this->wallet->id, + 'external_id' => $newExternalId, + 'public_key' => $this->wallet->public_key, + 'managed' => false, + ]); + } + + public function test_it_can_update_wallet_with_empty_external_id(): void + { + $response = $this->graphql($this->method, [ + 'account' => $this->wallet->public_key, + 'externalId' => null, + 'id' => null, + 'newExternalId' => '', + ]); + + $this->assertTrue($response); + $this->assertDatabaseHas('wallets', [ + 'id' => $this->wallet->id, + 'external_id' => null, + 'public_key' => $this->wallet->public_key, + 'managed' => false, + ]); + } + + public function test_it_can_update_wallet_with_external_id(): void + { + $newExternalId = fake()->uuid(); + + $response = $this->graphql($this->method, [ + 'address' => null, + 'id' => null, + 'externalId' => $this->wallet->external_id, + 'newExternalId' => $newExternalId, + ]); + + $this->assertTrue($response); + $this->assertDatabaseHas('wallets', [ + 'id' => $this->wallet->id, + 'external_id' => $newExternalId, + 'public_key' => $this->wallet->public_key, + 'managed' => false, + ]); + } + + public function test_it_can_update_wallet_with_address(): void + { + $newExternalId = fake()->uuid(); + + $response = $this->graphql($this->method, [ + 'externalId' => null, + 'id' => null, + 'account' => SS58Address::encode($this->wallet->public_key), + 'newExternalId' => $newExternalId, + ]); + + $this->assertTrue($response); + $this->assertDatabaseHas('wallets', [ + 'id' => $this->wallet->id, + 'external_id' => $newExternalId, + 'public_key' => $this->wallet->public_key, + 'managed' => false, + ]); + } + + // Exception Path + + public function test_it_will_fail_with_managed_wallet(): void + { + $wallet = Wallet::factory([ + 'external_id' => null, + 'managed' => true, + ])->create(); + + $response = $this->graphql($this->method, [ + 'id' => $wallet->id, + 'newExternalId' => fake()->uuid(), + ], true); + + $this->assertStringContainsString( + 'Cannot update the external id on a managed wallet.', + $response['error'] + ); + } + + public function test_it_will_fail_with_missing_new_external_id(): void + { + $response = $this->graphql($this->method, [ + 'id' => $this->wallet->id, + ], true); + + $this->assertStringContainsString( + 'Variable "$newExternalId" of required type "String!" was not provided.', + $response['error'] + ); + } + + public function test_it_will_fail_with_id_not_in_database(): void + { + $response = $this->graphql($this->method, [ + 'id' => fake()->numberBetween(1000), + 'newExternalId' => fake()->uuid(), + ], true); + + $this->assertArraySubset( + ['id' => ['The selected id is invalid.']], + $response['error'] + ); + } + + public function test_it_will_fail_with_external_id_not_in_database(): void + { + $response = $this->graphql($this->method, [ + 'externalId' => fake()->uuid(), + 'newExternalId' => fake()->uuid(), + ], true); + + $this->assertArraySubset( + ['externalId' => ['The selected external id is invalid.']], + $response['error'] + ); + } + + public function test_it_will_fail_with_address_not_in_database(): void + { + $response = $this->graphql($this->method, [ + 'account' => SS58Address::encode(app(Generator::class)->public_key()), + 'newExternalId' => fake()->uuid(), + ], true); + + $this->assertArraySubset( + ['account' => ['Could not find the account specified.']], + $response['error'] + ); + } + + public function test_it_will_fail_with_missing_or_null_filter(): void + { + $response = $this->graphql($this->method, [ + 'account' => null, + 'newExternalId' => fake()->uuid(), + ], true); + + $this->assertArraySubset( + [ + 'id' => ['The id field is required when none of external id / account are present.'], + 'externalId' => ['The external id field is required when none of id / account are present.'], + 'account' => ['The account field is required when none of id / external id are present.'], + ], + $response['error'] + ); + } + + public function test_it_will_fail_with_multiple_filters(): void + { + $response = $this->graphql($this->method, [ + 'id' => $this->wallet->id, + 'externalId' => fake()->uuid(), + 'account' => app(Generator::class)->public_key, + 'newExternalId' => fake()->uuid(), + ], true); + + $this->assertStringContainsString( + 'Only one of these filter(s) can be used: id, externalId, account', + $response['error'] + ); + } + + public function test_it_will_fail_with_invalid_address(): void + { + $response = $this->graphql($this->method, [ + 'account' => 'invalid_address', + 'newExternalId' => fake()->uuid(), + ], true); + + + $this->assertArraySubset( + ['account' => ['The account is not a valid substrate account.']], + $response['error'] + ); + } + + public function test_it_will_fail_with_existing_external_id(): void + { + $otherWallet = Wallet::factory([ + 'external_id' => fake()->uuid(), + ])->create(); + + $response = $this->graphql($this->method, [ + 'id' => $this->wallet->id, + 'newExternalId' => $otherWallet->external_id, + ], true); + + $this->assertArraySubset( + ['newExternalId' => ['The new external id has already been taken.']], + $response['error'] + ); + } +} diff --git a/tests/Feature/GraphQL/Mutations/VerifyAccountTest.php b/tests/Feature/GraphQL/Mutations/VerifyAccountTest.php new file mode 100644 index 00000000..17c43457 --- /dev/null +++ b/tests/Feature/GraphQL/Mutations/VerifyAccountTest.php @@ -0,0 +1,512 @@ +verification = Verification::factory()->create(); + } + + // Happy Path + + public function test_it_can_verify_without_auth(): void + { + $data = app(Generator::class)->sr25519_signature($this->verification->code, isCode: true); + $response = $this->httpGraphql( + $this->method, + [ + 'variables' => [ + 'verificationId' => $this->verification->verification_id, + 'signature' => $data['signature'], + 'account' => $data['address'], + 'cryptoSignatureType' => 'SR25519', + ], + ] + ); + + $this->assertTrue($response); + $this->assertDatabaseHas('verifications', [ + 'verification_id' => $this->verification->verification_id, + 'code' => $this->verification->code, + 'public_key' => $data['publicKey'], + ]); + $this->assertDatabaseHas('wallets', [ + 'public_key' => $data['publicKey'], + 'verification_id' => $this->verification->verification_id, + ]); + } + + public function test_it_can_verify_labelled_mutation_without_auth(): void + { + $data = app(Generator::class)->sr25519_signature($this->verification->code, isCode: true); + $response = $this->httpGraphql( + $this->method . 'WithLabel', + [ + 'variables' => [ + 'verificationId' => $this->verification->verification_id, + 'signature' => $data['signature'], + 'account' => $data['address'], + 'cryptoSignatureType' => 'SR25519', + ], + ] + ); + + $this->assertTrue($response); + $this->assertDatabaseHas('verifications', [ + 'verification_id' => $this->verification->verification_id, + 'code' => $this->verification->code, + 'public_key' => $data['publicKey'], + ]); + $this->assertDatabaseHas('wallets', [ + 'public_key' => $data['publicKey'], + 'verification_id' => $this->verification->verification_id, + ]); + } + + public function test_it_can_verify(): void + { + $data = app(Generator::class)->sr25519_signature($this->verification->code, isCode: true); + + $response = $this->graphql($this->method, [ + 'verificationId' => $this->verification->verification_id, + 'signature' => $data['signature'], + 'account' => $data['address'], + 'cryptoSignatureType' => 'SR25519', + ]); + + $this->assertTrue($response); + $this->assertDatabaseHas('verifications', [ + 'verification_id' => $this->verification->verification_id, + 'code' => $this->verification->code, + 'public_key' => $data['publicKey'], + ]); + $this->assertDatabaseHas('wallets', [ + 'public_key' => $data['publicKey'], + 'verification_id' => $this->verification->verification_id, + ]); + } + + public function test_it_can_verify_with_sr25519(): void + { + $data = app(Generator::class)->sr25519_signature($this->verification->code, isCode: true); + + $response = $this->graphql($this->method, [ + 'verificationId' => $this->verification->verification_id, + 'signature' => $data['signature'], + 'account' => $data['address'], + 'cryptoSignatureType' => 'SR25519', + ]); + + $this->assertTrue($response); + $this->assertDatabaseHas('verifications', [ + 'verification_id' => $this->verification->verification_id, + 'code' => $this->verification->code, + 'public_key' => $data['publicKey'], + ]); + $this->assertDatabaseHas('wallets', [ + 'public_key' => $data['publicKey'], + 'verification_id' => $this->verification->verification_id, + ]); + } + + public function test_it_can_verify_with_ed25519(): void + { + $data = app(Generator::class)->ed25519_signature($this->verification->code, isCode: true); + + $response = $this->graphql($this->method, [ + 'verificationId' => $this->verification->verification_id, + 'signature' => $data['signature'], + 'account' => $data['address'], + 'cryptoSignatureType' => 'ED25519', + ]); + + $this->assertTrue($response); + $this->assertDatabaseHas('verifications', [ + 'verification_id' => $this->verification->verification_id, + 'code' => $this->verification->code, + 'public_key' => $data['publicKey'], + ]); + $this->assertDatabaseHas('wallets', [ + 'public_key' => $data['publicKey'], + 'verification_id' => $this->verification->verification_id, + ]); + } + + public function test_it_can_verify_flow(): void + { + $request = $this->requestAccount(); + $this->verifyAccount($request['verificationId'], $request['verificationCode']); + } + + public function test_it_can_verify_flow_multiple_times(): void + { + $keypair = sodium_crypto_sign_keypair(); + + $request = $this->requestAccount(); + $this->verifyAccount($request['verificationId'], $request['verificationCode'], keypair: $keypair); + + $request = $this->requestAccount(); + $this->verifyAccount($request['verificationId'], $request['verificationCode'], keypair: $keypair); + } + + // Exception Path + + public function test_it_will_fail_with_verification_id_that_doesnt_exists(): void + { + $data = app(Generator::class)->sr25519_signature($this->verification->code, isCode: true); + + $response = $this->graphql($this->method, [ + 'verificationId' => fake()->uuid(), + 'signature' => $data['signature'], + 'account' => $data['address'], + ], true); + + $this->assertArraySubset( + ['verificationId' => ['The selected verification id is invalid.']], + $response['error'] + ); + } + + public function test_it_will_fail_using_wrong_address_for_sr25519(): void + { + $data = app(Generator::class)->sr25519_signature($this->verification->code, isCode: true); + + $response = $this->graphql($this->method, [ + 'verificationId' => $this->verification->verification_id, + 'signature' => $data['signature'], + 'account' => app(Generator::class)->public_key(), + ], true); + + $this->assertStringContainsString( + 'The signature provided is not valid.', + $response['error'] + ); + } + + public function test_it_will_fail_using_wrong_address_for_ed25519(): void + { + $data = app(Generator::class)->ed25519_signature($this->verification->code, isCode: true); + + $response = $this->graphql($this->method, [ + 'verificationId' => $this->verification->verification_id, + 'signature' => $data['signature'], + 'account' => app(Generator::class)->public_key(), + 'cryptoSignatureType' => 'ED25519', + ], true); + + $this->assertStringContainsString( + 'The signature provided is not valid.', + $response['error'] + ); + } + + public function test_it_will_fail_with_wrong_sr25519_signature(): void + { + $data = app(Generator::class)->sr25519_signature(fake()->word(), isCode: true); + + $response = $this->graphql($this->method, [ + 'verificationId' => $this->verification->verification_id, + 'signature' => $data['signature'], + 'account' => $data['address'], + ], true); + + $this->assertStringContainsString( + 'The signature provided is not valid.', + $response['error'] + ); + } + + public function test_it_will_fail_with_wrong_ed25519_signature(): void + { + $data = app(Generator::class)->ed25519_signature(fake()->word(), isCode: true); + + $response = $this->graphql($this->method, [ + 'verificationId' => $this->verification->verification_id, + 'signature' => $data['signature'], + 'account' => $data['address'], + 'cryptoSignatureType' => 'ED25519', + ], true); + + $this->assertStringContainsString( + 'The signature provided is not valid.', + $response['error'] + ); + } + + public function test_it_will_fail_with_empty_verification_id(): void + { + $data = app(Generator::class)->sr25519_signature($this->verification->code, isCode: true); + + $response = $this->graphql($this->method, [ + 'verificationId' => '', + 'signature' => $data['signature'], + 'account' => $data['address'], + ], true); + + $this->assertArraySubset( + [ + 'verificationId' => [ + 0 => 'The verification id field must have a value.', + ], + ], + $response['error'] + ); + } + + public function test_it_will_fail_no_verification_id(): void + { + $data = app(Generator::class)->ed25519_signature($this->verification->code, isCode: true); + + $response = $this->graphql($this->method, [ + 'signature' => $data['signature'], + 'account' => $data['address'], + ], true); + + $this->assertStringContainsString( + 'Variable "$verificationId" of required type "String!" was not provided.', + $response['error'] + ); + } + + public function test_it_will_fail_null_verification_id(): void + { + $data = app(Generator::class)->ed25519_signature($this->verification->code, isCode: true); + + $response = $this->graphql($this->method, [ + 'verificationId' => null, + 'signature' => $data['signature'], + 'account' => $data['address'], + ], true); + + $this->assertStringContainsString( + 'Variable "$verificationId" of non-null type "String!" must not be null.', + $response['error'] + ); + } + + public function test_it_will_fail_with_empty_signature(): void + { + $data = app(Generator::class)->ed25519_signature($this->verification->code, isCode: true); + + $response = $this->graphql($this->method, [ + 'verificationId' => $this->verification->verification_id, + 'signature' => '', + 'account' => $data['address'], + ], true); + + $this->assertArraySubset( + [ + 'signature' => [ + 0 => 'The signature field must have a value.', + ], + ], + $response['error'] + ); + } + + public function test_it_will_fail_no_signature(): void + { + $data = app(Generator::class)->ed25519_signature($this->verification->code, isCode: true); + + $response = $this->graphql($this->method, [ + 'verificationId' => $this->verification->verification_id, + 'account' => $data['address'], + ], true); + + $this->assertStringContainsString( + 'Variable "$signature" of required type "String!" was not provided.', + $response['error'] + ); + } + + public function test_it_will_fail_null_signature(): void + { + $data = app(Generator::class)->ed25519_signature($this->verification->code, isCode: true); + + $response = $this->graphql($this->method, [ + 'verificationId' => $this->verification->verification_id, + 'signature' => null, + 'public_key' => $data['address'], + ], true); + + $this->assertStringContainsString( + 'Variable "$signature" of non-null type "String!" must not be null.', + $response['error'] + ); + } + + public function test_it_will_fail_with_invalid_signature(): void + { + $data = app(Generator::class)->ed25519_signature($this->verification->code, isCode: true); + + $response = $this->graphql($this->method, [ + 'verificationId' => $this->verification->verification_id, + 'signature' => 'invalid', + 'account' => $data['address'], + ], true); + + $this->assertArraySubset( + [ + 'signature' => [ + 0 => 'The signature has an invalid hex string.', + ], + ], + $response['error'] + ); + } + + public function test_it_will_fail_no_address(): void + { + $data = app(Generator::class)->ed25519_signature($this->verification->code, isCode: true); + + $response = $this->graphql($this->method, [ + 'verificationId' => $this->verification->verification_id, + 'signature' => $data['signature'], + ], true); + + $this->assertStringContainsString( + 'Variable "$account" of required type "String!" was not provided.', + $response['error'] + ); + } + + public function test_it_will_fail_with_empty_address(): void + { + $data = app(Generator::class)->ed25519_signature($this->verification->code, isCode: true); + + $response = $this->graphql($this->method, [ + 'verificationId' => $this->verification->verification_id, + 'signature' => $data['signature'], + 'account' => '', + ], true); + + $this->assertArraySubset( + [ + 'account' => [ + 0 => 'The account field must have a value.', + ], + ], + $response['error'] + ); + } + + public function test_it_will_fail_null_address(): void + { + $data = app(Generator::class)->ed25519_signature($this->verification->code, isCode: true); + + $response = $this->graphql($this->method, [ + 'verificationId' => $this->verification->verification_id, + 'signature' => $data['signature'], + 'account' => null, + ], true); + + $this->assertStringContainsString( + 'Variable "$account" of non-null type "String!" must not be null.', + $response['error'] + ); + } + + public function test_it_will_fail_invalid_address(): void + { + $data = app(Generator::class)->ed25519_signature($this->verification->code, isCode: true); + + $response = $this->graphql($this->method, [ + 'verificationId' => $this->verification->verification_id, + 'signature' => $data['signature'], + 'account' => 'not_valid', + ], true); + + $this->assertArraySubset( + ['account' => ['The account is not a valid substrate account.']], + $response['error'] + ); + } + + public function test_it_will_fail_without_prefix_signature(): void + { + $data = app(Generator::class)->ed25519_signature($this->verification->code, isCode: true); + + $response = $this->graphql($this->method, [ + 'verificationId' => $this->verification->verification_id, + 'signature' => HexConverter::unPrefix($data['signature']), + 'account' => $data['address'], + ], true); + + $this->assertArraySubset( + ['signature' => ['The signature has an invalid hex string.']], + $response['error'] + ); + } + + protected function requestAccount(): array + { + $response = $this->graphql('RequestAccount', [ + 'callback' => fake()->url(), + ]); + + $this->assertNotEmpty($verificationId = $response['verificationId']); + $this->assertNotEmpty($verificationCode = explode(':', $response['qrCode'])[3]); + + return [ + 'verificationId' => $verificationId, + 'verificationCode' => $verificationCode, + ]; + } + + protected function verifyAccount( + string $verificationId, + string $verificationCode, + ?string $externalId = null, + ?string $keypair = null + ) { + $data = app(Generator::class)->ed25519_signature($verificationCode, isCode: true); + + if ($keypair) { + $publicKey = sodium_crypto_sign_publickey($keypair); + $signature = app(Generator::class)->signWithCode($verificationCode, $keypair); + $address = SS58Address::encode(HexConverter::hexToBytes(bin2hex($publicKey))); + + $data = [ + 'publicKey' => SS58Address::getPublicKey($address), + 'signature' => $signature, + ]; + } + + $response = $this->graphql($this->method, [ + 'verificationId' => $verificationId, + 'signature' => $data['signature'], + 'account' => $data['publicKey'], + 'cryptoSignatureType' => 'ED25519', + ]); + + $this->assertTrue($response); + $this->assertDatabaseHas('verifications', [ + 'verification_id' => $verificationId, + 'code' => $verificationCode, + 'public_key' => $data['publicKey'], + ]); + $this->assertDatabaseHas('wallets', [ + 'public_key' => $data['publicKey'], + 'verification_id' => $verificationId, + 'external_id' => $externalId, + ]); + } +} diff --git a/tests/Feature/GraphQL/Queries/GetAccountVerifiedTest.php b/tests/Feature/GraphQL/Queries/GetAccountVerifiedTest.php new file mode 100644 index 00000000..fab79ad6 --- /dev/null +++ b/tests/Feature/GraphQL/Queries/GetAccountVerifiedTest.php @@ -0,0 +1,261 @@ +verification = Verification::factory([ + 'public_key' => app(Generator::class)->public_key(), + ])->create(); + } + + public function test_it_can_get_a_verified_address_by_verification_id(): void + { + $response = $this->graphql($this->method, [ + 'verificationId' => $verificationId = $this->verification->verification_id, + ]); + + $this->assertArraySubset([ + 'verified' => true, + 'account' => [ + 'publicKey' => $publicKey = $this->verification->public_key, + ], + ], $response); + + $this->assertDatabaseHas('verifications', [ + 'verification_id' => $verificationId, + 'public_key' => $publicKey, + 'code' => $this->verification->code, + ]); + } + + public function test_it_can_get_by_verification_id_a_not_verified(): void + { + $verification = Verification::factory()->create(); + + $response = $this->graphql($this->method, [ + 'verificationId' => $verificationId = $verification->verification_id, + ]); + + $this->assertArraySubset([ + 'verified' => false, + 'account' => [ + 'publicKey' => null, + ], + ], $response); + + $this->assertDatabaseHas('verifications', [ + 'verification_id' => $verificationId, + 'public_key' => null, + 'code' => $verification->code, + ]); + } + + public function test_it_returns_false_to_a_not_verified_address(): void + { + Verification::where('public_key', '=', $publicKey = app(Generator::class)->public_key())?->delete(); + + $response = $this->graphql($this->method, [ + 'account' => SS58Address::encode($publicKey), + ]); + + $this->assertArraySubset([ + 'verified' => false, + 'account' => [ + 'publicKey' => null, + ], + ], $response); + } + + public function test_it_returns_false_to_a_verification_id_that_could_be_valid_but_doesnt_exists(): void + { + $verification = Verification::factory()->create(); + $verificationId = $verification->verification_id; + $verification->delete(); + + $response = $this->graphql($this->method, [ + 'verificationId' => $verificationId, + ]); + + $this->assertArraySubset([ + 'verified' => false, + 'account' => [ + 'publicKey' => null, + ], + ], $response); + } + + public function test_it_can_get_a_verification_by_address(): void + { + $response = $this->graphql($this->method, [ + 'account' => SS58Address::encode($publicKey = $this->verification->public_key), + ]); + + $this->assertArraySubset([ + 'verified' => true, + 'account' => [ + 'publicKey' => $publicKey, + ], + ], $response); + + $this->assertDatabaseHas('verifications', [ + 'verification_id' => $this->verification->verification_id, + 'public_key' => $publicKey, + 'code' => $this->verification->code, + ]); + } + + public function test_it_can_verify_a_second_request_with_same_address(): void + { + $verification = Verification::factory([ + 'public_key' => $publicKey = $this->verification->public_key, + ])->create(); + + $response = $this->graphql($this->method, [ + 'account' => $publicKey, + ]); + + $this->assertArraySubset([ + 'verified' => true, + 'account' => [ + 'publicKey' => $publicKey, + ], + ], $response); + + $this->assertDatabaseHas('verifications', [ + 'verification_id' => $verification->verification_id, + 'public_key' => $publicKey, + 'code' => $verification->code, + ]); + } + + public function test_it_will_fail_with_no_args(): void + { + $response = $this->graphql($this->method, [], true); + + $this->assertArraySubset( + [ + 'verificationId' => ['The verification id field is required when account is not present.'], + 'account' => ['The account field is required when verification id is not present.'], + ], + $response['error'] + ); + } + + public function test_it_will_fail_with_null_address(): void + { + $response = $this->graphql($this->method, [ + 'account' => null, + ], true); + + $this->assertArraySubset( + [ + 'verificationId' => ['The verification id field is required when account is not present.'], + 'account' => ['The account field is required when verification id is not present.'], + ], + $response['error'] + ); + } + + public function test_it_will_fail_with_null_verification_id(): void + { + $response = $this->graphql($this->method, [ + 'verificationId' => null, + ], true); + + $this->assertArraySubset( + [ + 'verificationId' => ['The verification id field is required when account is not present.'], + 'account' => ['The account field is required when verification id is not present.'], + ], + $response['error'] + ); + } + + // Exception Path + + public function test_it_will_fail_with_empty_address(): void + { + $response = $this->graphql($this->method, [ + 'account' => '', + ], true); + + $this->assertArraySubset( + [ + 'verificationId' => ['The verification id field is required when account is not present.'], + 'account' => ['The account field is required when verification id is not present.'], + ], + $response['error'] + ); + } + + public function test_it_will_fail_with_empty_verification_id(): void + { + $response = $this->graphql($this->method, [ + 'verificationId' => '', + ], true); + + $this->assertArraySubset( + [ + 'verificationId' => ['The verification id field is required when account is not present.'], + 'account' => ['The account field is required when verification id is not present.'], + ], + $response['error'] + ); + } + + public function test_it_will_fail_with_invalid_address(): void + { + $response = $this->graphql($this->method, [ + 'account' => 'invalid', + ], true); + + $this->assertArraySubset( + ['account' => ['The account is not a valid substrate account.']], + $response['error'] + ); + } + + public function test_it_will_fail_with_invalid_verification_id(): void + { + $response = $this->graphql($this->method, [ + 'verificationId' => 'invalid', + ], true); + + $this->assertArraySubset( + ['verificationId' => ['The verification ID is not valid.']], + $response['error'] + ); + } + + public function test_it_will_fail_with_with_both_args(): void + { + $response = $this->graphql($this->method, [ + 'verificationId' => $this->verification->verification_id, + 'account' => $this->verification->public_key, + ], true); + + $this->assertArraySubset( + [ + 'verificationId' => ['The verification id field prohibits account from being present.'], + 'account' => ['The account field prohibits verification id from being present.'], + ], + $response['error'] + ); + } +} diff --git a/tests/Feature/GraphQL/Queries/GetCollectionTest.php b/tests/Feature/GraphQL/Queries/GetCollectionTest.php new file mode 100644 index 00000000..2906f666 --- /dev/null +++ b/tests/Feature/GraphQL/Queries/GetCollectionTest.php @@ -0,0 +1,264 @@ +wallet = Wallet::factory()->create(); + $this->collectionOwner = Wallet::factory()->create(); + $this->collection = Collection::factory([ + 'owner_wallet_id' => $this->collectionOwner, + 'token_count' => 1, + 'attribute_count' => 1, + ])->create(); + $this->token = Token::factory([ + 'collection_id' => $this->collection, + 'attribute_count' => 1, + ])->create(); + $this->collectionAccount = CollectionAccount::factory([ + 'collection_id' => $this->collection, + 'wallet_id' => $this->wallet, + 'account_count' => 1, + ])->create(); + $this->collectionAccountApproval = CollectionAccountApproval::factory([ + 'collection_account_id' => $this->collectionAccount, + 'wallet_id' => $this->collectionOwner, + ])->create(); + $this->tokenAccount = TokenAccount::factory([ + 'collection_id' => $this->collection, + 'token_id' => $this->token, + 'wallet_id' => $this->wallet, + ])->create(); + $this->collectionAttribute = Attribute::factory([ + 'collection_id' => $this->collection, + 'token_id' => null, + ])->create(); + $this->tokenAttribute = Attribute::factory([ + 'collection_id' => $this->collection, + 'token_id' => $this->token, + ])->create(); + } + + public function test_it_can_get_a_collection_with_all_data(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId = $this->collection->collection_chain_id, + ]); + + $this->assertArraySubset([ + 'collectionId' => $collectionId, + 'maxTokenCount' => $this->collection->max_token_count, + 'maxTokenSupply' => $this->collection->max_token_supply, + 'forceSingleMint' => $this->collection->force_single_mint, + 'frozen' => $this->collection->is_frozen, + 'network' => $this->collection->network, + 'owner' => [ + 'account' => [ + 'publicKey' => $this->collectionOwner->public_key, + ], + ], + 'attributes' => [ + [ + 'key' => $this->collectionAttribute->key, + 'value' => $this->collectionAttribute->value, + ], + ], + 'tokens' => [ + 'edges' => [ + [ + 'node' => [ + 'tokenId' => $this->token->token_chain_id, + ], + ], + ], + ], + 'accounts' => [ + 'edges' => [ + [ + 'node' => [ + 'accountCount' => $this->collectionAccount->account_count, + 'isFrozen' => $this->collectionAccount->is_frozen, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->wallet->public_key, + ], + ], + 'approvals' => [ + [ + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->collectionOwner->public_key, + ], + ], + 'expiration' => $this->collectionAccountApproval->expiration, + ], + ], + ], + ], + ], + ], + ], $response); + } + + public function test_it_can_get_a_collection_with_big_int_collection_id(): void + { + $collection = Collection::factory([ + 'collection_chain_id' => Hex::MAX_UINT128, + ])->create(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId = $collection->collection_chain_id, + ]); + + $this->assertArraySubset([ + 'collectionId' => $collectionId, + ], $response); + } + + public function test_it_max_token_count_can_be_null(): void + { + $collection = Collection::factory([ + 'max_token_count' => null, + ])->create(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId = $collection->collection_chain_id, + ]); + + $this->assertArraySubset([ + 'collectionId' => $collectionId, + 'maxTokenCount' => null, + ], $response); + } + + public function test_it_max_token_supply_can_be_null(): void + { + $collection = Collection::factory([ + 'max_token_supply' => null, + ])->create(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId = $collection->collection_chain_id, + ]); + + $this->assertArraySubset([ + 'collectionId' => $collectionId, + 'maxTokenSupply' => null, + ], $response); + } + + public function test_it_will_fail_with_no_args(): void + { + $response = $this->graphql($this->method, [], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of required type "BigInt!" was not provided', + $response['error'], + ); + } + + // Exception Path + + public function test_it_will_fail_with_collection_id_null(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => null, + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of non-null type "BigInt!" must not be null.', + $response['error'], + ); + } + + public function test_it_will_fail_with_collection_id_negative(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => -1, + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" got invalid value -1; Cannot represent following value as uint256', + $response['error'], + ); + } + + public function test_it_will_fail_with_collection_id_empty_string(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => '', + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" got invalid value (empty string)', + $response['error'], + ); + } + + public function test_it_will_fail_with_invalid_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => 'invalid', + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" got invalid value "invalid"; Cannot represent following value as uint256', + $response['error'], + ); + } + + public function test_it_will_fail_if_collection_id_doesnt_exist(): void + { + Collection::where('collection_chain_id', '=', $collectionId = fake()->numberBetween(2000))?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + ], true); + + $this->assertArraySubset( + ['collectionId' => ['The selected collection id is invalid.']], + $response['error'], + ); + } + + public function test_it_will_fail_with_overflow_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => Hex::MAX_UINT256, + ], true); + + $this->assertArraySubset( + ['collectionId' => ['The selected collection id is invalid.']], + $response['error'], + ); + } +} diff --git a/tests/Feature/GraphQL/Queries/GetCollectionsTest.php b/tests/Feature/GraphQL/Queries/GetCollectionsTest.php new file mode 100644 index 00000000..46367bec --- /dev/null +++ b/tests/Feature/GraphQL/Queries/GetCollectionsTest.php @@ -0,0 +1,309 @@ +wallet = Wallet::factory()->create(); + $this->collections = $this->generateCollections(); + } + + protected function tearDown(): void + { + Collection::destroy($this->collections); + + parent::tearDown(); + } + + public function test_it_can_fetch_with_no_args(): void + { + $response = $this->graphql($this->method); + + $this->assertTrue(count($response['edges']) > 0); + } + + public function test_it_can_fetch_with_empty_args(): void + { + $response = $this->graphql($this->method, []); + + $this->assertTrue(count($response['edges']) > 0); + } + + public function test_it_can_fetch_with_null_collection_ids(): void + { + $response = $this->graphql($this->method, [ + 'collectionIds' => null, + ]); + + $this->assertTrue(count($response['edges']) > 0); + } + + public function test_it_can_fetch_with_empty_collection_ids(): void + { + $response = $this->graphql($this->method, [ + 'collectionIds' => [], + ]); + + $this->assertTrue(count($response['edges']) > 0); + } + + public function test_it_can_fetch_with_null_after(): void + { + $response = $this->graphql($this->method, [ + 'after' => null, + ]); + + $this->assertTrue(count($response['edges']) > 0); + $this->assertFalse($response['pageInfo']['hasPreviousPage']); + } + + public function test_it_can_fetch_with_null_first(): void + { + $response = $this->graphql($this->method, [ + 'first' => null, + ]); + + $this->assertTrue(count($response['edges']) > 0); + } + + public function test_it_can_filter_by_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionIds' => [$collectionId = $this->collections[0]->collection_chain_id], + ]); + + $this->assertTrue(1 === $response['totalCount']); + $this->assertEquals( + $collectionId, + $response['edges'][0]['node']['collectionId'] + ); + } + + public function test_it_can_get_a_single_collection_with_all_data(): void + { + $token = Token::firstWhere('collection_id', '=', ($collection = fake()->randomElement($this->collections))->id); + $attribute = Attribute::factory([ + 'collection_id' => $collection->id, + 'token_id' => null, + ])->create(); + $collectionAccount = CollectionAccount::firstWhere([ + 'collection_id' => $collection->id, + 'wallet_id' => $this->wallet->id, + ]); + $collectionAccountApproval = CollectionAccountApproval::firstWhere([ + 'collection_account_id' => $collectionAccount->id, + ]); + + $response = $this->graphql($this->method, [ + 'collectionIds' => [$collectionId = $collection->collection_chain_id], + ]); + + $this->assertArraySubset([ + 'collectionId' => $collectionId, + 'maxTokenCount' => $collection->max_token_count, + 'maxTokenSupply' => $collection->max_token_supply, + 'forceSingleMint' => $collection->force_single_mint, + 'frozen' => $collection->is_frozen, + 'network' => $collection->network, + 'owner' => [ + 'account' => [ + 'publicKey' => $this->wallet->public_key, + ], + ], + 'attributes' => [ + [ + 'key' => $attribute->key, + 'value' => $attribute->value, + ], + ], + 'tokens' => [ + 'edges' => [ + [ + 'node' => [ + 'tokenId' => $token->token_chain_id, + ], + ], + ], + ], + 'accounts' => [ + 'edges' => [ + [ + 'node' => [ + 'accountCount' => $collectionAccount->account_count, + 'isFrozen' => $collectionAccount->is_frozen, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->wallet->public_key, + ], + ], + 'approvals' => [ + [ + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->wallet->public_key, + ], + ], + 'expiration' => $collectionAccountApproval->expiration, + ], + ], + ], + ], + ], + ], + ], $response['edges'][0]['node']); + } + + public function test_it_can_get_a_collection_with_big_int_collection_id(): void + { + Collection::where('collection_chain_id', '=', $collectionId = Hex::MAX_UINT128)?->delete(); + Collection::factory([ + 'collection_chain_id' => $collectionId, + ])->create(); + + $response = $this->graphql($this->method, [ + 'collectionIds' => [$collectionId], + ]); + + $this->assertEquals( + $collectionId, + $response['edges'][0]['node']['collectionId'] + ); + } + + public function test_it_will_return_empty_for_a_collection_id_that_doesnt_exists(): void + { + Collection::where('collection_chain_id', '=', $collectionId = fake()->numberBetween(2000))?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionIds' => [$this->collections[0]->collection_chain_id, $collectionId], + ]); + + $this->assertTrue(1 === $response['totalCount']); + } + + public function test_it_will_fail_with_collection_id_negative(): void + { + $response = $this->graphql($this->method, [ + 'collectionIds' => [-1], + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionIds" got invalid value -1 at "collectionIds[0]"; Cannot represent following value as uint256', + $response['error'], + ); + } + + public function test_it_will_fail_with_invalid_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionIds' => 'invalid', + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionIds" got invalid value "invalid"; Cannot represent following value as uint256', + $response['error'], + ); + } + + // Exception Path + + public function test_it_will_fail_with_invalid_collection_ids(): void + { + $response = $this->graphql($this->method, [ + 'collectionIds' => ['invalid'], + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionIds" got invalid value "invalid" at "collectionIds[0]"; Cannot represent following value as uint256', + $response['error'], + ); + } + + public function test_it_will_fail_with_overflow_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionIds' => [Hex::MAX_UINT256], + ], true); + + $this->assertArraySubset( + [ + 'collectionIds.0' => [ + 0 => 'The collectionIds.0 is too large, the maximum value it can be is 340282366920938463463374607431768211455.', + ], + ], + $response['error'], + ); + } + + protected function generateCollections(?int $numberOfTransactions = 5): CollectionSupport + { + return collect(range(0, $numberOfTransactions))->map( + fn () => $this->createCollection(), + ); + } + + protected function createCollection(): Collection + { + $collection = Collection::factory([ + 'owner_wallet_id' => $this->wallet, + 'token_count' => 1, + 'attribute_count' => 1, + ])->create(); + + $token = Token::factory([ + 'collection_id' => $collection, + 'attribute_count' => 1, + ])->create(); + + $collectionAccount = CollectionAccount::factory([ + 'collection_id' => $collection, + 'wallet_id' => $this->wallet, + 'account_count' => 1, + ])->create(); + + CollectionAccountApproval::factory([ + 'collection_account_id' => $collectionAccount, + 'wallet_id' => $this->wallet, + ])->create(); + + TokenAccount::factory([ + 'collection_id' => $collection, + 'token_id' => $token, + 'wallet_id' => $this->wallet, + ])->create(); + + Attribute::factory([ + 'collection_id' => $collection, + ])->create(); + + Attribute::factory([ + 'collection_id' => $collection, + 'token_id' => $token, + ])->create(); + + return $collection; + } +} diff --git a/tests/Feature/GraphQL/Queries/GetPendingEventsTest.php b/tests/Feature/GraphQL/Queries/GetPendingEventsTest.php new file mode 100644 index 00000000..16e5e4b6 --- /dev/null +++ b/tests/Feature/GraphQL/Queries/GetPendingEventsTest.php @@ -0,0 +1,70 @@ +create()->load(['owner']); + CollectionCreated::safeBroadcast($collection); + } + + public function test_it_can_fetch_pending_events(): void + { + $response = $this->graphql($this->method); + $this->assertNotEmpty($response['edges']); + } + + public function test_it_can_acknowledge_pending_event(): void + { + $response = $this->graphql($this->method, [ + 'acknowledgeEvents' => true, + ]); + $this->assertNotEmpty($response); + + $response = $this->graphql($this->method); + $this->assertEmpty($response['edges']); + } + + public function test_it_can_fetch_with_acknowledge_equals_to_null(): void + { + $response = $this->graphql($this->method, [ + 'acknowledgeEvents' => null, + ]); + $this->assertNotEmpty($response['edges']); + } + + public function test_it_can_fetch_with_acknowledge_false_doesnt_clean_events(): void + { + $response = $this->graphql($this->method, [ + 'acknowledgeEvents' => false, + ]); + $this->assertNotEmpty($response['edges']); + } + + // Exception Path + + public function test_it_will_fail_with_invalid_acknowledge_events(): void + { + $response = $this->graphql($this->method, [ + 'acknowledgeEvents' => 'invalid', + ], true); + + $this->assertStringContainsString( + 'Variable "$acknowledgeEvents" got invalid value "invalid"; Boolean cannot represent a non boolean value', + $response['error'] + ); + } +} diff --git a/tests/Feature/GraphQL/Queries/GetPendingWalletsTest.php b/tests/Feature/GraphQL/Queries/GetPendingWalletsTest.php new file mode 100644 index 00000000..ba58d5a5 --- /dev/null +++ b/tests/Feature/GraphQL/Queries/GetPendingWalletsTest.php @@ -0,0 +1,95 @@ +wallet = Wallet::factory([ + 'managed' => true, + 'public_key' => null, + ])->create(); + } + + public function test_it_can_get_pending_wallets_without_auth(): void + { + $response = $this->httpGraphql($this->method); + $this->assertArraySubset( + [ + 'id' => $this->wallet->id, + 'account' => [ + 'publicKey' => $this->wallet->public_key, + ], + 'externalId' => $this->wallet->external_id, + 'managed' => $this->wallet->managed, + 'network' => $this->wallet->network, + ], + Arr::last($response['edges'])['node'], + ); + } + + public function test_it_can_get_pending_wallets(): void + { + $response = $this->graphql($this->method, []); + + $this->assertArraySubset( + [ + 'id' => $this->wallet->id, + 'account' => [ + 'publicKey' => $this->wallet->public_key, + ], + 'externalId' => $this->wallet->external_id, + 'managed' => $this->wallet->managed, + 'network' => $this->wallet->network, + ], + Arr::last($response['edges'])['node'], + ); + } + + public function test_it_will_not_appear_just_created_wallet_that_is_not_managed(): void + { + Wallet::factory([ + 'managed' => false, + 'public_key' => null, + ])->create(); + + $response = $this->graphql($this->method, []); + + $this->assertEmpty(array_filter( + $response['edges'], + fn ($wallet) => false === $wallet['node']['managed'], + )); + } + + public function test_it_will_not_appear_a_just_created_managed_wallet_with_address(): void + { + Wallet::where('public_key', '=', $publicKey = app(Generator::class)->public_key())?->delete(); + Wallet::factory([ + 'managed' => true, + 'public_key' => $publicKey, + ])->create(); + + $response = $this->graphql($this->method, []); + + $this->assertEmpty(array_filter( + $response['edges'], + fn ($wallet) => $wallet['node']['account']['publicKey'] === $publicKey, + )); + } +} diff --git a/tests/Feature/GraphQL/Queries/GetTokenTest.php b/tests/Feature/GraphQL/Queries/GetTokenTest.php new file mode 100644 index 00000000..c886de50 --- /dev/null +++ b/tests/Feature/GraphQL/Queries/GetTokenTest.php @@ -0,0 +1,377 @@ +wallet = Wallet::factory()->create(); + $this->collectionOwner = Wallet::factory()->create(); + $this->collection = Collection::factory([ + 'owner_wallet_id' => $this->collectionOwner, + 'token_count' => 1, + ])->create(); + $this->token = Token::factory([ + 'collection_id' => $this->collection, + 'attribute_count' => 1, + ])->create(); + $this->tokenIdEncoder = new Integer($this->token->token_chain_id); + CollectionAccount::factory([ + 'collection_id' => $this->collection, + 'wallet_id' => $this->wallet, + 'account_count' => 1, + ])->create(); + $this->tokenAccount = TokenAccount::factory([ + 'collection_id' => $this->collection, + 'token_id' => $this->token, + 'wallet_id' => $this->wallet, + ])->create(); + $this->tokenAttribute = Attribute::factory([ + 'collection_id' => $this->collection, + 'token_id' => $this->token, + ])->create(); + $this->tokenAccountApproval = TokenAccountApproval::factory([ + 'token_account_id' => $this->tokenAccount, + 'wallet_id' => $this->collectionOwner, + ])->create(); + $this->tokenAccountNamedReserve = TokenAccountNamedReserve::factory([ + 'token_account_id' => $this->tokenAccount, + ])->create(); + } + + public function test_it_can_replace_id_metadata(): void + { + $this->tokenAttribute->forceFill(['key' => 'uri', 'value' => 'https://example.com/{id}'])->save(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + ]); + + $this->assertArraySubset([ + 'attributes' => [[ + 'key' => 'uri', + 'value' => 'https://example.com/' . $this->token->token_chain_id, + ], + ], + ], $response); + } + + public function test_it_can_get_a_token_with_all_data(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId = $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + ]); + + $this->assertArraySubset([ + 'tokenId' => $this->tokenIdEncoder->encode(), + 'supply' => $this->token->supply, + 'cap' => $this->token->cap, + 'capSupply' => $this->token->cap_supply, + 'isFrozen' => $this->token->is_frozen, + 'minimumBalance' => $this->token->minimum_balance, + 'unitPrice' => $this->token->unit_price, + 'mintDeposit' => $this->token->mint_deposit, + 'attributeCount' => $this->token->attribute_count, + 'collection' => [ + 'collectionId' => $collectionId, + ], + 'attributes' => [ + [ + 'key' => $this->tokenAttribute->key, + 'value' => $this->tokenAttribute->value, + ], + ], + 'accounts' => [ + 'totalCount' => 1, + 'edges' => [ + [ + 'node' => [ + 'balance' => $this->tokenAccount->balance, + 'reservedBalance' => $this->tokenAccount->reserved_balance, + 'isFrozen' => $this->tokenAccount->is_frozen, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->wallet->public_key, + ], + ], + 'approvals' => [ + [ + 'amount' => $this->tokenAccountApproval->amount, + 'expiration' => $this->tokenAccountApproval->expiration, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->collectionOwner->public_key, + ], + ], + ], + ], + 'namedReserves' => [ + [ + 'pallet' => $this->tokenAccountNamedReserve->pallet, + 'amount' => $this->tokenAccountNamedReserve->amount, + ], + ], + ], + ], + ], + ], + ], $response); + } + + public function test_it_can_get_a_collection_with_big_int_token_id(): void + { + $token = Token::factory([ + 'token_chain_id' => Hex::MAX_UINT128, + ])->create(); + $collection = Collection::find($token->collection_id); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable($token->token_chain_id), + ]); + + $this->assertArraySubset([ + 'tokenId' => $this->tokenIdEncoder->encode($token->token_chain_id), + ], $response); + } + + public function test_it_can_fetch_token_metadata(): void + { + $collection = Collection::factory()->create(); + $token = Token::factory([ + 'collection_id' => $collection, + ])->create(); + + Attribute::factory([ + 'collection_id' => $collection, + 'token_id' => $token->id, + 'key' => 'uri', + 'value' => 'https://enjin.io/mock/metadata/token.json', + ])->create(); + + Http::fake(fn () => Http::response([ + 'name' => 'Mock Token', + 'description' => 'Mock token description', + 'image' => 'https://enjin.io/mock/metadata/token.png', + ])); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId = $collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable($token->token_chain_id), + 'metadata' => true, + ]); + + $this->assertArraySubset([ + 'tokenId' => $this->tokenIdEncoder->encode($token->token_chain_id), + 'metadata' => (object) [ + 'name' => 'Mock Token', + 'description' => 'Mock token description', + 'image' => 'https://enjin.io/mock/metadata/token.png', + ], + ], $response); + } + + public function test_it_will_fail_with_no_args(): void + { + $response = $this->graphql($this->method, [], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of required type "BigInt!" was not provided', + $response['error'], + ); + } + + // Exception Path + + public function test_it_will_fail_with_no_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'tokenId' => $this->token->token_chain_id, + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of required type "BigInt!" was not provided.', + $response['error'], + ); + } + + public function test_it_will_fail_with_no_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + ], true); + + $this->assertStringContainsString( + 'Variable "$tokenId" of required type "EncodableTokenIdInput!" was not provided.', + $response['error'], + ); + } + + public function test_it_will_fail_with_collection_id_equals_null(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => null, + 'tokenId' => $this->token->token_chain_id, + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" of non-null type "BigInt!" must not be null.', + $response['error'], + ); + } + + public function test_it_will_fail_with_token_id_equals_null(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => null, + ], true); + + $this->assertStringContainsString( + 'Variable "$tokenId" of non-null type "EncodableTokenIdInput!" must not be null.', + $response['error'], + ); + } + + public function test_it_will_fail_with_invalid_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => 'invalid', + 'tokenId' => $this->token->token_chain_id, + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" got invalid value "invalid"; Cannot represent following value as uint256', + $response['error'], + ); + } + + public function test_it_will_fail_with_invalid_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => 'invalid', + ], true); + + $this->assertStringContainsString( + 'Variable "$tokenId" got invalid value "invalid"; Expected type "EncodableTokenIdInput" to be an object', + $response['error'], + ); + } + + public function test_it_will_fail_with_collection_id_negative(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => -1, + 'tokenId' => $this->token->token_chain_id, + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" got invalid value -1; Cannot represent following value as uint256', + $response['error'], + ); + } + + public function test_it_will_fail_with_token_id_negative(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable(-1), + ], true); + + $this->assertStringContainsString( + 'Variable "$tokenId" got invalid value -1 at "tokenId.integer"; Cannot represent following value as uint256', + $response['error'], + ); + } + + public function test_it_will_fail_with_collection_id_that_doesnt_exists(): void + { + Collection::where('collection_chain_id', '=', $collectionId = fake()->numberBetween(2000))?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + 'tokenId' => $this->tokenIdEncoder->toEncodable(), + ], true); + + $this->assertArraySubset( + ['collectionId' => ['The selected collection id is invalid.']], + $response['error'], + ); + } + + public function test_it_will_fail_with_token_id_that_doesnt_exists(): void + { + Token::where('token_chain_id', '=', $tokenId = fake()->numberBetween())?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => $this->tokenIdEncoder->toEncodable($tokenId), + ], true); + + $this->assertArraySubset( + ['tokenId' => ['The token id doesn\'t exist.']], + $response['error'], + ); + } + + public function test_it_will_fail_with_empty_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => '', + 'tokenId' => $this->token->token_chain_id, + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" got invalid value (empty string); Cannot represent following value as uint256', + $response['error'], + ); + } + + public function test_it_will_fail_with_empty_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenId' => '', + ], true); + + $this->assertStringContainsString( + 'Variable "$tokenId" got invalid value (empty string); Expected type "EncodableTokenIdInput" to be an object', + $response['error'], + ); + } +} diff --git a/tests/Feature/GraphQL/Queries/GetTokensTest.php b/tests/Feature/GraphQL/Queries/GetTokensTest.php new file mode 100644 index 00000000..3186a8b3 --- /dev/null +++ b/tests/Feature/GraphQL/Queries/GetTokensTest.php @@ -0,0 +1,346 @@ +wallet = Wallet::factory()->create(); + $this->collection = Collection::factory([ + 'owner_wallet_id' => $this->wallet, + ])->create(); + $this->tokens = $this->generateTokens(); + $this->tokenIdEncoder = new Integer(); + } + + protected function tearDown(): void + { + $this->collection->delete(); + Token::destroy($this->tokens); + + parent::tearDown(); + } + + public function test_it_can_get_a_single_token_with_all_data(): void + { + $token = fake()->randomElement($this->tokens); + $tokenAttribute = Attribute::firstWhere([ + 'collection_id' => $this->collection->id, + 'token_id' => $token->id, + ]); + $tokenAccount = TokenAccount::firstWhere([ + 'collection_id' => $this->collection->id, + 'token_id' => $token->id, + 'wallet_id' => $this->wallet->id, + ]); + $tokenAccountApproval = TokenAccountApproval::firstWhere([ + 'token_account_id' => $tokenAccount->id, + ]); + $tokenAccountNamedReserve = TokenAccountNamedReserve::firstWhere([ + 'token_account_id' => $tokenAccount->id, + ]); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId = $this->collection->collection_chain_id, + 'tokenIds' => [$this->tokenIdEncoder->toEncodable($token->token_chain_id)], + ]); + + $this->assertTrue($response['totalCount'] === 1); + $this->assertArraySubset([ + 'tokenId' => $this->tokenIdEncoder->encode($token->token_chain_id), + 'supply' => $token->supply, + 'cap' => $token->cap, + 'capSupply' => $token->cap_supply, + 'isFrozen' => $token->is_frozen, + 'minimumBalance' => $token->minimum_balance, + 'unitPrice' => $token->unit_price, + 'mintDeposit' => $token->mint_deposit, + 'attributeCount' => $token->attribute_count, + 'collection' => [ + 'collectionId' => $collectionId, + ], + 'attributes' => [ + [ + 'key' => $tokenAttribute->key, + 'value' => $tokenAttribute->value, + ], + ], + 'accounts' => [ + 'totalCount' => 1, + 'edges' => [ + [ + 'node' => [ + 'balance' => $tokenAccount->balance, + 'reservedBalance' => $tokenAccount->reserved_balance, + 'isFrozen' => $tokenAccount->is_frozen, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->wallet->public_key, + ], + ], + 'approvals' => [ + [ + 'amount' => $tokenAccountApproval->amount, + 'expiration' => $tokenAccountApproval->expiration, + 'wallet' => [ + 'account' => [ + 'publicKey' => Wallet::find($tokenAccountApproval->wallet_id)->public_key, + ], + ], + ], + ], + 'namedReserves' => [ + [ + 'pallet' => $tokenAccountNamedReserve->pallet, + 'amount' => $tokenAccountNamedReserve->amount, + ], + ], + ], + ], + ], + ], + ], $response['edges'][0]['node']); + + $response = $this->graphql($this->method); + $this->assertNotEmpty($response['totalCount']); + } + + public function test_it_can_fetch_tokens_from_a_collection(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + ]); + + $this->assertTrue($response['totalCount'] >= 1); + } + + public function test_it_can_fetch_tokens_using_a_empty_list_for_token_ids(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenIds' => [], + ]); + + $this->assertTrue($response['totalCount'] >= 1); + } + + public function test_it_can_fetch_tokens_using_null_for_token_ids(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenIds' => null, + ]); + + $this->assertTrue($response['totalCount'] >= 1); + } + + public function test_it_can_use_a_big_int_for_collection_id(): void + { + Collection::where('collection_chain_id', '=', $collectionId = Hex::MAX_UINT128)?->delete(); + Collection::factory([ + 'collection_chain_id' => $collectionId, + ])->create(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + ]); + + $this->assertTrue($response['totalCount'] >= 0); + } + + public function test_it_can_use_a_big_int_for_token_ids(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenIds' => [$this->tokenIdEncoder->toEncodable(Hex::MAX_UINT128)], + ]); + + $this->assertTrue($response['totalCount'] >= 0); + } + + public function test_it_can_fetch_with_null_on_after(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'after' => null, + ]); + + $this->assertTrue($response['totalCount'] >= 1); + } + + public function test_it_can_fetch_with_null_on_first(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'first' => null, + ]); + + $this->assertTrue($response['totalCount'] >= 1); + } + + public function test_it_will_not_fail_using_a_token_id_that_doesnt_exists(): void + { + Token::where('token_chain_id', '=', $tokenId = fake()->numberBetween())?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenIds' => [$this->tokenIdEncoder->toEncodable($tokenId), $this->tokenIdEncoder->toEncodable(fake()->randomElement($this->tokens)->token_chain_id)], + ]); + + $this->assertTrue($response['totalCount'] === 1); + } + + // Exception Path + + public function test_it_will_fail_with_invalid_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => 'invalid', + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" got invalid value "invalid"; Cannot represent following value as uint256', + $response['error'], + ); + } + + public function test_it_will_fail_with_invalid_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenIds' => ['invalid'], + ], true); + + $this->assertStringContainsString( + 'Variable "$tokenIds" got invalid value "invalid" at "tokenIds[0]"; Expected type "EncodableTokenIdInput" to be an object', + $response['error'], + ); + } + + public function test_it_will_fail_with_collection_id_negative(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => -1, + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" got invalid value -1; Cannot represent following value as uint256', + $response['error'], + ); + } + + public function test_it_will_fail_with_token_id_negative(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenIds' => [-1], + ], true); + + $this->assertStringContainsString( + 'Variable "$tokenIds" got invalid value -1 at "tokenIds[0]"; Expected type "EncodableTokenIdInput" to be an object', + $response['error'], + ); + } + + public function test_it_will_fail_with_overflow_token_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => $this->collection->collection_chain_id, + 'tokenIds' => [$this->tokenIdEncoder->toEncodable(Hex::MAX_UINT256)], + ], true); + + $this->assertArraySubset( + ['integer' => ['The integer is too large, the maximum value it can be is 340282366920938463463374607431768211455.']], + $response['errors'], + ); + } + + public function test_it_will_fail_with_collection_id_that_doesnt_exists(): void + { + Collection::where('collection_chain_id', '=', $collectionId = fake()->numberBetween(2000))?->delete(); + + $response = $this->graphql($this->method, [ + 'collectionId' => $collectionId, + ], true); + + $this->assertArraySubset( + ['collectionId' => ['The selected collection id is invalid.']], + $response['error'], + ); + } + + public function test_it_will_fail_with_empty_collection_id(): void + { + $response = $this->graphql($this->method, [ + 'collectionId' => '', + ], true); + + $this->assertStringContainsString( + 'Variable "$collectionId" got invalid value (empty string); Cannot represent following value as uint256', + $response['error'], + ); + } + + protected function generateTokens(?int $numberOfTokens = 5): CollectionSupport + { + return collect(range(0, $numberOfTokens)) + ->map(fn () => $this->createToken()); + } + + protected function createToken(): Token + { + $token = Token::factory([ + 'collection_id' => $this->collection, + 'attribute_count' => 1, + ])->create(); + CollectionAccount::factory([ + 'collection_id' => $this->collection, + 'wallet_id' => $this->wallet, + 'account_count' => 1, + ])->create(); + $tokenAccount = TokenAccount::factory([ + 'collection_id' => $this->collection, + 'token_id' => $token, + 'wallet_id' => $this->wallet, + ])->create(); + Attribute::factory([ + 'collection_id' => $this->collection, + 'token_id' => $token, + ])->create(); + TokenAccountApproval::factory([ + 'token_account_id' => $tokenAccount, + ])->create(); + TokenAccountNamedReserve::factory([ + 'token_account_id' => $tokenAccount, + ])->create(); + + return $token; + } +} diff --git a/tests/Feature/GraphQL/Queries/GetTransactionTest.php b/tests/Feature/GraphQL/Queries/GetTransactionTest.php new file mode 100644 index 00000000..9c56e53a --- /dev/null +++ b/tests/Feature/GraphQL/Queries/GetTransactionTest.php @@ -0,0 +1,328 @@ +defaultAccount = config('enjin-platform.chains.daemon-account'); + $this->transaction = Transaction::factory()->create(); + Account::daemon(); + } + + // Happy path + public function test_it_can_get_a_transaction_with_all_data_by_id(): void + { + $response = $this->graphql($this->method, [ + 'id' => $transactionId = $this->transaction->id, + ]); + + $this->assertArraySubset([ + 'id' => $transactionId, + 'transactionId' => $this->transaction->transaction_chain_id, + 'transactionHash' => $this->transaction->transaction_chain_hash, + 'method' => $this->transaction->method, + 'state' => $this->transaction->state, + 'result' => $this->transaction->result, + 'encodedData' => $this->transaction->encoded_data, + 'signedAtBlock' => $this->transaction->signed_at_block, + 'createdAt' => $this->transaction->created_at->toIso8601String(), + 'updatedAt' => $this->transaction->updated_at->toIso8601String(), + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + } + + public function test_it_can_get_a_transaction_with_all_data_by_idempotency_key(): void + { + $response = $this->graphql($this->method, [ + 'idempotencyKey' => $idempotencyKey = $this->transaction->idempotency_key, + ]); + + $this->assertArraySubset([ + 'id' => $this->transaction->id, + 'idempotencyKey' => $idempotencyKey, + 'transactionId' => $this->transaction->transaction_chain_id, + 'transactionHash' => $this->transaction->transaction_chain_hash, + 'method' => $this->transaction->method, + 'state' => $this->transaction->state, + 'result' => $this->transaction->result, + 'encodedData' => $this->transaction->encoded_data, + 'signedAtBlock' => $this->transaction->signed_at_block, + 'createdAt' => $this->transaction->created_at->toIso8601String(), + 'updatedAt' => $this->transaction->updated_at->toIso8601String(), + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + } + + public function test_it_can_get_a_transaction_with_all_data_by_transaction_id(): void + { + $response = $this->graphql($this->method, [ + 'transactionId' => $transactionId = $this->transaction->transaction_chain_id, + ]); + + $this->assertArraySubset([ + 'id' => $this->transaction->id, + 'transactionId' => $transactionId, + 'transactionHash' => $this->transaction->transaction_chain_hash, + 'method' => $this->transaction->method, + 'state' => $this->transaction->state, + 'result' => $this->transaction->result, + 'encodedData' => $this->transaction->encoded_data, + 'signedAtBlock' => $this->transaction->signed_at_block, + 'createdAt' => $this->transaction->created_at->toIso8601String(), + 'updatedAt' => $this->transaction->updated_at->toIso8601String(), + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + } + + public function test_it_can_get_a_transaction_with_all_data_by_transaction_hash(): void + { + $response = $this->graphql($this->method, [ + 'transactionHash' => $transactionHash = $this->transaction->transaction_chain_hash, + ]); + + $this->assertArraySubset([ + 'id' => $this->transaction->id, + 'transactionId' => $this->transaction->transaction_chain_id, + 'transactionHash' => $transactionHash, + 'method' => $this->transaction->method, + 'state' => $this->transaction->state, + 'result' => $this->transaction->result, + 'encodedData' => $this->transaction->encoded_data, + 'signedAtBlock' => $this->transaction->signed_at_block, + 'createdAt' => $this->transaction->created_at->toIso8601String(), + 'updatedAt' => $this->transaction->updated_at->toIso8601String(), + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + } + + public function test_it_can_get_a_transaction_with_result(): void + { + $transaction = Transaction::factory([ + 'result' => fake()->randomElement([ + SystemEventType::EXTRINSIC_SUCCESS->name, + SystemEventType::EXTRINSIC_FAILED->name, + ]), + ])->create(); + + $response = $this->graphql($this->method, [ + 'transactionHash' => $transactionHash = $transaction->transaction_chain_hash, + ]); + + $this->assertArraySubset([ + 'id' => $transaction->id, + 'transactionId' => $transaction->transaction_chain_id, + 'transactionHash' => $transactionHash, + 'method' => $transaction->method, + 'state' => $transaction->state, + 'result' => $transaction->result, + 'encodedData' => $transaction->encoded_data, + 'signedAtBlock' => $transaction->signed_at_block, + 'createdAt' => $transaction->created_at->toIso8601String(), + 'updatedAt' => $transaction->updated_at->toIso8601String(), + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + } + + public function test_it_can_get_a_transaction_with_events(): void + { + $event = Event::factory([ + 'transaction_id' => $this->transaction->id, + ])->create(); + + $response = $this->graphql($this->method, [ + 'id' => $this->transaction->id, + ]); + + $this->assertArraySubset([ + 'id' => $this->transaction->id, + 'transactionId' => $this->transaction->transaction_chain_id, + 'transactionHash' => $this->transaction->transaction_chain_hash, + 'method' => $this->transaction->method, + 'state' => $this->transaction->state, + 'result' => $this->transaction->result, + 'events' => [ + 'edges' => [ + [ + 'node' => [ + 'phase' => $event->phase, + 'lookUp' => $event->look_up, + 'moduleId' => $event->module_id, + 'eventId' => $event->event_id, + 'params' => JSON::decode($event->params, true), + ], + ], + ], + ], + 'encodedData' => $this->transaction->encoded_data, + 'signedAtBlock' => $this->transaction->signed_at_block, + 'createdAt' => $this->transaction->created_at->toIso8601String(), + 'updatedAt' => $this->transaction->updated_at->toIso8601String(), + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response); + } + + // Exception Path + public function test_it_will_fail_with_no_args(): void + { + $response = $this->graphql($this->method, [], true); + + $this->assertStringContainsString( + 'Please supply just one field.', + $response['error'], + ); + } + + public function test_it_will_fail_with_null_id(): void + { + $response = $this->graphql($this->method, [ + 'id' => null, + ], true); + + $this->assertArraySubset( + ['id' => ['The id field must have a value.']], + $response['error'], + ); + } + + public function test_it_will_fail_with_invalid_id(): void + { + $response = $this->graphql($this->method, [ + 'id' => 'invalid', + ], true); + + $this->assertStringContainsString( + 'Variable "$id" got invalid value "invalid"; Cannot represent following value as uint256', + $response['error'], + ); + } + + public function test_it_will_fail_with_null_transaction_id(): void + { + $response = $this->graphql($this->method, [ + 'transactionId' => null, + ], true); + + $this->assertArraySubset( + ['transactionId' => ['The transaction id field must have a value.']], + $response['error'], + ); + } + + public function test_it_will_fail_with_invalid_transaction_id(): void + { + $response = $this->graphql($this->method, [ + 'transactionId' => 'invalid', + ], true); + + $this->assertArraySubset( + ['transactionId' => ['The transaction id has a not valid substrate transaction ID.']], + $response['error'], + ); + } + + public function test_it_will_fail_with_null_transaction_hash(): void + { + $response = $this->graphql($this->method, [ + 'transactionHash' => null, + ], true); + + $this->assertArraySubset( + ['transactionHash' => ['The transaction hash field must have a value.']], + $response['error'], + ); + } + + public function test_it_will_fail_with_invalid_transaction_hash(): void + { + $response = $this->graphql($this->method, [ + 'transactionHash' => 'invalid', + ], true); + + $this->assertArraySubset( + ['transactionHash' => ['The transaction hash has an invalid hex string.']], + $response['error'], + ); + } + + public function test_it_will_fail_using_id_and_hash(): void + { + $response = $this->graphql($this->method, [ + 'id' => $this->transaction->id, + 'transactionHash' => $this->transaction->transaction_chain_hash, + ], true); + + $this->assertStringContainsString( + 'Please supply just one field.', + $response['error'], + ); + } + + public function test_it_will_fail_using_id_and_transaction_id(): void + { + $response = $this->graphql($this->method, [ + 'id' => $this->transaction->id, + 'transactionId' => $this->transaction->transaction_chain_id, + ], true); + + $this->assertStringContainsString( + 'Please supply just one field.', + $response['error'], + ); + } + + public function test_it_will_fail_using_transaction_id_and_transaction_hash(): void + { + $response = $this->graphql($this->method, [ + 'transactionId' => $this->transaction->transaction_chain_id, + 'transactionHash' => $this->transaction->transaction_chain_hash, + ], true); + + $this->assertStringContainsString( + 'Please supply just one field.', + $response['error'], + ); + } +} diff --git a/tests/Feature/GraphQL/Queries/GetTransactionsTest.php b/tests/Feature/GraphQL/Queries/GetTransactionsTest.php new file mode 100644 index 00000000..d93ce0cd --- /dev/null +++ b/tests/Feature/GraphQL/Queries/GetTransactionsTest.php @@ -0,0 +1,725 @@ +defaultAccount = config('enjin-platform.chains.daemon-account'); + $this->transactions = $this->generateTransactions(); + } + + protected function tearDown(): void + { + Transaction::destroy($this->transactions); + + parent::tearDown(); + } + + public function test_it_can_get_a_single_transaction_using_ids_with_all_data(): void + { + $response = $this->graphql($this->method, [ + 'ids' => [($transaction = fake()->randomElement($this->transactions))->id], + ]); + + $this->assertArraySubset([ + 'id' => $transaction->id, + 'transactionId' => $transaction->transaction_chain_id, + 'transactionHash' => $transaction->transaction_chain_hash, + 'method' => $transaction->method, + 'state' => $transaction->state, + 'encodedData' => $transaction->encoded_data, + 'signedAtBlock' => $transaction->signed_at_block, + 'createdAt' => $transaction->created_at->toIso8601String(), + 'updatedAt' => $transaction->updated_at->toIso8601String(), + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response['edges'][0]['node']); + } + + public function test_it_can_get_a_single_transaction_using_any_filter_with_all_data(): void + { + $response = $this->graphql($this->method, [ + 'transactionIds' => [($transaction = fake()->randomElement($this->transactions))->transaction_chain_id], + ]); + + $this->assertArraySubset([ + 'id' => $transaction->id, + 'transactionId' => $transaction->transaction_chain_id, + 'transactionHash' => $transaction->transaction_chain_hash, + 'method' => $transaction->method, + 'state' => $transaction->state, + 'encodedData' => $transaction->encoded_data, + 'signedAtBlock' => $transaction->signed_at_block, + 'createdAt' => $transaction->created_at->toIso8601String(), + 'updatedAt' => $transaction->updated_at->toIso8601String(), + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response['edges'][0]['node']); + } + + public function test_it_can_get_a_single_transaction_using_idempotency_key_with_all_data(): void + { + $response = $this->graphql($this->method, [ + 'idempotencyKeys' => [($transaction = fake()->randomElement($this->transactions))->idempotency_key], + ]); + + $this->assertArraySubset([ + 'id' => $transaction->id, + 'idempotencyKey' => $transaction->idempotency_key, + 'transactionId' => $transaction->transaction_chain_id, + 'transactionHash' => $transaction->transaction_chain_hash, + 'method' => $transaction->method, + 'state' => $transaction->state, + 'encodedData' => $transaction->encoded_data, + 'signedAtBlock' => $transaction->signed_at_block, + 'createdAt' => $transaction->created_at->toIso8601String(), + 'updatedAt' => $transaction->updated_at->toIso8601String(), + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + ], $response['edges'][0]['node']); + } + + public function test_it_can_get_a_single_transaction_with_events(): void + { + $transaction = fake()->randomElement($this->transactions); + $event = Event::factory([ + 'transaction_id' => $transaction->id, + ])->create(); + + $response = $this->graphql($this->method, [ + 'transactionIds' => [$transaction->transaction_chain_id], + ]); + + $this->assertArraySubset([ + 'id' => $transaction->id, + 'transactionId' => $transaction->transaction_chain_id, + 'transactionHash' => $transaction->transaction_chain_hash, + 'method' => $transaction->method, + 'state' => $transaction->state, + 'encodedData' => $transaction->encoded_data, + 'signedAtBlock' => $transaction->signed_at_block, + 'createdAt' => $transaction->created_at->toIso8601String(), + 'updatedAt' => $transaction->updated_at->toIso8601String(), + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->defaultAccount, + ], + ], + 'events' => [ + 'edges' => [ + [ + 'node' => [ + 'phase' => $event->phase, + 'lookUp' => $event->look_up, + 'moduleId' => $event->module_id, + 'eventId' => $event->event_id, + 'params' => JSON::decode($event->params, true), + ], + ], + ], + ], + ], $response['edges'][0]['node']); + } + + public function test_it_can_fetch_with_no_args(): void + { + $response = $this->graphql($this->method); + + $this->assertTrue(count($response['edges']) > 0); + } + + public function test_it_can_fetch_with_empty_args(): void + { + $response = $this->graphql($this->method, []); + + $this->assertTrue(count($response['edges']) > 0); + } + + public function test_it_can_fetch_with_null_ids(): void + { + $response = $this->graphql($this->method, [ + 'ids' => null, + ]); + + $this->assertTrue(count($response['edges']) > 0); + } + + public function test_it_can_fetch_with_null_idempotency_keys(): void + { + $response = $this->graphql($this->method, [ + 'idempotencyKeys' => null, + ]); + + $this->assertTrue(count($response['edges']) > 0); + } + + public function test_it_can_fetch_with_null_transaction_ids(): void + { + $response = $this->graphql($this->method, [ + 'transactionIds' => null, + ]); + + $this->assertTrue(count($response['edges']) > 0); + } + + public function test_it_can_fetch_with_null_transaction_hashes(): void + { + $response = $this->graphql($this->method, [ + 'transactionHashes' => null, + ]); + + $this->assertTrue(count($response['edges']) > 0); + } + + public function test_it_can_fetch_with_null_methods(): void + { + $response = $this->graphql($this->method, [ + 'methods' => null, + ]); + + $this->assertTrue(count($response['edges']) > 0); + } + + public function test_it_can_fetch_with_null_states(): void + { + $response = $this->graphql($this->method, [ + 'states' => null, + ]); + + $this->assertTrue(count($response['edges']) > 0); + } + + public function test_it_can_fetch_with_null_results(): void + { + $response = $this->graphql($this->method, [ + 'results' => null, + ]); + + $this->assertTrue(count($response['edges']) > 0); + } + + public function test_it_can_fetch_with_null_event_ids(): void + { + $response = $this->graphql($this->method, [ + 'eventIds' => null, + ]); + + $this->assertTrue(count($response['edges']) > 0); + } + + public function test_it_can_fetch_with_empty_ids(): void + { + $response = $this->graphql($this->method, [ + 'ids' => [], + ]); + + $this->assertTrue(count($response['edges']) > 0); + } + + public function test_it_can_fetch_with_empty_transaction_ids(): void + { + $response = $this->graphql($this->method, [ + 'transactionIds' => [], + ]); + + $this->assertTrue(count($response['edges']) > 0); + } + + public function test_it_can_fetch_with_empty_transaction_hashes(): void + { + $response = $this->graphql($this->method, [ + 'transactionHashes' => [], + ]); + + $this->assertTrue(count($response['edges']) > 0); + } + + public function test_it_can_fetch_with_empty_methods(): void + { + $response = $this->graphql($this->method, [ + 'methods' => [], + ]); + + $this->assertTrue(count($response['edges']) > 0); + } + + public function test_it_can_fetch_with_empty_states(): void + { + $response = $this->graphql($this->method, [ + 'states' => [], + ]); + + $this->assertTrue(count($response['edges']) > 0); + } + + public function test_it_can_fetch_with_empty_results(): void + { + $response = $this->graphql($this->method, [ + 'results' => [], + ]); + + $this->assertTrue(count($response['edges']) > 0); + } + + public function test_it_can_fetch_with_empty_event_ids(): void + { + $response = $this->graphql($this->method, [ + 'eventIds' => [], + ]); + + $this->assertTrue(count($response['edges']) > 0); + } + + public function test_it_can_fetch_with_empty_event_types(): void + { + $response = $this->graphql($this->method, [ + 'eventTypes' => [], + ]); + + $this->assertTrue(count($response['edges']) > 0); + } + + public function test_it_can_get_filter_transactions_by_id(): void + { + $response = $this->graphql($this->method, [ + 'ids' => [$transactionId = fake()->randomElement($this->transactions)->id], + ]); + + $this->assertTrue(1 === $response['totalCount']); + $this->assertEquals($transactionId, $response['edges'][0]['node']['id']); + } + + public function test_it_can_get_filter_transactions_by_transaction_id(): void + { + $response = $this->graphql($this->method, [ + 'transactionIds' => [$transactionId = fake()->randomElement($this->transactions)->transaction_chain_id], + ]); + + $this->assertTrue(1 === $response['totalCount']); + $this->assertEquals($transactionId, $response['edges'][0]['node']['transactionId']); + } + + public function test_it_can_get_filter_transactions_by_transaction_hash(): void + { + $response = $this->graphql($this->method, [ + 'transactionHashes' => [$transactionHash = fake()->randomElement($this->transactions)->transaction_chain_hash], + ]); + + $this->assertTrue($response['totalCount'] >= 1); + $this->assertTrue($this->haveFieldEqualsTo($response['edges'], 'transactionHash', $transactionHash)); + } + + public function test_it_can_get_filter_transactions_by_methods(): void + { + $response = $this->graphql($this->method, [ + 'methods' => [$method = fake()->randomElement($this->transactions)->method], + ]); + + $this->assertTrue($response['totalCount'] >= 1); + $this->assertTrue($this->haveFieldEqualsTo($response['edges'], 'method', $method)); + } + + public function test_it_can_get_filter_transactions_by_states(): void + { + $response = $this->graphql($this->method, [ + 'states' => [$state = fake()->randomElement($this->transactions)->state], + ]); + + $this->assertTrue($response['totalCount'] >= 1); + $this->assertTrue($this->haveFieldEqualsTo($response['edges'], 'state', $state)); + } + + public function test_it_can_get_filter_transactions_by_results(): void + { + $response = $this->graphql($this->method, [ + 'results' => [$result = fake()->randomElement($this->transactions)->result], + ]); + + $this->assertTrue($response['totalCount'] >= 1); + $this->assertTrue($this->haveFieldEqualsTo($response['edges'], 'result', $result)); + } + + public function test_it_can_filter_by_transaction_hash_and_methods(): void + { + $transaction = fake()->randomElement($this->transactions); + + $response = $this->graphql($this->method, [ + 'transactionHashes' => [$transactionHash = $transaction->transaction_chain_hash], + 'methods' => [$method = $transaction->method], + ]); + + $this->assertTrue($response['totalCount'] >= 1); + $this->assertTrue($this->haveFieldEqualsTo($response['edges'], 'transactionHash', $transactionHash)); + $this->assertTrue($this->haveFieldEqualsTo($response['edges'], 'method', $method)); + } + + public function test_it_can_filter_by_transaction_hash_and_states(): void + { + $transaction = fake()->randomElement($this->transactions); + + $response = $this->graphql($this->method, [ + 'transactionHashes' => [$transactionHash = $transaction->transaction_chain_hash], + 'states' => [$state = $transaction->state], + ]); + + $this->assertTrue($response['totalCount'] >= 1); + $this->assertTrue($this->haveFieldEqualsTo($response['edges'], 'transactionHash', $transactionHash)); + $this->assertTrue($this->haveFieldEqualsTo($response['edges'], 'state', $state)); + } + + public function test_it_can_filter_by_transaction_hash_and_results(): void + { + $transaction = fake()->randomElement($this->transactions); + + $response = $this->graphql($this->method, [ + 'transactionHashes' => [$transactionHash = $transaction->transaction_chain_hash], + 'results' => [$result = $transaction->result], + ]); + + $this->assertTrue($response['totalCount'] >= 1); + $this->assertTrue($this->haveFieldEqualsTo($response['edges'], 'transactionHash', $transactionHash)); + $this->assertTrue($this->haveFieldEqualsTo($response['edges'], 'result', $result)); + } + + public function test_it_can_filter_with_methods_and_states(): void + { + $transaction = fake()->randomElement($this->transactions); + + $response = $this->graphql($this->method, [ + 'methods' => [$method = $transaction->method], + 'states' => [$state = $transaction->state], + ]); + + $this->assertTrue($response['totalCount'] >= 1); + $this->assertTrue($this->haveFieldEqualsTo($response['edges'], 'method', $method)); + $this->assertTrue($this->haveFieldEqualsTo($response['edges'], 'state', $state)); + } + + public function test_it_can_filter_with_methods_and_results(): void + { + $transaction = fake()->randomElement($this->transactions); + + $response = $this->graphql($this->method, [ + 'methods' => [$method = $transaction->method], + 'results' => [$result = $transaction->result], + ]); + + $this->assertTrue($response['totalCount'] >= 1); + $this->assertTrue($this->haveFieldEqualsTo($response['edges'], 'method', $method)); + $this->assertTrue($this->haveFieldEqualsTo($response['edges'], 'result', $result)); + } + + public function test_it_can_filter_with_states_and_results(): void + { + $transaction = fake()->randomElement($this->transactions); + + $response = $this->graphql($this->method, [ + 'states' => [$state = $transaction->state], + 'results' => [$result = $transaction->result], + ]); + + $this->assertTrue($response['totalCount'] >= 1); + $this->assertTrue($this->haveFieldEqualsTo($response['edges'], 'state', $state)); + $this->assertTrue($this->haveFieldEqualsTo($response['edges'], 'result', $result)); + } + + public function test_it_can_filter_with_hashes_methods_states(): void + { + $transaction = fake()->randomElement($this->transactions); + + $response = $this->graphql($this->method, [ + 'transactionHashes' => [$transactionHash = $transaction->transaction_chain_hash], + 'methods' => [$method = $transaction->method], + 'states' => [$state = $transaction->state], + ]); + + $this->assertTrue($response['totalCount'] >= 1); + $this->assertTrue($this->haveFieldEqualsTo($response['edges'], 'transactionHash', $transactionHash)); + $this->assertTrue($this->haveFieldEqualsTo($response['edges'], 'method', $method)); + $this->assertTrue($this->haveFieldEqualsTo($response['edges'], 'state', $state)); + } + + public function test_it_can_filter_with_hashes_methods_results(): void + { + $transaction = fake()->randomElement($this->transactions); + + $response = $this->graphql($this->method, [ + 'transactionHashes' => [$transactionHash = $transaction->transaction_chain_hash], + 'methods' => [$method = $transaction->method], + 'results' => [$result = $transaction->result], + ]); + + $this->assertTrue($response['totalCount'] >= 1); + $this->assertTrue($this->haveFieldEqualsTo($response['edges'], 'transactionHash', $transactionHash)); + $this->assertTrue($this->haveFieldEqualsTo($response['edges'], 'method', $method)); + $this->assertTrue($this->haveFieldEqualsTo($response['edges'], 'result', $result)); + } + + public function test_it_can_filter_with_methods_states_results(): void + { + $transaction = fake()->randomElement($this->transactions); + + $response = $this->graphql($this->method, [ + 'methods' => [$method = $transaction->method], + 'states' => [$state = $transaction->state], + 'results' => [$result = $transaction->result], + ]); + + $this->assertTrue($response['totalCount'] >= 1); + $this->assertTrue($this->haveFieldEqualsTo($response['edges'], 'method', $method)); + $this->assertTrue($this->haveFieldEqualsTo($response['edges'], 'state', $state)); + $this->assertTrue($this->haveFieldEqualsTo($response['edges'], 'result', $result)); + } + + public function test_it_can_filter_with_hashes_methods_states_results(): void + { + $transaction = fake()->randomElement($this->transactions); + + $response = $this->graphql($this->method, [ + 'transactionHashes' => [$transactionHash = $transaction->transaction_chain_hash], + 'methods' => [$method = $transaction->method], + 'states' => [$state = $transaction->state], + 'results' => [$result = $transaction->result], + ]); + + $this->assertTrue($response['totalCount'] >= 1); + $this->assertTrue($this->haveFieldEqualsTo($response['edges'], 'transactionHash', $transactionHash)); + $this->assertTrue($this->haveFieldEqualsTo($response['edges'], 'method', $method)); + $this->assertTrue($this->haveFieldEqualsTo($response['edges'], 'state', $state)); + $this->assertTrue($this->haveFieldEqualsTo($response['edges'], 'result', $result)); + } + + public function test_it_will_fail_with_invalid_id(): void + { + $response = $this->graphql($this->method, [ + 'ids' => ['invalid'], + ], true); + + $this->assertStringContainsString( + 'Variable "$ids" got invalid value "invalid" at "ids[0]"; Cannot represent following value as uint256', + $response['error'], + ); + } + + public function test_it_will_fail_with_invalid_transaction_id(): void + { + $response = $this->graphql($this->method, [ + 'transactionIds' => ['invalid'], + ], true); + + $this->assertArraySubset( + ['transactionIds' => ['The transaction ids has a not valid substrate transaction ID.']], + $response['error'], + ); + } + + // Exception Path + + public function test_it_will_fail_with_invalid_transaction_hash(): void + { + $response = $this->graphql($this->method, [ + 'transactionHashes' => ['invalid'], + ], true); + + $this->assertArraySubset( + ['transactionHashes' => ['The transaction hashes has an invalid hex string.']], + $response['error'], + ); + } + + public function test_it_will_fail_with_invalid_method(): void + { + $response = $this->graphql($this->method, [ + 'methods' => ['invalid'], + ], true); + + $this->assertStringContainsString( + 'Variable "$methods" got invalid value "invalid" at "methods[0]"; Value "invalid" does not exist in "TransactionMethod" enum', + $response['error'], + ); + } + + public function test_it_will_fail_with_invalid_states(): void + { + $response = $this->graphql($this->method, [ + 'states' => ['invalid'], + ], true); + + $this->assertStringContainsString( + 'Variable "$states" got invalid value "invalid" at "states[0]"; Value "invalid" does not exist in "TransactionState" enum', + $response['error'], + ); + } + + public function test_it_will_fail_with_invalid_results(): void + { + $response = $this->graphql($this->method, [ + 'results' => ['invalid'], + ], true); + + $this->assertStringContainsString( + 'Variable "$results" got invalid value "invalid" at "results[0]"; Value "invalid" does not exist in "TransactionResult" enum', + $response['error'], + ); + } + + public function test_it_will_fail_with_ids_and_hashes(): void + { + $response = $this->graphql($this->method, [ + 'ids' => [fake()->randomElement($this->transactions)->id], + 'transactionHashes' => [fake()->randomElement($this->transactions)->transaction_chain_hash], + ], true); + + $this->assertStringContainsString( + 'The filter(s) "ids, transactionIds, idempotencyKeys" can only be used alone. You cannot combine them with other filters', + $response['error'], + ); + } + + public function test_it_will_fail_with_ids_and_transaction_ids(): void + { + $response = $this->graphql($this->method, [ + 'ids' => [fake()->randomElement($this->transactions)->id], + 'transactionIds' => [fake()->randomElement($this->transactions)->transaction_chain_id], + ], true); + + $this->assertStringContainsString( + 'Only one of these filter(s) can be used: ids, transactionIds, idempotencyKeys', + $response['error'], + ); + } + + public function test_it_will_fail_with_transaction_ids_and_hashes(): void + { + $response = $this->graphql($this->method, [ + 'transactionIds' => [fake()->randomElement($this->transactions)->transaction_chain_id], + 'transactionHashes' => [fake()->randomElement($this->transactions)->transaction_chain_hash], + ], true); + + $this->assertStringContainsString( + 'The filter(s) "ids, transactionIds, idempotencyKeys" can only be used alone. You cannot combine them with other filters', + $response['error'], + ); + } + + public function test_it_will_fail_with_ids_and_methods(): void + { + $response = $this->graphql($this->method, [ + 'ids' => [fake()->randomElement($this->transactions)->id], + 'methods' => [fake()->randomElement($this->transactions)->method], + ], true); + + $this->assertStringContainsString( + 'The filter(s) "ids, transactionIds, idempotencyKeys" can only be used alone. You cannot combine them with other filters', + $response['error'], + ); + } + + public function test_it_will_fail_with_transactions_ids_and_methods(): void + { + $response = $this->graphql($this->method, [ + 'transactionIds' => [fake()->randomElement($this->transactions)->transaction_chain_id], + 'methods' => [fake()->randomElement($this->transactions)->method], + ], true); + + $this->assertStringContainsString( + 'The filter(s) "ids, transactionIds, idempotencyKeys" can only be used alone. You cannot combine them with other filters', + $response['error'], + ); + } + + public function test_it_will_fail_with_ids_and_states(): void + { + $response = $this->graphql($this->method, [ + 'ids' => [fake()->randomElement($this->transactions)->id], + 'states' => [fake()->randomElement($this->transactions)->state], + ], true); + + $this->assertStringContainsString( + 'The filter(s) "ids, transactionIds, idempotencyKeys" can only be used alone. You cannot combine them with other filters', + $response['error'], + ); + } + + public function test_it_will_fail_with_transactions_ids_and_states(): void + { + $response = $this->graphql($this->method, [ + 'transactionIds' => [fake()->randomElement($this->transactions)->transaction_chain_id], + 'states' => [fake()->randomElement($this->transactions)->state], + ], true); + + $this->assertStringContainsString( + 'The filter(s) "ids, transactionIds, idempotencyKeys" can only be used alone. You cannot combine them with other filters', + $response['error'], + ); + } + + public function test_it_will_fail_withids_and_results(): void + { + $response = $this->graphql($this->method, [ + 'ids' => [fake()->randomElement($this->transactions)->id], + 'results' => [fake()->randomElement($this->transactions)->result], + ], true); + + $this->assertStringContainsString( + 'The filter(s) "ids, transactionIds, idempotencyKeys" can only be used alone. You cannot combine them with other filters', + $response['error'], + ); + } + + public function test_it_will_fail_with_transactions_ids_and_results(): void + { + $response = $this->graphql($this->method, [ + 'transactionIds' => [fake()->randomElement($this->transactions)->transaction_chain_id], + 'results' => [fake()->randomElement($this->transactions)->result], + ], true); + + $this->assertStringContainsString( + 'The filter(s) "ids, transactionIds, idempotencyKeys" can only be used alone. You cannot combine them with other filters', + $response['error'], + ); + } + + protected function haveFieldEqualsTo(array $transactions, string $field, mixed $value): bool + { + return empty(array_filter($transactions, fn ($tx) => $tx['node'][$field] !== $value)); + } + + protected function generateTransactions(?int $numberOfTransactions = 5): Collection + { + return collect(range(0, $numberOfTransactions)) + ->map(fn () => Transaction::factory([ + 'result' => fake()->randomElement([ + SystemEventType::EXTRINSIC_SUCCESS->name, + SystemEventType::EXTRINSIC_FAILED->name, + ]), + ])->create()); + } +} diff --git a/tests/Feature/GraphQL/Queries/GetWalletAuthTest.php b/tests/Feature/GraphQL/Queries/GetWalletAuthTest.php new file mode 100644 index 00000000..14147dda --- /dev/null +++ b/tests/Feature/GraphQL/Queries/GetWalletAuthTest.php @@ -0,0 +1,105 @@ +codec = new Codec(); + $this->verification = Verification::factory([ + 'public_key' => $address = app(Generator::class)->public_key(), + ])->create(); + $this->wallet = Wallet::factory([ + 'public_key' => $address, + 'verification_id' => $this->verification->verification_id, + ])->create(); + } + + public function test_it_can_get_wallet_without_token(): void + { + $this->app['config']->set('enjin-platform.auth', null); + $this->getWallet(); + } + + public function test_it_can_get_wallet_with_basic_token(): void + { + $this->getWallet(); + } + + protected function getWallet(): void + { + $this->mockNonceAndBalancesFor($this->wallet->public_key); + $response = $this->httpGraphql( + $this->method, + ['variables' => ['account' => SS58Address::encode($this->wallet->public_key)]], + ['Authorization' => $this->token] + ); + + $this->assertArraySubset([ + 'id' => $this->wallet->id, + 'account' => [ + 'publicKey' => $this->wallet->public_key, + ], + ], $response); + } + + protected function mockNonceAndBalancesFor(string $account): void + { + $this->mockWebsocketClient( + 'state_getStorage', + [ + $this->codec->encode()->systemAccountStorageKey($account), + ], + json_encode( + [ + 'jsonrpc' => '2.0', + 'result' => '0x1d000000000000000100000000000000331f60a549ec45201cd30000000000000080ebc061752bf3a5000000000000000000000000000000000000000000000000000000000000000000000000000000', + 'id' => 1, + ], + JSON_THROW_ON_ERROR + ) + ); + } + + protected function getPackageProviders($app): array + { + return [ + CoreServiceProvider::class, + GraphQLServiceProvider::class, + ]; + } + + protected function defineEnvironment($app): void + { + parent::defineEnvironment($app); + $app['config']->set( + 'enjin-platform.auth_drivers.basic_token', + $this->token = Str::random(20) + ); + } +} diff --git a/tests/Feature/GraphQL/Queries/GetWalletTest.php b/tests/Feature/GraphQL/Queries/GetWalletTest.php new file mode 100644 index 00000000..744ee0ca --- /dev/null +++ b/tests/Feature/GraphQL/Queries/GetWalletTest.php @@ -0,0 +1,756 @@ +codec = new Codec(); + $this->verification = Verification::factory([ + 'public_key' => $address = app(Generator::class)->public_key(), + ])->create(); + $this->wallet = Wallet::factory([ + 'public_key' => $address, + 'verification_id' => $this->verification->verification_id, + ])->create(); + $this->transaction = Transaction::factory([ + 'wallet_public_key' => $this->wallet->public_key, + ])->create(); + + $this->anotherWallet = Wallet::factory()->create(); + $this->approvedWallet = Wallet::factory()->create(); + + $this->collection = Collection::factory([ + 'owner_wallet_id' => $this->wallet, + 'token_count' => 1, + ])->create(); + $this->token = Token::factory([ + 'collection_id' => $this->collection, + 'attribute_count' => 1, + ])->create(); + $this->collectionAccount = CollectionAccount::factory([ + 'collection_id' => $this->collection, + 'wallet_id' => $this->wallet, + 'account_count' => 1, + ])->create(); + $this->collectionAccountApproval = CollectionAccountApproval::factory([ + 'collection_account_id' => $this->collectionAccount, + 'wallet_id' => $this->approvedWallet, + ])->create(); + $this->collectionAttribute = Attribute::factory([ + 'collection_id' => $this->collection, + 'token_id' => null, + ])->create(); + $this->tokenAccount = TokenAccount::factory([ + 'collection_id' => $this->collection, + 'token_id' => $this->token, + 'wallet_id' => $this->wallet, + ])->create(); + $this->tokenAttribute = Attribute::factory([ + 'collection_id' => $this->collection, + 'token_id' => $this->token, + ])->create(); + $this->tokenAccountApproval = TokenAccountApproval::factory([ + 'token_account_id' => $this->tokenAccount, + 'wallet_id' => $this->approvedWallet, + ])->create(); + $this->tokenAccountNamedReserve = TokenAccountNamedReserve::factory([ + 'token_account_id' => $this->tokenAccount, + ])->create(); + + $this->anotherCollection = Collection::factory([ + 'owner_wallet_id' => $this->anotherWallet, + 'token_count' => 1, + ])->create(); + $this->anotherToken = Token::factory([ + 'collection_id' => $this->anotherCollection, + 'attribute_count' => 1, + ])->create(); + $this->anotherCollectionAccount = CollectionAccount::factory([ + 'collection_id' => $this->anotherCollection, + 'wallet_id' => $this->anotherWallet, + 'account_count' => 1, + ])->create(); + $this->anotherCollectionAccountApprovedToWallet = CollectionAccountApproval::factory([ + 'collection_account_id' => $this->anotherCollectionAccount, + 'wallet_id' => $this->wallet, + ])->create(); + $this->anotherTokenAccount = TokenAccount::factory([ + 'collection_id' => $this->anotherCollection, + 'token_id' => $this->anotherToken, + 'wallet_id' => $this->anotherWallet, + ])->create(); + $this->anotherTokenAccountApprovedToWallet = TokenAccountApproval::factory([ + 'token_account_id' => $this->anotherTokenAccount, + 'wallet_id' => $this->wallet, + ])->create(); + } + + public function test_it_can_get_wallet_with_all_data_by_id(): void + { + $this->mockNonceAndBalancesFor($this->wallet->public_key); + + $response = $this->graphql($this->method, [ + 'id' => $this->wallet->id, + ]); + + $this->assertArraySubset([ + 'id' => $this->wallet->id, + 'account' => [ + 'publicKey' => $this->wallet->public_key, + ], + 'externalId' => $this->wallet->external_id, + 'managed' => $this->wallet->managed, + 'network' => $this->wallet->network, + 'nonce' => $this->mockedData()['nonce'], + 'balances' => [ + 'free' => $this->mockedData()['balances']['free'], + 'reserved' => $this->mockedData()['balances']['reserved'], + 'miscFrozen' => $this->mockedData()['balances']['miscFrozen'], + 'feeFrozen' => $this->mockedData()['balances']['feeFrozen'], + ], + 'collectionAccounts' => [ + 'edges' => [ + [ + 'node' => [ + 'accountCount' => $this->collectionAccount->account_count, + 'isFrozen' => $this->collectionAccount->is_frozen, + 'collection' => [ + 'collectionId' => $this->collection->collection_chain_id, + ], + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->wallet->public_key, + ], + ], + 'approvals' => [ + [ + 'expiration' => $this->collectionAccountApproval->expiration, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->approvedWallet->public_key, + ], + ], + ], + ], + ], + ], + ], + ], + 'tokenAccounts' => [ + 'edges' => [ + [ + 'node' => [ + 'balance' => $this->tokenAccount->balance, + 'reservedBalance' => $this->tokenAccount->reserved_balance, + 'isFrozen' => $this->tokenAccount->is_frozen, + 'collection' => [ + 'collectionId' => $this->collection->collection_chain_id, + ], + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->wallet->public_key, + ], + ], + 'token' => [ + 'tokenId' => $this->token->token_chain_id, + ], + 'approvals' => [ + [ + 'amount' => $this->tokenAccountApproval->amount, + 'expiration' => $this->tokenAccountApproval->expiration, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->approvedWallet->public_key, + ], + ], + ], + ], + 'namedReserves' => [ + [ + 'pallet' => $this->tokenAccountNamedReserve->pallet, + 'amount' => $this->tokenAccountNamedReserve->amount, + ], + ], + ], + ], + ], + ], + 'collectionAccountApprovals' => [ + 'edges' => [ + [ + 'node' => [ + 'expiration' => $this->anotherCollectionAccountApprovedToWallet->expiration, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->wallet->public_key, + ], + ], + ], + ], + ], + ], + 'tokenAccountApprovals' => [ + 'edges' => [ + [ + 'node' => [ + 'amount' => $this->anotherTokenAccountApprovedToWallet->amount, + 'expiration' => $this->anotherTokenAccountApprovedToWallet->expiration, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->wallet->public_key, + ], + ], + ], + ], + ], + ], + 'transactions' => [ + 'edges' => [ + [ + 'node' => [ + 'id' => $this->transaction->id, + 'transactionId' => $this->transaction->transaction_chain_id, + 'transactionHash' => $this->transaction->transaction_chain_hash, + 'method' => $this->transaction->method, + 'state' => $this->transaction->state, + 'encodedData' => $this->transaction->encoded_data, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->wallet->public_key, + ], + ], + ], + ], + ], + ], + 'ownedCollections' => [ + 'edges' => [ + [ + 'node' => [ + 'collectionId' => $this->collection->collection_chain_id, + 'maxTokenCount' => $this->collection->max_token_count, + 'maxTokenSupply' => $this->collection->max_token_supply, + 'forceSingleMint' => $this->collection->force_single_mint, + 'network' => $this->collection->network, + 'owner' => [ + 'account' => [ + 'publicKey' => $this->wallet->public_key, + ], + ], + 'attributes' => [ + [ + 'key' => $this->collectionAttribute->key, + 'value' => $this->collectionAttribute->value, + ], + ], + 'accounts' => [ + 'edges' => [ + [ + 'node' => [ + 'accountCount' => $this->collectionAccount->account_count, + 'isFrozen' => $this->collectionAccount->is_frozen, + 'collection' => [ + 'collectionId' => $this->collection->collection_chain_id, + ], + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->wallet->public_key, + ], + ], + 'approvals' => [ + [ + 'expiration' => $this->collectionAccountApproval->expiration, + 'wallet' => [ + 'account' => [ + 'publicKey' => $this->approvedWallet->public_key, + ], + ], + ], + ], + ], + ], + ], + ], + 'tokens' => [ + 'edges' => [ + [ + 'node' => [ + 'tokenId' => $this->token->token_chain_id, + 'supply' => $this->token->supply, + 'cap' => $this->token->cap, + 'capSupply' => $this->token->cap_supply, + 'isFrozen' => $this->token->is_frozen, + 'minimumBalance' => $this->token->minimum_balance, + 'unitPrice' => $this->token->unit_price, + 'attributeCount' => $this->token->attribute_count, + 'collection' => [ + 'collectionId' => $this->collection->collection_chain_id, + ], + 'attributes' => [ + [ + 'key' => $this->tokenAttribute->key, + 'value' => $this->tokenAttribute->value, + ], + ], + 'accounts' => [ + 'edges' => [ + [ + 'node' => [ + 'balance' => $this->tokenAccount->balance, + ], + ], + ], + ], + 'metadata' => $this->token->metadata, + 'nonFungible' => $this->token->non_fungible, + ], + ], + ], + ], + ], + ], + ], + ], + ], $response); + } + + public function test_it_can_get_a_wallet_and_filter_collection_accounts(): void + { + $this->mockNonceAndBalancesFor($this->wallet->public_key); + + $response = $this->graphql($this->method, [ + 'id' => $id = $this->wallet->id, + 'collectionAccountsCollectionIds' => [$this->collection->collection_chain_id], + ]); + + $this->assertTrue($response['id'] == $id); + $this->assertTrue(1 === $response['collectionAccounts']['totalCount']); + $this->assertArraySubset([ + 'accountCount' => $this->collectionAccount->account_count, + 'isFrozen' => $this->collectionAccount->is_frozen, + ], $response['collectionAccounts']['edges'][0]['node']); + } + + public function test_it_can_get_a_wallet_and_filter_token_accounts(): void + { + $this->mockNonceAndBalancesFor($this->wallet->public_key); + + $response = $this->graphql($this->method, [ + 'id' => $id = $this->wallet->id, + 'tokenAccountsCollectionIds' => [$this->collection->collection_chain_id], + 'tokenAccountsTokenIds' => [$this->token->token_chain_id], + ]); + + $this->assertTrue($response['id'] == $id); + $this->assertTrue(1 === $response['tokenAccounts']['totalCount']); + $this->assertArraySubset([ + 'balance' => $this->tokenAccount->balance, + 'reservedBalance' => $this->tokenAccount->reserved_balance, + 'isFrozen' => $this->tokenAccount->is_frozen, + ], $response['tokenAccounts']['edges'][0]['node']); + } + + public function test_it_can_get_a_wallet_and_filter_owned_collections(): void + { + $this->mockNonceAndBalancesFor($this->wallet->public_key); + + $response = $this->graphql($this->method, [ + 'id' => $id = $this->wallet->id, + 'ownedCollectionsCollectionIds' => [$this->collection->collection_chain_id], + ]); + + $this->assertTrue($response['id'] == $id); + $this->assertTrue(1 === $response['ownedCollections']['totalCount']); + $this->assertArraySubset([ + 'collectionId' => $this->collection->collection_chain_id, + 'maxTokenCount' => $this->collection->max_token_count, + 'maxTokenSupply' => $this->collection->max_token_supply, + 'forceSingleMint' => $this->collection->force_single_mint, + 'network' => $this->collection->network, + ], $response['ownedCollections']['edges'][0]['node']); + } + + public function test_it_will_not_get_any_transactions_for_a_wallet_that_doesnt_have_an_address(): void + { + $wallet = Wallet::factory([ + 'public_key' => null, + ])->create(); + + $response = $this->graphql($this->method, [ + 'id' => $id = $wallet->id, + ]); + + $this->assertTrue($response['id'] == $id); + $this->assertTrue(0 === $response['transactions']['totalCount']); + } + + public function test_it_will_have_null_balance_and_nonce_for_a_wallet_that_doesnt_have_an_address(): void + { + $wallet = Wallet::factory([ + 'public_key' => null, + ])->create(); + + $response = $this->graphql($this->method, [ + 'id' => $id = $wallet->id, + ]); + + $this->assertTrue($response['id'] == $id); + $this->assertTrue(null === $response['nonce']); + $this->assertTrue(null === $response['balances']); + } + + public function test_it_can_get_wallet_by_external_id(): void + { + $this->mockNonceAndBalancesFor($this->wallet->public_key); + + $response = $this->graphql($this->method, [ + 'externalId' => $externalId = $this->wallet->external_id, + ]); + + $this->assertArraySubset([ + 'id' => $this->wallet->id, + 'externalId' => $externalId, + ], $response); + } + + public function test_it_can_get_wallet_by_address(): void + { + $this->mockNonceAndBalancesFor($this->wallet->public_key); + + $response = $this->graphql($this->method, [ + 'account' => SS58Address::encode($this->wallet->public_key), + ]); + + $this->assertArraySubset([ + 'id' => $this->wallet->id, + 'account' => [ + 'publicKey' => $this->wallet->public_key, + ], + ], $response); + } + + // Exception Path + + public function test_it_can_get_wallet_by_verification_id(): void + { + $this->mockNonceAndBalancesFor($this->wallet->public_key); + + $response = $this->graphql($this->method, [ + 'verificationId' => $this->wallet->verification_id, + ]); + + $this->assertArraySubset([ + 'id' => $this->wallet->id, + 'account' => [ + 'publicKey' => $this->wallet->public_key, + ], + ], $response); + } + + public function test_it_will_fail_with_no_args(): void + { + $response = $this->graphql($this->method, [], true); + + $this->assertStringContainsString( + 'Please supply just one field.', + $response['error'], + ); + } + + public function test_it_will_fail_with_null_id(): void + { + $response = $this->graphql($this->method, [ + 'id' => null, + ], true); + + $this->assertArraySubset( + ['id' => ['The id field must have a value.']], + $response['error'], + ); + } + + public function test_it_will_fail_with_invalid_id(): void + { + $response = $this->graphql($this->method, [ + 'id' => 'invalid', + ], true); + + $this->assertStringContainsString( + 'Variable "$id" got invalid value "invalid"; Int cannot represent non-integer value', + $response['error'], + ); + } + + public function test_it_will_fail_with_null_external_id(): void + { + $response = $this->graphql($this->method, [ + 'externalId' => null, + ], true); + + $this->assertArraySubset( + ['externalId' => ['The external id field must have a value.']], + $response['error'], + ); + } + + public function test_it_will_fail_with_null_verification_id(): void + { + $response = $this->graphql($this->method, [ + 'verificationId' => null, + ], true); + + $this->assertArraySubset( + ['verificationId' => ['The verification id field must have a value.']], + $response['error'], + ); + } + + public function test_it_will_fail_with_null_address(): void + { + $response = $this->graphql($this->method, [ + 'account' => null, + ], true); + + $this->assertArraySubset( + ['account' => ['The account field must have a value.']], + $response['error'], + ); + } + + public function test_it_will_fail_with_invalid_address(): void + { + $response = $this->graphql($this->method, [ + 'account' => 'invalid', + ], true); + + $this->assertArraySubset( + ['account' => ['The account is not a valid substrate account.']], + $response['error'], + ); + } + + public function test_it_will_fail_with_invalid_verification_id(): void + { + $response = $this->graphql($this->method, [ + 'verificationId' => 'invalid', + ], true); + + $this->assertArraySubset( + ['verificationId' => ['The verification ID is not valid.']], + $response['error'], + ); + } + + public function test_it_will_fail_using_id_and_external_id(): void + { + $response = $this->graphql($this->method, [ + 'id' => $this->wallet->id, + 'externalId' => $this->wallet->external_id, + ], true); + + $this->assertStringContainsString( + 'Please supply just one field.', + $response['error'], + ); + } + + public function test_it_will_fail_using_id_and_verification_id(): void + { + $response = $this->graphql($this->method, [ + 'id' => $this->wallet->id, + 'verificationId' => $this->wallet->verification_id, + ], true); + + $this->assertStringContainsString( + 'Please supply just one field.', + $response['error'], + ); + } + + public function test_it_will_fail_using_id_and_address(): void + { + $response = $this->graphql($this->method, [ + 'id' => $this->wallet->id, + 'account' => $this->wallet->public_key, + ], true); + + $this->assertStringContainsString( + 'Please supply just one field.', + $response['error'], + ); + } + + public function test_it_will_fail_using_external_id_and_verification_id(): void + { + $response = $this->graphql($this->method, [ + 'externalId' => $this->wallet->external_id, + 'verificationId' => $this->wallet->verification_id, + ], true); + + $this->assertStringContainsString( + 'Please supply just one field.', + $response['error'], + ); + } + + public function test_it_will_fail_using_external_id_and_address(): void + { + $response = $this->graphql($this->method, [ + 'externalId' => $this->wallet->external_id, + 'account' => $this->wallet->public_key, + ], true); + + $this->assertStringContainsString( + 'Please supply just one field.', + $response['error'], + ); + } + + public function test_it_will_fail_using_verification_id_and_address(): void + { + $response = $this->graphql($this->method, [ + 'verificationId' => $this->wallet->verification_id, + 'account' => $this->wallet->public_key, + ], true); + + $this->assertStringContainsString( + 'Please supply just one field.', + $response['error'], + ); + } + + public function test_it_will_fail_with_empty_id(): void + { + $response = $this->graphql($this->method, [ + 'id' => '', + ], true); + + $this->assertStringContainsString( + 'Variable "$id" got invalid value (empty string)', + $response['error'], + ); + } + + public function test_it_will_fail_with_empty_external_id(): void + { + $response = $this->graphql($this->method, [ + 'externalId' => '', + ], true); + + $this->assertArraySubset( + ['externalId' => ['The external id field must have a value.']], + $response['error'], + ); + } + + public function test_it_will_fail_with_empty_verification_id(): void + { + $response = $this->graphql($this->method, [ + 'verificationId' => '', + ], true); + + $this->assertArraySubset( + ['verificationId' => ['The verification id field must have a value.']], + $response['error'], + ); + } + + public function test_it_will_fail_with_empty_address(): void + { + $response = $this->graphql($this->method, [ + 'account' => '', + ], true); + + $this->assertArraySubset( + ['account' => ['The account field must have a value.']], + $response['error'], + ); + } + + protected function mockNonceAndBalancesFor(string $address): void + { + $this->mockWebsocketClient( + 'state_getStorage', + [ + $this->codec->encode()->systemAccountStorageKey($address), + ], + json_encode( + [ + 'jsonrpc' => '2.0', + 'result' => '0x1d000000000000000100000000000000331f60a549ec45201cd30000000000000080ebc061752bf3a5000000000000000000000000000000000000000000000000000000000000000000000000000000', + 'id' => 1, + ], + JSON_THROW_ON_ERROR + ) + ); + } + + protected function mockedData(): array + { + return [ + 'nonce' => 29, + 'balances' => [ + 'free' => '996938162244142665572147', + 'reserved' => '3061235000000000000000', + 'miscFrozen' => '0', + 'feeFrozen' => '0', + ], + ]; + } +} diff --git a/tests/Feature/GraphQL/Queries/GetWalletsTest.php b/tests/Feature/GraphQL/Queries/GetWalletsTest.php new file mode 100644 index 00000000..ee22da49 --- /dev/null +++ b/tests/Feature/GraphQL/Queries/GetWalletsTest.php @@ -0,0 +1,270 @@ +codec = new Codec(); + $this->verification = Verification::factory([ + 'public_key' => $address = app(Generator::class)->public_key(), + ])->create(); + $this->wallet = Wallet::factory([ + 'public_key' => $address, + 'verification_id' => $this->verification->verification_id, + ])->create(); + $this->transaction = Transaction::factory([ + 'wallet_public_key' => $this->wallet->public_key, + ])->create(); + + $this->anotherWallet = Wallet::factory()->create(); + $this->approvedWallet = Wallet::factory()->create(); + + $this->collection = Collection::factory([ + 'owner_wallet_id' => $this->wallet, + 'token_count' => 1, + ])->create(); + $this->token = Token::factory([ + 'collection_id' => $this->collection, + 'attribute_count' => 1, + ])->create(); + $this->collectionAccount = CollectionAccount::factory([ + 'collection_id' => $this->collection, + 'wallet_id' => $this->wallet, + 'account_count' => 1, + ])->create(); + $this->collectionAccountApproval = CollectionAccountApproval::factory([ + 'collection_account_id' => $this->collectionAccount, + 'wallet_id' => $this->approvedWallet, + ])->create(); + $this->collectionAttribute = Attribute::factory([ + 'collection_id' => $this->collection, + 'token_id' => null, + ])->create(); + $this->tokenAccount = TokenAccount::factory([ + 'collection_id' => $this->collection, + 'token_id' => $this->token, + 'wallet_id' => $this->wallet, + ])->create(); + $this->tokenAttribute = Attribute::factory([ + 'collection_id' => $this->collection, + 'token_id' => $this->token, + ])->create(); + $this->tokenAccountApproval = TokenAccountApproval::factory([ + 'token_account_id' => $this->tokenAccount, + 'wallet_id' => $this->approvedWallet, + ])->create(); + $this->tokenAccountNamedReserve = TokenAccountNamedReserve::factory([ + 'token_account_id' => $this->tokenAccount, + ])->create(); + + $this->anotherCollection = Collection::factory([ + 'owner_wallet_id' => $this->anotherWallet, + 'token_count' => 1, + ])->create(); + $this->anotherToken = Token::factory([ + 'collection_id' => $this->anotherCollection, + 'attribute_count' => 1, + ])->create(); + $this->anotherCollectionAccount = CollectionAccount::factory([ + 'collection_id' => $this->anotherCollection, + 'wallet_id' => $this->anotherWallet, + 'account_count' => 1, + ])->create(); + $this->anotherCollectionAccountApprovedToWallet = CollectionAccountApproval::factory([ + 'collection_account_id' => $this->anotherCollectionAccount, + 'wallet_id' => $this->wallet, + ])->create(); + $this->anotherTokenAccount = TokenAccount::factory([ + 'collection_id' => $this->anotherCollection, + 'token_id' => $this->anotherToken, + 'wallet_id' => $this->anotherWallet, + ])->create(); + $this->anotherTokenAccountApprovedToWallet = TokenAccountApproval::factory([ + 'token_account_id' => $this->anotherTokenAccount, + 'wallet_id' => $this->wallet, + ])->create(); + } + + public function test_it_can_get_wallets(): void + { + $response = $this->graphql($this->method, ['ids' => [$this->wallet->id]]); + $this->assertNotEmpty($response['totalCount']); + + $response = $this->graphql($this->method, ['externalIds' => [$this->wallet->external_id]]); + $this->assertNotEmpty($response['totalCount']); + + $response = $this->graphql($this->method, ['verificationIds' => [$this->verification->verification_id]]); + $this->assertNotEmpty($response['totalCount']); + + $response = $this->graphql($this->method, ['accounts' => [$this->wallet->public_key]]); + $this->assertNotEmpty($response['totalCount']); + } + + public function test_it_will_fail_with_invalid_id(): void + { + $response = $this->graphql($this->method, [ + 'ids' => [$this->wallet->id], + 'externalIds' => [$this->wallet->external_id], + 'verificationIds' => [$this->verification->verification_id], + 'accounts' => [$this->wallet->public_key], + ], true); + $this->assertArraySubset([ + 'ids' => ['The ids field is prohibited.'], + 'externalIds' => ['The external ids field is prohibited.'], + 'verificationIds' => ['The verification ids field is prohibited.'], + 'accounts' => ['The accounts field is prohibited.'], + ], $response['error']); + + $response = $this->graphql($this->method, [ + 'ids' => SupportCollection::range(1, 200)->toArray(), + ], true); + $this->assertArraySubset([ + 'ids' => ['The ids field must not have more than 100 items.'], + ], $response['error']); + + $response = $this->graphql($this->method, [ + 'ids' => [Hex::MAX_UINT256 + 1], + ], true); + $this->assertEquals( + 'Variable "$ids" got invalid value 1.1579208923732E+77 at "ids[0]"; Int cannot represent non 32-bit signed integer value: 1.1579208923732E+77', + $response['error'] + ); + } + + public function test_it_will_fail_with_invalid_external_id(): void + { + $response = $this->graphql($this->method, [ + 'externalIds' => SupportCollection::range(1, 200)->map(fn ($val) => (string) $val)->toArray(), + ], true); + $this->assertArraySubset([ + 'externalIds' => ['The external ids field must not have more than 100 items.'], + ], $response['error']); + + $response = $this->graphql($this->method, [ + 'externalIds' => [Str::random(2000)], + ], true); + $this->assertArraySubset([ + 'externalIds.0' => ['The externalIds.0 field must not be greater than 1000 characters.'], + ], $response['error']); + + $response = $this->graphql($this->method, [ + 'externalIds' => [''], + ], true); + $this->assertArraySubset([ + 'externalIds.0' => ['The externalIds.0 field must have a value.'], + ], $response['error']); + } + + public function test_it_will_fail_with_invalid_verification_id(): void + { + $response = $this->graphql($this->method, [ + 'verificationIds' => SupportCollection::range(1, 200)->map(fn ($val) => (string) $val)->toArray(), + ], true); + $this->assertArraySubset([ + 'verificationIds' => ['The verification ids field must not have more than 100 items.'], + ], $response['error']); + + $response = $this->graphql($this->method, [ + 'verificationIds' => [Str::random(2000)], + ], true); + $this->assertArraySubset([ + 'verificationIds.0' => ['The verificationIds.0 field must not be greater than 1000 characters.'], + ], $response['error']); + + $response = $this->graphql($this->method, [ + 'verificationIds' => [''], + ], true); + $this->assertArraySubset([ + 'verificationIds.0' => ['The verificationIds.0 field must have a value.'], + ], $response['error']); + } + + public function test_it_will_fail_with_invalid_accounts(): void + { + $response = $this->graphql($this->method, [ + 'accounts' => SupportCollection::range(1, 200)->map(fn ($val) => (string) $val)->toArray(), + ], true); + $this->assertArraySubset([ + 'accounts' => ['The accounts field must not have more than 100 items.'], + ], $response['error']); + + $response = $this->graphql($this->method, [ + 'accounts' => [Str::random(2000)], + ], true); + $this->assertArraySubset([ + 'accounts.0' => ['The accounts.0 field must not be greater than 255 characters.'], + ], $response['error']); + + $response = $this->graphql($this->method, [ + 'accounts' => [''], + ], true); + $this->assertArraySubset([ + 'accounts.0' => ['The accounts.0 field must have a value.'], + ], $response['error']); + + $response = $this->graphql($this->method, [ + 'accounts' => [Str::random(255)], + ], true); + $this->assertArraySubset([ + 'accounts.0' => ['The accounts.0 is not a valid substrate account.'], + ], $response['error']); + } +} diff --git a/tests/Feature/GraphQL/Queries/RequestAccountTest.php b/tests/Feature/GraphQL/Queries/RequestAccountTest.php new file mode 100644 index 00000000..19c56f2b --- /dev/null +++ b/tests/Feature/GraphQL/Queries/RequestAccountTest.php @@ -0,0 +1,108 @@ +callback = fake()->url(); + } + + public function test_can_request_an_address(): void + { + $response = $this->graphql($this->method, [ + 'callback' => $this->callback, + ]); + + $this->assertNotEmpty($qrCode = $response['qrCode']); + $this->assertNotEmpty($verificationId = $response['verificationId']); + $this->assertDatabaseHas('verifications', [ + 'code' => explode(':', $qrCode)[3], + 'verification_id' => $verificationId, + 'public_key' => null, + ]); + } + + public function test_callback_is_embedded_in_qr_code(): void + { + $response = $this->graphql($this->method, [ + 'callback' => $this->callback, + ]); + + $this->assertNotEmpty($qrCode = $response['qrCode']); + $this->assertTrue( + base64_decode(explode(':', $qrCode)[4]) === $this->callback + ); + } + + public function test_is_valid_wallet_deep_link(): void + { + $response = $this->graphql($this->method, [ + 'callback' => $this->callback, + ]); + + $this->assertNotEmpty($qrCode = $response['qrCode']); + $this->assertStringContainsString( + config('enjin-platform.deep_links.proof'), + $qrCode + ); + } + + public function test_it_will_fail_with_no_callback(): void + { + $response = $this->graphql($this->method, [], true); + + $this->assertStringContainsString( + 'Variable "$callback" of required type "String!" was not provided.', + $response['error'], + ); + } + + // Exception Path + + public function test_it_will_fail_with_null_callback(): void + { + $response = $this->graphql($this->method, [ + 'callback' => null, + ], true); + + $this->assertStringContainsString( + 'Variable "$callback" of non-null type "String!" must not be null.', + $response['error'], + ); + } + + public function test_it_will_fail_with_empty_callback(): void + { + $response = $this->graphql($this->method, [ + 'callback' => '', + ], true); + + $this->assertArraySubset( + ['callback' => ['The callback field must have a value.']], + $response['error'], + ); + } + + public function test_it_will_fail_with_invalid_callback(): void + { + $response = $this->graphql($this->method, [ + 'callback' => 'not_a_valid_url', + ], true); + + $this->assertArraySubset( + ['callback' => ['The callback field must be a valid URL.']], + $response['error'], + ); + } +} diff --git a/tests/Feature/GraphQL/Queries/VerifyMessageTest.php b/tests/Feature/GraphQL/Queries/VerifyMessageTest.php new file mode 100644 index 00000000..4d0ff322 --- /dev/null +++ b/tests/Feature/GraphQL/Queries/VerifyMessageTest.php @@ -0,0 +1,375 @@ +message = HexConverter::stringToHexPrefixed(fake()->realText()); + } + + public function test_it_can_verify_a_message(): void + { + $data = app(Generator::class)->sr25519_signature($this->message); + + $response = $this->graphql($this->method, [ + 'message' => $this->message, + 'signature' => $data['signature'], + 'publicKey' => $data['publicKey'], + ]); + + $this->assertTrue($response); + } + + public function test_it_can_verify_a_message_with_signature_type_sr25519(): void + { + $data = app(Generator::class)->sr25519_signature($this->message); + + $response = $this->graphql($this->method, [ + 'message' => $this->message, + 'signature' => $data['signature'], + 'publicKey' => $data['publicKey'], + 'cryptoSignatureType' => 'SR25519', + ]); + + $this->assertTrue($response); + } + + public function test_it_can_verify_a_message_with_signature_type_ed25519(): void + { + $data = app(Generator::class)->ed25519_signature($this->message); + + $response = $this->graphql($this->method, [ + 'message' => $this->message, + 'signature' => $data['signature'], + 'publicKey' => $data['publicKey'], + 'cryptoSignatureType' => 'ED25519', + ]); + + $this->assertTrue($response); + } + + public function test_it_will_return_false_with_invalid_sr25519_signature(): void + { + $otherMessage = HexConverter::stringToHexPrefixed(fake()->text()); + $data = app(Generator::class)->sr25519_signature($otherMessage); + + $response = $this->graphql($this->method, [ + 'message' => $this->message, + 'signature' => $data['signature'], + 'publicKey' => $data['publicKey'], + ]); + + $this->assertFalse($response); + } + + public function test_it_will_return_false_with_invalid_ed25519_signature(): void + { + $otherMessage = HexConverter::stringToHexPrefixed(fake()->text()); + $data = app(Generator::class)->ed25519_signature($otherMessage); + + $response = $this->graphql($this->method, [ + 'message' => $this->message, + 'signature' => $data['signature'], + 'publicKey' => $data['publicKey'], + 'cryptoSignatureType' => 'ED25519', + ]); + + $this->assertFalse($response); + } + + // Exception Path + + public function test_it_will_fail_with_no_args(): void + { + $response = $this->graphql($this->method, [], true); + + $this->assertStringContainsString( + 'Variable "$message" of required type "String!" was not provided.', + $response['error'] + ); + } + + public function test_it_will_fail_with_no_message(): void + { + $data = app(Generator::class)->ed25519_signature($this->message); + + $response = $this->graphql($this->method, [ + 'signature' => $data['signature'], + 'publicKey' => $data['publicKey'], + ], true); + + $this->assertStringContainsString( + 'Variable "$message" of required type "String!" was not provided.', + $response['error'] + ); + } + + public function test_it_will_fail_with_no_signature(): void + { + $data = app(Generator::class)->ed25519_signature($this->message); + + $response = $this->graphql($this->method, [ + 'message' => $this->message, + 'publicKey' => $data['publicKey'], + ], true); + + $this->assertStringContainsString( + 'Variable "$signature" of required type "String!" was not provided.', + $response['error'] + ); + } + + public function test_it_will_fail_with_no_public_key(): void + { + $data = app(Generator::class)->ed25519_signature($this->message); + + $response = $this->graphql($this->method, [ + 'message' => $this->message, + 'signature' => $data['signature'], + ], true); + + $this->assertStringContainsString( + 'Variable "$publicKey" of required type "String!" was not provided.', + $response['error'] + ); + } + + public function test_it_will_fail_with_message_null(): void + { + $data = app(Generator::class)->ed25519_signature($this->message); + + $response = $this->graphql($this->method, [ + 'message' => null, + 'signature' => $data['signature'], + 'publicKey' => $data['publicKey'], + ], true); + + $this->assertStringContainsString( + 'Variable "$message" of non-null type "String!" must not be null.', + $response['error'] + ); + } + + public function test_it_will_fail_with_signature_null(): void + { + $data = app(Generator::class)->ed25519_signature($this->message); + + $response = $this->graphql($this->method, [ + 'message' => $this->message, + 'signature' => null, + 'publicKey' => $data['publicKey'], + ], true); + + $this->assertStringContainsString( + 'Variable "$signature" of non-null type "String!" must not be null.', + $response['error'] + ); + } + + public function test_it_will_fail_with_public_key_null(): void + { + $data = app(Generator::class)->ed25519_signature($this->message); + + $response = $this->graphql($this->method, [ + 'message' => $this->message, + 'signature' => $data['signature'], + 'publicKey' => null, + ], true); + + $this->assertStringContainsString( + 'Variable "$publicKey" of non-null type "String!" must not be null.', + $response['error'] + ); + } + + public function test_it_will_fail_with_message_not_hex(): void + { + $data = app(Generator::class)->ed25519_signature($this->message); + + $response = $this->graphql($this->method, [ + 'message' => fake()->word(), + 'signature' => $data['signature'], + 'publicKey' => $data['publicKey'], + ], true); + + $this->assertArraySubset( + ['message' => ['The message has an invalid hex string.']], + $response['error'] + ); + } + + public function test_it_will_fail_with_message_without_hex_prefix(): void + { + $data = app(Generator::class)->ed25519_signature($this->message); + + $response = $this->graphql($this->method, [ + 'message' => HexConverter::unPrefix($this->message), + 'signature' => $data['signature'], + 'publicKey' => $data['publicKey'], + ], true); + + $this->assertArraySubset( + ['message' => ['The message has an invalid hex string.']], + $response['error'] + ); + } + + public function test_it_will_fail_with_signature_not_hex(): void + { + $data = app(Generator::class)->ed25519_signature($this->message); + + $response = $this->graphql($this->method, [ + 'message' => $this->message, + 'signature' => fake()->word(), + 'publicKey' => $data['publicKey'], + ], true); + + $this->assertArraySubset( + ['signature' => ['The signature has an invalid hex string.']], + $response['error'] + ); + } + + public function test_it_will_fail_with_signature_without_hex_prefix(): void + { + $data = app(Generator::class)->ed25519_signature($this->message); + + $response = $this->graphql($this->method, [ + 'message' => $this->message, + 'signature' => HexConverter::unPrefix($data['signature']), + 'publicKey' => $data['publicKey'], + ], true); + + $this->assertArraySubset( + ['signature' => ['The signature has an invalid hex string.']], + $response['error'] + ); + } + + public function test_it_will_fail_with_invalid_public_key_for_sr25519(): void + { + $data = app(Generator::class)->sr25519_signature($this->message); + + $response = $this->graphql($this->method, [ + 'message' => $this->message, + 'signature' => $data['signature'], + 'publicKey' => '0x01234', + ], true); + + $this->assertArraySubset( + ['publicKey' => ['The public key has an invalid hex string.']], + $response['error'] + ); + } + + public function test_it_will_fail_with_invalid_public_key_for_ed25519(): void + { + $data = app(Generator::class)->ed25519_signature($this->message); + + $response = $this->graphql($this->method, [ + 'message' => $this->message, + 'signature' => $data['signature'], + 'publicKey' => '0x01234', + ], true); + + $this->assertArraySubset( + ['publicKey' => ['The public key has an invalid hex string.']], + $response['error'] + ); + } + + public function test_it_will_fail_with_public_key_without_hex_prefix(): void + { + $data = app(Generator::class)->ed25519_signature($this->message); + + $response = $this->graphql($this->method, [ + 'message' => $this->message, + 'signature' => $data['signature'], + 'publicKey' => HexConverter::unPrefix($data['publicKey']), + ], true); + + $this->assertArraySubset( + ['publicKey' => ['The public key has an invalid hex string.']], + $response['error'] + ); + } + + public function test_it_will_fail_with_empty_signature(): void + { + $data = app(Generator::class)->ed25519_signature($this->message); + + $response = $this->graphql($this->method, [ + 'message' => $this->message, + 'signature' => '', + 'publicKey' => $data['publicKey'], + ], true); + + $this->assertArraySubset( + ['signature' => ['The signature field must have a value.']], + $response['error'] + ); + } + + public function test_it_will_fail_with_empty_public_key(): void + { + $data = app(Generator::class)->ed25519_signature($this->message); + + $response = $this->graphql($this->method, [ + 'message' => $this->message, + 'signature' => $data['signature'], + 'publicKey' => '', + ], true); + + $this->assertArraySubset( + ['publicKey' => ['The public key field must have a value.']], + $response['error'] + ); + } + + public function test_it_will_fail_with_invalid_crypto_signature_type(): void + { + $data = app(Generator::class)->ed25519_signature($this->message); + + $response = $this->graphql($this->method, [ + 'message' => $this->message, + 'signature' => $data['signature'], + 'publicKey' => $data['publicKey'], + 'cryptoSignatureType' => 'invalid', + ], true); + + $this->assertStringContainsString( + 'Variable "$cryptoSignatureType" got invalid value "invalid"; Value "invalid" does not exist in "CryptoSignatureType" enum', + $response['error'] + ); + } + + public function test_it_will_fail_with_empty_signature_type(): void + { + $data = app(Generator::class)->ed25519_signature($this->message); + + $response = $this->graphql($this->method, [ + 'message' => $this->message, + 'signature' => $data['signature'], + 'publicKey' => $data['publicKey'], + 'cryptoSignatureType' => '', + ], true); + + $this->assertStringContainsString( + 'Variable "$cryptoSignatureType" got invalid value (empty string); Value "" does not exist in "CryptoSignatureType" enum', + $response['error'] + ); + } +} diff --git a/tests/Feature/GraphQL/Resources/AcknowledgeEvents.graphql b/tests/Feature/GraphQL/Resources/AcknowledgeEvents.graphql new file mode 100644 index 00000000..d98d0f9d --- /dev/null +++ b/tests/Feature/GraphQL/Resources/AcknowledgeEvents.graphql @@ -0,0 +1,7 @@ +mutation AcknowledgeEvents( + $uuids: [String!]! +) { + AcknowledgeEvents( + uuids: $uuids + ) +} \ No newline at end of file diff --git a/tests/Feature/GraphQL/Resources/ApproveCollection.graphql b/tests/Feature/GraphQL/Resources/ApproveCollection.graphql new file mode 100644 index 00000000..28d962b5 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/ApproveCollection.graphql @@ -0,0 +1,24 @@ +mutation ApproveCollection( + $collectionId: BigInt! + $operator: String! + $expiration: Int +) { + ApproveCollection( + collectionId: $collectionId + operator: $operator + expiration: $expiration + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} diff --git a/tests/Feature/GraphQL/Resources/ApproveToken.graphql b/tests/Feature/GraphQL/Resources/ApproveToken.graphql new file mode 100644 index 00000000..fe18fd5c --- /dev/null +++ b/tests/Feature/GraphQL/Resources/ApproveToken.graphql @@ -0,0 +1,30 @@ +mutation ApproveToken( + $collectionId: BigInt! + $tokenId: EncodableTokenIdInput! + $operator: String! + $amount: BigInt! + $currentAmount: BigInt! + $expiration: Int +) { + ApproveToken( + collectionId: $collectionId + tokenId: $tokenId + operator: $operator + amount: $amount + currentAmount: $currentAmount + expiration: $expiration + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} diff --git a/tests/Feature/GraphQL/Resources/BatchMint.graphql b/tests/Feature/GraphQL/Resources/BatchMint.graphql new file mode 100644 index 00000000..cee2c2fb --- /dev/null +++ b/tests/Feature/GraphQL/Resources/BatchMint.graphql @@ -0,0 +1,22 @@ +mutation BatchMint( + $collectionId: BigInt! + $recipients: [MintRecipient!]! +) { + BatchMint( + collectionId: $collectionId + recipients: $recipients + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} diff --git a/tests/Feature/GraphQL/Resources/BatchSetAttribute.graphql b/tests/Feature/GraphQL/Resources/BatchSetAttribute.graphql new file mode 100644 index 00000000..f0eb5597 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/BatchSetAttribute.graphql @@ -0,0 +1,24 @@ +mutation BatchSetAttribute( + $collectionId: BigInt! + $tokenId: EncodableTokenIdInput + $attributes: [AttributeInput!]! +) { + BatchSetAttribute( + collectionId: $collectionId + tokenId: $tokenId + attributes: $attributes + ) { + id + transactionId + transactionHash + method + encodedData + state + wallet { + account { + publicKey + address + } + } + } +} diff --git a/tests/Feature/GraphQL/Resources/BatchTransfer.graphql b/tests/Feature/GraphQL/Resources/BatchTransfer.graphql new file mode 100644 index 00000000..dda0ff10 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/BatchTransfer.graphql @@ -0,0 +1,24 @@ +mutation BatchTransfer( + $collectionId: BigInt! + $recipients: [TransferRecipient!]! + $signingAccount: String +) { + BatchTransfer( + collectionId: $collectionId + recipients: $recipients + signingAccount: $signingAccount + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} diff --git a/tests/Feature/GraphQL/Resources/Burn.graphql b/tests/Feature/GraphQL/Resources/Burn.graphql new file mode 100644 index 00000000..cd94866f --- /dev/null +++ b/tests/Feature/GraphQL/Resources/Burn.graphql @@ -0,0 +1,22 @@ +mutation Burn( + $collectionId: BigInt! + $params: BurnParamsInput! +) { + Burn( + collectionId: $collectionId + params: $params + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} diff --git a/tests/Feature/GraphQL/Resources/CreateCollection.graphql b/tests/Feature/GraphQL/Resources/CreateCollection.graphql new file mode 100644 index 00000000..186494ba --- /dev/null +++ b/tests/Feature/GraphQL/Resources/CreateCollection.graphql @@ -0,0 +1,27 @@ +mutation CreateCollection( + $mintPolicy: MintPolicy! + $marketPolicy: MarketPolicy + $explicitRoyaltyCurrencies: [MultiTokenIdInput] + $idempotencyKey: String +) { + CreateCollection( + mintPolicy: $mintPolicy + marketPolicy: $marketPolicy + explicitRoyaltyCurrencies: $explicitRoyaltyCurrencies + idempotencyKey: $idempotencyKey + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + idempotencyKey + } +} diff --git a/tests/Feature/GraphQL/Resources/CreateToken.graphql b/tests/Feature/GraphQL/Resources/CreateToken.graphql new file mode 100644 index 00000000..41023649 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/CreateToken.graphql @@ -0,0 +1,24 @@ +mutation CreateToken( + $recipient: String! + $collectionId: BigInt! + $params: CreateTokenParams! +) { + CreateToken( + recipient: $recipient + collectionId: $collectionId + params: $params + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} diff --git a/tests/Feature/GraphQL/Resources/CreateWallet.graphql b/tests/Feature/GraphQL/Resources/CreateWallet.graphql new file mode 100644 index 00000000..80ce7d94 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/CreateWallet.graphql @@ -0,0 +1,7 @@ +mutation CreateWallet( + $externalId: String! +) { + CreateWallet( + externalId: $externalId + ) +} \ No newline at end of file diff --git a/tests/Feature/GraphQL/Resources/DestroyCollection.graphql b/tests/Feature/GraphQL/Resources/DestroyCollection.graphql new file mode 100644 index 00000000..28c937e4 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/DestroyCollection.graphql @@ -0,0 +1,16 @@ +mutation DestroyCollection($collectionId: BigInt!) { + DestroyCollection(collectionId: $collectionId) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} diff --git a/tests/Feature/GraphQL/Resources/Freeze.graphql b/tests/Feature/GraphQL/Resources/Freeze.graphql new file mode 100644 index 00000000..30accf18 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/Freeze.graphql @@ -0,0 +1,28 @@ +mutation Freeze( + $freezeType: FreezeType! + $collectionId: BigInt! + $tokenId: EncodableTokenIdInput + $collectionAccount: String + $tokenAccount: String +) { + Freeze( + freezeType: $freezeType + collectionId: $collectionId + tokenId: $tokenId + collectionAccount: $collectionAccount + tokenAccount: $tokenAccount + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} diff --git a/tests/Feature/GraphQL/Resources/GetAccountVerified.graphql b/tests/Feature/GraphQL/Resources/GetAccountVerified.graphql new file mode 100644 index 00000000..1ec94b19 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/GetAccountVerified.graphql @@ -0,0 +1,15 @@ +query GetAccountVerified( + $verificationId: String + $account: String +) { + GetAccountVerified( + verificationId: $verificationId + account: $account + ) { + verified + account { + publicKey + address + } + } +} \ No newline at end of file diff --git a/tests/Feature/GraphQL/Resources/GetCollection.graphql b/tests/Feature/GraphQL/Resources/GetCollection.graphql new file mode 100644 index 00000000..1c5f9740 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/GetCollection.graphql @@ -0,0 +1,72 @@ +query GetCollection( + $collectionId: BigInt! + $tokensCursor: String + $tokensLimit: Int + $accountsCursor: String + $accountsLimit: Int +) { + GetCollection(collectionId: $collectionId) { + collectionId + maxTokenCount + maxTokenSupply + forceSingleMint + frozen + network + owner { + account { + publicKey + address + } + } + attributes { + key + value + } + tokens(after: $tokensCursor, first: $tokensLimit) { + edges { + cursor + node { + tokenId + } + } + totalCount + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + accounts(after: $accountsCursor, first: $accountsLimit) { + edges { + cursor + node { + accountCount + isFrozen + wallet { + account { + publicKey + address + } + } + approvals { + expiration + wallet { + account { + publicKey + address + } + } + } + } + } + totalCount + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + } +} diff --git a/tests/Feature/GraphQL/Resources/GetCollections.graphql b/tests/Feature/GraphQL/Resources/GetCollections.graphql new file mode 100644 index 00000000..7296298d --- /dev/null +++ b/tests/Feature/GraphQL/Resources/GetCollections.graphql @@ -0,0 +1,84 @@ +query GetCollections( + $collectionIds: [BigInt] + $tokensCursor: String + $tokensLimit: Int + $accountsCursor: String + $accountsLimit: Int +) { + GetCollections(collectionIds: $collectionIds) { + edges { + cursor + node { + collectionId + maxTokenCount + maxTokenSupply + forceSingleMint + frozen + network + owner { + account { + publicKey + address + } + } + attributes { + key + value + } + tokens(after: $tokensCursor, first: $tokensLimit) { + edges { + cursor + node { + tokenId + } + } + totalCount + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + accounts(after: $accountsCursor, first: $accountsLimit) { + edges { + cursor + node { + accountCount + isFrozen + wallet { + account { + publicKey + address + } + } + approvals { + expiration + wallet { + account { + publicKey + address + } + } + } + } + } + totalCount + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + } + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + totalCount + } +} diff --git a/tests/Feature/GraphQL/Resources/GetLinkingCode.graphql b/tests/Feature/GraphQL/Resources/GetLinkingCode.graphql new file mode 100644 index 00000000..37be2569 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/GetLinkingCode.graphql @@ -0,0 +1,9 @@ +query GetLinkingCode( + $externalId: String! +) { + GetLinkingCode( + externalId: $externalId + ) { + code + } +} \ No newline at end of file diff --git a/tests/Feature/GraphQL/Resources/GetPendingEvents.graphql b/tests/Feature/GraphQL/Resources/GetPendingEvents.graphql new file mode 100644 index 00000000..df5f7449 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/GetPendingEvents.graphql @@ -0,0 +1,30 @@ +query GetPendingEvents( + $after: String + $first: Int + $acknowledgeEvents: Boolean +) { + GetPendingEvents( + after: $after + first: $first + acknowledgeEvents: $acknowledgeEvents + ) { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + id + uuid + name + sent + channels + data + } + } + } +} diff --git a/tests/Feature/GraphQL/Resources/GetPendingWallets.graphql b/tests/Feature/GraphQL/Resources/GetPendingWallets.graphql new file mode 100644 index 00000000..9abfd5d0 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/GetPendingWallets.graphql @@ -0,0 +1,30 @@ +query GetPendingWallets( + $after: String + $first: Int +) { + GetPendingWallets( + after: $after + first: $first + ) { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + id + account { + publicKey + address + } + externalId + managed + network + } + } + } +} \ No newline at end of file diff --git a/tests/Feature/GraphQL/Resources/GetToken.graphql b/tests/Feature/GraphQL/Resources/GetToken.graphql new file mode 100644 index 00000000..9736435a --- /dev/null +++ b/tests/Feature/GraphQL/Resources/GetToken.graphql @@ -0,0 +1,67 @@ +query GetToken( + $collectionId: BigInt! + $tokenId: EncodableTokenIdInput! + $accountsCursor: String + $accountsLimit: Int +) { + GetToken( + collectionId: $collectionId + tokenId: $tokenId + ) { + tokenId + supply + cap + capSupply + isFrozen + minimumBalance + unitPrice + mintDeposit + attributeCount + nonFungible + metadata + collection { + collectionId + } + attributes { + key + value + } + accounts(after: $accountsCursor, first: $accountsLimit) { + totalCount + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + edges { + cursor + node { + balance + reservedBalance + isFrozen + wallet { + account { + publicKey + address + } + } + approvals { + amount + expiration + wallet { + account { + publicKey + address + } + } + } + namedReserves { + pallet + amount + } + } + } + } + } +} diff --git a/tests/Feature/GraphQL/Resources/GetTokens.graphql b/tests/Feature/GraphQL/Resources/GetTokens.graphql new file mode 100644 index 00000000..17ebb53a --- /dev/null +++ b/tests/Feature/GraphQL/Resources/GetTokens.graphql @@ -0,0 +1,82 @@ +query GetTokens( + $collectionId: BigInt + $tokenIds: [EncodableTokenIdInput] + $after: String + $first: Int + $accountsCursor: String + $accountsLimit: Int +) { + GetTokens( + collectionId: $collectionId + tokenIds: $tokenIds + after: $after + first: $first + ) { + edges { + cursor + node { + tokenId + supply + cap + capSupply + isFrozen + minimumBalance + unitPrice + mintDeposit + attributeCount + collection { + collectionId + } + attributes { + key + value + } + accounts(after: $accountsCursor, first: $accountsLimit) { + totalCount + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + edges { + cursor + node { + balance + reservedBalance + isFrozen + wallet { + account { + publicKey + address + } + } + approvals { + amount + expiration + wallet { + account { + publicKey + address + } + } + } + namedReserves { + pallet + amount + } + } + } + } + nonFungible + } + } + totalCount + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } +} diff --git a/tests/Feature/GraphQL/Resources/GetTransaction.graphql b/tests/Feature/GraphQL/Resources/GetTransaction.graphql new file mode 100644 index 00000000..7a724516 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/GetTransaction.graphql @@ -0,0 +1,53 @@ +query GetTransaction( + $id: BigInt + $transactionId: String + $transactionHash: String + $idempotencyKey: String +) { + GetTransaction( + id: $id + transactionId: $transactionId + transactionHash: $transactionHash + idempotencyKey: $idempotencyKey + ) { + id + idempotencyKey + transactionId + transactionHash + method + state + result + encodedData + signedAtBlock + createdAt + updatedAt + wallet { + account { + publicKey + address + } + } + events { + edges { + cursor + node { + phase + lookUp + moduleId + eventId + params { + type + value + } + } + } + totalCount + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } +} \ No newline at end of file diff --git a/tests/Feature/GraphQL/Resources/GetTransactions.graphql b/tests/Feature/GraphQL/Resources/GetTransactions.graphql new file mode 100644 index 00000000..b8848fc2 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/GetTransactions.graphql @@ -0,0 +1,77 @@ +query GetTransactions( + $ids: [BigInt] + $transactionIds: [String] + $transactionHashes: [String] + $methods: [TransactionMethod] + $states: [TransactionState] + $results: [TransactionResult] + $idempotencyKeys: [String] + $signedAtBlocks: [Int] + $after: String + $first: Int +) { + GetTransactions( + ids: $ids + transactionIds: $transactionIds + transactionHashes: $transactionHashes + methods: $methods + states: $states + results: $results + idempotencyKeys: $idempotencyKeys + signedAtBlocks: $signedAtBlocks + after: $after + first: $first + ) { + edges { + cursor + node { + id + idempotencyKey + transactionId + transactionHash + method + state + result + encodedData + signedAtBlock + createdAt + updatedAt + wallet { + account { + publicKey + address + } + } + events { + edges { + cursor + node { + phase + lookUp + moduleId + eventId + params { + type + value + } + } + } + totalCount + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + } + totalCount + pageInfo { + startCursor + endCursor + hasPreviousPage + hasNextPage + } + } +} \ No newline at end of file diff --git a/tests/Feature/GraphQL/Resources/GetWallet.graphql b/tests/Feature/GraphQL/Resources/GetWallet.graphql new file mode 100644 index 00000000..782ffd37 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/GetWallet.graphql @@ -0,0 +1,358 @@ +query GetWallet( + $id: Int + $externalId: String + $verificationId: String + $account: String + $collectionAccountsCollectionIds: [BigInt] + $collectionAccountsCursor: String + $collectionAccountsLimit: Int + $tokenAccountsCollectionIds: [BigInt] + $tokenAccountsTokenIds: [BigInt] + $tokenAccountsCursor: String + $tokenAccountsLimit: Int + $collectionAccountApprovalsCursor: String + $collectionAccountApprovalsLimit: Int + $tokenAccountApprovalsCursor: String + $tokenAccountApprovalsLimit: Int + $transactionsTransactionIds: [String] + $transactionsTransactionHashes: [String] + $transactionsMethods: [TransactionMethod] + $transactionsStates: [TransactionState] + $transactionsCursor: String + $transactionsLimit: Int + $ownedCollectionsCollectionIds: [BigInt] + $ownedCollectionsCursor: String + $ownedCollectionsLimit: Int + $tokenTokenAccountsCursor: String + $tokenTokenAccountsLimit: Int +) { + GetWallet( + id: $id + externalId: $externalId + verificationId: $verificationId + account: $account + ) { + id + account { + publicKey + address + } + externalId + managed + network + nonce + balances { + free + reserved + miscFrozen + feeFrozen + } + collectionAccounts( + collectionIds: $collectionAccountsCollectionIds + after: $collectionAccountsCursor + first: $collectionAccountsLimit + ) { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + accountCount + isFrozen + collection { + collectionId + } + wallet { + account { + publicKey + address + } + } + approvals { + expiration + wallet { + account { + publicKey + address + } + } + } + } + } + } + tokenAccounts( + collectionIds: $tokenAccountsCollectionIds + tokenIds: $tokenAccountsTokenIds + after: $tokenAccountsCursor + first: $tokenAccountsLimit + ) { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + balance + reservedBalance + isFrozen + collection { + collectionId + } + wallet { + account { + publicKey + address + } + } + token { + tokenId + } + approvals { + amount + expiration + wallet { + account { + publicKey + address + } + } + } + namedReserves { + pallet + amount + } + } + } + } + collectionAccountApprovals( + after: $collectionAccountApprovalsCursor + first: $collectionAccountApprovalsLimit + ) { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + expiration + wallet { + account { + publicKey + address + } + } + } + } + } + tokenAccountApprovals( + after: $tokenAccountApprovalsCursor + first: $tokenAccountApprovalsLimit + ) { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + amount + expiration + wallet { + account { + publicKey + address + } + } + } + } + } + transactions( + transactionIds: $transactionsTransactionIds + transactionHashes: $transactionsTransactionHashes + methods: $transactionsMethods + states: $transactionsStates + after: $transactionsCursor + first: $transactionsLimit + ) { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + id + transactionId + transactionHash + method + state + encodedData + events { + edges { + cursor + node { + phase + lookUp + moduleId + eventId + params { + type + value + } + } + } + totalCount + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + wallet { + account { + publicKey + address + } + } + } + } + } + ownedCollections( + collectionIds: $ownedCollectionsCollectionIds + after: $ownedCollectionsCursor + first: $ownedCollectionsLimit + ) { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + collectionId + maxTokenCount + maxTokenSupply + forceSingleMint + network + owner { + account { + publicKey + address + } + } + attributes { + key + value + } + accounts { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + accountCount + isFrozen + collection { + collectionId + } + wallet { + account { + publicKey + address + } + } + approvals { + expiration + wallet { + account { + publicKey + address + } + } + } + } + } + } + tokens { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + tokenId + supply + cap + capSupply + isFrozen + minimumBalance + unitPrice + mintDeposit + attributeCount + collection { + collectionId + } + attributes { + key + value + } + accounts( + after: $tokenTokenAccountsCursor, + first: $tokenTokenAccountsLimit + ) { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + balance + } + } + } + metadata + nonFungible + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/Feature/GraphQL/Resources/GetWallets.graphql b/tests/Feature/GraphQL/Resources/GetWallets.graphql new file mode 100644 index 00000000..fe023134 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/GetWallets.graphql @@ -0,0 +1,370 @@ +query GetWallets( + $ids: [Int!] + $externalIds: [String!] + $verificationIds: [String!] + $accounts: [String!] + $collectionAccountsCollectionIds: [BigInt] + $collectionAccountsCursor: String + $collectionAccountsLimit: Int + $tokenAccountsCollectionIds: [BigInt] + $tokenAccountsTokenIds: [BigInt] + $tokenAccountsCursor: String + $tokenAccountsLimit: Int + $collectionAccountApprovalsCursor: String + $collectionAccountApprovalsLimit: Int + $tokenAccountApprovalsCursor: String + $tokenAccountApprovalsLimit: Int + $transactionsTransactionIds: [String] + $transactionsTransactionHashes: [String] + $transactionsMethods: [TransactionMethod] + $transactionsStates: [TransactionState] + $transactionsCursor: String + $transactionsLimit: Int + $ownedCollectionsCollectionIds: [BigInt] + $ownedCollectionsCursor: String + $ownedCollectionsLimit: Int + $tokenTokenAccountsCursor: String + $tokenTokenAccountsLimit: Int +) { + GetWallets( + ids: $ids + externalIds: $externalIds + verificationIds: $verificationIds + accounts: $accounts + ) { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + id + account { + publicKey + address + } + externalId + managed + network + nonce + balances { + free + reserved + miscFrozen + feeFrozen + } + collectionAccounts( + collectionIds: $collectionAccountsCollectionIds + after: $collectionAccountsCursor + first: $collectionAccountsLimit + ) { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + accountCount + isFrozen + collection { + collectionId + } + wallet { + account { + publicKey + address + } + } + approvals { + expiration + wallet { + account { + publicKey + address + } + } + } + } + } + } + tokenAccounts( + collectionIds: $tokenAccountsCollectionIds + tokenIds: $tokenAccountsTokenIds + after: $tokenAccountsCursor + first: $tokenAccountsLimit + ) { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + balance + reservedBalance + isFrozen + collection { + collectionId + } + wallet { + account { + publicKey + address + } + } + token { + tokenId + } + approvals { + amount + expiration + wallet { + account { + publicKey + address + } + } + } + namedReserves { + pallet + amount + } + } + } + } + collectionAccountApprovals( + after: $collectionAccountApprovalsCursor + first: $collectionAccountApprovalsLimit + ) { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + expiration + wallet { + account { + publicKey + address + } + } + } + } + } + tokenAccountApprovals( + after: $tokenAccountApprovalsCursor + first: $tokenAccountApprovalsLimit + ) { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + amount + expiration + wallet { + account { + publicKey + address + } + } + } + } + } + transactions( + transactionIds: $transactionsTransactionIds + transactionHashes: $transactionsTransactionHashes + methods: $transactionsMethods + states: $transactionsStates + after: $transactionsCursor + first: $transactionsLimit + ) { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + id + transactionId + transactionHash + method + state + encodedData + events { + edges { + cursor + node { + phase + lookUp + moduleId + eventId + params { + type + value + } + } + } + totalCount + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + wallet { + account { + publicKey + address + } + } + } + } + } + ownedCollections( + collectionIds: $ownedCollectionsCollectionIds + after: $ownedCollectionsCursor + first: $ownedCollectionsLimit + ) { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + collectionId + maxTokenCount + maxTokenSupply + forceSingleMint + network + owner { + account { + publicKey + address + } + } + attributes { + key + value + } + accounts { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + accountCount + isFrozen + collection { + collectionId + } + wallet { + account { + publicKey + address + } + } + approvals { + expiration + wallet { + account { + publicKey + address + } + } + } + } + } + } + tokens { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + tokenId + supply + cap + capSupply + isFrozen + minimumBalance + unitPrice + mintDeposit + attributeCount + collection { + collectionId + } + attributes { + key + value + } + accounts( + after: $tokenTokenAccountsCursor + first: $tokenTokenAccountsLimit + ) { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + balance + } + } + } + metadata + nonFungible + } + } + } + } + } + } + } + } + } +} diff --git a/tests/Feature/GraphQL/Resources/MarkAndListPendingTransactions.graphql b/tests/Feature/GraphQL/Resources/MarkAndListPendingTransactions.graphql new file mode 100644 index 00000000..f637fb85 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/MarkAndListPendingTransactions.graphql @@ -0,0 +1,37 @@ +mutation MarkAndListPendingTransactions( + $accounts: [String] + $markAsProcessing: Boolean + $after: String + $first: Int +) { + MarkAndListPendingTransactions( + accounts: $accounts + markAsProcessing: $markAsProcessing + after: $after + first: $first + ) { + edges { + cursor + node { + id + transactionId + transactionHash + state + encodedData + wallet { + account { + publicKey + address + } + } + } + } + totalCount + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } +} \ No newline at end of file diff --git a/tests/Feature/GraphQL/Resources/MintToken.graphql b/tests/Feature/GraphQL/Resources/MintToken.graphql new file mode 100644 index 00000000..a2dbf9b9 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/MintToken.graphql @@ -0,0 +1,24 @@ +mutation MintToken( + $recipient: String! + $collectionId: BigInt! + $params: MintTokenParams! +) { + MintToken( + recipient: $recipient + collectionId: $collectionId + params: $params + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} diff --git a/tests/Feature/GraphQL/Resources/MutateCollection.graphql b/tests/Feature/GraphQL/Resources/MutateCollection.graphql new file mode 100644 index 00000000..a711c347 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/MutateCollection.graphql @@ -0,0 +1,22 @@ +mutation MutateCollection( + $collectionId: BigInt! + $mutation: CollectionMutationInput! +) { + MutateCollection( + collectionId: $collectionId + mutation: $mutation + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} diff --git a/tests/Feature/GraphQL/Resources/MutateToken.graphql b/tests/Feature/GraphQL/Resources/MutateToken.graphql new file mode 100644 index 00000000..e7c2543d --- /dev/null +++ b/tests/Feature/GraphQL/Resources/MutateToken.graphql @@ -0,0 +1,24 @@ +mutation MutateToken( + $collectionId: BigInt! + $tokenId: EncodableTokenIdInput! + $mutation: TokenMutationInput! +) { + MutateToken( + collectionId: $collectionId + tokenId: $tokenId + mutation: $mutation + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} diff --git a/tests/Feature/GraphQL/Resources/OperatorTransferToken.graphql b/tests/Feature/GraphQL/Resources/OperatorTransferToken.graphql new file mode 100644 index 00000000..2a6e3a5d --- /dev/null +++ b/tests/Feature/GraphQL/Resources/OperatorTransferToken.graphql @@ -0,0 +1,26 @@ +mutation OperatorTransferToken( + $collectionId: BigInt! + $recipient: String! + $params: OperatorTransferParams! + $signingAccount: String +) { + OperatorTransferToken( + collectionId: $collectionId + recipient: $recipient + params: $params + signingAccount: $signingAccount + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} diff --git a/tests/Feature/GraphQL/Resources/RemoveAllAttributes.graphql b/tests/Feature/GraphQL/Resources/RemoveAllAttributes.graphql new file mode 100644 index 00000000..dd2e6d3e --- /dev/null +++ b/tests/Feature/GraphQL/Resources/RemoveAllAttributes.graphql @@ -0,0 +1,24 @@ +mutation RemoveAllAttributes( + $collectionId: BigInt! + $tokenId: EncodableTokenIdInput + $attributeCount: Int +) { + RemoveAllAttributes( + collectionId: $collectionId + tokenId: $tokenId + attributeCount: $attributeCount + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} diff --git a/tests/Feature/GraphQL/Resources/RemoveCollectionAttribute.graphql b/tests/Feature/GraphQL/Resources/RemoveCollectionAttribute.graphql new file mode 100644 index 00000000..49eb5073 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/RemoveCollectionAttribute.graphql @@ -0,0 +1,22 @@ +mutation RemoveCollectionAttribute( + $collectionId: BigInt! + $key: String! +) { + RemoveCollectionAttribute( + collectionId: $collectionId + key: $key + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} diff --git a/tests/Feature/GraphQL/Resources/RemoveTokenAttribute.graphql b/tests/Feature/GraphQL/Resources/RemoveTokenAttribute.graphql new file mode 100644 index 00000000..172bb2b4 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/RemoveTokenAttribute.graphql @@ -0,0 +1,24 @@ +mutation RemoveTokenAttribute( + $collectionId: BigInt! + $tokenId: EncodableTokenIdInput! + $key: String! +) { + RemoveTokenAttribute( + collectionId: $collectionId + tokenId: $tokenId + key: $key + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} diff --git a/tests/Feature/GraphQL/Resources/RequestAccount.graphql b/tests/Feature/GraphQL/Resources/RequestAccount.graphql new file mode 100644 index 00000000..80ac472a --- /dev/null +++ b/tests/Feature/GraphQL/Resources/RequestAccount.graphql @@ -0,0 +1,10 @@ +query RequestAccount( + $callback: String! +) { + RequestAccount( + callback: $callback + ) { + qrCode + verificationId + } +} diff --git a/tests/Feature/GraphQL/Resources/RetryTransactions.graphql b/tests/Feature/GraphQL/Resources/RetryTransactions.graphql new file mode 100644 index 00000000..3575ac65 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/RetryTransactions.graphql @@ -0,0 +1,3 @@ +mutation RetryTransactions($ids: [BigInt!], $idempotencyKeys: [String!]) { + RetryTransactions(ids: $ids, idempotencyKeys: $idempotencyKeys) +} diff --git a/tests/Feature/GraphQL/Resources/SetCollectionAttribute.graphql b/tests/Feature/GraphQL/Resources/SetCollectionAttribute.graphql new file mode 100644 index 00000000..4f81384c --- /dev/null +++ b/tests/Feature/GraphQL/Resources/SetCollectionAttribute.graphql @@ -0,0 +1,24 @@ +mutation SetCollectionAttribute( + $collectionId: BigInt! + $key: String! + $value: String! +) { + SetCollectionAttribute( + collectionId: $collectionId + key: $key + value: $value + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} diff --git a/tests/Feature/GraphQL/Resources/SetTokenAttribute.graphql b/tests/Feature/GraphQL/Resources/SetTokenAttribute.graphql new file mode 100644 index 00000000..aaa5ebf3 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/SetTokenAttribute.graphql @@ -0,0 +1,26 @@ +mutation SetTokenAttribute( + $collectionId: BigInt! + $tokenId: EncodableTokenIdInput! + $key: String! + $value: String! +) { + SetTokenAttribute( + collectionId: $collectionId + tokenId: $tokenId + key: $key + value: $value + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} diff --git a/tests/Feature/GraphQL/Resources/SetWalletAccount.graphql b/tests/Feature/GraphQL/Resources/SetWalletAccount.graphql new file mode 100644 index 00000000..a76969a7 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/SetWalletAccount.graphql @@ -0,0 +1,11 @@ +mutation SetWalletAccount( + $id: Int + $externalId: String + $account: String! +) { + SetWalletAccount( + id: $id + externalId: $externalId + account: $account + ) +} diff --git a/tests/Feature/GraphQL/Resources/SimpleTransferToken.graphql b/tests/Feature/GraphQL/Resources/SimpleTransferToken.graphql new file mode 100644 index 00000000..93f58318 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/SimpleTransferToken.graphql @@ -0,0 +1,26 @@ +mutation SimpleTransferToken( + $collectionId: BigInt! + $recipient: String! + $params: SimpleTransferParams! + $signingAccount: String +) { + SimpleTransferToken( + collectionId: $collectionId + recipient: $recipient + params: $params + signingAccount: $signingAccount + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} diff --git a/tests/Feature/GraphQL/Resources/Thaw.graphql b/tests/Feature/GraphQL/Resources/Thaw.graphql new file mode 100644 index 00000000..f5fbd655 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/Thaw.graphql @@ -0,0 +1,28 @@ +mutation Thaw( + $freezeType: FreezeType! + $collectionId: BigInt! + $tokenId: EncodableTokenIdInput + $collectionAccount: String + $tokenAccount: String +) { + Thaw( + freezeType: $freezeType + collectionId: $collectionId + tokenId: $tokenId + collectionAccount: $collectionAccount + tokenAccount: $tokenAccount + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} diff --git a/tests/Feature/GraphQL/Resources/TransferAllBalance.graphql b/tests/Feature/GraphQL/Resources/TransferAllBalance.graphql new file mode 100644 index 00000000..db819420 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/TransferAllBalance.graphql @@ -0,0 +1,24 @@ +mutation TransferAllBalance( + $recipient: String! + $keepAlive: Boolean + $signingAccount: String +) { + TransferAllBalance( + recipient: $recipient + keepAlive: $keepAlive + signingAccount: $signingAccount + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} diff --git a/tests/Feature/GraphQL/Resources/TransferBalance.graphql b/tests/Feature/GraphQL/Resources/TransferBalance.graphql new file mode 100644 index 00000000..272f8d97 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/TransferBalance.graphql @@ -0,0 +1,26 @@ +mutation TransferBalance( + $recipient: String! + $amount: BigInt! + $keepAlive: Boolean + $signingAccount: String +) { + TransferBalance( + recipient: $recipient + amount: $amount + keepAlive: $keepAlive + signingAccount: $signingAccount + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} diff --git a/tests/Feature/GraphQL/Resources/UnapproveCollection.graphql b/tests/Feature/GraphQL/Resources/UnapproveCollection.graphql new file mode 100644 index 00000000..458c402e --- /dev/null +++ b/tests/Feature/GraphQL/Resources/UnapproveCollection.graphql @@ -0,0 +1,22 @@ +mutation UnapproveCollection( + $collectionId: BigInt! + $operator: String! +) { + UnapproveCollection( + collectionId: $collectionId + operator: $operator + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} diff --git a/tests/Feature/GraphQL/Resources/UnapproveToken.graphql b/tests/Feature/GraphQL/Resources/UnapproveToken.graphql new file mode 100644 index 00000000..d87d239a --- /dev/null +++ b/tests/Feature/GraphQL/Resources/UnapproveToken.graphql @@ -0,0 +1,24 @@ +mutation UnapproveToken( + $collectionId: BigInt! + $tokenId: EncodableTokenIdInput! + $operator: String! +) { + UnapproveToken( + collectionId: $collectionId + tokenId: $tokenId + operator: $operator + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} diff --git a/tests/Feature/GraphQL/Resources/UpdateTransaction.graphql b/tests/Feature/GraphQL/Resources/UpdateTransaction.graphql new file mode 100644 index 00000000..57954623 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/UpdateTransaction.graphql @@ -0,0 +1,15 @@ +mutation UpdateTransaction( + $id: Int! + $state: TransactionState + $transactionId: String + $transactionHash: String + $signedAtBlock: Int +) { + UpdateTransaction( + id: $id + state: $state + transactionId: $transactionId + transactionHash: $transactionHash + signedAtBlock: $signedAtBlock + ) +} \ No newline at end of file diff --git a/tests/Feature/GraphQL/Resources/UpdateWalletExternalId.graphql b/tests/Feature/GraphQL/Resources/UpdateWalletExternalId.graphql new file mode 100644 index 00000000..5299f134 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/UpdateWalletExternalId.graphql @@ -0,0 +1,13 @@ +mutation UpdateWalletExternalId( + $id: Int + $externalId: String + $newExternalId: String! + $account: String +) { + UpdateWalletExternalId( + id: $id + externalId: $externalId + newExternalId: $newExternalId + account: $account + ) +} diff --git a/tests/Feature/GraphQL/Resources/VerifyAccount.graphql b/tests/Feature/GraphQL/Resources/VerifyAccount.graphql new file mode 100644 index 00000000..e84989df --- /dev/null +++ b/tests/Feature/GraphQL/Resources/VerifyAccount.graphql @@ -0,0 +1,13 @@ +mutation VerifyAccount( + $verificationId: String! + $signature: String! + $account: String! + $cryptoSignatureType: CryptoSignatureType +) { + VerifyAccount( + verificationId: $verificationId + signature: $signature + account: $account + cryptoSignatureType: $cryptoSignatureType + ) +} \ No newline at end of file diff --git a/tests/Feature/GraphQL/Resources/VerifyAccountWithLabel.graphql b/tests/Feature/GraphQL/Resources/VerifyAccountWithLabel.graphql new file mode 100644 index 00000000..5c7d19b8 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/VerifyAccountWithLabel.graphql @@ -0,0 +1,13 @@ +mutation VerifyAccount( + $verificationId: String! + $signature: String! + $account: String! + $cryptoSignatureType: CryptoSignatureType +) { + result: VerifyAccount( + verificationId: $verificationId + signature: $signature + account: $account + cryptoSignatureType: $cryptoSignatureType + ) +} \ No newline at end of file diff --git a/tests/Feature/GraphQL/Resources/VerifyMessage.graphql b/tests/Feature/GraphQL/Resources/VerifyMessage.graphql new file mode 100644 index 00000000..89b63c26 --- /dev/null +++ b/tests/Feature/GraphQL/Resources/VerifyMessage.graphql @@ -0,0 +1,13 @@ +query VerifyMessage( + $message: String! + $signature: String! + $publicKey: String! + $cryptoSignatureType: CryptoSignatureType +) { + VerifyMessage( + message: $message + signature: $signature + publicKey: $publicKey + cryptoSignatureType: $cryptoSignatureType + ) +} \ No newline at end of file diff --git a/tests/Feature/GraphQL/TestCaseGraphQL.php b/tests/Feature/GraphQL/TestCaseGraphQL.php new file mode 100644 index 00000000..f352edcf --- /dev/null +++ b/tests/Feature/GraphQL/TestCaseGraphQL.php @@ -0,0 +1,213 @@ +artisan('migrate:fresh'); + $this->loadQueries(); + + self::$initialized = true; + } + } + + public function graphql(string $query, array $arguments = [], ?bool $expectError = false, ?array $opts = []) + { + $result = GraphQL::queryAndReturnResult(self::$queries[$query], $arguments, $opts); + $data = $result->toArray(); + + $assertMessage = null; + + if (!$expectError && isset($data['errors'])) { + $appendErrors = ''; + + if (isset($data['errors'][0]['trace'])) { + $appendErrors = "\n\n" . $this->formatSafeTrace($data['errors'][0]['trace']); + } + + $assertMessage = "Probably unexpected error in GraphQL response:\n" + . var_export($data, true) + . $appendErrors; + } + + unset($data['errors'][0]['trace']); + + if ($assertMessage) { + throw new ExpectationFailedException($assertMessage); + } + + $previous = Arr::first($result->errors)?->getPrevious(); + + if (!is_null($previous) && ValidationException::class === get_class($previous)) { + $data['errors'] = $previous->validator->errors()->getMessages(); + $data['error'] = $previous->getMessage(); + } elseif ('validation' === Arr::get($data, 'errors.0.message')) { + $data['error'] = $previous->getValidatorMessages()->toArray(); + } elseif (null !== Arr::get($data, 'errors.0.message')) { + $data['error'] = $data['errors'][0]['message']; + } + + return $expectError ? $data : Arr::get($data['data'], $query); + } + + /** + * Helper to dispatch an HTTP GraphQL requests. + */ + protected function httpGraphql(string $method, array $options = [], array $headers = []): mixed + { + $query = self::$queries[$method]; + $expectedHttpStatusCode = $options['httpStatusCode'] ?? 200; + $expectErrors = $options['expectErrors'] ?? false; + $variables = $options['variables'] ?? null; + $schemaName = $options['schemaName'] ?? null; + + $payload = ['query' => $query]; + if ($variables) { + $payload['variables'] = $variables; + } + + $response = $this->json( + 'POST', + '/graphql' . ($schemaName ? "/{$schemaName}" : ''), + $payload, + $headers + ); + $result = $response->getData(true); + + $httpStatusCode = $response->getStatusCode(); + if ($expectedHttpStatusCode !== $httpStatusCode) { + self::assertSame($expectedHttpStatusCode, $httpStatusCode, var_export($result, true) . "\n"); + } + + $assertMessage = null; + if (!$expectErrors && isset($result['errors'])) { + $appendErrors = ''; + if (isset($result['errors'][0]['trace'])) { + $appendErrors = "\n\n" . $this->formatSafeTrace($result['errors'][0]['trace']); + } + + $assertMessage = "Probably unexpected error in GraphQL response:\n" + . var_export($result, true) + . $appendErrors; + } + unset($result['errors'][0]['trace']); + + if ($assertMessage) { + throw new ExpectationFailedException($assertMessage); + } + + return Arr::first($result['data']); + } + + /** + * Load all queries from the Resources directory. + */ + protected function loadQueries(): void + { + $files = scandir(__DIR__ . '/Resources'); + collect($files) + ->filter(fn ($file) => str_ends_with($file, '.gql') || str_ends_with($file, '.graphql')) + ->each( + fn ($file) => self::$queries[str_replace(['.gql', '.graphql'], '', $file)] = file_get_contents(__DIR__ . '/Resources/' . $file) + ); + } + + /** + * Get the package providers. + */ + protected function getPackageProviders($app): array + { + return [ + CoreServiceProvider::class, + ]; + } + + /** + * Get the package aliases. + */ + protected function getPackageAliases($app): array + { + return [ + 'Package' => Package::class, + ]; + } + + /** + * Define environment setup. + */ + protected function defineEnvironment($app): void + { + $app->useEnvironmentPath(__DIR__ . '/..'); + $app->bootstrapWith([LoadEnvironmentVariables::class]); + + $app['config']->set('database.default', env('DB_DRIVER', 'mysql')); + $app['config']->set('database.connections.mysql', [ + 'driver' => 'mysql', + 'host' => env('DB_HOST', '127.0.0.1:3306'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', 'password'), + 'database' => env('DB_DATABASE', 'platform'), + 'prefix' => '', + ]); + + $app['config']->set('app.debug', true); + + if ($this->fakeEvents) { + Event::fake(); + } + } + + /** + * Converts the trace as generated from \GraphQL\Error\FormattedError::toSafeTrace + * to a more human-readable string for a failed test. + */ + private function formatSafeTrace(array $trace): string + { + return implode( + "\n", + array_map(static function (array $row, int $index): string { + $line = "#{$index} "; + $line .= $row['file'] ?? ''; + + if (isset($row['line'])) { + $line .= "({$row['line']}) :"; + } + + if (isset($row['call'])) { + $line .= ' ' . $row['call']; + } + + if (isset($row['function'])) { + $line .= ' ' . $row['function']; + } + + return $line; + }, $trace, array_keys($trace)) + ); + } +} diff --git a/tests/Feature/GraphQL/Traits/HasConvertableObject.php b/tests/Feature/GraphQL/Traits/HasConvertableObject.php new file mode 100644 index 00000000..c8bd76a8 --- /dev/null +++ b/tests/Feature/GraphQL/Traits/HasConvertableObject.php @@ -0,0 +1,16 @@ +tokenIdInput = ['integer' => $tokenIdInt]; + } + + if ($this->tokenIdInput && !$new) { + return $this->tokenIdInput; + } + + $this->tokenIdInput = [ + 'hash' => $this->toObject([ + 'test1' => fake()->sentence(1), + 'test2' => [1], + 'test3' => [ + ['name' => fake()->name()], + ['name' => fake()->name()], + ], + ]), + ]; + + return $this->tokenIdInput; + } + + /** + * Get encoded token. + */ + protected function getEncodedToken(): string + { + return resolve(TokenIdManager::class)->encode(['tokenId' => $this->tokenIdInput]); + } + + /** + * Update token chain ID. + */ + protected function updateTokenChainId(): string + { + $this->token->forceFill(['token_chain_id' => $encoded = $this->getEncodedToken()])->save(); + + return $encoded; + } + + /** + * Generate new token. + */ + protected function newToken(): Token + { + $this->token = (new Token())->forceFill([ + 'collection_id' => $this->collection->id, + 'token_chain_id' => $this->getEncodedToken(), + 'supply' => (string) $supply = fake()->numberBetween(1), + 'cap' => TokenMintCapType::INFINITE->name, + 'cap_supply' => null, + 'is_frozen' => false, + 'unit_price' => (string) $unitPrice = fake()->numberBetween(1 / $supply * 10 ** 17), + 'mint_deposit' => (string) ($unitPrice * $supply), + 'minimum_balance' => '1', + 'attribute_count' => '0', + ]); + $this->token->save(); + + return $this->token; + } +} diff --git a/tests/Feature/GraphQL/Traits/HasHttp.php b/tests/Feature/GraphQL/Traits/HasHttp.php new file mode 100644 index 00000000..58c3a874 --- /dev/null +++ b/tests/Feature/GraphQL/Traits/HasHttp.php @@ -0,0 +1,20 @@ +resolveType('IntegerRange', '1') + ->assertScalarValueEquals([1]); + } + + public function test_negative_integer_is_expanded_to_array() + { + $this->resolveType('IntegerRange', '-1') + ->assertScalarValueEquals([-1]); + } + + public function test_integer_range_is_expanded_to_array() + { + $this->resolveType('IntegerRange', '1..3') + ->assertScalarValueEquals([1, 2, 3]); + } + + public function test_negative_integer_range_is_expanded_to_array() + { + $this->resolveType('IntegerRange', '-1..2') + ->assertScalarValueEquals([-1, 0, 1, 2]); + } + + public function test_big_integer_range_is_expanded_to_array() + { + $this->resolveType('IntegerRange', '340282366920938463463374607431768211453..340282366920938463463374607431768211455') + ->assertScalarValueEquals(['340282366920938463463374607431768211453', '340282366920938463463374607431768211454', '340282366920938463463374607431768211455']); + } + + public function test_integer_array_is_serialized_to_range() + { + $this->resolveType('IntegerRange', [1, 2, 3]) + ->assertSerializedScalarValueEquals('1..3'); + } + + public function test_big_integer_array_is_serialized_to_range() + { + $this->resolveType('IntegerRange', ['340282366920938463463374607431768211453', '340282366920938463463374607431768211454', '340282366920938463463374607431768211455']) + ->assertSerializedScalarValueEquals('340282366920938463463374607431768211453..340282366920938463463374607431768211455'); + } + + public function test_inverted_integer_range_array_fails() + { + $this->expectExceptionMessage('Cannot represent following value as integer range: "3..1"'); + + $this->resolveType('IntegerRange', '3..1') + ->assertScalarValueEquals([1, 2, 3]); + } + + public function test_integer_range_array_fails() + { + $this->expectExceptionMessage('Cannot represent following value as integer range: ["1..3","5"]'); + + $this->resolveType('IntegerRange', ['1..3', '5']) + ->assertScalarValueEquals([1, 2, 3, 5]); + } + + public function test_float_fails() + { + $this->expectExceptionMessage('Cannot represent following value as integer range: "1.3"'); + + $this->resolveType('IntegerRange', '1.3') + ->assertScalarValueEquals([1, 2, 3]); + } + + public function test_integer_range_with_extra_dot_fails() + { + $this->expectExceptionMessage('Cannot represent following value as integer range: "1...3"'); + + $this->resolveType('IntegerRange', '1...3') + ->assertScalarValueEquals([1, 2, 3]); + } + + public function test_invalid_input_fails() + { + $this->expectExceptionMessage('Cannot represent following value as integer range: "a"'); + + $this->resolveType('IntegerRange', 'a') + ->assertScalarValueEquals([10]); + } +} diff --git a/tests/Feature/GraphQL/Types/IntegerRangesArrayTypeTest.php b/tests/Feature/GraphQL/Types/IntegerRangesArrayTypeTest.php new file mode 100644 index 00000000..dfba0070 --- /dev/null +++ b/tests/Feature/GraphQL/Types/IntegerRangesArrayTypeTest.php @@ -0,0 +1,82 @@ +resolveType('IntegerRangesArray', ['1']) + ->assertScalarValueEquals([1]); + } + + public function test_integer_range_is_expanded_to_array() + { + $this->resolveType('IntegerRangesArray', ['1..3', '5']) + ->assertScalarValueEquals([1, 2, 3, 5]); + } + + public function test_negative_integer_range_is_expanded_to_array() + { + $this->resolveType('IntegerRangesArray', ['-4', '-1..2', '5']) + ->assertScalarValueEquals([-4, -1, 0, 1, 2, 5]); + } + + public function test_integer_array_is_serialized_to_ranges() + { + $this->resolveType('IntegerRangesArray', [1, 2, 3, 5]) + ->assertSerializedScalarValueEquals(['1..3', '5']); + } + + public function test_big_integer_range_is_expanded_to_array() + { + $this->resolveType('IntegerRangesArray', ['340282366920938463463374607431768211450', '340282366920938463463374607431768211453..340282366920938463463374607431768211455']) + ->assertScalarValueEquals(['340282366920938463463374607431768211450', '340282366920938463463374607431768211453', '340282366920938463463374607431768211454', '340282366920938463463374607431768211455']); + } + + public function test_big_integer_array_is_serialized_to_ranges() + { + $this->resolveType('IntegerRangesArray', ['340282366920938463463374607431768211450', '340282366920938463463374607431768211453', '340282366920938463463374607431768211454', '340282366920938463463374607431768211455']) + ->assertSerializedScalarValueEquals(['340282366920938463463374607431768211450', '340282366920938463463374607431768211453..340282366920938463463374607431768211455']); + } + + public function test_inverted_integer_range_array_fails() + { + $this->expectExceptionMessage('Cannot represent following value as integer range: ["3..1","5"]'); + + $this->resolveType('IntegerRange', ['3..1', '5']) + ->assertScalarValueEquals([1, 2, 3, 5]); + } + + public function test_non_array_fails() + { + $this->expectExceptionMessage('Cannot represent following value as integer ranges array: "1..3"'); + + $this->resolveType('IntegerRangesArray', '1..3') + ->assertScalarValueEquals([1, 2, 3]); + } + + public function test_float_fails() + { + $this->expectExceptionMessage('Cannot represent following value as integer ranges array: ["1.3","5"]'); + + $this->resolveType('IntegerRangesArray', ['1.3', '5']) + ->assertScalarValueEquals([1, 2, 3]); + } + + public function test_integer_range_with_extra_dot_fails() + { + $this->expectExceptionMessage('Cannot represent following value as integer ranges array: ["1...3","5"]'); + + $this->resolveType('IntegerRangesArray', ['1...3', '5']) + ->assertScalarValueEquals([1, 2, 3]); + } + + public function test_invalid_input_fails() + { + $this->expectExceptionMessage('Cannot represent following value as integer ranges array: ["a","16"]'); + + $this->resolveType('IntegerRangesArray', ['a', '16']) + ->assertScalarValueEquals([10]); + } +} diff --git a/tests/Feature/GraphQL/Types/TestCase.php b/tests/Feature/GraphQL/Types/TestCase.php new file mode 100644 index 00000000..eca65405 --- /dev/null +++ b/tests/Feature/GraphQL/Types/TestCase.php @@ -0,0 +1,36 @@ +createTestType($name)->resolve($subject); + } + + /** + * Sets the default schema. + */ + public function schema(string $name): self + { + Config::set('graphql.default_schema', $name); + + return $this; + } +} diff --git a/tests/Feature/GraphQL/Types/TestType.php b/tests/Feature/GraphQL/Types/TestType.php new file mode 100644 index 00000000..4e87512d --- /dev/null +++ b/tests/Feature/GraphQL/Types/TestType.php @@ -0,0 +1,99 @@ +type = $type; + } + + public function resolve($subject): self + { + $this->subject = $subject; + + return $this; + } + + public function actingAs(Authorizable $context): self + { + $this->context = $context; + + return $this; + } + + public function assertHasField(string $field): self + { + try { + $hasField = (bool) $this->resolveField($field); + } catch (InvariantViolation $e) { + $hasField = false; + } + + PHPUnit::assertTrue($hasField); + + return $this; + } + + public function assertDoesntHaveField(string $field): self + { + try { + $hasField = (bool) $this->resolveField($field); + } catch (InvariantViolation $e) { + $hasField = false; + } + + PHPUnit::assertFalse($hasField); + + return $this; + } + + public function assertFieldEquals(string $field, $expected): self + { + PHPUnit::assertEquals($expected, $this->resolveField($field)); + + return $this; + } + + public function assertFieldNull(string $field): self + { + PHPUnit::assertNull($this->resolveField($field)); + + return $this; + } + + public function assertScalarValueEquals($expected): self + { + PHPUnit::assertEquals($expected, $this->type->parseValue($this->subject)); + + return $this; + } + + public function assertSerializedScalarValueEquals($expected): self + { + PHPUnit::assertEquals($expected, $this->type->serialize($this->subject)); + + return $this; + } + + private function getFieldResolver(string $field): mixed + { + return $this->type->getField($field)->resolveFn; + } + + private function resolveField(string $field, array $args = []): mixed + { + $resolver = $this->getFieldResolver($field); + + return $resolver($this->subject, $args, $this->context); + } +} diff --git a/tests/Support/Mocks/StorageMock.php b/tests/Support/Mocks/StorageMock.php new file mode 100644 index 00000000..0f6e6ab7 --- /dev/null +++ b/tests/Support/Mocks/StorageMock.php @@ -0,0 +1,25 @@ + '2.0', + 'result' => $result, + 'id' => 1, + ], JSON_THROW_ON_ERROR); + } +} diff --git a/tests/Support/Mocks/SyncMock.php b/tests/Support/Mocks/SyncMock.php new file mode 100644 index 00000000..12e55a59 --- /dev/null +++ b/tests/Support/Mocks/SyncMock.php @@ -0,0 +1,139 @@ + [ + 'extrinsics' => [ + '0x45280401007503b1e1fcc9d19cbdbea2498d394bf84d59280bd6987bcba77e9bb1d74f6690cfdf5089055906c772afc7cb2715bf6f9ee9b0c8e47d460b8bf911630d94cfc8271b1005f4a5d8e51556d70188216414936b937d71b388d074569880813ffb9f02bda30c066175726120b3253208000000000470726f64808eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a4805617572610101c037bf84effc14d75c89c18c1182a23431f3c4a3795fc3e3628af62e6428526fc0071f66bb8bc1ebe396c55012c3d4e52c506e094ae2c966d7244e949d50758033000000681be06f164d1cff3e8d1932afbf1848047112ebfa303599593de589f03b384b000050003869035f04b49d95320d9021994c850f25b8e3851d030000300000800000080000000000100000c8000005000000050000000200000002000000000050000000100000e8764817000000040000000400000000000000000000000000000000000000000000000000000000000000000000000800000000200000040000000400000000001000b0040000000000000000000014000000040000000400000000000000010100000000060000006400000002000000c8000000020000001900000000000000020000000200000000c817a8040000000002000000050000001501800110804d68634f086d3047898d92f3bfcd0926570015be232e800c833b42e3e023b12d80114a7bc19738090504cd50c321014cd7acf21a0e742e68e76584b9f5e63850af1d0280046480c9a24b50739d07279c4606504f391ad3306283250a09af8de340eb6e9c3fabc580f6f6801e4b41e2e6d8ec194dba122bfb9eb33feb2545ef5144cea79551f7cc5280d7ebdb9b0d5e4edbc0d6e01f859242cd92011bcaae75e686b00b76c8c6036d61801daa946df66f92f0ed75c7eb5b7ed1665f6247c64943bee2adcde1c07ed907819901800814809f3ada68c357b5e0a3ebb39ef181acfa9943af4725c244330a4b2c60837612e88059a01c2dc15c768dbdd089eabdbb6d1f2d78ec57baf66a47f5749661819dbe7f80517736f059379ae47402cf9b5d9306d732b3a57362f23690da174c9ace2d1684a102801017806ad8bda250a43737b26933c1b7bebe6f4fe689b2888ff5ab4833d7394853350f8038f5c136db5bdb74d6a62722e6ea23af4fe820eb7fe28dee3a5f76d637dd3376803419c1ea3c5c7873325866f82a3d4bd7d3f222733227fcf3af1423fed4781c998065a1214a991ac85d93f38385d6f52af7b4ddcf56e589fcb92c8d9e8f27291c9280eb1c4ffb13d1a83f6219a7494ce2d6df3cfe3cd8943f631aa051ab04922a93609901802102804ab6e19583892b6d179b87d048d583d9e28d8de152d4cbf5fa162ae98d2996e3801a2ff24096295cfccf1adda80b8dfffe380b9f3b54d7a3cdb67864a4655e62968022a699b2cc90a6654c84163d2a498506b192afe7cd9777227e5288e8ff069c0f1501804001808f555192d233d23287109c58713abcff53d517a44669ecc9f1b07d91c0fa4409807f67bb40ce9b93cb702f4e492c4de26a3dec02548ef95e11f316e7ffdb1cca39c90780fffe8006bd36dca7ab65a8a4adb24435e26a56f559fa4e579471a3ad0ca5f301b34aed808c6a480f5a9b9104c6651f124843d3be3943f43e0a7131cca18f1d404c5bdca2803010f4d5c349099db3c05f36eb4d1d081bc7ff20a3eac6f1f70b39c950b5b8cb805fff3a55eac4c5e77e86fee98ae86cc2c9729e0a4389061c077cfeac11e0180e80a8db1bbd04d635677979fc1ff62f7e5cb5c3aee4b03a87eeca3a9c7dc5007a0580f29828d29bb94b2a5957535bff7ac2bfa8ab47f0f7e9e0220d402d83a212c8998018fe289993ff3fe6bbac1912895a9c76c7ba012c88b6cdca2114cf2c35604fa08051aa3b2e1aee3bc08a579f37d2573e0fd7b5d190acee0e778fe020c230b9fa2c804cb1690225a374fee84e65f70602fa8d986c551e51cdbed8c880f4637b7b66ee80f228b9951369bd8c106d5c7a69334f4bd9782065c4e2901427a31237a2702cb8808281dad592a0b768756d5d1708be8152dcfb836bb10fa837435c947127a315d68029fe154d2868eeac9adff9f932a9d502dbad4752d312fde1e85bfc11badff412804d35e7c5fa1ba32bd1389c3f4daa77f6bec6f93ec98fb8eadc7b96ed68a93f3e8031a31eeb0bce5a8dcd32bcb7d3d881316c2c3b23cbd085238d7519eee630929d80d36015aa18ae2295f2c71067f8cdd2daa27b92cf4da428977466c678e82365b1f1019d0da05ca59913bc38a8630590f2627c154080fc502d2c10712b3105d5cba6c72448537428feb5e5704397492c81a58eb047fb4c5f0a351b6a99a5b21324516e668bb86a570400505f0e7b9012096b41c4eb3aaf947f6ea4290800007c7700e67da63472835bb0b737093a19ad4c63f5a4efb16ffa83d00700000400ec9e207f03cfdce586301014700e2c25931040505f0e7b9012096b41c4eb3aaf947f6ea4290800004c5f0ec2d17a76153ff51817f12d9cfc3c7f040051049e710b30bd2eab0352ddcc26417aa1945f4380699a53b51a9709a3a86039c49b5ef278e9fc244dae27e1a0380c91bff5b048858083b5575a6023a6b27403c3e192243887514d0852218b5de0b8ee1dca583e69e07c77081e0bfde17b36573208a06cb5cfba6b63f5a4efb16ffa83d007000004028068929b004f083e9ebcde573ab02fe252318e592ebc044b1729d0221c44204518505f0e7b9012096b41c4eb3aaf947f6ea4290800004c5f03c716fb8fff3de61a883bb76adb34a204008044ed425f6cad108cdffc29da43a2f6ff28a0c516e7aa2a6b5875886b27b2ae814c5f0f4993f016e2d2f8e5f43be7bb259486040080dcfd46ec9e3416782d410378b544bb5e04dfdbee2048ce6f12452072e686a5c8f9049eb6f36e027abb2091cfb5110ab5087ff96c685f06155b3cd9a8c9e5e9a23fd5dc13a5ed20684b641000000000685f08316cbf8fa0da822a20ac1c55bf1be3200500000000000000505f0e7b9012096b41c4eb3aaf947f6ea42908000080b91b4c3190c3c118f75aeba80be97cd9bb57cb46b2ac89d5e147466f71b506a5802b6c80a27791fb6cb482670531a8ae57c38b0043df701daea42e21fa61d7b8aa801001e043969eeedb5e9fb4e30965716d4c01a045e9ca8ab2be85df98cf3cff7980b10bc2087a1a925318d66168effcd0100f1104d353039ae5e89c42a287fe741e80f481f3ec4fa1af51b77652a3ef12db8e5e90890289fb73dc9f64c432f96daa6d804f1f5c90cbb150285b9a78616039ce012f25ee71a53ba8697a376fbebece99d1685f090e2fbf2d792cb324bffa9427fe1f0e20290000003300000021019ede3d8a54d27e44a9d5ce189618f22d1008505f0e7b9012096b41c4eb3aaf947f6ea4290802008050713e1a71be4b80260261e77c0c3fa9b365a9bf188c30be2c0c16aa9e0fe8a61d019ef78c98723ddc9073523ef3beefda0c1004505f0e7b9012096b41c4eb3aaf947f6ea4290800007c77095dac46c07a40d91506e7637ec4ba5763f5a4efb16ffa83d007000004000000', '0x280403000b9155a72e8001', '0x8c041405d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d', + ], + 'header' => [ + 'digest' => [ + 'logs' => [ + '0x066175726120b425320800000000', + '0x0470726f6480d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d', + '0x056175726101019a90bf78f99ae4fb2d959e2b65791e82ecf202ced4500ca21a4a61b969c89e23700baf0b3508941d55d8200f75ebf313ec3188ee7b6410d60bd7b655ce45d688', + ], + ], + 'extrinsicsRoot' => '0x128866a2c1c86efc6fcbafaec22d56cdc37142b5e257260b85a1cf6d96d6a565', + 'number' => '0x15', + 'parentHash' => '0xc1b7fe768b8d18e4480badfb388ef46f7f6f0b4010419522a54724a76fe2b879', + 'stateRoot' => '0x7882ba9da089138f7e159390ba8db8399c9da7d228e4c482197635e18c938bfd', + ], + ], + 'justifications' => null, + ]; + + return self::jsonRpcIt($block); + } + + public static function keysPaged(string $step) + { + $result = match ($step) { + 'parseCollectionsStorage' => [ + '0xfa7484c926e764ee2a64df96876c81459200647b8c99af7b8b52752114831bdb3742dd54af1315a13982603b66180cf7d0070000000000000000000000000000', + ], + 'parseTokensStorage' => [ + '0xfa7484c926e764ee2a64df96876c814599971b5749ac43e0235e41b0d37869183742dd54af1315a13982603b66180cf7d00700000000000000000000000000003ba80a3778f04ebf45e806d19a05202501000000000000000000000000000000', + ], + 'parseCollectionAccountsStorage' => [ + '0xfa7484c926e764ee2a64df96876c814555aac77eef55f610e609e395282fe9a23742dd54af1315a13982603b66180cf7d0070000000000000000000000000000de1e86a9a8c739864cf3cc5ec2bea59fd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d', + ], + 'parseTokenAccountsStorage' => [ + '0xfa7484c926e764ee2a64df96876c8145091ba7dd8dcd80d727d06b71fe08a103de1e86a9a8c739864cf3cc5ec2bea59fd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d3742dd54af1315a13982603b66180cf7d00700000000000000000000000000003ba80a3778f04ebf45e806d19a05202501000000000000000000000000000000', + ], + 'parseAttributesStorage' => [ + '0xfa7484c926e764ee2a64df96876c8145761e97790c81676703ce25cc0ffeb3773742dd54af1315a13982603b66180cf7d00700000000000000000000000000007025e075d5e2f6cde3cc051a31f07660006eb1501c909e2b877fbd045ffc11bc26106e616d65', + '0xfa7484c926e764ee2a64df96876c8145761e97790c81676703ce25cc0ffeb3773742dd54af1315a13982603b66180cf7d00700000000000000000000000000006672ff13ca54e47ff32bef734879556601010000000000000000000000000000006eb1501c909e2b877fbd045ffc11bc26106e616d65', + ], + 'empty' => [], + }; + + return self::jsonRpcIt($result); + } + + public static function storageAt(string $step) + { + $result = match ($step) { + 'parseCollectionsStorage' => [ + [ + 'block' => '0x6ab9f2da5088c7c25734975c1711ec654fe533268db6bda09a1a24885b5d73b9', + 'changes' => [ + [ + '0xfa7484c926e764ee2a64df96876c81459200647b8c99af7b8b52752114831bdb3742dd54af1315a13982603b66180cf7d0070000000000000000000000000000', + '0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d00000000018eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48025a62020c04170080ba934b7b720c46040000', + ], + ], + ], + ], + 'parseTokensStorage' => [ + [ + 'block' => '0x6ab9f2da5088c7c25734975c1711ec654fe533268db6bda09a1a24885b5d73b9', + 'changes' => [ + [ + '0xfa7484c926e764ee2a64df96876c814599971b5749ac43e0235e41b0d37869183742dd54af1315a13982603b66180cf7d00700000000000000000000000000003ba80a3778f04ebf45e806d19a05202501000000000000000000000000000000', + '0x0400000413000064a7b3b6e00d13000064a7b3b6e00d0401008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48025a620200', + ], + ], + ], + ], + 'parseCollectionAccountsStorage' => [ + [ + 'block' => '0x6ab9f2da5088c7c25734975c1711ec654fe533268db6bda09a1a24885b5d73b9', + 'changes' => [ + [ + '0xfa7484c926e764ee2a64df96876c814555aac77eef55f610e609e395282fe9a23742dd54af1315a13982603b66180cf7d0070000000000000000000000000000de1e86a9a8c739864cf3cc5ec2bea59fd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d', + '0x00048eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a4801e8eb030008', + ], + ], + ], + ], + 'parseTokenAccountsStorage' => [ + [ + 'block' => '0x6ab9f2da5088c7c25734975c1711ec654fe533268db6bda09a1a24885b5d73b9', + 'changes' => [ + [ + '0xfa7484c926e764ee2a64df96876c8145091ba7dd8dcd80d727d06b71fe08a103de1e86a9a8c739864cf3cc5ec2bea59fd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d3742dd54af1315a13982603b66180cf7d00700000000000000000000000000003ba80a3778f04ebf45e806d19a05202501000000000000000000000000000000', + '0x04000000000490b5ab205c6974c9ea841be688864633dc9ca8a357843eeacf2314649965fe22040120a1070000', + ], + ], + ], + ], + 'parseAttributesStorage' => [ + [ + 'block' => '0x6ab9f2da5088c7c25734975c1711ec654fe533268db6bda09a1a24885b5d73b9', + 'changes' => [ + [ + '0xfa7484c926e764ee2a64df96876c8145761e97790c81676703ce25cc0ffeb3773742dd54af1315a13982603b66180cf7d00700000000000000000000000000007025e075d5e2f6cde3cc051a31f07660006eb1501c909e2b877fbd045ffc11bc26106e616d65', + '0x3c44656d6f20436f6c6c656374696f6e1700804513135cc31601', + ], + [ + '0xfa7484c926e764ee2a64df96876c8145761e97790c81676703ce25cc0ffeb3773742dd54af1315a13982603b66180cf7d00700000000000000000000000000006672ff13ca54e47ff32bef734879556601010000000000000000000000000000006eb1501c909e2b877fbd045ffc11bc26106e616d65', + '0x2844656d6f20546f6b656e17000065db9998b11601', + ], + ], + ], + ], + }; + + return self::jsonRpcIt($result); + } + + protected static function jsonRpcIt(array|string $result) + { + return json_encode([ + 'jsonrpc' => '2.0', + 'result' => $result, + 'id' => 1, + ], JSON_THROW_ON_ERROR); + } +} diff --git a/tests/Support/Mocks/WorkerMock.php b/tests/Support/Mocks/WorkerMock.php new file mode 100644 index 00000000..195a02cf --- /dev/null +++ b/tests/Support/Mocks/WorkerMock.php @@ -0,0 +1,253 @@ + self::jsonRpcIt('0xffe504ef35e599b1134a81dfa38feefef97ffd1bc0cbb99e5a5135fdd2faf3a6'), + 2 => self::jsonRpcIt('0xe77a2851c43e7bd24e709f581249623aecc31258d3f6c3bf6b6f4b5eebf89f02'), + 3 => self::jsonRpcIt('0xf55884a8c0cbae557a09a1b4299bd74a564dc062319eeca6c1b0ae0363068f9b'), + 4 => self::jsonRpcIt('0x23ed880fc25272e03746053a9ac2fb208a888f250c4dbc1c356a1a8214ad7afe'), + 5 => self::jsonRpcIt('0xe5ab1e2b3d15f7ba42b9d2e0faab65711837e5b35aca52487cd94d92608ca1c8'), + 6 => self::jsonRpcIt('0xee143cd74cf3dce0ee765f2ffbba5975da048a2f8f3725724af2f89310823dc9'), + 7 => self::jsonRpcIt('0xf48396b969a26b1951a716a6e6a94c177ee5757c7143ed965c7d6b701f96dbb3'), + 8 => self::jsonRpcIt('0xce4d098196eaf9522b985963ca271fa5adc765b3880bc92f098b0e7ee786efa3'), + 9 => self::jsonRpcIt('0x991ebca428eb888be71ef4609c7ac08b94ff4b298f58a784ea12e0bd1b418590'), + 10 => self::jsonRpcIt('0xae386baefd84b4be19975f17e0ed614e14a8f489793ff080e139227568bd020b'), + 11 => self::jsonRpcIt('0x1f622e185493de61c87815bee034e189abf24bc5be16409db093fdc4c75c33eb'), + 12 => self::jsonRpcIt('0x51a192435c2db8bb064e460eedd90b2f573e7717ab0a18ae6f779a083c8a1d6f'), + 13 => self::jsonRpcIt('0xeb6b73c8b55577190f7dfcc968974e7f1c007daa066bb16add131405e0993b7a'), + 14 => self::jsonRpcIt('0x3ef75db371bd613c57ab7ca6a32e1b3b8c877b03be7e8ac8ca5d5c98b50f5b10'), + 15 => self::jsonRpcIt('0x4ba181b5cf78d936fef2bfa95f30253f3158f03fb349046a88af6cfc439f4a4c'), + 16 => self::jsonRpcIt('0xd7ec244c50fef8379f772b33f8e74087d9cb0df76c584ad458b6819dad555bbd'), + default => '' + }; + } + + public static function block(int $step) + { + $block = [ + 'block' => [ + 'header' => [ + 'number' => HexConverter::intToHexPrefixed($step), + ], + ], + ]; + + return self::jsonRpcIt($block); + } + + public static function emptyEvents() + { + return self::jsonRpcIt('0x0c000000000000000000000000000000020000000100000000006882ef0700000000020000000200000000000000000000000000020000'); + } + + public static function collectionCreatedEvent() + { + return self::jsonRpcIt('0x2000000000000000000000000000000002010000010000000000a88ff2070000000002000000020000000a08d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27dbe047b849ed45a0000000000000000000000020000000a04d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d0000c4588bd7f15a0100000000000000000002000000280055080000000000000000000000000000d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d0000020000000a076d6f646c65662f66656469730000000000000000000000000000000000000000be047b849ed45a0000000000000000000000020000000b00d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27dbe047b849ed45a0000000000000000000000000000000000000000000000000000000200000000004090781600000000000000'); + } + + public static function collectionStorage() + { + return self::jsonRpcIt('0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d01ff0000000000000001f8130000000000000000000000000000000001d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d02093d000000170000c4588bd7f15a01040000'); + } + + public static function tokenCreatedEvent() + { + return self::jsonRpcIt('0x2c00000000000000000000000000000002010000010000000000a88ff2070000000002000000020000000a08d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d98f9fa2ee11e680000000000000000000000020000000a04d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d0000a0dec5adc935360000000000000000000200000028045508000000000000000000000000000088130000000000000000000000000000d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d01000000000000000000000000000000000002000000280f55080000000000000000000000000000d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d00000200000028105508000000000000000000000000000088130000000000000000000000000000d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d0100000000000000000000000000000000000200000028035508000000000000000000000000000088130000000000000000000000000000d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27dd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d010000000000000000000000000000000000020000000a076d6f646c65662f6665646973000000000000000000000000000000000000000098f9fa2ee11e680000000000000000000000020000000b00d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d98f9fa2ee11e680000000000000000000000000000000000000000000000000000000200000000008075692800000000000000'); + } + + public static function tokenStorage() + { + return self::jsonRpcIt('0x0401010c0004170000a0dec5adc93536170000a0dec5adc93536000000'); + } + + public static function collectionAccountStorage() + { + return self::jsonRpcIt('0x000004'); + } + + public static function tokenAccountStorage() + { + return self::jsonRpcIt('0x04000000000000'); + } + + public static function tokenMintedEvent() + { + return self::jsonRpcIt('0x2800000000000000000000000000000002010000010000000000a88ff2070000000002000000020000000a08d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27dbf3178a24b22630000000000000000000000020000000a04d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d0000a0dec5adc9353600000000000000000002000000280f550800000000000000000000000000008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48000002000000281055080000000000000000000000000000881300000000000000000000000000008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a480100000000000000000000000000000000000200000028035508000000000000000000000000000088130000000000000000000000000000d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48010000000000000000000000000000000000020000000a076d6f646c65662f66656469730000000000000000000000000000000000000000bf3178a24b22630000000000000000000000020000000b00d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27dbf3178a24b22630000000000000000000000000000000000000000000000000000000200000000008075692800000000000000'); + } + + public static function mintTokenStorage() + { + return self::jsonRpcIt('0x0801010c0004170000a0dec5adc9353617000040bd8b5b936b6c000000'); + } + + public static function mintCollectionAccountStorage() + { + return self::jsonRpcIt('0x000004'); + } + + public static function mintTokenAccountStorage() + { + return self::jsonRpcIt('0x04000000000000'); + } + + public static function setAttributeToCollectionEvent() + { + return self::jsonRpcIt('0x2000000000000000000000000000000002010000010000000000a88ff2070000000002000000020000000a08d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27dfe51c4bad69d4d0000000000000000000000020000000a04d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d00c053e80156e01b0000000000000000000002000000280b5508000000000000000000000000000000106e616d653c44656d6f20436f6c6c656374696f6e0000020000000a076d6f646c65662f66656469730000000000000000000000000000000000000000fe51c4bad69d4d0000000000000000000000020000000b00d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27dfe51c4bad69d4d000000000000000000000000000000000000000000000000000000020000000000009f8e1800000000000000'); + } + + public static function collectionAttributeStorage() + { + return self::jsonRpcIt('0x3c44656d6f20436f6c6c656374696f6e1300c053e80156e01b'); + } + + public static function setAttributeToTokenEvent() + { + return self::jsonRpcIt('0x2000000000000000000000000000000002010000010000000000a88ff2070000000002000000020000000a08d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d7234b5630b50590000000000000000000000020000000a04d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d0080f095428fde1b0000000000000000000002000000280b550800000000000000000000000000000188130000000000000000000000000000106e616d652844656d6f20546f6b656e0000020000000a076d6f646c65662f666564697300000000000000000000000000000000000000007234b5630b50590000000000000000000000020000000b00d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d7234b5630b5059000000000000000000000000000000000000000000000000000000020000000000c0855d2000000000000000'); + } + + public static function tokenAttributeStorage() + { + return self::jsonRpcIt('0x2844656d6f20546f6b656e130080f095428fde1b'); + } + + public static function tokenTransferEvent() + { + return self::jsonRpcIt('0x2c00000000000000000000000000000002010000010000000000a88ff2070000000002000000020000000a08d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27dbbb796d1c4f46400000000000000000000000200000028125508000000000000000000000000000088130000000000000000000000000000d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d000002000000280f5508000000000000000000000000000090b5ab205c6974c9ea841be688864633dc9ca8a357843eeacf2314649965fe220000020000002810550800000000000000000000000000008813000000000000000000000000000090b5ab205c6974c9ea841be688864633dc9ca8a357843eeacf2314649965fe2201000000000000000000000000000000000002000000281155080000000000000000000000000000d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d00000200000028085508000000000000000000000000000088130000000000000000000000000000d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27dd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d90b5ab205c6974c9ea841be688864633dc9ca8a357843eeacf2314649965fe22010000000000000000000000000000000000020000000a076d6f646c65662f66656469730000000000000000000000000000000000000000bbb796d1c4f4640000000000000000000000020000000b00d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27dbbb796d1c4f464000000000000000000000000000000000000000000000000000000020000000000407b422a00000000000000'); + } + + public static function transferFromCollectionAccountStorage() + { + return self::jsonRpcIt(null); + } + + public static function transferToCollectionAccountStorage() + { + return self::jsonRpcIt('0x000004'); + } + + public static function transferFromTokenAccountStorage() + { + return self::jsonRpcIt(null); + } + + public static function transferToTokenAccountStorage() + { + return self::jsonRpcIt('0x04000000000000'); + } + + public static function burnTokenEvent() + { + return self::jsonRpcIt('0x2800000000000000000000000000000002010000010000000000a88ff2070000000002000000020000000a088eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48eecfefdc17e05e000000000000000000000002000000281255080000000000000000000000000000881300000000000000000000000000008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a480000020000002811550800000000000000000000000000008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a480000020000000a05d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d0000a0dec5adc9353600000000000000000002000000280655080000000000000000000000000000881300000000000000000000000000008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48010000000000000000000000000000000000020000000a076d6f646c65662f66656469730000000000000000000000000000000000000000eecfefdc17e05e0000000000000000000000020000000b008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48eecfefdc17e05e000000000000000000000000000000000000000000000000000000020000000000809e473000000000000000'); + } + + public static function burnTokenStorage() + { + return self::jsonRpcIt('0x0401010c0004170000a0dec5adc93536170000a0dec5adc93536040000'); + } + + public static function burnCollectionAccountStorage() + { + return self::jsonRpcIt(null); + } + + public static function burnTokenAccountStorage() + { + return self::jsonRpcIt(null); + } + + public static function freezeCollectionEvent() + { + return self::jsonRpcIt('0x2000000000000000000000000000000002010000010000000000a88ff2070000000002000000020000000a08d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d77dd4fe992f93500000000000000000000000200000028095521000000020000000a07d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d028d8458757c010000000000000000000000020000000a076d6f646c65662f666564697300000000000000000000000000000000000000007550cb901d7d340000000000000000000000020000000b00d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d7550cb901d7d34000000000000000000000000000000000000000000000000000000020000000000c0c2da0600000000000000'); + } + + public static function thawCollectionEvent() + { + return self::jsonRpcIt('0x2000000000000000000000000000000002010000010000000000a88ff2070000000002000000020000000a08d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d3d37ea6832ea35000000000000000000000002000000280a5521000000020000000a07d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27dfa4ecf0a6e7c010000000000000000000000020000000a076d6f646c65662f6665646973000000000000000000000000000000000000000043e81a5ec46d340000000000000000000000020000000b00d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d43e81a5ec46d340000000000000000000000000000000000000000000000000000000200000000008080cb0600000000000000'); + } + + public static function removeAttributeEvent() + { + return self::jsonRpcIt('0x2000000000000000000000000000000002010000010000000000a88ff2070000000002000000020000000a08d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d5631e7cc5272550000000000000000000000020000000a05d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d0080f095428fde1b0000000000000000000002000000280c550800000000000000000000000000000188130000000000000000000000000000106e616d650000020000000a076d6f646c65662f666564697300000000000000000000000000000000000000005631e7cc5272550000000000000000000000020000000b00d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d5631e7cc52725500000000000000000000000000000000000000000000000000000002000000000000c86c2000000000000000'); + } + + public static function approveTokenEvent() + { + return self::jsonRpcIt('0x1c00000000000000000000000000000002010000010000000000a88ff2070000000002000000020000000a0890b5ab205c6974c9ea841be688864633dc9ca8a357843eeacf2314649965fe229495d9cb515e44000000000000000000000002000000280d55080000000000000000000000000000018813000000000000000000000000000090b5ab205c6974c9ea841be688864633dc9ca8a357843eeacf2314649965fe22d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d0101000000000000000000000000000000000000020000000a076d6f646c65662f666564697300000000000000000000000000000000000000009495d9cb515e440000000000000000000000020000000b0090b5ab205c6974c9ea841be688864633dc9ca8a357843eeacf2314649965fe229495d9cb515e4400000000000000000000000000000000000000000000000000000002000000000040c54f0a00000000000000'); + } + + public static function approveTokenStorage() + { + return self::jsonRpcIt('0x040000000004d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d040000'); + } + + public static function transferByOperatorEvent() + { + return self::jsonRpcIt('0x2c00000000000000000000000000000002010000010000000000a88ff2070000000002000000020000000a08d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d1aa810b9124c700000000000000000000000020000002812550800000000000000000000000000008813000000000000000000000000000090b5ab205c6974c9ea841be688864633dc9ca8a357843eeacf2314649965fe22000002000000280f55080000000000000000000000000000d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d00000200000028105508000000000000000000000000000088130000000000000000000000000000d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d0100000000000000000000000000000000000200000028115508000000000000000000000000000090b5ab205c6974c9ea841be688864633dc9ca8a357843eeacf2314649965fe2200000200000028085508000000000000000000000000000088130000000000000000000000000000d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d90b5ab205c6974c9ea841be688864633dc9ca8a357843eeacf2314649965fe22d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d010000000000000000000000000000000000020000000a076d6f646c65662f666564697300000000000000000000000000000000000000001aa810b9124c700000000000000000000000020000000b00d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d1aa810b9124c70000000000000000000000000000000000000000000000000000000020000000000407b422a00000000000000'); + } + + public static function transferByOperatorFromCollectionAccount() + { + return self::jsonRpcIt(null); + } + + public static function transferByOperatorToCollectionAccount() + { + return self::jsonRpcIt('0x000004'); + } + + public static function transferByOperatorFromTokenAccount() + { + return self::jsonRpcIt(null); + } + + public static function transferByOperatorToTokenAccount() + { + return self::jsonRpcIt('0x04000000000000'); + } + + public static function burnTokenWithRemoveStorageEvent() + { + return self::jsonRpcIt('0x3000000000000000000000000000000002010000010000000000a88ff2070000000002000000020000000a08d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27dc6510f1230335f00000000000000000000000200000028125508000000000000000000000000000088130000000000000000000000000000d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d000002000000281155080000000000000000000000000000d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d0000020000000a05d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d0000a0dec5adc935360000000000000000000200000028075508000000000000000000000000000088130000000000000000000000000000d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d00000200000028065508000000000000000000000000000088130000000000000000000000000000d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d010000000000000000000000000000000000020000000a07d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d4ee4bf2d4b6d070000000000000000000000020000000a076d6f646c65662f66656469730000000000000000000000000000000000000000786d4fe4e4c5570000000000000000000000020000000b00d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d786d4fe4e4c5570000000000000000000000000000000000000000000000000000000200000000004045d42800000000000000'); + } + + public static function burnTokenWithRemoveStorageTokenStorage() + { + return self::jsonRpcIt(null); + } + + public static function burnTokenWithRemoveStorageCollectionAccountStorage() + { + return self::jsonRpcIt(null); + } + + public static function burnTokenWithRemoveStorageTokenAccountStorage() + { + return self::jsonRpcIt(null); + } + + public static function removeAttributeFromCollectionEvent() + { + return self::jsonRpcIt('0x2000000000000000000000000000000002010000010000000000a88ff2070000000002000000020000000a08d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d3d4926370af7470000000000000000000000020000000a05d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d00c053e80156e01b0000000000000000000002000000280c5508000000000000000000000000000000106e616d650000020000000a076d6f646c65662f666564697300000000000000000000000000000000000000003d4926370af7470000000000000000000000020000000b00d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d3d4926370af74700000000000000000000000000000000000000000000000000000002000000000040e19d1800000000000000'); + } + + public static function destroyedCollectionEvent() + { + return self::jsonRpcIt('0x2000000000000000000000000000000002010000010000000000a88ff2070000000002000000020000000a08d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27db1d1283130df3d0000000000000000000000020000000a05d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d0000c4588bd7f15a0100000000000000000002000000280155080000000000000000000000000000d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d0000020000000a076d6f646c65662f66656469730000000000000000000000000000000000000000b1d1283130df3d0000000000000000000000020000000b00d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27db1d1283130df3d000000000000000000000000000000000000000000000000000000020000000000c033a11000000000000000'); + } + + protected static function jsonRpcIt(array|string|null $result) + { + return json_encode([ + 'jsonrpc' => '2.0', + 'result' => $result, + 'id' => 1, + ], JSON_THROW_ON_ERROR); + } +} diff --git a/tests/Support/MocksWebsocketClient.php b/tests/Support/MocksWebsocketClient.php new file mode 100644 index 00000000..5df332e1 --- /dev/null +++ b/tests/Support/MocksWebsocketClient.php @@ -0,0 +1,74 @@ +assertEquals($expected, $actual); + } + + /** + * @throws \JsonException + */ + private function mockWebsocketClient(string $method, array $params, string $responseJson): void + { + $expectedRpcRequest = Util::createJsonRpc($method, $params); + + app()->bind(Client::class, function () use ($expectedRpcRequest, $responseJson) { + $mock = Mockery::mock(Client::class); + $mock->shouldReceive('send') + ->once() + ->with(Mockery::on(function ($rpcRequest) use ($expectedRpcRequest) { + $this->assertRpcResponseEquals($expectedRpcRequest, $rpcRequest); + + return true; + })); + + $mock->shouldReceive('receive') + ->once() + ->andReturn($responseJson); + + return $mock; + }); + } + + /** + * @throws \JsonException + */ + private function mockWebsocketClientSequence(array $responseSequence): void + { + app()->bind(Client::class, function () use ($responseSequence) { + $mock = Mockery::mock(Client::class); + + $mock->shouldReceive('isConnected') + ->zeroOrMoreTimes() + ->andReturn(true); + + $mock->shouldReceive('send') + ->zeroOrMoreTimes() + ->withAnyArgs(); + + $mock->shouldReceive('receive') + ->zeroOrMoreTimes() + ->andReturnValues($responseSequence); + + return $mock; + }); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 00000000..ad213105 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,69 @@ +useEnvironmentPath(__DIR__ . '/..'); + $app->bootstrapWith([LoadEnvironmentVariables::class]); + + $app['config']->set('database.default', env('DB_DRIVER', 'mysql')); + + // MySQL config + $app['config']->set('database.connections.mysql', [ + 'driver' => 'mysql', + 'host' => env('DB_HOST', '127.0.0.1:3306'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', 'password'), + 'database' => env('DB_DATABASE', 'platform'), + 'prefix' => '', + ]); + + if ($this->fakeEvents) { + Event::fake(); + } + } + + protected function usesNullDaemonAccount($app) + { + $app->config->set('enjin-platform.chains.daemon-account', '0x0000000000000000000000000000000000000000000000000000000000000000'); + } + + protected function usesEnjinNetwork($app) + { + $app->config->set('enjin-platform.chains.network', 'enjin'); + } + + protected function usesCanaryNetwork($app) + { + $app->config->set('enjin-platform.chains.network', 'canary'); + } + + protected function usesDeveloperNetwork($app) + { + $app->config->set('enjin-platform.chains.network', 'developer'); + } +} diff --git a/tests/Unit/AddressTest.php b/tests/Unit/AddressTest.php new file mode 100644 index 00000000..0a0df730 --- /dev/null +++ b/tests/Unit/AddressTest.php @@ -0,0 +1,111 @@ +assertEquals('0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48', $address); + } + + public function test_it_can_get_daemon_account() + { + $address = SS58Address::getDaemonAccount(); + + $this->assertEquals('6a03b1a3d40d7e344dfb27157931b14b59fe2ff11d7352353321fe400e956802', $address); + } + + public function test_it_can_decode_efinity_address() + { + $address = SS58Address::getPublicKey('efTwqopZgd4Yqefg2NzVPW4THfFmsSsSbxTLN2uq7kmadDaC5'); + + $this->assertEquals('0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d', $address); + } + + public function test_it_can_decode_rocfinity_address() + { + $address = SS58Address::getPublicKey('rf8YmxhSe9WGJZvCH8wtzAndweEmz6dTV6DjmSHgHvPEFNLAJ'); + + $this->assertEquals('0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d', $address); + } + + public function test_it_can_encode_address_from_public_key() + { + $address = SS58Address::encode('8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48', 0); + + $this->assertEquals('14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3', $address); + } + + public function test_it_can_encode_efinity_address_from_public_key() + { + $address = SS58Address::encode('d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d', 1110); + + $this->assertEquals('efTwqopZgd4Yqefg2NzVPW4THfFmsSsSbxTLN2uq7kmadDaC5', $address); + } + + public function test_it_can_encode_rocfinity_address_from_public_key() + { + $address = SS58Address::encode('d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d', 195); + + $this->assertEquals('rf8YmxhSe9WGJZvCH8wtzAndweEmz6dTV6DjmSHgHvPEFNLAJ', $address); + } + + /** + * @test + * @define-env usesNullDaemonAccount + */ + public function test_it_can_get_null_daemon_account() + { + $address = SS58Address::getDaemonAccount(); + + $this->assertEquals('00', $address); + } + + /** + * @test + * @define-env usesEnjinNetwork + */ + public function test_it_will_encode_efinity_address_if_polkadot_is_the_selected_chain() + { + $address = SS58Address::encode('d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d'); + + $this->assertEquals('exXZe1U9dLp78MhrMG2vT59vxBEE9qD7cDJ4pEnobvzG1Ly8R', $address); + } + + /** + * @test + * @define-env usesDeveloperNetwork + */ + public function test_it_will_encode_rocfinity_address_if_developer_is_the_selected_chain() + { + $address = SS58Address::encode('d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d'); + + $this->assertEquals('rf8YmxhSe9WGJZvCH8wtzAndweEmz6dTV6DjmSHgHvPEFNLAJ', $address); + } + + public function test_it_fails_to_decode_invalid_address() + { + $this->expectExceptionMessage(__( + 'enjin-platform::ss58_address.error.cannot_decode_address', + [ + 'address' => '14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HP123invalid', + 'message' => 'Data contains invalid characters "l"', + ] + )); + + SS58Address::decode('14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HP123invalid'); + } + + public function test_it_fails_to_decode_eth_address() + { + $this->expectExceptionMessage(__('enjin-platform::ss58_address.error.invalid_empty_address')); + + SS58Address::decode(''); + } +} diff --git a/tests/Unit/BitMaskTest.php b/tests/Unit/BitMaskTest.php new file mode 100644 index 00000000..e2db8df1 --- /dev/null +++ b/tests/Unit/BitMaskTest.php @@ -0,0 +1,57 @@ +assertTrue(BitMask::getBit(10, 1024)); + } + + public function test_get_bit_is_not_set() + { + $this->assertFalse(BitMask::getBit(0, 1024)); + } + + public function test_get_bits() + { + $bits = [0, 1, 3, 5, 9]; + + $this->assertEquals($bits, BitMask::getBits(555)); + } + + public function test_set_bit() + { + $this->assertEquals(1025, BitMask::setBit(10, 1)); + } + + public function test_unset_bit() + { + $this->assertEquals(1, BitMask::unsetBit(10, 1025)); + } + + public function test_set_bits() + { + $bits = [0, 1, 3, 5, 9]; + + $this->assertEquals(555, BitMask::setBits($bits, 0)); + } + + public function test_unset_bits() + { + $bits = [3, 5, 9]; + + $this->assertEquals(3, BitMask::unsetBits($bits, 555)); + } + + public function test_toggle_bits() + { + $bits = [0, 1, 3, 5, 9, 10]; + + $this->assertEquals(1024, BitMask::toggleBits($bits, 555)); + } +} diff --git a/tests/Unit/DecodingTest.php b/tests/Unit/DecodingTest.php new file mode 100644 index 00000000..ef120f81 --- /dev/null +++ b/tests/Unit/DecodingTest.php @@ -0,0 +1,433 @@ +codec = new Codec(); + } + + public function test_it_can_decode_system_account() + { + $data = $this->codec->decode()->systemAccount('0x1f00000000000000010000000000000000424ed9cbe55f0b91010000000000000000e65e4b9feedf56000000000000000000000000000000000000000000000000000000000000000000000000000000'); + + $this->assertEquals( + [ + 'nonce' => 31, + 'consumers' => 0, + 'providers' => 1, + 'sufficients' => 0, + 'balances' => [ + 'free' => '7397963999878421824000', + 'reserved' => '1602556000000000000000', + 'miscFrozen' => '0', + 'feeFrozen' => '0', + ], + ], + $data + ); + } + + public function test_it_can_decode_create_collection() + { + $data = $this->codec->decode()->createCollection('0x280001ff0000000000000001adde00000000000000000000000000000101301cb3057d43941d5f631613aa1661be0354d39e34f23d4ef527396b10d2bb7a0208af2f'); + + $this->assertEquals( + [ + 'mintPolicy' => [ + 'maxTokenCount' => '255', + 'maxTokenSupply' => '57005', + 'forceSingleMint' => true, + ], + 'marketPolicy' => [ + 'beneficiary' => '0x301cb3057d43941d5f631613aa1661be0354d39e34f23d4ef527396b10d2bb7a', + 'percentage' => 20.0, + ], + ], + $data + ); + } + + public function test_it_can_decode_create_collection_other() + { + $data = $this->codec->decode()->createCollection('0x280001ff0000000000000001adde00000000000000000000000000000100'); + + $this->assertEquals( + [ + 'mintPolicy' => [ + 'maxTokenCount' => '255', + 'maxTokenSupply' => '57005', + 'forceSingleMint' => true, + ], + 'marketPolicy' => null, + ], + $data + ); + + $data = $this->codec->decode()->createCollection('0x280001ff0000000000000001adde00000000000000000000000000000101301cb3057d43941d5f631613aa1661be0354d39e34f23d4ef527396b10d2bb7a0208af2f'); + + $this->assertEquals( + [ + 'mintPolicy' => [ + 'maxTokenCount' => '255', + 'maxTokenSupply' => '57005', + 'forceSingleMint' => true, + ], + 'marketPolicy' => [ + 'beneficiary' => '0x301cb3057d43941d5f631613aa1661be0354d39e34f23d4ef527396b10d2bb7a', + 'percentage' => 20.0, + ], + ], + $data + ); + } + + public function test_it_can_decode_destroy_collection() + { + $data = $this->codec->decode()->destroyCollection('0x2801b67a0300'); + + $this->assertEquals( + [ + 'collectionId' => '57005', + ], + $data + ); + } + + public function test_it_can_decode_create_token() + { + $data = $this->codec->decode()->mint('0x280300d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d0400fd03b67a03000f0000c16ff286230101b67a030000'); + + $this->assertEquals( + [ + 'recipientId' => '0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d', + 'collectionId' => '1', + 'params' => [ + 'CreateToken' => [ + 'tokenId' => '255', + 'initialSupply' => '57005', + 'unitPrice' => '10000000000000000', + 'cap' => [ + 'type' => 'SUPPLY', + 'amount' => '57005', + ], + 'behavior' => null, + 'listingForbidden' => false, + 'attributes' => [], + ], + ], + ], + $data + ); + } + + public function test_it_can_decode_create_token_with_attributes() + { + $data = $this->codec->decode()->mint('0x2804000824170e49c79846e7c0931e64df98605d93fa5f2cd42fec85ce045321071614411f00c90f0413000064a7b3b6e00d0001008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48025a62020104106e616d652844656d6f20546f6b656e'); + + $this->assertEquals( + [ + 'recipientId' => '0x0824170e49c79846e7c0931e64df98605d93fa5f2cd42fec85ce045321071614', + 'collectionId' => '2000', + 'params' => [ + 'CreateToken' => [ + 'tokenId' => '1010', + 'initialSupply' => '1', + 'unitPrice' => '1000000000000000000', + 'cap' => [ + 'type' => TokenMintCapType::INFINITE->name, + 'amount' => null, + ], + 'behavior' => [ + 'hasRoyalty' => [ + 'beneficiary' => '0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48', + 'percentage' => 1.0, + ], + ], + 'listingForbidden' => true, + 'attributes' => [ + [ + 'key' => 'name', + 'value' => 'Demo Token', + ], + ], + ], + ], + ], + $data + ); + } + + public function test_it_can_decode_mint() + { + $data = $this->codec->decode()->mint('0x280300d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d0401fd03b67a0300010000c16ff28623000000000000000000'); + + $this->assertEquals( + [ + 'recipientId' => '0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d', + 'collectionId' => '1', + 'params' => [ + 'Mint' => [ + 'tokenId' => '255', + 'amount' => '57005', + 'unitPrice' => '10000000000000000', + ], + ], + ], + $data + ); + } + + public function test_it_can_decode_mint_other() + { + $data = $this->codec->decode()->mint('0x280300d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d0400fd03b67a03000f0000c16ff286230101b67a030000'); + + $this->assertEquals( + [ + 'recipientId' => '0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d', + 'collectionId' => '1', + 'params' => [ + 'CreateToken' => [ + 'tokenId' => '255', + 'initialSupply' => '57005', + 'unitPrice' => '10000000000000000', + 'cap' => [ + 'type' => 'SUPPLY', + 'amount' => '57005', + ], + 'behavior' => null, + 'listingForbidden' => false, + 'attributes' => [], + ], + ], + ], + $data + ); + + $data = $this->codec->decode()->mint('0x280300d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d0401fd03b67a0300010000c16ff28623000000000000000000'); + + $this->assertEquals( + [ + 'recipientId' => '0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d', + 'collectionId' => '1', + 'params' => [ + 'Mint' => [ + 'tokenId' => '255', + 'amount' => '57005', + 'unitPrice' => '10000000000000000', + ], + ], + ], + $data + ); + } + + public function test_it_can_decode_burn() + { + $data = $this->codec->decode()->burn('0x2804b67a0300fd03040100'); + + $this->assertEquals( + [ + 'collectionId' => '57005', + 'tokenId' => '255', + 'amount' => '1', + 'keepAlive' => true, + 'removeTokenStorage' => false, + ], + $data + ); + } + + public function test_it_can_decode_freeze_collection() + { + $data = $this->codec->decode()->freeze('0x2806b67a030000'); + + $this->assertEquals( + [ + 'collectionId' => '57005', + 'freezeType' => [ + 'Collection' => null, + ], + ], + $data + ); + } + + public function test_it_can_decode_freeze_token() + { + $data = $this->codec->decode()->freeze('0x2806b67a030001ff000000000000000000000000000000'); + + $this->assertEquals( + [ + 'collectionId' => '57005', + 'freezeType' => [ + 'Token' => '255', + ], + ], + $data + ); + } + + public function test_it_can_decode_freeze_collection_account() + { + $data = $this->codec->decode()->freeze('0x2806b67a030002d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d'); + + $this->assertEquals( + [ + 'collectionId' => '57005', + 'freezeType' => [ + 'CollectionAccount' => '0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d', + ], + ], + $data + ); + } + + public function test_it_can_decode_freeze_token_account() + { + $data = $this->codec->decode()->freeze('0x2806b67a030003fd03d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d'); + + $this->assertEquals( + [ + 'collectionId' => '57005', + 'freezeType' => [ + 'TokenAccount' => [ + '255', + '0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d', + ], + ], + ], + $data + ); + } + + public function test_it_can_decode_thaw_collection() + { + $data = $this->codec->decode()->thaw('0x2807b67a030000'); + + $this->assertEquals( + [ + 'collectionId' => '57005', + 'freezeType' => [ + 'Collection' => null, + ], + ], + $data + ); + } + + public function test_it_can_decode_thaw_token() + { + $data = $this->codec->decode()->thaw('0x2807b67a030001ff000000000000000000000000000000'); + + $this->assertEquals( + [ + 'collectionId' => '57005', + 'freezeType' => [ + 'Token' => '255', + ], + ], + $data + ); + } + + public function test_it_can_decode_thaw_collection_account() + { + $data = $this->codec->decode()->thaw('0x2807b67a030002d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d'); + + $this->assertEquals( + [ + 'collectionId' => '57005', + 'freezeType' => [ + 'CollectionAccount' => '0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d', + ], + ], + $data + ); + } + + public function test_it_can_decode_thaw_token_account() + { + $data = $this->codec->decode()->thaw('0x2807b67a030003fd03d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d'); + + $this->assertEquals( + [ + 'collectionId' => '57005', + 'freezeType' => [ + 'TokenAccount' => [ + '255', + '0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d', + ], + ], + ], + $data + ); + } + + public function test_it_can_decode_set_attribute_from_collection() + { + $data = $this->codec->decode()->setAttribute('0x2808b67a030000106e616d6540456e6a696e20436f6c6c656374696f6e'); + + $this->assertEquals( + [ + 'collectionId' => '57005', + 'tokenId' => null, + 'key' => 'name', + 'value' => 'Enjin Collection', + ], + $data + ); + } + + public function test_it_can_decode_set_attribute_from_token() + { + $data = $this->codec->decode()->setAttribute('0x2808b67a030001ff000000000000000000000000000000106e616d6530476f6c64656e2053776f7264'); + + $this->assertEquals( + [ + 'collectionId' => '57005', + 'tokenId' => '255', + 'key' => 'name', + 'value' => 'Golden Sword', + ], + $data + ); + } + + public function test_it_can_decode_remove_attribute_from_collection() + { + $data = $this->codec->decode()->removeAttribute('0x2809b67a030000106e616d65'); + + $this->assertEquals( + [ + 'collectionId' => '57005', + 'tokenId' => null, + 'key' => 'name', + ], + $data + ); + } + + public function test_it_can_decode_remove_attribute_from_token() + { + $data = $this->codec->decode()->removeAttribute('0x2809b67a030001ff000000000000000000000000000000106e616d65'); + + $this->assertEquals( + [ + 'collectionId' => '57005', + 'tokenId' => '255', + 'key' => 'name', + ], + $data + ); + } +} diff --git a/tests/Unit/EncodingTest.php b/tests/Unit/EncodingTest.php new file mode 100644 index 00000000..fe3d60a7 --- /dev/null +++ b/tests/Unit/EncodingTest.php @@ -0,0 +1,1218 @@ +codec = new Codec(); + } + + public function test_it_can_encode_transfer_balance() + { + $data = $this->codec->encode()->transferBalance( + '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + '3256489678678963378387378312' + ); + + $callIndex = $this->codec->encode()->callIndexes['Balances.transfer']; + $this->assertEquals( + "0x{$callIndex}0052e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e238860bfb2b3660b3783b4850a", + $data + ); + } + + public function test_it_can_encode_transfer_balance_keep_alive() + { + $data = $this->codec->encode()->transferBalanceKeepAlive( + '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + '3256489678678963378387378312' + ); + + $callIndex = $this->codec->encode()->callIndexes['Balances.transfer_keep_alive']; + $this->assertEquals( + "0x{$callIndex}0052e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e238860bfb2b3660b3783b4850a", + $data + ); + } + + public function test_it_can_encode_transfer_all_balance_with_keep_alive() + { + $data = $this->codec->encode()->transferAllBalance( + '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + true + ); + + $callIndex = $this->codec->encode()->callIndexes['Balances.transfer_all']; + $this->assertEquals( + "0x{$callIndex}0052e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e01", + $data + ); + } + + public function test_it_can_encode_transfer_all_balance_without_keep_alive() + { + $data = $this->codec->encode()->transferAllBalance( + '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e' + ); + + $callIndex = $this->codec->encode()->callIndexes['Balances.transfer_all']; + $this->assertEquals( + "0x{$callIndex}0052e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e00", + $data + ); + } + + public function test_it_can_encode_approve_collection_with_expiration() + { + $data = $this->codec->encode()->approveCollection( + '2000', + '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + 535000 + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.approve_collection']; + $this->assertEquals( + "0x{$callIndex}411f52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e01d8290800", + $data + ); + } + + public function test_it_can_approve_collection_without_expiration() + { + $data = $this->codec->encode()->approveCollection( + '2000', + '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.approve_collection']; + $this->assertEquals( + "0x{$callIndex}411f52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e00", + $data + ); + } + + public function test_it_can_encode_unapprove_collection() + { + $data = $this->codec->encode()->unapproveCollection( + '2000', + '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.unapprove_collection']; + $this->assertEquals( + "0x{$callIndex}411f52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e", + $data + ); + } + + public function test_it_can_encode_approve_token_with_expiration() + { + $data = $this->codec->encode()->approveToken( + '2000', + '5050', + '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + '10', + '0', + 535000 + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.approve_token']; + $this->assertEquals( + "0x{$callIndex}411fe94e52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e2801d829080000", + $data + ); + } + + public function test_it_can_encode_approve_token_without_expiration() + { + $data = $this->codec->encode()->approveToken( + '2000', + '57005', + '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + '500', + '10', + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.approve_token']; + $this->assertEquals( + "0x{$callIndex}411fb67a030052e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15ed1070028", + $data + ); + } + + public function test_it_can_encode_unapprove_token() + { + $data = $this->codec->encode()->unapproveToken( + '2000', + '5050', + '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.unapprove_token']; + $this->assertEquals( + "0x{$callIndex}411fe94e52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e", + $data + ); + } + + public function test_it_can_encode_batch_transfer_simple_transfer() + { + $recipient = [ + 'accountId' => '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + 'params' => new SimpleTransferParams( + tokenId: '255', + amount: '1', + keepAlive: false, + ), + ]; + + $data = $this->codec->encode()->batchTransfer( + '2000', + [$recipient] + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.batch_transfer']; + $this->assertEquals( + "0x{$callIndex}411f0452e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e00fd030400", + $data + ); + } + + public function test_it_can_encode_batch_transfer_operator_transfer() + { + $recipient = [ + 'accountId' => '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + 'params' => new OperatorTransferParams( + tokenId: '1', + source: '0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d', + amount: '1', + keepAlive: false, + ), + ]; + + $data = $this->codec->encode()->batchTransfer( + '2000', + [$recipient] + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.batch_transfer']; + $this->assertEquals( + "0x{$callIndex}411f0452e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e0104d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d0400", + $data + ); + } + + public function test_it_can_encode_simple_transfer() + { + $data = $this->codec->encode()->transferToken( + '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + '2000', + new SimpleTransferParams( + tokenId: '255', + amount: '1', + keepAlive: false, + ), + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.transfer']; + $this->assertEquals( + "0x{$callIndex}0052e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e411f00fd030400", + $data + ); + } + + public function test_it_can_encode_operator_transfer() + { + $data = $this->codec->encode()->transferToken( + '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + '2000', + new OperatorTransferParams( + tokenId: '255', + source: 'rf8YmxhSe9WGJZvCH8wtzAndweEmz6dTV6DjmSHgHvPEFNLAJ', + amount: '1', + keepAlive: false, + ), + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.transfer']; + $this->assertEquals( + "0x{$callIndex}0052e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e411f01fd03d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d0400", + $data + ); + } + + public function test_it_can_encode_create_collection_with_args() + { + $mintPolicy = new MintPolicyParams( + forceSingleMint: true, + maxTokenCount: '255', + maxTokenSupply: '57005', + ); + + $marketPolicy = new RoyaltyPolicyParams( + beneficiary: '0x301cb3057d43941d5f631613aa1661be0354d39e34f23d4ef527396b10d2bb7a', + percentage: 20, + ); + + $explicitRoyaltyCurrencies = [ + [ + 'collectionId' => '0', + 'tokenId' => '0', + ], + ]; + + $attributes = [ + [ + 'key' => 'name', + 'value' => 'Demo Collection', + ], + [ + 'key' => 'description', + 'value' => 'My demo collection', + ], + ]; + + $data = $this->codec->encode()->createCollection($mintPolicy, $marketPolicy, $explicitRoyaltyCurrencies, $attributes); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.create_collection']; + $this->assertEquals( + "0x{$callIndex}01ff0000000000000001adde00000000000000000000000000000101301cb3057d43941d5f631613aa1661be0354d39e34f23d4ef527396b10d2bb7a0208af2f04000008106e616d653c44656d6f20436f6c6c656374696f6e2c6465736372697074696f6e484d792064656d6f20636f6c6c656374696f6e", + $data + ); + } + + public function test_it_can_encode_create_collection_with_only_required_args() + { + $data = $this->codec->encode()->createCollection(new MintPolicyParams(forceSingleMint: true)); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.create_collection']; + $this->assertEquals( + "0x{$callIndex}000001000000", + $data + ); + } + + public function test_it_can_encode_destroy_collection() + { + $data = $this->codec->encode()->destroyCollection( + collectionId: '57005' + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.destroy_collection']; + $this->assertEquals( + "0x{$callIndex}b67a0300", + $data + ); + } + + public function test_it_can_encode_mutate_collection_with_owner() + { + $data = $this->codec->encode()->mutateCollection( + collectionId: '2000', + owner: '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e' + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.mutate_collection']; + $this->assertEquals( + "0x{$callIndex}411f0152e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e010000", + $data + ); + } + + public function test_it_can_encode_mutate_collection_with_royalty() + { + $data = $this->codec->encode()->mutateCollection( + collectionId: '2000', + royalty: new RoyaltyPolicyParams( + beneficiary: '0x301cb3057d43941d5f631613aa1661be0354d39e34f23d4ef527396b10d2bb7a', + percentage: 20, + ) + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.mutate_collection']; + $this->assertEquals( + "0x{$callIndex}411f000101301cb3057d43941d5f631613aa1661be0354d39e34f23d4ef527396b10d2bb7a0208af2f00", + $data + ); + } + + public function test_it_can_encode_mutate_collection_with_empty_royalties() + { + $data = $this->codec->encode()->mutateCollection( + collectionId: '2000', + royalty: [] + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.mutate_collection']; + $this->assertEquals( + "0x{$callIndex}411f000000", + $data + ); + } + + public function test_it_can_encode_mutate_collection_with_royalty_equals_null() + { + $data = $this->codec->encode()->mutateCollection( + collectionId: '57005', + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.mutate_collection']; + $this->assertEquals( + "0x{$callIndex}b67a030000010000", + $data + ); + } + + public function test_it_can_encode_mutate_token() + { + $data = $this->codec->encode()->mutateToken( + collectionId: '57005', + tokenId: '255', + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.mutate_token']; + $this->assertEquals( + "0x{$callIndex}b67a0300fd0301000000", + $data + ); + } + + public function test_it_can_encode_mutate_token_with_empty_behavior() + { + $data = $this->codec->encode()->mutateToken( + collectionId: '57005', + tokenId: '255', + behavior: [] + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.mutate_token']; + $this->assertEquals( + "0x{$callIndex}b67a0300fd03000000", + $data + ); + } + + public function test_it_can_encode_mutate_token_with_listing_true() + { + $data = $this->codec->encode()->mutateToken( + collectionId: '57005', + tokenId: '255', + listingForbidden: true + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.mutate_token']; + $this->assertEquals( + "0x{$callIndex}b67a0300fd030100010100", + $data + ); + } + + public function test_it_can_encode_mutate_token_with_is_currency_true() + { + $data = $this->codec->encode()->mutateToken( + collectionId: '57005', + tokenId: '255', + behavior: new TokenMarketBehaviorParams(isCurrency: true) + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.mutate_token']; + $this->assertEquals( + "0x{$callIndex}b67a0300fd030101010000", + $data + ); + } + + /** + * @test + * @define-env usesCanaryNetwork + */ + public function test_it_can_encode_batch_create_token_on_canary() + { + $recipient = [ + 'accountId' => '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + 'params' => new CreateTokenParams( + tokenId: '255', + initialSupply: '57005', + unitPrice: '10000000000000', + cap: TokenMintCapType::INFINITE, + behavior: null, + listingForbidden: null, + ), + ]; + + $data = $this->codec->encode()->batchMint( + '2000', + [$recipient] + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.batch_mint']; + $this->assertEquals( + "0x{$callIndex}411f0452e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e00fd03b67a0300000100a0724e180900000000000000000000000000000000", + $data + ); + } + + public function test_it_can_encode_batch_create_token() + { + $recipient = [ + 'accountId' => '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + 'params' => new CreateTokenParams( + tokenId: '255', + initialSupply: '57005', + unitPrice: '10000000000000', + cap: TokenMintCapType::INFINITE, + behavior: null, + listingForbidden: null, + ), + ]; + + $data = $this->codec->encode()->batchMint( + '2000', + [$recipient] + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.batch_mint']; + $this->assertEquals( + "0x{$callIndex}411f0452e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e00fd03b67a0300000100a0724e180900000000000000000000000000000000", + $data + ); + } + + public function test_it_can_encode_batch_mint_token() + { + $recipient = [ + 'accountId' => '0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d', + 'params' => new MintParams( + tokenId: '1', + amount: '1', + unitPrice: '100000000000000000' + ), + ]; + + $data = $this->codec->encode()->batchMint( + '2000', + [$recipient] + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.batch_mint']; + $this->assertEquals( + "0x{$callIndex}411f04d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d0104040100008a5d784563010000000000000000", + $data + ); + } + + public function test_it_can_encode_create_token_no_cap() + { + $params = new CreateTokenParams( + tokenId: '255', + initialSupply: '57005', + unitPrice: '10000000000000', + cap: TokenMintCapType::INFINITE, + behavior: null, + ); + + $data = $this->codec->encode()->mint( + recipientId: '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + collectionId: '1', + params: $params + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.mint']; + $this->assertEquals( + "0x{$callIndex}0052e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e0400fd03b67a0300000100a0724e180900000000000000000000000000000000", + $data + ); + } + + /** + * @test + * @define-env usesCanaryNetwork + */ + public function test_it_can_encode_create_token_on_canary() + { + $params = new CreateTokenParams( + tokenId: '255', + initialSupply: '1', + unitPrice: '10000000000000', + cap: TokenMintCapType::INFINITE, + behavior: null, + ); + + $data = $this->codec->encode()->mint( + recipientId: '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + collectionId: '2000', + params: $params + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.mint']; + $this->assertEquals( + "0x{$callIndex}0052e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e411f00fd0304000100a0724e180900000000000000000000000000000000", + $data + ); + } + + public function test_it_can_encode_create_token_cap_supply() + { + $params = new CreateTokenParams( + tokenId: '255', + initialSupply: '57005', + unitPrice: '10000000000000', + cap: TokenMintCapType::SUPPLY, + supply: '57005', + ); + + $data = $this->codec->encode()->mint( + recipientId: '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + collectionId: '1', + params: $params + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.mint']; + $this->assertEquals( + "0x{$callIndex}0052e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e0400fd03b67a0300000100a0724e1809000000000000000000000101b67a03000000000000", + $data + ); + } + + public function test_it_can_encode_mint_with_other_args_as_null() + { + $params = new CreateTokenParams( + tokenId: '255', + initialSupply: '57005', + unitPrice: '10000000000000', + cap: TokenMintCapType::INFINITE, + behavior: null, + listingForbidden: null + ); + + $data = $this->codec->encode()->mint( + recipientId: '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + collectionId: '1', + params: $params + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.mint']; + $this->assertEquals( + "0x{$callIndex}0052e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e0400fd03b67a0300000100a0724e180900000000000000000000000000000000", + $data + ); + } + + public function test_it_can_encode_mint_with_supply_and_behavior_null() + { + $params = new CreateTokenParams( + tokenId: '255', + initialSupply: '57005', + unitPrice: '10000000000000', + cap: TokenMintCapType::SUPPLY, + supply: '57005', + behavior: null, + listingForbidden: null, + ); + + $data = $this->codec->encode()->mint( + recipientId: '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + collectionId: '1', + params: $params + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.mint']; + $this->assertEquals( + "0x{$callIndex}0052e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e0400fd03b67a0300000100a0724e1809000000000000000000000101b67a03000000000000", + $data + ); + } + + public function test_it_can_encode_mint_with_no_listing_forbidden() + { + $params = new CreateTokenParams( + tokenId: '255', + initialSupply: '57005', + unitPrice: '10000000000000', + cap: TokenMintCapType::SINGLE_MINT, + behavior: null, + listingForbidden: null, + ); + + $data = $this->codec->encode()->mint( + recipientId: '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + collectionId: '1', + params: $params + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.mint']; + $this->assertEquals( + "0x{$callIndex}0052e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e0400fd03b67a0300000100a0724e18090000000000000000000001000000000000", + $data + ); + } + + public function test_it_can_encode_mint_with_no_behavior() + { + $params = new CreateTokenParams( + tokenId: '255', + initialSupply: '57005', + unitPrice: '10000000000000', + cap: TokenMintCapType::SINGLE_MINT, + behavior: null, + listingForbidden: false, + ); + + $data = $this->codec->encode()->mint( + recipientId: '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + collectionId: '1', + params: $params + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.mint']; + $this->assertEquals( + "0x{$callIndex}0052e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e0400fd03b67a0300000100a0724e18090000000000000000000001000000000000", + $data + ); + } + + public function test_it_can_encode_mint_with_listing_forbidden() + { + $params = new CreateTokenParams( + tokenId: '255', + initialSupply: '57005', + unitPrice: '10000000000000', + cap: TokenMintCapType::SINGLE_MINT, + behavior: null, + listingForbidden: true, + ); + + $data = $this->codec->encode()->mint( + recipientId: '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + collectionId: '1', + params: $params + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.mint']; + $this->assertEquals( + "0x{$callIndex}0052e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e0400fd03b67a0300000100a0724e18090000000000000000000001000001000000", + $data + ); + } + + public function test_it_can_encode_mint_with_no_cap() + { + $params = new CreateTokenParams( + tokenId: '255', + initialSupply: '57005', + unitPrice: '10000000000000', + cap: TokenMintCapType::INFINITE, + behavior: new TokenMarketBehaviorParams( + hasRoyalty: new RoyaltyPolicyParams( + beneficiary: '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + percentage: 20, + ), + ), + listingForbidden: null, + ); + + $data = $this->codec->encode()->mint( + recipientId: '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + collectionId: '1', + params: $params + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.mint']; + $this->assertEquals( + "0x{$callIndex}0052e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e0400fd03b67a0300000100a0724e18090000000000000000000000010052e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e0208af2f00000000", + $data + ); + } + + public function test_it_can_encode_mint_with_supply() + { + $params = new CreateTokenParams( + tokenId: '255', + initialSupply: '57005', + unitPrice: '10000000000000', + cap: TokenMintCapType::SUPPLY, + supply: '57005', + behavior: new TokenMarketBehaviorParams( + hasRoyalty: new RoyaltyPolicyParams( + beneficiary: '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + percentage: '20', + ), + ), + listingForbidden: null, + ); + + $data = $this->codec->encode()->mint( + recipientId: '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + collectionId: '1', + params: $params + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.mint']; + $this->assertEquals( + "0x{$callIndex}0052e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e0400fd03b67a0300000100a0724e1809000000000000000000000101b67a0300010052e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e0208af2f00000000", + $data + ); + } + + public function test_it_can_encode_create_token_with_attributes() + { + $params = new CreateTokenParams( + tokenId: '1010', + initialSupply: '1', + unitPrice: '10000000000000', + cap: TokenMintCapType::INFINITE, + listingForbidden: true, + attributes: [ + [ + 'key' => 'name', + 'value' => 'Demo Token', + ], + ] + ); + + $data = $this->codec->encode()->mint( + recipientId: '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + collectionId: '2000', + params: $params + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.mint']; + $this->assertEquals( + "0x{$callIndex}0052e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e411f00c90f04000100a0724e1809000000000000000000000000010004106e616d652844656d6f20546f6b656e00", + $data + ); + } + + public function test_it_can_encode_mint_with_single_mint() + { + $params = new CreateTokenParams( + tokenId: '255', + initialSupply: '57005', + unitPrice: '10000000000000', + cap: TokenMintCapType::SINGLE_MINT, + behavior: new TokenMarketBehaviorParams( + hasRoyalty: new RoyaltyPolicyParams( + beneficiary: '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + percentage: '20', + ), + ), + listingForbidden: null, + ); + + $data = $this->codec->encode()->mint( + recipientId: '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + collectionId: '1', + params: $params + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.mint']; + $this->assertEquals( + "0x{$callIndex}0052e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e0400fd03b67a0300000100a0724e1809000000000000000000000100010052e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e0208af2f00000000", + $data + ); + } + + public function test_it_can_encode_mint_with_behavior_is_currency() + { + $params = new CreateTokenParams( + tokenId: '255', + initialSupply: '57005', + unitPrice: '10000000000000', + cap: TokenMintCapType::SINGLE_MINT, + behavior: new TokenMarketBehaviorParams( + isCurrency: true, + ), + listingForbidden: null, + ); + + $data = $this->codec->encode()->mint( + recipientId: '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + collectionId: '1', + params: $params + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.mint']; + $this->assertEquals( + "0x{$callIndex}0052e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e0400fd03b67a0300000100a0724e1809000000000000000000000100010100000000", + $data + ); + } + + public function test_it_can_encode_mint_no_args() + { + $params = new MintParams( + tokenId: '255', + amount: '57005', + unitPrice: '10000000000000' + ); + + $data = $this->codec->encode()->mint( + recipientId: '0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d', + collectionId: '1', + params: $params + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.mint']; + $this->assertEquals( + "0x{$callIndex}00d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d0401fd03b67a03000100a0724e180900000000000000000000", + $data + ); + } + + public function test_it_can_encode_create_token_cap_single_mint() + { + $params = new CreateTokenParams( + tokenId: '255', + initialSupply: '57005', + unitPrice: '10000000000000', + cap: TokenMintCapType::SINGLE_MINT, + ); + + $data = $this->codec->encode()->mint( + recipientId: '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + collectionId: '1', + params: $params + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.mint']; + $this->assertEquals( + "0x{$callIndex}0052e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e0400fd03b67a0300000100a0724e18090000000000000000000001000000000000", + $data + ); + } + + public function test_it_can_encode_burn() + { + $data = $this->codec->encode()->burn( + collectionId: '2000', + params: new BurnParams( + tokenId: '57005', + amount: 100, + keepAlive: true, + removeTokenStorage: true + ) + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.burn']; + $this->assertEquals( + "0x{$callIndex}411fb67a030091010101", + $data + ); + } + + public function test_it_can_encode_freeze_collection() + { + $params = new FreezeTypeParams( + type: FreezeType::COLLECTION, + token: null, + account: null + ); + + $data = $this->codec->encode()->freeze( + collectionId: '57005', + params: $params + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.freeze']; + $this->assertEquals( + "0x{$callIndex}b67a030000", + $data + ); + } + + /** + * @test + * @define-env usesCanaryNetwork + */ + public function test_it_can_encode_freeze_token_on_canary() + { + $params = new FreezeTypeParams( + type: FreezeType::TOKEN, + token: '255', + account: null + ); + + $data = $this->codec->encode()->freeze( + collectionId: '57005', + params: $params + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.freeze']; + $this->assertEquals( + "0x{$callIndex}b67a030001ff0000000000000000000000000000000101", + $data + ); + } + + public function test_it_can_encode_freeze_token() + { + $params = new FreezeTypeParams( + type: FreezeType::TOKEN, + token: '255', + account: null + ); + + $data = $this->codec->encode()->freeze( + collectionId: '57005', + params: $params + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.freeze']; + $this->assertEquals( + "0x{$callIndex}b67a030001ff0000000000000000000000000000000101", + $data + ); + } + + public function test_it_can_encode_freeze_collection_account() + { + $params = new FreezeTypeParams( + type: FreezeType::COLLECTION_ACCOUNT, + token: null, + account: '0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d' + ); + + $data = $this->codec->encode()->freeze( + collectionId: '57005', + params: $params + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.freeze']; + $this->assertEquals( + "0x{$callIndex}b67a030002d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", + $data + ); + } + + public function test_it_can_encode_freeze_token_account() + { + $params = new FreezeTypeParams( + type: FreezeType::TOKEN_ACCOUNT, + token: '255', + account: '0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d' + ); + + $data = $this->codec->encode()->freeze( + collectionId: '57005', + params: $params + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.freeze']; + $this->assertEquals( + "0x{$callIndex}b67a030003fd03d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", + $data + ); + } + + public function test_it_can_encode_thaw_collection() + { + $params = new FreezeTypeParams( + type: FreezeType::COLLECTION, + token: null, + account: null + ); + + $data = $this->codec->encode()->thaw( + collectionId: '57005', + params: $params + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.thaw']; + $this->assertEquals( + "0x{$callIndex}b67a030000", + $data + ); + } + + /** + * @test + * @define-env usesCanaryNetwork + */ + public function test_it_can_encode_thaw_token_on_canary() + { + $params = new FreezeTypeParams( + type: FreezeType::TOKEN, + token: '255', + account: null + ); + + $data = $this->codec->encode()->thaw( + collectionId: '57005', + params: $params + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.thaw']; + $this->assertEquals( + "0x{$callIndex}b67a030001ff0000000000000000000000000000000101", + $data + ); + } + + public function test_it_can_encode_thaw_token() + { + $params = new FreezeTypeParams( + type: FreezeType::TOKEN, + token: '255', + account: null + ); + + $data = $this->codec->encode()->thaw( + collectionId: '57005', + params: $params + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.thaw']; + $this->assertEquals( + "0x{$callIndex}b67a030001ff0000000000000000000000000000000101", + $data + ); + } + + public function test_it_can_encode_thaw_collection_account() + { + $params = new FreezeTypeParams( + type: FreezeType::COLLECTION_ACCOUNT, + token: null, + account: '0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d' + ); + + $data = $this->codec->encode()->thaw( + collectionId: '57005', + params: $params + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.thaw']; + $this->assertEquals( + "0x{$callIndex}b67a030002d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", + $data + ); + } + + public function test_it_can_encode_thaw_token_account() + { + $params = new FreezeTypeParams( + type: FreezeType::TOKEN_ACCOUNT, + token: '255', + account: '0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d' + ); + + $data = $this->codec->encode()->thaw( + collectionId: '57005', + params: $params + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.thaw']; + $this->assertEquals( + "0x{$callIndex}b67a030003fd03d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", + $data + ); + } + + public function test_it_can_encode_set_attribute_from_token() + { + $data = $this->codec->encode()->setAttribute( + '57005', + '255', + 'name', + 'Golden Sword' + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.set_attribute']; + $this->assertEquals( + "0x{$callIndex}b67a030001ff000000000000000000000000000000106e616d6530476f6c64656e2053776f7264", + $data + ); + } + + public function test_it_can_encode_remove_attribute_from_collection() + { + $data = $this->codec->encode()->removeAttribute( + collectionId: '57005', + tokenId: null, + key: 'name', + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.remove_attribute']; + $this->assertEquals( + "0x{$callIndex}b67a030000106e616d65", + $data + ); + } + + public function test_it_can_encode_remove_attribute_from_token() + { + $data = $this->codec->encode()->removeAttribute( + collectionId: '57005', + tokenId: '255', + key: 'name', + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.remove_attribute']; + $this->assertEquals( + "0x{$callIndex}b67a030001ff000000000000000000000000000000106e616d65", + $data + ); + } + + public function test_it_can_encode_remove_all_attributes() + { + $data = $this->codec->encode()->removeAllAttributes( + collectionId: '57005', + tokenId: '255', + attributeCount: '1', + ); + + $callIndex = $this->codec->encode()->callIndexes['MultiTokens.remove_all_attributes']; + $this->assertEquals( + "0x{$callIndex}b67a030001ff00000000000000000000000000000001000000", + $data + ); + } + + public function test_it_can_encode_efinity_utility_batch() + { + $call = $this->codec->encode()->mint( + recipientId: '0x52e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e', + collectionId: '1', + params: new CreateTokenParams( + tokenId: '255', + initialSupply: '57005', + unitPrice: '10000000000000', + cap: TokenMintCapType::INFINITE, + behavior: null, + listingForbidden: null + ) + ); + + $data = $this->codec->encode()->batch( + calls: [$call, $call], + continueOnFailure: false, + ); + + $callIndex = $this->codec->encode()->callIndexes['EfinityUtility.batch']; + $this->assertEquals( + "0x{$callIndex}0828040052e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e0400fd03b67a0300000100a0724e18090000000000000000000000000000000028040052e3c0eb993523286d19954c7e3ada6f791fa3f32764e44b9c1df0c2723bc15e0400fd03b67a0300000100a0724e18090000000000000000000000000000000000", + $data + ); + } +} diff --git a/tests/Unit/StorageTest.php b/tests/Unit/StorageTest.php new file mode 100644 index 00000000..06059628 --- /dev/null +++ b/tests/Unit/StorageTest.php @@ -0,0 +1,281 @@ +codec = new Codec(); + } + + public function test_it_can_encode_twoxs() + { + $twox128 = Twox::hash('MultiTokens'); + + $this->assertEquals( + 'fa7484c926e764ee2a64df96876c8145', + $twox128 + ); + } + + public function test_it_can_encode_blake2() + { + $publicKey = '4d1bf8eb687839f94c706719717b4ad2ddf001eebd650c24fe94f5ce21f6acd6'; + $blake = Blake2::hash($publicKey, 128); + + $this->assertEquals( + '7834ca8ec759d72404963785f497bd89', + $blake + ); + } + + public function test_it_can_call_right_slot() + { + $publicKey = '4d1bf8eb687839f94c706719717b4ad2ddf001eebd650c24fe94f5ce21f6acd6'; + + $systemHashed = Twox::hash('System'); + $accountHashed = Twox::hash('Account'); + $keyHashed = Blake2::hash($publicKey, 128); + + $slot = $systemHashed . $accountHashed . $keyHashed . $publicKey; + + $this->assertEquals( + '26aa394eea5630e07c48ae0c9558cef7b99d880ec681799c0cf30e8886371da97834ca8ec759d72404963785f497bd894d1bf8eb687839f94c706719717b4ad2ddf001eebd650c24fe94f5ce21f6acd6', + $slot + ); + } + + public function test_it_can_call_multi_tokens_balances_account() + { + $publicKey = 'd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d'; + $multiTokensHashed = Twox::hash('MultiTokens'); + $balancesHashed = Twox::hash('Balances'); + $keyHashed = Blake2::hash($publicKey, 128); + + $slot = $multiTokensHashed . $balancesHashed . $keyHashed . $publicKey; + + $this->assertEquals('fa7484c926e764ee2a64df96876c8145c2261276cc9d1f8598ea4b6a74b15c2fde1e86a9a8c739864cf3cc5ec2bea59fd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d', $slot); + } + + public function test_it_can_decode_attribute_storage_key() + { + $content = $this->codec->decode()->attributeStorageKey('0xfa7484c926e764ee2a64df96876c8145761e97790c81676703ce25cc0ffeb3773ba80a3778f04ebf45e806d19a0520250100000000000000000000000000000007f95f6b3baacab308323526a6eedc2201adde00000000000000000000000000006eb1501c909e2b877fbd045ffc11bc26106e616d65'); + + $this->assertEquals( + [ + 'collectionId' => '1', + 'tokenId' => '57005', + 'attribute' => '6e616d65', + ], + $content + ); + } + + public function test_it_can_decode_u128() + { + $codec = new ScaleInstance(Base::create()); + $this->assertEquals(0, gmp_cmp(gmp_init('57005'), $codec->process('U128', new ScaleBytes('0xadde0000000000000000000000000000')))); + } + + public function test_it_can_decode_collection_storage_key() + { + $content = $this->codec->decode()->collectionStorageKey('0xfa7484c926e764ee2a64df96876c81459200647b8c99af7b8b52752114831bdba68417e9769fad205e3d67e4cef9d822dc050000000000000000000000000000'); + $this->assertEquals( + [ + 'collectionId' => '1500', + ], + $content + ); + } + + public function test_it_can_decode_collection_storage_data() + { + $content = $this->codec->decode()->collectionStorageData('0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d00000000018eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48025a62020c0017000010a59e86fdde43040000'); + + $this->assertEquals( + [ + 'owner' => '0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d', + 'maxTokenCount' => null, + 'maxTokenSupply' => null, + 'forceSingleMint' => false, + 'burn' => null, + 'isFrozen' => false, + 'attribute' => null, + 'royaltyBeneficiary' => '0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48', + 'royaltyPercentage' => 1, + 'tokenCount' => '3', + 'attributeCount' => '0', + 'totalDeposit' => '1252000000000000000000', + 'explicitRoyaltyCurrencies' => [ + [ + 'collectionId' => '0', + 'tokenId' => '0', + ], + ], + ], + $content + ); + } + + public function test_it_can_decode_token_storage_key() + { + $content = $this->codec->decode()->tokenStorageKey('0xfa7484c926e764ee2a64df96876c814599971b5749ac43e0235e41b0d37869183ba80a3778f04ebf45e806d19a052025010000000000000000000000000000003d4d415ebb3ec1e0f570a4086ca65d5fff000000000000000000000000000000'); + $this->assertEquals( + [ + 'collectionId' => '1', + 'tokenId' => '255', + ], + $content + ); + } + + public function test_it_can_decode_token_storage_data_with_single_mint() + { + $content = $this->codec->decode()->tokenStorageData('0x0401000004010f0000c16ff286230f0000c16ff2862304000000'); + $this->assertEquals( + [ + 'supply' => '1', + 'cap' => TokenMintCapType::SINGLE_MINT, + 'capSupply' => null, + 'isFrozen' => false, + 'minimumBalance' => '1', + 'unitPrice' => '10000000000000000', + 'mintDeposit' => '10000000000000000', + 'attributeCount' => '1', + 'royaltyBeneficiary' => null, + 'royaltyPercentage' => null, + 'isCurrency' => false, + 'listingForbidden' => false, + ], + $content + ); + } + + public function test_it_can_decode_token_storage_data_with_supply() + { + $content = $this->codec->decode()->tokenStorageData('0x214e0101419c0004010f0000c16ff2862317000088b116afe3b50204000000'); + $this->assertEquals( + [ + 'supply' => '5000', + 'cap' => TokenMintCapType::SUPPLY, + 'capSupply' => '10000', + 'isFrozen' => false, + 'minimumBalance' => '1', + 'unitPrice' => '10000000000000000', + 'mintDeposit' => '50000000000000000000', + 'attributeCount' => '1', + 'royaltyBeneficiary' => null, + 'royaltyPercentage' => null, + 'isCurrency' => false, + 'listingForbidden' => false, + ], + $content + ); + } + + public function test_it_can_decode_collection_accounts_storage_key() + { + $content = $this->codec->decode()->collectionAccountStorageKey('0x0bf891b100bbc75a3aaa261402ae0e8bc8511ac575318ec7f93a67d1cdf292da3ba80a3778f04ebf45e806d19a05202501000000000000000000000000000000de1e86a9a8c739864cf3cc5ec2bea59fd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d'); + $this->assertEquals( + [ + 'collectionId' => '1', + 'accountId' => '0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d', + ], + $content + ); + } + + public function test_it_can_decode_collection_accounts_storage_data() + { + $content = $this->codec->decode()->collectionAccountStorageData('0x0004d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d0004'); + $this->assertEquals( + [ + 'isFrozen' => false, + 'approvals' => [ + [ + 'accountId' => '0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d', + 'expiration' => null, + ], + ], + 'accountCount' => '1', + ], + $content + ); + } + + public function test_it_can_decode_token_accounts_storage_key() + { + $content = $this->codec->decode()->tokenAccountStorageKey('0xfa7484c926e764ee2a64df96876c8145091ba7dd8dcd80d727d06b71fe08a1030137310a1fc3eee361a5ba3e0250053c3e0800000000000000000000000000000127b5fce16694cf2ce4e2ada82c2f1a050000000000000000000000000000006693450ba38c572dc228966702d125c12ea037a549132b4f4e8a372c7e288014ac324c8e97e1c647c8e4bac2bb9ddd18'); + $this->assertEquals( + [ + 'accountId' => '0x2ea037a549132b4f4e8a372c7e288014ac324c8e97e1c647c8e4bac2bb9ddd18', + 'collectionId' => '2110', + 'tokenId' => '5', + ], + $content + ); + } + + public function test_it_can_decode_token_accounts_storage_data() + { + $content = $this->codec->decode()->tokenAccountStorageData('0x04000000000880f4bee67ab5c177239bfc89d9d307c65afaf10fb6d7d63487a9a2d9df8f460504009cc25de9d468a701b070397bc63b94a7aa5afb72c33cc2990ae004ce014ab333040150c3000000'); + $this->assertEquals( + [ + 'balance' => '1', + 'reservedBalance' => '0', + 'lockedBalance' => '0', + 'namedReserves' => [], + 'approvals' => [ + [ + 'accountId' => '0x80f4bee67ab5c177239bfc89d9d307c65afaf10fb6d7d63487a9a2d9df8f4605', + 'amount' => '1', + 'expiration' => null, + ], + [ + 'accountId' => '0x9cc25de9d468a701b070397bc63b94a7aa5afb72c33cc2990ae004ce014ab333', + 'amount' => '1', + 'expiration' => '50000', + ], + ], + 'isFrozen' => false, + ], + $content + ); + } + + public function test_it_can_decode_token_accounts_storage_with_named_reserves() + { + $content = $this->codec->decode()->tokenAccountStorageData('0x140c00046d61726b74706c6303000000000000000000000000000000000000'); + $this->assertEquals( + [ + 'balance' => '5', + 'reservedBalance' => '3', + 'lockedBalance' => '0', + 'namedReserves' => [ + [ + 'pallet' => PalletIdentifier::MARKETPLACE, + 'amount' => 3, + ], + ], + 'approvals' => [], + 'isFrozen' => false, + ], + $content + ); + } +} diff --git a/tests/Unit/TokenIdEncoderTest.php b/tests/Unit/TokenIdEncoderTest.php new file mode 100644 index 00000000..2fee3eda --- /dev/null +++ b/tests/Unit/TokenIdEncoderTest.php @@ -0,0 +1,207 @@ + [ + 'hash' => $this->toObject([ + 'test1' => fake()->sentence(1), + 'test2' => [1], + 'test3' => [ + ['name' => fake()->name()], + ['name' => fake()->name()], + ], + ]), + ], + ]; + + $expectedResult = HexConverter::hexToUInt(Blake2::hash(HexConverter::stringToHex(json_encode($dataToEncode['tokenId']['hash'])), 128)); + + $result = resolve(TokenIdManager::class)->encode($dataToEncode); + + $this->assertEquals($expectedResult, $result); + } + + public function test_it_encodes_using_string_id() + { + $dataToEncode = [ + 'tokenId' => [ + 'stringId' => fake()->asciify('********'), + ], + ]; + + $expectedResult = HexConverter::hexToUInt(HexConverter::stringToHex($dataToEncode['tokenId']['stringId'])); + + $result = resolve(TokenIdManager::class)->encode($dataToEncode); + + $this->assertEquals($expectedResult, $result); + } + + public function test_it_throws_error_using_string_id_with_array() + { + $dataToEncode = [ + 'tokenId' => [ + 'stringId' => [ + 'string' => ['test'], + ], + ], + ]; + + $this->expectExceptionMessage('The string id field must be a string.'); + + resolve(TokenIdManager::class)->encode($dataToEncode); + } + + public function test_it_throws_error_using_string_id_with_object() + { + $dataToEncode = [ + 'tokenId' => [ + 'stringId' => $this->toObject([ + 'string' => ['key' => 'test'], + ]), + ], + ]; + + $this->expectExceptionMessage('The string id field must be a string.'); + + resolve(TokenIdManager::class)->encode($dataToEncode); + } + + public function test_it_throws_error_using_string_id_with_array_of_object() + { + $dataToEncode = [ + 'tokenId' => [ + 'stringId' => [ + 'string' => [$this->toObject(['key' => 'test'])], + ], + ], + ]; + + $this->expectExceptionMessage('The string id field must be a string.'); + + resolve(TokenIdManager::class)->encode($dataToEncode); + } + + public function test_it_throws_error_using_string_id_with_no_string() + { + $dataToEncode = [ + 'tokenId' => [ + 'stringId' => [ + 'name' => fake()->sentence(1), + ], + ], + ]; + + $this->expectExceptionMessage('The string id field must be a string.'); + + resolve(TokenIdManager::class)->encode($dataToEncode); + } + + public function test_it_throws_error_using_string_id_with_out_of_bounds_conversion() + { + $dataToEncode = [ + 'tokenId' => [ + 'stringId' => [ + 'string' => 'thisMyTOKENID_123', + ], + ], + ]; + + $this->expectExceptionMessage('The string id field must be a string.'); + + resolve(TokenIdManager::class)->encode($dataToEncode); + } + + public function test_it_encodes_using_erc1155() + { + $dataToEncode = [ + 'tokenId' => [ + 'erc1155' => [ + 'tokenId' => HexConverter::prefix(app(Generator::class)->erc1155_token_id()), + 'index' => fake()->numberBetween(), + ], + ], + ]; + + $expectedResult = HexConverter::padRight(HexConverter::padRight($dataToEncode['tokenId']['erc1155']['tokenId'], 16) . HexConverter::padLeft(HexConverter::intToHex($dataToEncode['tokenId']['erc1155']['index']), 16), 32); + + $result = resolve(TokenIdManager::class)->encode($dataToEncode); + + $this->assertEquals(HexConverter::hexToUInt($expectedResult), $result); + } + + public function test_it_encodes_using_erc1155_with_no_index() + { + $dataToEncode = [ + 'tokenId' => [ + 'erc1155' => [ + 'tokenId' => HexConverter::prefix(app(Generator::class)->erc1155_token_id()), + ], + ], + ]; + + $expectedResult = HexConverter::padRight(HexConverter::padRight($dataToEncode['tokenId']['erc1155']['tokenId'], 16), 32); + + $result = resolve(TokenIdManager::class)->encode($dataToEncode); + + $this->assertEquals(HexConverter::hexToUInt($expectedResult), $result); + } + + public function test_it_fails_to_encode_using_invalid_erc1155() + { + $dataToEncode = [ + 'tokenId' => [ + 'erc1155' => [ + 'tokenId' => HexConverter::prefix('0x123456'), + 'index' => fake()->numberBetween(), + ], + ], + ]; + + $this->expectExceptionMessage('The erc1155.token id field must be 18 characters.'); + + resolve(TokenIdManager::class)->encode($dataToEncode); + } + + public function test_it_fails_to_encode_using_invalid_erc1155_hex() + { + $dataToEncode = [ + 'tokenId' => [ + 'erc1155' => [ + 'tokenId' => HexConverter::prefix('0x123456789abcdefg'), + 'index' => fake()->numberBetween(), + ], + ], + ]; + + $this->expectExceptionMessage('The erc1155.token id has an invalid hex string.'); + + resolve(TokenIdManager::class)->encode($dataToEncode); + } + + public function test_it_fails_to_encode_using_invalid_erc1155_index() + { + $dataToEncode = [ + 'tokenId' => [ + 'erc1155' => [ + 'tokenId' => HexConverter::prefix(app(Generator::class)->erc1155_token_id()), + 'index' => 'abc', + ], + ], + ]; + + $this->expectExceptionMessage('The erc1155.index field must be an integer.'); + + resolve(TokenIdManager::class)->encode($dataToEncode); + } +} diff --git a/tests/Unit/WalletTest.php b/tests/Unit/WalletTest.php new file mode 100644 index 00000000..25816c23 --- /dev/null +++ b/tests/Unit/WalletTest.php @@ -0,0 +1,152 @@ +blockchainService = new Substrate($ws); + } + + public function test_it_returns_null_if_no_saved_wallet_and_no_address() + { + $result = $this->blockchainService->walletWithBalanceAndNonce(null); + + $this->assertNull($result); + } + + public function test_it_returns_a_zero_balance_if_no_saved_wallet_and_no_storage_for_the_address() + { + $this->mockWebsocketClientSequence([ + StorageMock::null_account_storage(), + ]); + + $result = $this->blockchainService->walletWithBalanceAndNonce(SS58Address::encode($publicKey = app(Generator::class)->public_key))->toArray(); + + $this->assertArraySubset([ + 'public_key' => $publicKey, + 'nonce' => 0, + 'balances' => [ + 'free' => '0', + 'reserved' => '0', + 'miscFrozen' => '0', + 'feeFrozen' => '0', + ], + ], $result, true); + + $this->assertDatabaseHas('wallets', [ + 'public_key' => $publicKey, + ]); + } + + public function test_it_returns_zero_balance_when_there_is_a_saved_wallet_and_no_storage() + { + $this->mockWebsocketClientSequence([ + StorageMock::null_account_storage(), + ]); + + $wallet = new Wallet(); + $wallet->public_key = $publicKey = app(Generator::class)->public_key; + $wallet->network = 'rococo'; + $wallet->external_id = $externalId = 'savedWallet'; + $wallet->save(); + + $result = $this->blockchainService->walletWithBalanceAndNonce($wallet)->toArray(); + + $this->assertArraySubset([ + 'public_key' => $publicKey, + 'external_id' => $externalId, + 'network' => 'rococo', + 'nonce' => 0, + 'balances' => [ + 'free' => '0', + 'reserved' => '0', + 'miscFrozen' => '0', + 'feeFrozen' => '0', + ], + ], $result, true); + + $this->assertDatabaseHas('wallets', [ + 'public_key' => $publicKey, + 'external_id' => $externalId, + ]); + } + + public function test_it_returns_a_balance_when_no_saved_wallet_and_has_storage() + { + $this->mockWebsocketClientSequence([ + StorageMock::account_with_balance(), + ]); + + $result = $this->blockchainService->walletWithBalanceAndNonce(SS58Address::encode($publicKey = app(Generator::class)->public_key))->toArray(); + + $this->assertArraySubset([ + 'public_key' => $publicKey, + 'nonce' => 0, + 'balances' => [ + 'free' => '2000000000000000000', + 'reserved' => '0', + 'miscFrozen' => '0', + 'feeFrozen' => '0', + ], + ], $result, true); + + $this->assertDatabaseHas('wallets', [ + 'public_key' => $publicKey, + ]); + } + + public function test_it_returns_a_balance_when_there_is_saved_wallet_and_has_storage() + { + $this->mockWebsocketClientSequence([ + StorageMock::account_with_balance(), + ]); + + $wallet = new Wallet(); + $wallet->public_key = $publicKey = app(Generator::class)->public_key; + $wallet->network = 'rococo'; + $wallet->external_id = $externalId = 'savedWallet2'; + $wallet->save(); + + $result = $this->blockchainService->walletWithBalanceAndNonce($wallet)->toArray(); + + $this->assertArraySubset([ + 'public_key' => $publicKey, + 'network' => 'rococo', + 'external_id' => $externalId, + 'nonce' => 0, + 'balances' => [ + 'free' => '2000000000000000000', + 'reserved' => '0', + 'miscFrozen' => '0', + 'feeFrozen' => '0', + ], + ], $result, true); + + $this->assertDatabaseHas('wallets', [ + 'public_key' => $publicKey, + 'external_id' => $externalId, + ]); + } +}