Sql.js ❤️ Ethers.js
Mendekode data log transaksi Ethereum dengan Sql.js dan Ethers.js
Musim dingin kripto datang. Banyak orang merugi. Tapi ada juga orang-orang yang menyambutnya. Vitalik salah satunya. "Jatuhnya harga (kripto) memisahkan yang penasaran dari yang serius", katanya. "Inilah saatnya membangun," katanya lagi.
Selain membangun, barangkali juga belajar.
Bulan Maret lalu, saya menulis tentang percobaan menjelajahi data pasarloka NFT OpenSea di BigQuery dengan mengambil contoh kasus NFT Ghozali. Dalam prosesnya saya belajar cukup banyak hal. Mendekode data transaksi, memahami proses transaksi NFT di OPENSTORE OpenSea, dan tentunya, bagaimana mengambil data itu dari BigQuery.
Salah satu hal yang membatasi proses analitika blockchain adalah harga kueri yang mahal.
SELECT
block_timestamp,
transaction_hash,
data,
ARRAY_TO_STRING((SELECT ARRAY_AGG(x) FROM UNNEST(topics) AS x), ",") AS topics
FROM
`public-data-finance.crypto_polygon.logs`
WHERE
DATE(block_timestamp) = "2022-01-11"
AND address = "0x2953399124f0cbb46d2cbacd8a89cf0599974963" -- OPENSTORE CONTRACT ADDRESS
AND topics[SAFE_OFFSET(0)] = "0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62" -- TransferSingle ()
AND REGEXP_CONTAINS(DATA, '(?i)0xc1e05e98466908547f30fcfc34e405b9c84dfcb7') -- Ghozali_Ghozalu Address
LIMIT
3
BigQuery akan memproses 7.01 GB data dari kueri di atas (Query 1), meski hanya membaca data satu hari DATE(block_timestamp) = "2022-01-11"
saja. Itu juga tak mengambil semua kolom yang ada. Dalam proses pembuatan tulisan OpenSea itu, saya menghabiskan jatah kuota kueri gratis 1TB.
Lalu apa yang bisa dilakukan untuk membuat biaya belajar jadi lebih murah?
Saya memilih untuk mengekspor data dari BigQuery ke tempat penyimpanan lain, dan mengolahnya dengan mesin yang berbeda–tapi tetap dalam SQL.
Tulisan ini hadir untuk membahas bagaimana melakukan penjelajahan data blockchain (EVM) menggunakan SQLite.
Demonstrasi sql.js
Sebelum lanjut, Silakan coba jalankan kueri di bawah ini dengan memencet tombol "Execute".
Kueri SQL di atas berjalan dalam peramban Anda, ditenagai oleh sql.js–tanpa server.
sql.js adalah database SQL JavaScript. Pustaka ini memungkinkan kita untuk membuat database relasional dan mengkuerinya di browser. File database virtual disimpan dalam memori, dan dengan demikian tidak akan mempertahankan perubahan apa pun yang dilakukan terhadap database (hilang saat laman di-refresh). sql.js menggunakan emscripten untuk mengkompilasi SQLite ke webassembly.
Sekarang, mari melihat apa yang terjadi pada kueri tersebut. Kueri dalam CTE prepare
(WITH prepare AS (SELECT ...)
) adalah kueri untuk mensimulasikan hasil kueri dari Query 1 (BigQuery) ke dalam sebuah views sementara. CTE itu akan membuat 3 kolom dan mengisi masing-masing dengan teks: Kolom transaction_hash
, data
, dan topics
. Untuk penjelasan apa itu data dan topics, silakan baca tulisan sebelumnya.
CTE kedua adalah parse
. Di situlah terjadi proses dekode data log transaksi blockchain menjadi data yang bisa dibaca oleh manusia. Fungsi parse(topics, data)
dibuat dengan menggunakan JS UDF dengan pustaka Ethers.js. Pustaka ini juga dipakai untuk mendekode data blockchain yang ada di BigQuery.
Sebelum lanjut, silakan jalankan kueri kedua dengan memencet tombol "Execute".
Kueri di atas akan mengambil data dari tabel seaport
.
sql.js di dalam laman ini diinstantiasi dengan menggunakan file database SQLite (seaport.db) yang saya simpan di Cloudflare R2. File database itu adalah hasil ekspor data dari BigQuery dengan kueri seperti di bawah ini:
SELECT
block_timestamp,
block_number,
transaction_hash,
data,
ARRAY_TO_STRING((SELECT ARRAY_AGG(x) FROM UNNEST(topics) AS x), ",") topics
FROM `bigquery-public-data.crypto_ethereum.logs`
WHERE
DATE(block_timestamp) >= "2022-01-05"
AND LOWER(address) = LOWER("0x00000000006c3852cbEf3e08E8dF289169EdE581")
Query 2 di atas akan memproses 281.92 GB data. Mahal. Hasil dari kueri itu saya unduh ke dalam csv, kemudian saya konversi ke dalam file SQLite.
Untuk kebutuhan demonstrasi, file seaport.db di laman ini hanya memiliki 500 baris data saja untuk memperkecil ukuran data dan mempercepat pemuatan laman (222KB).
Membuat fungsi untuk mendekode log transaksi blockchain dalam sql.js
SQLite hanya memiliki tipe data TEXT, NUMERIC, INTEGER, REAL, BLOB. Tidak ada ARRAY seperti di BigQuery. Yang perlu kita lakukan adalah mengubah ARRAY menjadi TEXT ketika melakukan ekspor data. Sebab itulah kolom topics
diambil dengan cara demikian:
ARRAY_TO_STRING((SELECT ARRAY_AGG(x) FROM UNNEST(topics) AS x), ",") topics
JS UDF parse_seaport(topics, data)
import { ethers } from "https://cdnjs.cloudflare.com/ajax/libs/ethers/5.7.2/ethers.esm.min.js";
async function getData(url) {
const response = await fetch(url);
return response.json();
}
let seaportAbi = await getData('https://api.etherscan.io/api?module=contract&action=getabi&address=0x00000000006c3852cbEf3e08E8dF289169EdE581&format=raw')
function stringy(args) {
Object.keys(args).forEach(function (key) { args[key] = args[key].toString() });
return args;
}
function parser(topics, data, interface_instance) {
let arraifyTopics = topics.split(",");
let parsedLog = interface_instance.parseLog({ topics: arraifyTopics, data: data });
let stringed = stringy({ ...parsedLog.args });
let method = {method_name: parsedLog.name};
let returnedTarget = Object.assign(stringed, method);
return JSON.stringify(returnedTarget);
}
function parseSeaport(topics, data) {
let seaportInterface = new ethers.utils.Interface(seaportAbi);
return parser(topics, data, seaportInterface);
}
(async function () {
const sqlPromise = initSqlJs({ locateFile: file => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.6.2/sql-wasm.wasm` });
const dataPromise = fetch("https://r2.adityawarmanfw.id/seaport.db").then(res => res.arrayBuffer());
const [SQL, buf] = await Promise.all([sqlPromise, dataPromise]);
const db = new SQL.Database(new Uint8Array(buf));
db.create_function("parse", decodeLog);
db.create_function("parse_seaport", parseSeaport);
})();
ABI dari Kontrak Pintar dibutuhkan dalam membuat fungsi untuk mendekode log transaksi. Di contoh ini, saya menggunakan Kontrak Pintar SEAPORT dari OpenSea. Kita bisa ambil ABI dari Etherscan (lihat seaportAbi
).
Karena topics disimpan dalam database SQLite dengan tipe TEXT, kita perlu mengubahnya kembali ke dalam bentuk ARRAY agar bisa diterima oleh utils.Interface
Ethers.js. Dalam proses dekode log ini, kita melakukan beberapa perubahan tipe data: ARRAY (BQ) -> TEXT (SQLite) -> ARRAY (JS) -> OBJECT (JS) -> STRING (JS) -> TEXT (SQLite).
Di situlah perbedaan praktis antara SQLite dengan BigQuery dalam memproses data blockchain. Fungsi untuk mendekode log akan mengembalikan JSON yang disimpan dalam bentuk TEXT di SQLite, yang kemudian bisa diakses dengan JSON_EXTRACT()
. Di BigQuery, fungsi akan mengembalikan JSON yang kemudian bisa di petakan dalam kolom dengan tipe data STRUCT sehingga bisa dikueri secara langsung dan lebih mudah.
Begitulah, sql.js ❤️ Ethers.js! Sampai jumpa di akhir musim dingin.
Catatan tambahan
Saya sudah mencoba menggunakan pustaka sql.js-httpvfs yang memungkinkan kueri database di R2 tanpa perlu mengunduh keseluruhan database ke dalam browser, tapi gagal ketika membuat fungsi JS.