Skip to content

Commit

Permalink
refactor: prevent exchange exceptions (#918)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexbarnsley authored Jun 25, 2024
1 parent d06b75d commit 19a0a7d
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 57 deletions.
29 changes: 25 additions & 4 deletions app/Console/Commands/FetchExchangesDetails.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@

namespace App\Console\Commands;

use App\Jobs\FetchExchangeDetails;
use App\Contracts\MarketDataProvider;
use App\Exceptions\CoinGeckoThrottledException;
use App\Models\Exchange;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Arr;

final class FetchExchangesDetails extends Command
{
Expand All @@ -26,9 +29,27 @@ final class FetchExchangesDetails extends Command

public function handle(): int
{
Exchange::coingecko()->each(function ($exchange) {
FetchExchangeDetails::dispatch($exchange);
});
$exchanges = Exchange::coingecko()
->orderBy('volume', 'desc')
->get()
->filter(fn ($exchange) => $exchange->updated_at < Carbon::now()->subHours(1))
->sort(fn ($a, $b) => ($a->updated_at?->unix() ?? 0) - ($b->updated_at?->unix() ?? 0));

foreach ($exchanges as $exchange) {
try {
$result = app(MarketDataProvider::class)->exchangeDetails($exchange);
} catch (CoinGeckoThrottledException) {
continue;
}

$exchange->price = Arr::get($result, 'price', $exchange->price);
$exchange->volume = Arr::get($result, 'volume', $exchange->volume);

$exchange->save();

// We touch to make sure the updated_at was changed to prevent unnecessary updates.
$exchange->touch();
}

return Command::SUCCESS;
}
Expand Down
2 changes: 1 addition & 1 deletion app/Console/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ protected function schedule(Schedule $schedule)
$schedule->command(LoadExchanges::class)->daily();

if (Network::canBeExchanged()) {
$schedule->command(FetchExchangesDetails::class)->hourly();
$schedule->command(FetchExchangesDetails::class)->everyMinute();
}

if (config('arkscan.scout.run_jobs', false) === true) {
Expand Down
2 changes: 2 additions & 0 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,7 @@ protected function refreshTestDatabase()
'--database' => 'explorer',
'--path' => 'tests/migrations',
]);

Artisan::call('migrate', ['--path' => 'database/migrations']);
}
}
105 changes: 93 additions & 12 deletions tests/Unit/Console/Commands/FetchExchangesDetailsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,112 @@

declare(strict_types=1);

use App\Jobs\FetchExchangeDetails;
use App\Contracts\MarketDataProvider;
use App\Models\Exchange;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Bus;
use App\Services\MarketDataProviders\CoinGecko;
use Carbon\Carbon;
use Illuminate\Support\Facades\Http;

beforeEach(function () {
Artisan::call('migrate:fresh');
Exchange::truncate();

Bus::fake(FetchExchangeDetails::class);
$this->freezeTime();

$this->travelTo(Carbon::parse('2024-05-14 12:22:49'));

$this->app->singleton(
MarketDataProvider::class,
fn () => new CoinGecko()
);
});

it('calls the job for fetching the exchange details for exchanges with coingecko id', function () {
Exchange::factory()->create([
it('should update exchange details for coingecko exchanges once per hour', function () {
Http::fake([
'https://api.coingecko.com/api/v3/exchanges/binance/tickers?coin_ids=ark' => Http::response([
'tickers' => [
[
'converted_last' => [
'usd' => 123,
],
'converted_volume' => [
'usd' => 456,
],
],
],
], 200),
]);

$coingeckoExchange = Exchange::factory()->create([
'coingecko_id' => 'binance',
'volume' => null,
'price' => null,
]);

Exchange::factory()->create([
$genericExchange = Exchange::factory()->create([
'coingecko_id' => null,
'volume' => null,
'price' => null,
]);

$this->artisan('exchanges:fetch-details');

Bus::assertDispatchedTimes(FetchExchangeDetails::class, 1);
$this->travel(59)->minutes();

expect($coingeckoExchange->fresh()->price)->toBeNull();
expect($coingeckoExchange->fresh()->volume)->toBeNull();

expect($genericExchange->fresh()->price)->toBeNull();
expect($genericExchange->fresh()->volume)->toBeNull();

$this->travel(2)->minutes();

$this->artisan('exchanges:fetch-details');

expect($coingeckoExchange->fresh()->price)->toBe('123');
expect($coingeckoExchange->fresh()->volume)->toBe('456');

expect($genericExchange->fresh()->price)->toBeNull();
expect($genericExchange->fresh()->volume)->toBeNull();
});

it('should do nothing if there is a coingecko error', function () {
Http::fake([
'https://api.coingecko.com/api/v3/exchanges/binance/tickers?coin_ids=ark' => Http::response([
'status' => [
'error_code' => 1234,
],
], 500),
]);

$coingeckoExchange = Exchange::factory()->create([
'coingecko_id' => 'binance',
'volume' => null,
'price' => null,
]);

$genericExchange = Exchange::factory()->create([
'coingecko_id' => null,
'volume' => null,
'price' => null,
]);

$this->artisan('exchanges:fetch-details');

$this->travel(59)->minutes();

expect($coingeckoExchange->fresh()->price)->toBeNull();
expect($coingeckoExchange->fresh()->volume)->toBeNull();

expect($genericExchange->fresh()->price)->toBeNull();
expect($genericExchange->fresh()->volume)->toBeNull();

$this->travel(2)->minutes();

$this->artisan('exchanges:fetch-details');

expect($coingeckoExchange->fresh()->price)->toBeNull();
expect($coingeckoExchange->fresh()->volume)->toBeNull();

Bus::assertDispatched(FetchExchangeDetails::class, function ($job) {
return $job->exchange->coingecko_id === 'binance';
});
expect($genericExchange->fresh()->price)->toBeNull();
expect($genericExchange->fresh()->volume)->toBeNull();
});

This file was deleted.

This file was deleted.

0 comments on commit 19a0a7d

Please sign in to comment.