Skip to content

Commit

Permalink
refactor: store collection volume changes (#576)
Browse files Browse the repository at this point in the history
  • Loading branch information
crnkovic authored Jan 18, 2024
1 parent 55aec99 commit dd7ebab
Show file tree
Hide file tree
Showing 21 changed files with 691 additions and 39 deletions.
70 changes: 70 additions & 0 deletions app/Console/Commands/FetchAverageCollectionVolume.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

declare(strict_types=1);

namespace App\Console\Commands;

use App\Enums\Period;
use App\Jobs\FetchAverageCollectionVolume as FetchAverageCollectionVolumeJob;
use Exception;
use Illuminate\Console\Command;

class FetchAverageCollectionVolume extends Command
{
use InteractsWithCollections;

/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'collections:fetch-average-volumes {--collection-id=} {--period=}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Fetch the initial average volume stats for collections';

/**
* Execute the console command.
*/
public function handle(): int
{
$period = $this->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'),
}];
}
}
8 changes: 6 additions & 2 deletions app/Console/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
Expand Down
37 changes: 33 additions & 4 deletions app/Http/Client/Mnemonic/MnemonicPendingRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<string, mixed> $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;
}
Expand Down
72 changes: 72 additions & 0 deletions app/Jobs/FetchAverageCollectionVolume.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

namespace App\Jobs;

use App\Enums\Period;
use App\Jobs\Traits\RecoversFromProviderErrors;
use App\Models\Collection;
use App\Support\Facades\Mnemonic;
use App\Support\Queues;
use DateTime;
use Exception;
use Illuminate\Bus\Batchable;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class FetchAverageCollectionVolume implements ShouldQueue
{
use Batchable, Dispatchable, InteractsWithQueue, Queueable, RecoversFromProviderErrors, SerializesModels;

/**
* Create a new job instance.
*/
public function __construct(
public Collection $collection,
public Period $period,
) {
$this->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);
}
}
37 changes: 24 additions & 13 deletions app/Jobs/FetchCollectionVolume.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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
Expand Down
1 change: 0 additions & 1 deletion app/Jobs/RefreshWalletCollections.php
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down
1 change: 0 additions & 1 deletion app/Jobs/SyncCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
17 changes: 17 additions & 0 deletions app/Models/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -714,4 +715,20 @@ public static function getFiatValueSum(): array
GROUP BY key;'
);
}

/**
* @return HasMany<TradingVolume>
*/
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');
}
}
24 changes: 24 additions & 0 deletions app/Models/TradingVolume.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class TradingVolume extends Model
{
use HasFactory;

protected $guarded = [];

/**
* @return BelongsTo<Collection, TradingVolume>
*/
public function collection(): BelongsTo
{
return $this->belongsTo(Collection::class);
}
}
2 changes: 2 additions & 0 deletions app/Support/Facades/Mnemonic.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<int, Web3NftCollectionTrait> getCollectionTraits(Chain $chain, string $contractAddress)
* @method static Collection<int, CollectionActivity> getCollectionActivity(Chain $chain, string $contractAddress, int $limit, ?Carbon $from = null)
* @method static Collection<int, CollectionActivity> getBurnActivity(Chain $chain, string $contractAddress, int $limit, ?Carbon $from = null)
Expand Down
Loading

0 comments on commit dd7ebab

Please sign in to comment.