From 39e08ee4a813707c4cc59be00346a2f1c4c05908 Mon Sep 17 00:00:00 2001 From: Leonardo Custodio Date: Thu, 4 Jul 2024 21:54:21 -0300 Subject: [PATCH] [PLA-1878] Mutation support for v1010 (#57) --- .github/workflows/run_tests.yml | 4 +- .github/workflows/sast.yml | 58 +++++++++++++++++ .github/workflows/security_checker.yml | 2 - composer.json | 6 +- rector.php | 17 +++++ .../Mutations/CreateListingMutation.php | 25 +++++-- src/GraphQL/Types/MarketplaceListingType.php | 60 ++++++++--------- .../Laravel/Traits/EagerLoadSelectFields.php | 65 ++++++++----------- src/Rules/MinimumPrice.php | 2 +- .../Processor/Substrate/Codec/Encoder.php | 1 + .../MarketplaceSubstrateEvent.php | 2 +- src/Services/Processor/Substrate/Parser.php | 5 +- testbench.yaml | 2 +- tests/Feature/GraphQL/TestCaseGraphQL.php | 2 +- 14 files changed, 160 insertions(+), 91 deletions(-) create mode 100644 .github/workflows/sast.yml create mode 100644 rector.php diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index f9f0ae0..a076515 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -1,9 +1,7 @@ -name: Run Tests +name: Unit & Functional Tests on: pull_request: - paths-ignore: - - "**.md" push: paths-ignore: - "**.md" diff --git a/.github/workflows/sast.yml b/.github/workflows/sast.yml new file mode 100644 index 0000000..923c947 --- /dev/null +++ b/.github/workflows/sast.yml @@ -0,0 +1,58 @@ +name: Static Application Security Testing + +on: + pull_request: + 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.2] + + name: PHP ${{ matrix.php }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - 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, pcov + tools: composer:v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - 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 dump-autoload + + - name: Run Rector + run: | + ./vendor/bin/rector process --dry-run diff --git a/.github/workflows/security_checker.yml b/.github/workflows/security_checker.yml index 0cbd8bc..8b51153 100644 --- a/.github/workflows/security_checker.yml +++ b/.github/workflows/security_checker.yml @@ -2,8 +2,6 @@ name: Dependencies Security Checker on: pull_request: - paths-ignore: - - '**.md' push: paths-ignore: - '**.md' diff --git a/composer.json b/composer.json index d1ffb5a..c7c5dbc 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ } ], "require": { - "php": "^8.2", + "php": "^8.2|^8.3", "ext-bcmath": "*", "ext-json": "*", "ext-openssl": "*", @@ -41,6 +41,7 @@ "phpstan/phpstan-phpunit": "^1.0", "phpunit/php-code-coverage": "^10.0", "phpunit/phpunit": "^10.0", + "rector/rector": "^1.0", "roave/security-advisories": "dev-latest" }, "autoload": { @@ -60,7 +61,8 @@ "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", - "fix": "vendor/bin/pint", + "dry-fix": "vendor/bin/rector process --dry-run && vendor/bin/pint --test --config ./pint.json", + "fix": "vendor/bin/rector process && vendor/bin/pint --config ./pint.json", "test": "vendor/bin/phpunit", "test-coverage": "vendor/bin/phpunit --coverage-html ../../temp/coverage", "post-autoload-dump": [ diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..8b98d80 --- /dev/null +++ b/rector.php @@ -0,0 +1,17 @@ +withPaths([ + __DIR__ . '/config', + __DIR__ . '/lang', + __DIR__ . '/src', + __DIR__ . '/tests', + ]) + ->withPhpSets(php82: true) + ->withPreparedSets(deadCode: true) + ->withRules([Spatie\Ray\Rector\RemoveRayCallRector::class]) + ->withTypeCoverageLevel(0); diff --git a/src/GraphQL/Mutations/CreateListingMutation.php b/src/GraphQL/Mutations/CreateListingMutation.php index da95b8d..7a127d6 100644 --- a/src/GraphQL/Mutations/CreateListingMutation.php +++ b/src/GraphQL/Mutations/CreateListingMutation.php @@ -85,6 +85,11 @@ public function args(): array 'type' => GraphQL::type('String'), 'description' => __('enjin-platform-marketplace::type.marketplace_listing.field.salt'), ], + // TODO: We should remove `auctionData` and replace it with `listingData` + // listingData = FixedPrice, Auction, Offer + // FixedPrice => null, + // Auction => { startBlock, endBlock }, + // Offer => { expiration } 'auctionData' => [ 'type' => GraphQL::type('AuctionDataInputType'), 'description' => __('enjin-platform-marketplace::input_type.auction_data.description'), @@ -106,7 +111,8 @@ public function resolve( ResolveInfo $resolveInfo, Closure $getSelectFields, ) { - $encodedData = TransactionSerializer::encode($this->getMutationName(), static::getEncodableParams( + $method = isRunningLatest() ? $this->getMutationName() . 'V1010' : $this->getMutationName(); + $encodedData = TransactionSerializer::encode($method, static::getEncodableParams( makeAssetId: new MultiTokensTokenAssetIdParams( Arr::get($args, 'makeAssetId.collectionId'), $this->encodeTokenId(Arr::get($args, 'makeAssetId')) @@ -136,7 +142,17 @@ public static function getEncodableParams(...$params): array $amount = Arr::get($params, 'amount', 0); $price = Arr::get($params, 'price', 0); $salt = Arr::get($params, 'salt', Str::random(10)); - $auctionData = Arr::get($params, 'auctionData', null); + $auctionData = Arr::get($params, 'auctionData'); + + $extra = isRunningLatest() ? [ + 'listingData' => $auctionData ? [ + 'Auction' => $auctionData->toEncodable(), + ] : [ + 'FixedPrice' => null, + ], + ] : [ + 'auctionData' => $auctionData?->toEncodable(), + ]; return [ 'makeAssetId' => $makeAsset->toEncodable(), @@ -144,7 +160,8 @@ public static function getEncodableParams(...$params): array 'amount' => gmp_init($amount), 'price' => gmp_init($price), 'salt' => HexConverter::stringToHexPrefixed($salt), - 'auctionData' => $auctionData?->toEncodable(), + ...$extra, + 'depositor' => null, ]; } @@ -158,7 +175,7 @@ protected function makeOrTakeRuleExist(?string $collectionId = null, ?bool $isMa 'required_with:' . $makeOrTake . '.tokenId', new MinBigInt(), new MaxBigInt(Hex::MAX_UINT128), - function (string $attribute, mixed $value, Closure $fail) { + function (string $attribute, mixed $value, Closure $fail): void { if (!Collection::where('collection_chain_id', $value)->exists()) { $fail('validation.exists')->translate(); } diff --git a/src/GraphQL/Types/MarketplaceListingType.php b/src/GraphQL/Types/MarketplaceListingType.php index 5b68ac7..2d76bb3 100644 --- a/src/GraphQL/Types/MarketplaceListingType.php +++ b/src/GraphQL/Types/MarketplaceListingType.php @@ -42,12 +42,10 @@ public function fields(): array 'makeAssetId' => [ 'type' => GraphQL::type('Asset!'), 'description' => __('enjin-platform-marketplace::type.marketplace_listing.field.makeAssetId'), - 'resolve' => function ($listing) { - return [ - 'collectionId' => $listing->make_collection_chain_id, - 'tokenId' => $listing->make_token_chain_id, - ]; - }, + 'resolve' => fn ($listing) => [ + 'collectionId' => $listing->make_collection_chain_id, + 'tokenId' => $listing->make_token_chain_id, + ], 'is_relation' => false, 'selectable' => false, 'always' => ['make_collection_chain_id', 'make_token_chain_id'], @@ -55,12 +53,10 @@ public function fields(): array 'takeAssetId' => [ 'type' => GraphQL::type('Asset!'), 'description' => __('enjin-platform-marketplace::type.marketplace_listing.field.takeAssetId'), - 'resolve' => function ($listing) { - return [ - 'collectionId' => $listing->take_collection_chain_id, - 'tokenId' => $listing->take_token_chain_id, - ]; - }, + 'resolve' => fn ($listing) => [ + 'collectionId' => $listing->take_collection_chain_id, + 'tokenId' => $listing->take_token_chain_id, + ], 'is_relation' => false, 'selectable' => false, 'always' => ['take_collection_chain_id', 'take_token_chain_id'], @@ -120,34 +116,30 @@ public function fields(): array 'description' => __('enjin-platform-marketplace::type.marketplace_sale.description'), 'args' => ConnectionInput::args(), 'is_relation' => true, - 'resolve' => function ($listing, $args) { - return [ - 'items' => new CursorPaginator( - $listing?->sales, - $args['first'], - Arr::get($args, 'after') ? Cursor::fromEncoded($args['after']) : null, - ['parameters' => ['id']] - ), - 'total' => (int) $listing?->sales_count, - ]; - }, + 'resolve' => fn ($listing, $args) => [ + 'items' => new CursorPaginator( + $listing?->sales, + $args['first'], + Arr::get($args, 'after') ? Cursor::fromEncoded($args['after']) : null, + ['parameters' => ['id']] + ), + 'total' => (int) $listing?->sales_count, + ], ], 'bids' => [ 'type' => GraphQL::paginate('MarketplaceBid', 'MarketplaceBidConnection'), 'description' => __('enjin-platform-marketplace::type.marketplace_bid.description'), 'args' => ConnectionInput::args(), 'is_relation' => true, - 'resolve' => function ($listing, $args) { - return [ - 'items' => new CursorPaginator( - $listing?->bids, - $args['first'], - Arr::get($args, 'after') ? Cursor::fromEncoded($args['after']) : null, - ['parameters' => ['id']] - ), - 'total' => (int) $listing?->bids_count, - ]; - }, + 'resolve' => fn ($listing, $args) => [ + 'items' => new CursorPaginator( + $listing?->bids, + $args['first'], + Arr::get($args, 'after') ? Cursor::fromEncoded($args['after']) : null, + ['parameters' => ['id']] + ), + 'total' => (int) $listing?->bids_count, + ], ], 'states' => [ 'type' => GraphQL::type('[MarketplaceState!]'), diff --git a/src/Models/Laravel/Traits/EagerLoadSelectFields.php b/src/Models/Laravel/Traits/EagerLoadSelectFields.php index 5dfc61a..7a19839 100644 --- a/src/Models/Laravel/Traits/EagerLoadSelectFields.php +++ b/src/Models/Laravel/Traits/EagerLoadSelectFields.php @@ -29,41 +29,30 @@ public static function selectFields(ResolveInfo $resolveInfo, string $query): ar static::$query = $query; $queryPlan = $resolveInfo->lookAhead()->queryPlan(); - switch ($query) { - case 'GetListings': - case 'GetListing': - [$select, $with, $withCount] = static::loadListings( - $queryPlan, - $query == 'GetListings' ? 'edges.fields.node.fields' : '', - [], - null, - true - ); - - break; - case 'GetBids': - case 'GetBid': - [$select, $with, $withCount] = static::loadBids( - $queryPlan, - $query == 'GetBids' ? 'edges.fields.node.fields' : '', - [], - null, - true - ); - - break; - case 'GetSales': - case 'GetSale': - [$select, $with, $withCount] = static::loadSales( - $queryPlan, - $query == 'GetSales' ? 'edges.fields.node.fields' : '', - [], - null, - true - ); - - break; - } + [$select, $with, $withCount] = match ($query) { + 'GetListings', 'GetListing' => static::loadListings( + $queryPlan, + $query == 'GetListings' ? 'edges.fields.node.fields' : '', + [], + null, + true + ), + 'GetBids', 'GetBid' => static::loadBids( + $queryPlan, + $query == 'GetBids' ? 'edges.fields.node.fields' : '', + [], + null, + true + ), + 'GetSales', 'GetSale' => static::loadSales( + $queryPlan, + $query == 'GetSales' ? 'edges.fields.node.fields' : '', + [], + null, + true + ), + default => [$select, $with, $withCount], + }; return [$select, $with, $withCount]; @@ -97,7 +86,7 @@ public static function loadListings( if (!$isParent) { $with = [ - $key => function ($query) use ($select, $args) { + $key => function ($query) use ($select, $args): void { $query->select(array_unique($select)) ->when($cursor = Cursor::fromEncoded(Arr::get($args, 'after')), fn ($q) => $q->where('id', '>', $cursor->parameter('id'))) ->orderBy('marketplace_listings.id'); @@ -155,7 +144,7 @@ public static function loadBids( if (!$isParent) { $with = [ - $key => function ($query) use ($select, $args) { + $key => function ($query) use ($select, $args): void { $query->select(array_unique($select)) ->when($cursor = Cursor::fromEncoded(Arr::get($args, 'after')), fn ($q) => $q->where('id', '>', $cursor->parameter('id'))) ->orderBy('marketplace_bids.id'); @@ -206,7 +195,7 @@ public static function loadSales( if (!$isParent) { $with = [ - $key => function ($query) use ($select, $args) { + $key => function ($query) use ($select, $args): void { $query->select(array_unique($select)) ->when($cursor = Cursor::fromEncoded(Arr::get($args, 'after')), fn ($q) => $q->where('id', '>', $cursor->parameter('id'))) ->orderBy('marketplace_sales.id'); diff --git a/src/Rules/MinimumPrice.php b/src/Rules/MinimumPrice.php index 0aa4caa..6b580c7 100644 --- a/src/Rules/MinimumPrice.php +++ b/src/Rules/MinimumPrice.php @@ -46,7 +46,7 @@ public function validate(string $attribute, mixed $value, Closure $fail): void $listing?->highestBid?->price ?? $listing?->price, 1.05 ); - if (bccomp($value, $price) < 0) { + if (bccomp((string) $value, $price) < 0) { $fail('enjin-platform-marketplace::validation.minimum_price')->translate(['price' => $price]); } } diff --git a/src/Services/Processor/Substrate/Codec/Encoder.php b/src/Services/Processor/Substrate/Codec/Encoder.php index a681691..56cdab3 100644 --- a/src/Services/Processor/Substrate/Codec/Encoder.php +++ b/src/Services/Processor/Substrate/Codec/Encoder.php @@ -8,6 +8,7 @@ class Encoder extends BaseEncoder { protected static array $callIndexKeys = [ 'CreateListing' => 'Marketplace.create_listing', + 'CreateListingV1010' => 'Marketplace.create_listing', 'CancelListing' => 'Marketplace.cancel_listing', 'FillListing' => 'Marketplace.fill_listing', 'FinalizeAuction' => 'Marketplace.finalize_auction', diff --git a/src/Services/Processor/Substrate/Events/Implementations/MarketplaceSubstrateEvent.php b/src/Services/Processor/Substrate/Events/Implementations/MarketplaceSubstrateEvent.php index e3e82fb..2d346f9 100644 --- a/src/Services/Processor/Substrate/Events/Implementations/MarketplaceSubstrateEvent.php +++ b/src/Services/Processor/Substrate/Events/Implementations/MarketplaceSubstrateEvent.php @@ -17,7 +17,7 @@ abstract class MarketplaceSubstrateEvent extends SubstrateEvent protected function getListing(string $listingId): Model { if (!$listing = MarketplaceListing::where(['listing_chain_id' => $listingId])->first()) { - throw new PlatformException(__('enjin-platform::traits.query_data_or_fail.unable_to_find_listing', ['class' => __CLASS__, 'listingId' => $listingId])); + throw new PlatformException(__('enjin-platform::traits.query_data_or_fail.unable_to_find_listing', ['class' => self::class, 'listingId' => $listingId])); } return $listing; diff --git a/src/Services/Processor/Substrate/Parser.php b/src/Services/Processor/Substrate/Parser.php index 1d7c7d5..6b401be 100644 --- a/src/Services/Processor/Substrate/Parser.php +++ b/src/Services/Processor/Substrate/Parser.php @@ -15,17 +15,14 @@ class Parser extends BaseParser { protected static $listingCache = []; - protected MarketplaceService $marketplaceService; protected Codec $codec; /** * Creates the parser instance. */ - public function __construct(MarketplaceService $marketplaceService) + public function __construct(protected MarketplaceService $marketplaceService) { parent::__construct(); - - $this->marketplaceService = $marketplaceService; $this->codec = new Codec(); } diff --git a/testbench.yaml b/testbench.yaml index da09677..a33d910 100644 --- a/testbench.yaml +++ b/testbench.yaml @@ -10,7 +10,7 @@ env: - CACHE_STORE="redis" - QUEUE_CONNECTION="redis" - CHAIN="substrate" - - NETWORK="enjin-matrixchain" + - NETWORK="canary-matrixchain" - SYNC_ALL="true" - DAEMON_ACCOUNT="0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" diff --git a/tests/Feature/GraphQL/TestCaseGraphQL.php b/tests/Feature/GraphQL/TestCaseGraphQL.php index aee3f04..64914e7 100644 --- a/tests/Feature/GraphQL/TestCaseGraphQL.php +++ b/tests/Feature/GraphQL/TestCaseGraphQL.php @@ -152,7 +152,7 @@ protected function createListing(?int $count = null, ?string $state = 'ACTIVE'): $this->seedRelatedData($listing, $state); } else { $listing->each( - function (MarketplaceListing $listing) use ($state) { + function (MarketplaceListing $listing) use ($state): void { $this->seedRelatedData($listing, $state); } );