diff --git a/app/Console/Commands/FetchAverageCollectionVolume.php b/app/Console/Commands/FetchAverageCollectionVolume.php new file mode 100644 index 000000000..89f3b7cfc --- /dev/null +++ b/app/Console/Commands/FetchAverageCollectionVolume.php @@ -0,0 +1,70 @@ +option('period'); + + if ($period !== null && ! in_array($period, ['1d', '7d', '30d'])) { + $this->error('Invalid period value. Supported: 1d, 7d, 30d'); + + return Command::FAILURE; + } + + $this->forEachCollection(function ($collection) { + collect($this->periods())->each(fn ($period) => FetchAverageCollectionVolumeJob::dispatch($collection, $period)); + }); + + return Command::SUCCESS; + } + + /** + * @return Period[] + */ + private function periods(): array + { + if ($this->option('period') === null) { + return [ + Period::DAY, + Period::WEEK, + Period::MONTH, + ]; + } + + return [match ($this->option('period')) { + '1d' => Period::DAY, + '7d' => Period::WEEK, + '30d' => Period::MONTH, + default => throw new Exception('Unsupported period value'), + }]; + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 03c6d78a2..9970c64c8 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -12,6 +12,7 @@ use App\Console\Commands\FetchCollectionNfts; use App\Console\Commands\FetchCollectionOpenseaSlug; use App\Console\Commands\FetchCollectionTotalVolume; +use App\Console\Commands\FetchCollectionVolume; use App\Console\Commands\FetchEnsDetails; use App\Console\Commands\FetchNativeBalances; use App\Console\Commands\FetchTokens; @@ -123,12 +124,15 @@ private function scheduleJobsForCollectionsOrGalleries(Schedule $schedule): void ->hourly(); $schedule - // Command only fetches collections that don't have a slug yet - // so in most cases it will not run any request ->command(FetchCollectionTotalVolume::class) ->withoutOverlapping() ->daily(); + $schedule + ->command(FetchCollectionVolume::class) + ->withoutOverlapping() + ->daily(); + $schedule ->command(FetchCollectionActivity::class) ->withoutOverlapping() diff --git a/app/Http/Client/Mnemonic/MnemonicPendingRequest.php b/app/Http/Client/Mnemonic/MnemonicPendingRequest.php index c1b428bae..26a06a295 100644 --- a/app/Http/Client/Mnemonic/MnemonicPendingRequest.php +++ b/app/Http/Client/Mnemonic/MnemonicPendingRequest.php @@ -13,6 +13,7 @@ use App\Enums\ImageSize; use App\Enums\MnemonicChain; use App\Enums\NftTransferType; +use App\Enums\Period; use App\Enums\TraitDisplayType; use App\Exceptions\ConnectionException; use App\Exceptions\RateLimitException; @@ -22,6 +23,7 @@ use App\Support\NftImageUrl; use App\Support\Web3NftHandler; use Carbon\Carbon; +use Exception; use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Exception\ServerException; use Illuminate\Http\Client\ConnectionException as LaravelConnectionException; @@ -231,18 +233,45 @@ public function getCollectionOwners(Chain $chain, string $contractAddress): ?int return intval($data['ownersCount']); } + /** + * @see https://docs.mnemonichq.com/reference/collectionsservice_getsalesvolume + */ + public function getAverageCollectionVolume(Chain $chain, string $contractAddress, Period $period): ?string + { + $this->chain = MnemonicChain::fromChain($chain); + + $duration = match ($period) { + Period::DAY => 'DURATION_1_DAY', + Period::WEEK => 'DURATION_7_DAYS', + Period::MONTH => 'DURATION_30_DAYS', + default => throw new Exception('Unsupported period value'), + }; + + $volume = self::get(sprintf( + '/collections/v1beta2/%s/sales_volume/'.$duration.'/GROUP_BY_PERIOD_1_DAY', + $contractAddress, + ), [])->json('dataPoints.0.volume'); + + if (empty($volume)) { + return null; + } + + $currency = $chain->nativeCurrency(); + $decimals = CryptoCurrencyDecimals::forCurrency($currency); + + return CryptoUtils::convertToWei($volume, $decimals); + } + // https://docs.mnemonichq.com/reference/collectionsservice_getsalesvolume public function getCollectionVolume(Chain $chain, string $contractAddress): ?string { $this->chain = MnemonicChain::fromChain($chain); - /** @var array $data */ - $data = self::get(sprintf( + $volume = self::get(sprintf( '/collections/v1beta2/%s/sales_volume/DURATION_1_DAY/GROUP_BY_PERIOD_1_DAY', $contractAddress, - ), [])->json(); + ), [])->json('dataPoints.0.volume'); - $volume = Arr::get($data, 'dataPoints.0.volume'); if (empty($volume)) { return null; } diff --git a/app/Jobs/FetchAverageCollectionVolume.php b/app/Jobs/FetchAverageCollectionVolume.php new file mode 100644 index 000000000..b2e3abe63 --- /dev/null +++ b/app/Jobs/FetchAverageCollectionVolume.php @@ -0,0 +1,72 @@ +onQueue(Queues::SCHEDULED_COLLECTIONS); + } + + /** + * Execute the job. + */ + public function handle(): void + { + $volume = Mnemonic::getAverageCollectionVolume( + chain: $this->collection->network->chain(), + contractAddress: $this->collection->address, + period: $this->period, + ); + + $this->collection->update([ + $this->field() => $volume ?? 0, + ]); + + ResetCollectionRanking::dispatch(); + } + + private function field(): string + { + return match ($this->period) { + Period::DAY => 'avg_volume_1d', + Period::WEEK => 'avg_volume_7d', + Period::MONTH => 'avg_volume_30d', + default => throw new Exception('Unsupported period value'), + }; + } + + public function uniqueId(): string + { + return static::class.':'.$this->collection->id; + } + + public function retryUntil(): DateTime + { + return now()->addMinutes(10); + } +} diff --git a/app/Jobs/FetchCollectionVolume.php b/app/Jobs/FetchCollectionVolume.php index 5d9a0bb88..2d3eb0590 100644 --- a/app/Jobs/FetchCollectionVolume.php +++ b/app/Jobs/FetchCollectionVolume.php @@ -15,7 +15,7 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\DB; class FetchCollectionVolume implements ShouldQueue { @@ -35,25 +35,36 @@ public function __construct( */ public function handle(): void { - Log::info('FetchCollectionVolume Job: Processing', [ - 'collection' => $this->collection->address, - ]); - $volume = Mnemonic::getCollectionVolume( chain: $this->collection->network->chain(), contractAddress: $this->collection->address ); - $this->collection->update([ - 'volume' => $volume, - ]); + DB::transaction(function () use ($volume) { + $this->collection->volumes()->create([ + 'volume' => $volume ?? '0', + ]); - ResetCollectionRanking::dispatch(); + $this->collection->volume = $volume; + + // We only want to update average volumes in the given period if we have enough data for that period... + + if ($this->collection->volumes()->where('created_at', '<', now()->subDays(1))->exists()) { + $this->collection->avg_volume_1d = $this->collection->averageVolumeSince(now()->subDays(1)); + } - Log::info('FetchCollectionVolume Job: Handled', [ - 'collection' => $this->collection->address, - 'volume' => $volume, - ]); + if ($this->collection->volumes()->where('created_at', '<', now()->subDays(7))->exists()) { + $this->collection->avg_volume_7d = $this->collection->averageVolumeSince(now()->subDays(7)); + } + + if ($this->collection->volumes()->where('created_at', '<', now()->subDays(30))->exists()) { + $this->collection->avg_volume_30d = $this->collection->averageVolumeSince(now()->subDays(30)); + } + + $this->collection->save(); + }); + + ResetCollectionRanking::dispatch(); } public function uniqueId(): string diff --git a/app/Jobs/RefreshWalletCollections.php b/app/Jobs/RefreshWalletCollections.php index aaef8d845..cd358e01d 100644 --- a/app/Jobs/RefreshWalletCollections.php +++ b/app/Jobs/RefreshWalletCollections.php @@ -44,7 +44,6 @@ public function handle(): void new FetchCollectionFloorPrice($collection->network->chain_id, $collection->address), new FetchCollectionTraits($collection), new FetchCollectionOwners($collection), - new FetchCollectionVolume($collection), new FetchCollectionTotalVolume($collection), ]))->finally(function () use ($wallet) { $wallet->update([ diff --git a/app/Jobs/SyncCollection.php b/app/Jobs/SyncCollection.php index 7d3093b34..370cedb07 100644 --- a/app/Jobs/SyncCollection.php +++ b/app/Jobs/SyncCollection.php @@ -33,7 +33,6 @@ public function handle(): void Bus::batch([ new FetchCollectionTraits($this->collection), new FetchCollectionOwners($this->collection), - new FetchCollectionVolume($this->collection), ])->name('Syncing Collection #'.$this->collection->id)->dispatch(); } diff --git a/app/Models/Collection.php b/app/Models/Collection.php index 1ba56e451..11aacedca 100644 --- a/app/Models/Collection.php +++ b/app/Models/Collection.php @@ -13,6 +13,7 @@ use App\Models\Traits\Reportable; use App\Notifications\CollectionReport; use App\Support\BlacklistedCollections; +use Carbon\Carbon; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -714,4 +715,20 @@ public static function getFiatValueSum(): array GROUP BY key;' ); } + + /** + * @return HasMany + */ + public function volumes(): HasMany + { + return $this->hasMany(TradingVolume::class); + } + + public function averageVolumeSince(Carbon $date): string + { + return $this->volumes() + ->selectRaw('avg(volume::numeric) as aggregate') + ->where('created_at', '>', $date) + ->value('aggregate'); + } } diff --git a/app/Models/TradingVolume.php b/app/Models/TradingVolume.php new file mode 100644 index 000000000..fcf579168 --- /dev/null +++ b/app/Models/TradingVolume.php @@ -0,0 +1,24 @@ + + */ + public function collection(): BelongsTo + { + return $this->belongsTo(Collection::class); + } +} diff --git a/app/Support/Facades/Mnemonic.php b/app/Support/Facades/Mnemonic.php index 8a6dc8061..376c4017f 100644 --- a/app/Support/Facades/Mnemonic.php +++ b/app/Support/Facades/Mnemonic.php @@ -8,6 +8,7 @@ use App\Data\Web3\Web3NftCollectionFloorPrice; use App\Data\Web3\Web3NftCollectionTrait; use App\Enums\Chain; +use App\Enums\Period; use App\Http\Client\Mnemonic\MnemonicFactory; use App\Models\Network; use App\Models\Wallet; @@ -21,6 +22,7 @@ * @method static string | null getCollectionBanner(Chain $chain, string $contractAddress) * @method static int | null getCollectionOwners(Chain $chain, string $contractAddress) * @method static string | null getCollectionVolume(Chain $chain, string $contractAddress) + * @method static string | null getAverageCollectionVolume(Chain $chain, string $contractAddress, Period $period) * @method static Collection getCollectionTraits(Chain $chain, string $contractAddress) * @method static Collection getCollectionActivity(Chain $chain, string $contractAddress, int $limit, ?Carbon $from = null) * @method static Collection getBurnActivity(Chain $chain, string $contractAddress, int $limit, ?Carbon $from = null) diff --git a/app/Support/Web3NftHandler.php b/app/Support/Web3NftHandler.php index e71528fb2..0779ae488 100644 --- a/app/Support/Web3NftHandler.php +++ b/app/Support/Web3NftHandler.php @@ -6,8 +6,10 @@ use App\Data\Web3\Web3NftData; use App\Enums\Features; +use App\Enums\Period; use App\Enums\TokenType; use App\Jobs\DetermineCollectionMintingDate; +use App\Jobs\FetchAverageCollectionVolume; use App\Jobs\FetchCollectionActivity; use App\Jobs\FetchCollectionFloorPrice; use App\Models\Collection as CollectionModel; @@ -195,13 +197,20 @@ public function store(Collection $nfts, bool $dispatchJobs = false): void }); // Index activity only for newly created collections... - CollectionModel::query() - ->where('is_fetching_activity', false) - ->whereNull('activity_updated_at') - ->whereIn('id', $ids) - ->chunkById(100, function ($collections) { - $collections->each(fn ($collection) => FetchCollectionActivity::dispatch($collection)->onQueue(Queues::NFTS)); - }); + CollectionModel::whereIn('id', $ids)->chunkById(100, function ($collections) { + $collections->each(function ($collection) { + if (! $collection->is_fetching_activity && $collection->activity_updated_at === null) { + FetchCollectionActivity::dispatch($collection)->onQueue(Queues::NFTS); + } + + // If the collection has just been created, then prefill average volumes until we have enough data... + if ($collection->created_at->gte(now()->subMinutes(3))) { + FetchAverageCollectionVolume::dispatch($collection, Period::DAY); + FetchAverageCollectionVolume::dispatch($collection, Period::WEEK); + FetchAverageCollectionVolume::dispatch($collection, Period::MONTH); + } + }); + }); } // Passing an empty array means we update all collections which is undesired here. diff --git a/database/factories/TradingVolumeFactory.php b/database/factories/TradingVolumeFactory.php new file mode 100644 index 000000000..33b1c8c5a --- /dev/null +++ b/database/factories/TradingVolumeFactory.php @@ -0,0 +1,27 @@ + + */ +class TradingVolumeFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'collection_id' => Collection::factory(), + 'volume' => random_int(1, 1000000), + ]; + } +} diff --git a/database/migrations/2023_12_19_122041_create_trading_volumes_table.php b/database/migrations/2023_12_19_122041_create_trading_volumes_table.php new file mode 100644 index 000000000..b6c104211 --- /dev/null +++ b/database/migrations/2023_12_19_122041_create_trading_volumes_table.php @@ -0,0 +1,24 @@ +id(); + $table->foreignIdFor(Collection::class)->constrained()->cascadeOnDelete(); + $table->string('volume'); + $table->timestamps(); + }); + } +}; diff --git a/database/migrations/2023_12_19_181634_add_volume_change_columns_to_collections_table.php b/database/migrations/2023_12_19_181634_add_volume_change_columns_to_collections_table.php new file mode 100644 index 000000000..3086859bc --- /dev/null +++ b/database/migrations/2023_12_19_181634_add_volume_change_columns_to_collections_table.php @@ -0,0 +1,22 @@ +string('avg_volume_1d')->nullable(); + $table->string('avg_volume_7d')->nullable(); + $table->string('avg_volume_30d')->nullable(); + }); + } +}; diff --git a/tests/App/Console/Commands/FetchAverageCollectionVolumeTest.php b/tests/App/Console/Commands/FetchAverageCollectionVolumeTest.php new file mode 100644 index 000000000..ca29d1e60 --- /dev/null +++ b/tests/App/Console/Commands/FetchAverageCollectionVolumeTest.php @@ -0,0 +1,77 @@ +count(3)->create(); + + Bus::assertDispatchedTimes(FetchAverageCollectionVolume::class, 0); + + $this->artisan('collections:fetch-average-volumes'); + + Bus::assertDispatchedTimes(FetchAverageCollectionVolume::class, 9); +}); + +it('dispatches job for all supported periods for a specific collection', function () { + Bus::fake(); + + $collection = Collection::factory()->create(); + + Bus::assertDispatchedTimes(FetchAverageCollectionVolume::class, 0); + + $this->artisan('collections:fetch-average-volumes', [ + '--collection-id' => $collection->id, + ]); + + Bus::assertDispatchedTimes(FetchAverageCollectionVolume::class, 3); +}); + +it('dispatches job for specific period for all collections', function () { + Bus::fake(); + + Collection::factory()->count(3)->create(); + + Bus::assertDispatchedTimes(FetchAverageCollectionVolume::class, 0); + + $this->artisan('collections:fetch-average-volumes', [ + '--period' => '7d', + ]); + + Bus::assertDispatchedTimes(FetchAverageCollectionVolume::class, 3); +}); + +it('dispatches job for specific period for a specific collection', function () { + Bus::fake(); + + $collection = Collection::factory()->create(); + + Bus::assertDispatchedTimes(FetchAverageCollectionVolume::class, 0); + + $this->artisan('collections:fetch-average-volumes', [ + '--collection-id' => $collection->id, + '--period' => '7d', + ]); + + Bus::assertDispatchedTimes(FetchAverageCollectionVolume::class, 1); +}); + +it('handles unsupported period values', function () { + Bus::fake(); + + $collection = Collection::factory()->create(); + + Bus::assertDispatchedTimes(FetchAverageCollectionVolume::class, 0); + + $this->artisan('collections:fetch-average-volumes', [ + '--collection-id' => $collection->id, + '--period' => '1m', + ]); + + Bus::assertDispatchedTimes(FetchAverageCollectionVolume::class, 0); +}); diff --git a/tests/App/Http/Client/Mnemonic/MnemonicPendingRequestTest.php b/tests/App/Http/Client/Mnemonic/MnemonicPendingRequestTest.php index bdb9c93a6..55d72737d 100644 --- a/tests/App/Http/Client/Mnemonic/MnemonicPendingRequestTest.php +++ b/tests/App/Http/Client/Mnemonic/MnemonicPendingRequestTest.php @@ -4,6 +4,7 @@ use App\Enums\Chain; use App\Enums\NftTransferType; +use App\Enums\Period; use App\Exceptions\ConnectionException; use App\Exceptions\RateLimitException; use App\Models\Collection; @@ -126,6 +127,48 @@ expect($data)->toBe('12300000000000000000'); }); +it('should get average volume', function () { + Mnemonic::fake([ + 'https://polygon-rest.api.mnemonichq.com/collections/v1beta2/*/sales_volume/DURATION_7_DAYS/GROUP_BY_PERIOD_1_DAY' => Http::sequence() + ->push([ + 'dataPoints' => [ + ['volume' => '12.3'], + ], + ], 200), + ]); + + $network = Network::polygon(); + + $collection = Collection::factory()->create([ + 'network_id' => $network->id, + ]); + + $data = Mnemonic::getAverageCollectionVolume(Chain::Polygon, $collection->address, Period::WEEK); + + expect($data)->toBe('12300000000000000000'); +}); + +it('should get no average volume', function () { + Mnemonic::fake([ + 'https://polygon-rest.api.mnemonichq.com/collections/v1beta2/*/sales_volume/DURATION_7_DAYS/GROUP_BY_PERIOD_1_DAY' => Http::sequence() + ->push([ + 'dataPoints' => [ + ['volume' => null], + ], + ], 200), + ]); + + $network = Network::polygon(); + + $collection = Collection::factory()->create([ + 'network_id' => $network->id, + ]); + + $data = Mnemonic::getAverageCollectionVolume(Chain::Polygon, $collection->address, Period::WEEK); + + expect($data)->toBeNull(); +}); + it('should handle no volume', function ($request) { Mnemonic::fake([ 'https://polygon-rest.api.mnemonichq.com/collections/v1beta2/*/sales_volume/DURATION_1_DAY/GROUP_BY_PERIOD_1_DAY' => Http::sequence() diff --git a/tests/App/Jobs/FetchAverageCollectionVolumeTest.php b/tests/App/Jobs/FetchAverageCollectionVolumeTest.php new file mode 100644 index 000000000..00ba942d1 --- /dev/null +++ b/tests/App/Jobs/FetchAverageCollectionVolumeTest.php @@ -0,0 +1,41 @@ +andReturn(753); + + $network = Network::polygon(); + + $collection = Collection::factory()->for($network)->create([ + 'avg_volume_1d' => '1', + 'avg_volume_7d' => '2', + 'avg_volume_30d' => '3', + ]); + + (new FetchAverageCollectionVolume($collection, Period::WEEK))->handle(); + + $collection->refresh(); + + expect($collection->avg_volume_1d)->toBe('1'); + expect($collection->avg_volume_7d)->toBe('753'); + expect($collection->avg_volume_30d)->toBe('3'); +}); + +it('has a unique ID', function () { + $collection = Collection::factory()->create(); + + expect((new FetchAverageCollectionVolume($collection, Period::WEEK))->uniqueId())->toBeString(); +}); + +it('has a retry limit', function () { + $collection = Collection::factory()->create(); + + expect((new FetchAverageCollectionVolume($collection, Period::WEEK))->retryUntil())->toBeInstanceOf(DateTime::class); +}); diff --git a/tests/App/Jobs/FetchCollectionVolumeTest.php b/tests/App/Jobs/FetchCollectionVolumeTest.php index 4bb2c8414..a6f67c579 100644 --- a/tests/App/Jobs/FetchCollectionVolumeTest.php +++ b/tests/App/Jobs/FetchCollectionVolumeTest.php @@ -5,34 +5,107 @@ use App\Jobs\FetchCollectionVolume; use App\Models\Collection; use App\Models\Network; +use App\Models\TradingVolume; use App\Support\Facades\Mnemonic; use Illuminate\Support\Facades\Http; it('should fetch nft collection volume', function () { + Mnemonic::shouldReceive('getCollectionVolume')->andReturn('10'); + + $collection = Collection::factory()->for(Network::polygon())->create([ + 'volume' => null, + ]); + + expect($collection->volume)->toBeNull(); + + (new FetchCollectionVolume($collection))->handle(); + + expect($collection->fresh()->volume)->toBe('10'); +}); + +it('logs volume changes', function () { Mnemonic::fake([ - 'https://polygon-rest.api.mnemonichq.com/collections/v1beta2/*/sales_volume/DURATION_1_DAY/GROUP_BY_PERIOD_1_DAY' => Http::response([ - 'dataPoints' => [ - ['volume' => '12.3'], - ], + '*' => Http::response([ + 'dataPoints' => [[ + 'volume' => '12.3', + ]], ], 200), ]); $network = Network::polygon(); - $collection = Collection::factory()->create([ - 'network_id' => $network->id, - 'volume' => null, + $collection = Collection::factory()->for($network)->create([ + 'volume' => '11000000000000000000', ]); - $this->assertDatabaseCount('collections', 1); + TradingVolume::factory()->for($collection)->create([ + 'volume' => '11000000000000000000', + 'created_at' => now()->subHours(10), + ]); - expect($collection->volume)->toBeNull(); + (new FetchCollectionVolume($collection))->handle(); + + $collection->refresh(); + + expect($collection->volumes()->count())->toBe(2); + expect($collection->volumes()->oldest('id')->pluck('volume')->toArray())->toBe(['11000000000000000000', '12300000000000000000']); +}); + +it('calculates average volume when there is enough historical data', function () { + Mnemonic::fake([ + '*' => Http::response([ + 'dataPoints' => [[ + 'volume' => '12.3', + ]], + ], 200), + ]); + + $network = Network::polygon(); + + $collection = Collection::factory()->for($network)->create([ + 'volume' => '11000000000000000000', + ]); + + TradingVolume::factory()->for($collection)->create([ + 'volume' => '10000000000000000000', + 'created_at' => now()->subDays(2), + ]); + + TradingVolume::factory()->for($collection)->create([ + 'volume' => '11000000000000000000', + 'created_at' => now()->subDays(8), + ]); + + TradingVolume::factory()->for($collection)->create([ + 'volume' => '12000000000000000000', + 'created_at' => now()->subDays(31), + ]); + + (new FetchCollectionVolume($collection))->handle(); + + $collection->refresh(); + + expect($collection->volumes()->count())->toBe(4); + expect($collection->volumes()->oldest('id')->pluck('volume')->toArray())->toBe(['10000000000000000000', '11000000000000000000', '12000000000000000000', '12300000000000000000']); + + expect($collection->avg_volume_1d)->toBe('12300000000000000000'); + expect($collection->avg_volume_7d)->toBe('11150000000000000000'); + expect($collection->avg_volume_30d)->toBe('11100000000000000000'); +}); + +it('does not log volume changes if there is no volume', function () { + Mnemonic::shouldReceive('getCollectionVolume')->andReturn(null); + + $collection = Collection::factory()->for(Network::polygon())->create([ + 'volume' => '10', + ]); (new FetchCollectionVolume($collection))->handle(); $collection->refresh(); - expect($collection->volume)->toBe('12300000000000000000'); + expect(TradingVolume::count())->toBe(1); + expect(TradingVolume::first()->volume)->toBe('0'); }); it('has a retry limit', function () { diff --git a/tests/App/Jobs/RefreshWalletCollectionsTest.php b/tests/App/Jobs/RefreshWalletCollectionsTest.php index 0272b45c4..b9fbc4a48 100644 --- a/tests/App/Jobs/RefreshWalletCollectionsTest.php +++ b/tests/App/Jobs/RefreshWalletCollectionsTest.php @@ -24,7 +24,7 @@ $job->handle(); Bus::assertBatched(function (PendingBatch $batch) { - return $batch->jobs->count() === 10; + return $batch->jobs->count() === 8; }); $user->wallet->refresh(); diff --git a/tests/App/Models/TradingVolumeTest.php b/tests/App/Models/TradingVolumeTest.php new file mode 100644 index 000000000..ece0bad67 --- /dev/null +++ b/tests/App/Models/TradingVolumeTest.php @@ -0,0 +1,16 @@ +create(); + + $volume = TradingVolume::factory()->create([ + 'collection_id' => $collection->id, + ]); + + expect($volume->collection->is($collection))->toBeTrue(); +}); diff --git a/tests/App/Support/Web3NftHandlerTest.php b/tests/App/Support/Web3NftHandlerTest.php index c5992c1ac..6611bd290 100644 --- a/tests/App/Support/Web3NftHandlerTest.php +++ b/tests/App/Support/Web3NftHandlerTest.php @@ -6,6 +6,7 @@ use App\Enums\NftInfo; use App\Enums\TokenType; use App\Enums\TraitDisplayType; +use App\Jobs\FetchAverageCollectionVolume; use App\Models\Collection; use App\Models\Network; use App\Models\NftTrait; @@ -801,3 +802,95 @@ traits: [], expect(Collection::count())->toBe(1); expect($collection->nfts->first()->info)->toBe(null); }); + +it('should dispatch jobs to fetch average collection volume when collection is first added', function () { + Bus::fake(); + + $network = Network::polygon(); + + $token = Token::factory()->create([ + 'network_id' => $network->id, + ]); + + $wallet = Wallet::factory()->create(); + + $handler = new Web3NftHandler( + network: $network, + wallet: $wallet, + ); + + Collection::factory()->for($network)->create([ + 'address' => '0x999', + 'created_at' => now()->subHour(), + ]); + + $oldData = new Web3NftData( + tokenAddress: '0x999', + tokenNumber: '123', + networkId: $token->network_id, + collectionName: 'Collection Name', + collectionSymbol: 'AME', + collectionImage: null, + collectionWebsite: null, + collectionDescription: null, + collectionSocials: null, + collectionSupply: 3000, + collectionBannerImageUrl: null, + collectionBannerUpdatedAt: now(), + collectionOpenSeaSlug: null, + name: null, + description: null, + extraAttributes: [ + 'image' => null, + 'website' => null, + 'banner' => null, + 'banner_updated_at' => now(), + 'opensea_slug' => null, + ], + floorPrice: null, + traits: [], + mintedBlock: 1000, + mintedAt: null, + hasError: true, + info: null, + type: TokenType::Erc721, + ); + + $data = new Web3NftData( + tokenAddress: '0x1234', + tokenNumber: '123', + networkId: $token->network_id, + collectionName: 'Collection Name', + collectionSymbol: 'AME', + collectionImage: null, + collectionWebsite: null, + collectionDescription: null, + collectionSocials: null, + collectionSupply: 3000, + collectionBannerImageUrl: null, + collectionBannerUpdatedAt: now(), + collectionOpenSeaSlug: null, + name: null, + description: null, + extraAttributes: [ + 'image' => null, + 'website' => null, + 'banner' => null, + 'banner_updated_at' => now(), + 'opensea_slug' => null, + ], + floorPrice: null, + traits: [], + mintedBlock: 1000, + mintedAt: null, + hasError: true, + info: null, + type: TokenType::Erc721, + ); + + $handler->store(collect([$data, $oldData]), dispatchJobs: true); + + expect(Collection::count())->toBe(2); + + Bus::assertDispatchedTimes(FetchAverageCollectionVolume::class, 3); +});