Tutorial: Descargar precios de blockchain desde Laravel
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-CoinGecko
Además, vamos a instalar Laravel Actions con el siguiente comando:
composer require lorisleiva/laravel-actions
Conociendo 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:
ethereum
bitcoin
litecoin
bitcoin-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=sqlite
Creando 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 -m
Al 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 migrate
A 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 DownloadBlockchain
Una 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 índiceprices
y 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 elkeyBy
nos quedamos con el último.→values()
: lo reconvertimos a una colección secuencial, sin que la clave sea eldate
- Por último, ejecutamos un
upsert
para 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 DownloadBlockchains
Y 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:blockchains
Veremos 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 :-)