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-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 :-)