Skip to content

Commit

Permalink
Add transaction checker command (#56)
Browse files Browse the repository at this point in the history
  • Loading branch information
leonardocustodio authored Sep 7, 2023
1 parent 71dcfef commit c608903
Show file tree
Hide file tree
Showing 2 changed files with 199 additions and 0 deletions.
186 changes: 186 additions & 0 deletions src/Commands/TransactionChecker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
<?php

namespace Enjin\Platform\Commands;

use Carbon\Carbon;
use Enjin\Platform\Enums\Global\TransactionState;
use Enjin\Platform\Enums\Substrate\StorageKey;
use Enjin\Platform\Models\Laravel\Block;
use Enjin\Platform\Models\Laravel\Transaction;
use Enjin\Platform\Services\Blockchain\Implementations\Substrate;
use Enjin\Platform\Services\Processor\Substrate\Codec\Codec;
use Enjin\Platform\Services\Processor\Substrate\ExtrinsicProcessor;
use Facades\Enjin\Platform\Services\Processor\Substrate\State;
use Illuminate\Console\Command;
use Illuminate\Support\Arr;
use Symfony\Component\Console\Helper\ProgressBar;

class TransactionChecker extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
public $signature = 'platform:transaction-checker';

/**
* The console command description.
*
* @var string
*/
public $description;

protected Codec $codec;
protected Substrate $client;
protected ProgressBar $progressBar;
protected Carbon $start;

public function __construct()
{
parent::__construct();

$this->description = __('enjin-platform::commands.transactions.description');
$this->start = now();
}

/**
* Execute the job.
*/
public function handle(Substrate $client, Codec $codec): void
{
$this->codec = $codec;
$this->client = $client;

$blockNumber = Block::where('synced', true)->max('number');
$maxBlockToCheck = $blockNumber - 100;

$transactions = collect(Transaction::where([
'state' => TransactionState::BROADCAST,
])->whereNotNull(['signed_at_block', 'transaction_chain_hash'])
->where('signed_at_block', '<', $maxBlockToCheck)->get());

if ($transactions->isEmpty()) {
$this->info('There are no transactions to check.');

return;
}

$minSignedAtBlock = $transactions->min('signed_at_block');
$this->info(__('enjin-platform::commands.transactions.header'));
$this->info(__('enjin-platform::commands.transactions.syncing', ['fromBlock' => $minSignedAtBlock, 'toBlock' => $maxBlockToCheck]));

if ($minSignedAtBlock > $maxBlockToCheck) {
$this->info('There are no transactions to check in those blocks');

return;
}

$this->info(__('enjin-platform::commands.transactions.fetching'));
$counter = $transactions->count();
$hashes = array_filter($transactions->pluck('transaction_chain_hash')->toArray());

$this->progressBar = $this->output->createProgressBar($counter);
$this->progressBar->setFormat('debug');
$this->progressBar->start();

for ($i = $minSignedAtBlock; $i <= $maxBlockToCheck; $i++) {
$this->progressBar->setProgress($counter - collect($transactions)->count());

$block = Block::firstWhere('number', $i);
if (!($block?->hash)) {
$block = Block::updateOrCreate(
['number' => $i],
['hash' => $client->callMethod('chain_getBlockHash', [$i])],
);
}

$extrinsics = $this->fetchExtrinsics($block, $client);
$hashesFromThisBlock = collect($extrinsics)->pluck('hash')->toArray();

if (($i - $minSignedAtBlock) > 300) {
$this->displayMessageAboveBar("Did not find transaction signed at block {$minSignedAtBlock} in the last 300 blocks");

$transactions = collect($transactions)->filter(fn ($transaction) => $transaction->signed_at_block != $minSignedAtBlock);
$minSignedAtBlock = collect($transactions)->min('signed_at_block');

if (empty($minSignedAtBlock) || $minSignedAtBlock >= $maxBlockToCheck) {
$this->displayMessageAboveBar('There are no more transactions to search for.');

break;
}

if ($minSignedAtBlock <= $i) {
$this->displayMessageAboveBar("Continuing trying to find transaction signed at block {$minSignedAtBlock}");
} else {
$this->displayMessageAboveBar("Skipping from block {$i} to block {$minSignedAtBlock}");
$i = $minSignedAtBlock - 1;
}
}

if (count(array_intersect($hashes, $hashesFromThisBlock)) > 0) {
$block->events = $this->fetchEvents($block, $client);
$block->extrinsics = $extrinsics;

$hasExtrinsicErrors = (new ExtrinsicProcessor($block, $this->codec))->run();
if (!empty($hasExtrinsicErrors)) {
$this->error(json_encode($hasExtrinsicErrors));
}

$this->displayMessageAboveBar(sprintf('Took %s blocks to find the transaction signed at block %s', $i - $minSignedAtBlock, $minSignedAtBlock));
$hashes = array_diff($hashes, $hashesFromThisBlock);
$transactions = collect($transactions)->filter(fn ($transaction) => !in_array($transaction->transaction_chain_hash, $hashesFromThisBlock));
$minSignedAtBlock = collect($transactions)->min('signed_at_block');

if ($minSignedAtBlock > $i) {
$this->displayMessageAboveBar(sprintf("Skipping from block {$i} to block %s", $minSignedAtBlock));
$i = $minSignedAtBlock - 1;
}
}

if (empty($hashes)) {
break;
}
}

$this->displayOverview($counter, $hashes);
}

protected function fetchExtrinsics($block, Substrate $client): mixed
{
$data = $client->callMethod('chain_getBlock', [$block->hash]);
if ($extrinsics = Arr::get($data, 'block.extrinsics')) {
return State::extrinsicsForBlock(['number' => $block->number, 'extrinsics' => json_encode($extrinsics)]) ?? [];
}

return [];
}

protected function fetchEvents($block, Substrate $client): mixed
{
if ($events = $client->callMethod('state_getStorage', [StorageKey::EVENTS->value, $block->hash])) {
return State::eventsForBlock(['number' => $block->number, 'events' => $events]) ?? [];
}

return [];
}

protected function displayOverview(int $counter, array $hashes): void
{
$this->progressBar->finish();
$this->newLine();

$this->info(__('enjin-platform::commands.transactions.overview'));
$this->info(sprintf('We did not find the following transactions: %s', json_encode($hashes)));
$this->info(sprintf('The command has fixed %s transactions.', $counter - collect($hashes)->count()));
$this->info(sprintf('Command run for the total of %s seconds.', now()->diffInMilliseconds($this->start) / 1000));
$this->info('=======================================================');
}

protected function displayMessageAboveBar(string $message): void
{
$this->progressBar->clear();
$this->info($message);
$this->progressBar->display();
}
}
13 changes: 13 additions & 0 deletions src/CoreServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Enjin\Platform\Commands\ClearCache;
use Enjin\Platform\Commands\Ingest;
use Enjin\Platform\Commands\Sync;
use Enjin\Platform\Commands\TransactionChecker;
use Enjin\Platform\Commands\Transactions;
use Enjin\Platform\Enums\Global\PlatformCache;
use Enjin\Platform\Events\Substrate\Commands\PlatformSynced;
Expand All @@ -18,6 +19,7 @@
use Enjin\Platform\Providers\FakerServiceProvider;
use Enjin\Platform\Providers\GraphQlServiceProvider;
use Enjin\Platform\Services\Processor\Substrate\BlockProcessor;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
Expand Down Expand Up @@ -64,6 +66,7 @@ public function configurePackage(Package $package): void
->hasCommand(Ingest::class)
->hasCommand(Transactions::class)
->hasCommand(ClearCache::class)
->hasCommand(TransactionChecker::class)
->hasTranslations();
}

Expand All @@ -86,6 +89,16 @@ public function boot()
$this->app->register(FakerServiceProvider::class);
$this->app->register(AuthServiceProvider::class);

$this->app->booted(function () {
$schedule = $this->app->make(Schedule::class);
$schedule->command('platform:transaction-checker')
->everyFiveMinutes()
->withoutOverlapping(3)
->onOneServer()
->runInBackground()
->appendOutputTo(storage_path('logs/txchecker.log'));
});

Event::listen(PlatformSyncing::class, fn () => BlockProcessor::synching());
Event::listen(PlatformSynced::class, fn () => BlockProcessor::synchingDone());
Event::listen(PlatformSyncError::class, fn () => BlockProcessor::synchingDone());
Expand Down

0 comments on commit c608903

Please sign in to comment.