Tutorial: Descargar precios de blockchain desde Laravel
Pequeño tutorial de descarga de precios de diferentes blockchains para ver en vivo Laravel Actions, llamadas HTTP, Carbon, Colecciones y los Upsert.
Hoy quiero hacer un pequeño tutorial sobre cómo guardar precios de diferentes blockchains en Laravel, con los que pretendo ver en acción los siguientes elementos:
- Laravel Actions
- Llamadas HTTP
- Un poquito de manejo de fechas con Carbon
- Colecciones
- Upsert con Eloquent
Setup del proyecto
En primer lugar, voy a crear un nuevo proyecto Laravel usando Composer:
composer create-project laravel/laravel Laravel-CoinGeckoAdemás, vamos a instalar Laravel Actions con el siguiente comando:
composer require lorisleiva/laravel-actionsConociendo la API de CoinGecko
Para nuestro ejemplo, vamos a descargarnos los precios de Ethereum, Bitcoin, Litecoin y Bitcoin Cash a través de la API de CoinGecko. Los identificadores de estas chains en CoinGecko son los siguientes:
ethereumbitcoinlitecoinbitcoin-cash
La URL de la API es la siguiente:
Y devuelve una respuesta más o menos así:
{
"prices": [
[1684454400000, 115.36267196694956],
[1684531299000, 115.30340299861236]
],
"market_caps": [
[1684454400000, 2242285841.689056],
[1684531299000, 2236489117.0076494]
],
"total_volumes": [
[1684454400000, 65871504.85742252],
[1684531299000, 49276496.37595198]
]
}Nos interesa coger el índice prices, en el que cada elemento es un array donde el primer item es el timestamp en milisegundos, y el segundo elemento es el precio en dólares en ese instante de tiempo.
Configurando la aplicación para usar SQLite
Para trabajar en local suelo ir bastante rápido y suelo utilizar SQLite por la flexibilidad de creación. Nos vamos al fichero .env y borramos las variables que empiezan por DB_, y creamos la siguiente:
DB_CONNECTION=sqliteCreando el modelo y la migración
Sabiendo esto, podemos crear nuestro modelo y nuestra migración de la siguiente manera:
php artisan make:model BlockchainPrice -mAl añadir la opción -m, nos creará automáticamente la migración correspondiente. Ahora, nos vamos al fichero de la migración y lo configuramos de la siguiente manera:
public function up(): void
{
Schema::create('blockchain_prices', function (Blueprint $table) {
$table->id();
$table->string('chain', 32);
$table->date('date');
$table->decimal('price');
$table->timestamps();
$table->unique(['chain', 'date']);
});
}Tras hacer esto, podemos ejecutar las migraciones:
php artisan migrateA continuación, vamos a configurar Laravel Actions.
Configurando Laravel Actions para ejecutar acciones
Vamos a crear dos acciones:
DownloadBlockchainPrices, que usaremos principalmente como comandoDownloadBlockchainPrice, que usaremos como Job para poder descargar varias blockchains a la vez
Descargando los datos de una blockchain
Vamos a crear una acción con la que podemos descargar e importar los datos de una blockchain determinada. En primer lugar, creamos una acción:
php artisan make:action DownloadBlockchainUna vez hecho esto, nos vamos al fichero app/Actions/DownloadBlockchain.php. Si no estás familiarizado con las acciones, puedes leer este post en el que hablo sobre ellas.
<?php
namespace App\Actions;
use App\Models\BlockchainPrice;
use Illuminate\Support\Facades\Http;
use Lorisleiva\Actions\Concerns\AsAction;
class DownloadBlockchain
{
use AsAction;
public function handle(string $chain, int $days = 365)
{
$data = Http::retry(3, 60000)
->get('https://api.coingecko.com/api/v3/coins/' . $chain . '/market_chart?vs_currency=USD&days=' . $days . '&interval=daily')
->throw()
->collect('prices')
->map(fn ($item) => [
'chain' => $chain,
'date' => now()->parse($item[0] / 1000)->toDateString(),
'price' => $item[1],
])
->keyBy('date')
->values();
BlockchainPrice::upsert($data->toArray(), ['chain', 'date'], ['price']);
}
}
Desgranemos un poquito lo que hacemos en el código:
Http::retry(3, 60000): Inicializamos una llamada HTTP y la configuramos para que, en caso de fallar, la reintente hasta 3 veces esperando 1 minuto entre cada petición. La razón de esto es el límite de llamadas por minuto de CoinGecko, así que me aseguro de no sobrepasarlo de esta manera.→ get($url): Especificamos la URL de la petición, no tiene mucho misterio. También podríamos pasar los query params en un array en el segundo parámetro de la función.→throw(): En caso de no devolver una respuesta con código 200, quiero que lance una excepción.→collect('prices'): convierte la respuesta a JSON, busca el índicepricesy me lo devuelve en una colección→map(...): cojo cada elemento del array de precios y lo convierto a un array con las columnas que necesitamos en la tabla.now()→parse($item[0] / 1000)→toDateString(): parseamos el timestamp pasándolo a segundos y lo convertimos a una string de fecha→ keyBy('date'): nos quedamos únicamente con un elemento por cada fecha. Aunque la API ya nos devuelve 1 único elemento por día, en el caso del día de hoy devuelve 2 elementos, así que con elkeyBynos quedamos con el último.→values(): lo reconvertimos a una colección secuencial, sin que la clave sea eldate- Por último, ejecutamos un
upsertpara actualizar o insertar los datos en la tabla
Ahora, creamos otra acción en la que lanzaremos diferentes jobs para cada blockchain:
php artisan make:action DownloadBlockchainsY la configuramos de la siguiente forma:
<?php
namespace App\Actions;
use Lorisleiva\Actions\Concerns\AsAction;
class DownloadBlockchains
{
use AsAction;
public string $commandSignature = 'download:blockchains';
public function handle()
{
$chains = [
'bitcoin',
'ethereum',
'litecoin',
'bitcoin-cash',
];
foreach ($chains as $chain) {
DownloadBlockchain::dispatch($chain);
}
}
}
Al añadir el $commandSignature, estamos configurando nuestra action para que funcione también como un comando. Por otro lado, llamar a DownloadBlockchain::dispatch($chain) lanzará un job por cada chain, permitiendo su procesamiento de forma paralela y en background.
Si ejecutamos ahora nuestro download:blockchains:
php artisan download:blockchainsVeremos un grandioso error :-)

Esto es porque las acciones no se autoregistran de forma automática en Laravel. Tenemos que configurarlas nosotros.
Para poder usar automáticamente las acciones como comando nos vamos a nuestro AppServiceProvider y añadimos el siguiente código:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Lorisleiva\Actions\Facades\Actions;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Actions::registerCommands();
}
}
Al añadir el Actions::registerCommands(), las acciones quedarán automáticamente registradas en Laravel y podremos acceder a ellas.
Ahora, si ejecutamos nuestro php artisan download:blockchains deberíamos de poder ver en nuestra base de datos que efectivamente se han descargado todos los precios:

Por último, podemos configurar nuestra acción para correr automáticamente cada día y disponer de los precios actualizados. Para ello, nos vamos a nuestro app/Console/Kernel.php y agregamos la siguiente línea dentro del método schedule:
$schedule->command('download:blockchains')->dailyAt('01:00');Con esto, configuramos nuestra aplicación para ejecutar la acción que acabamos de crear cada madrugada.
Espero que os haya sido útil para conocer cómo realizamos ciertas acciones y cómo organizamos nuestro código en algunos de nuestros proyectos :-)