From 6b7a773845a7a73b37792a724277e885bd4e4f5a Mon Sep 17 00:00:00 2001 From: TZGyn Date: Sat, 16 May 2026 21:09:22 +0800 Subject: [PATCH] cache transaction and receipt data in sqlite --- .../migration.sql | 41 ++ .../snapshot.json | 467 ++++++++++++++++++ apps/desktop/src/bun/index.ts | 2 +- apps/desktop/src/bun/lib/db/schema.ts | 54 ++ .../bun/lib/viem/get-transaction-details.ts | 114 ++++- .../src/routes/tx/[chainid]/[hash].svelte | 1 - 6 files changed, 676 insertions(+), 3 deletions(-) create mode 100644 apps/desktop/drizzle/20260516080547_short_firebird/migration.sql create mode 100644 apps/desktop/drizzle/20260516080547_short_firebird/snapshot.json diff --git a/apps/desktop/drizzle/20260516080547_short_firebird/migration.sql b/apps/desktop/drizzle/20260516080547_short_firebird/migration.sql new file mode 100644 index 0000000..d55b752 --- /dev/null +++ b/apps/desktop/drizzle/20260516080547_short_firebird/migration.sql @@ -0,0 +1,41 @@ +CREATE TABLE `transaction_receipts` ( + `transactionHash` text NOT NULL, + `chainId` text NOT NULL, + `blockHash` text NOT NULL, + `blockNumber` text NOT NULL, + `from` text NOT NULL, + `to` text, + `cumulativeGasUsed` text NOT NULL, + `gasUsed` text NOT NULL, + `status` text NOT NULL, + `effectiveGasPrice` text, + `contractAddress` text, + `type` text NOT NULL, + `transactionIndex` integer NOT NULL, + `logsBloom` text +); +--> statement-breakpoint +CREATE TABLE `transactions` ( + `hash` text NOT NULL, + `chainId` text NOT NULL, + `from` text NOT NULL, + `to` text, + `value` text NOT NULL, + `blockNumber` text, + `blockHash` text, + `gas` text NOT NULL, + `gasPrice` text, + `maxFeePerGas` text, + `maxPriorityFeePerGas` text, + `input` text NOT NULL, + `nonce` integer NOT NULL, + `transactionIndex` integer, + `type` text, + `v` text, + `r` text, + `s` text, + `yParity` integer +); +--> statement-breakpoint +CREATE UNIQUE INDEX `receipt_unique` ON `transaction_receipts` (`chainId`,`transactionHash`);--> statement-breakpoint +CREATE UNIQUE INDEX `tx_unique` ON `transactions` (`chainId`,`hash`); \ No newline at end of file diff --git a/apps/desktop/drizzle/20260516080547_short_firebird/snapshot.json b/apps/desktop/drizzle/20260516080547_short_firebird/snapshot.json new file mode 100644 index 0000000..32b2cfc --- /dev/null +++ b/apps/desktop/drizzle/20260516080547_short_firebird/snapshot.json @@ -0,0 +1,467 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "3271a353-c528-4d23-a2ff-9be22f763b9d", + "prevIds": [ + "2845d968-33cd-48b6-8944-03e0e46a4a90" + ], + "ddl": [ + { + "name": "token_metadata", + "entityType": "tables" + }, + { + "name": "transaction_receipts", + "entityType": "tables" + }, + { + "name": "transactions", + "entityType": "tables" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "token_metadata" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "symbol", + "entityType": "columns", + "table": "token_metadata" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "decimals", + "entityType": "columns", + "table": "token_metadata" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "logo", + "entityType": "columns", + "table": "token_metadata" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "contractAddress", + "entityType": "columns", + "table": "token_metadata" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "chainId", + "entityType": "columns", + "table": "token_metadata" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "transactionHash", + "entityType": "columns", + "table": "transaction_receipts" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "chainId", + "entityType": "columns", + "table": "transaction_receipts" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "blockHash", + "entityType": "columns", + "table": "transaction_receipts" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "blockNumber", + "entityType": "columns", + "table": "transaction_receipts" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "from", + "entityType": "columns", + "table": "transaction_receipts" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "to", + "entityType": "columns", + "table": "transaction_receipts" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "cumulativeGasUsed", + "entityType": "columns", + "table": "transaction_receipts" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "gasUsed", + "entityType": "columns", + "table": "transaction_receipts" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "status", + "entityType": "columns", + "table": "transaction_receipts" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "effectiveGasPrice", + "entityType": "columns", + "table": "transaction_receipts" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "contractAddress", + "entityType": "columns", + "table": "transaction_receipts" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "transaction_receipts" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "transactionIndex", + "entityType": "columns", + "table": "transaction_receipts" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "logsBloom", + "entityType": "columns", + "table": "transaction_receipts" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "hash", + "entityType": "columns", + "table": "transactions" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "chainId", + "entityType": "columns", + "table": "transactions" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "from", + "entityType": "columns", + "table": "transactions" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "to", + "entityType": "columns", + "table": "transactions" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "value", + "entityType": "columns", + "table": "transactions" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "blockNumber", + "entityType": "columns", + "table": "transactions" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "blockHash", + "entityType": "columns", + "table": "transactions" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "gas", + "entityType": "columns", + "table": "transactions" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "gasPrice", + "entityType": "columns", + "table": "transactions" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "maxFeePerGas", + "entityType": "columns", + "table": "transactions" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "maxPriorityFeePerGas", + "entityType": "columns", + "table": "transactions" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "input", + "entityType": "columns", + "table": "transactions" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "nonce", + "entityType": "columns", + "table": "transactions" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "transactionIndex", + "entityType": "columns", + "table": "transactions" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "transactions" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "v", + "entityType": "columns", + "table": "transactions" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "r", + "entityType": "columns", + "table": "transactions" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "s", + "entityType": "columns", + "table": "transactions" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "yParity", + "entityType": "columns", + "table": "transactions" + }, + { + "columns": [ + { + "value": "chainId", + "isExpression": false + }, + { + "value": "contractAddress", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "contract_unique", + "entityType": "indexes", + "table": "token_metadata" + }, + { + "columns": [ + { + "value": "chainId", + "isExpression": false + }, + { + "value": "transactionHash", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "receipt_unique", + "entityType": "indexes", + "table": "transaction_receipts" + }, + { + "columns": [ + { + "value": "chainId", + "isExpression": false + }, + { + "value": "hash", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "tx_unique", + "entityType": "indexes", + "table": "transactions" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/apps/desktop/src/bun/index.ts b/apps/desktop/src/bun/index.ts index 2eff18f..66bae5d 100644 --- a/apps/desktop/src/bun/index.ts +++ b/apps/desktop/src/bun/index.ts @@ -434,7 +434,7 @@ const rpc = BrowserView.defineRPC({ if (!key) return null try { const client = getClient(chainid, key) - const details = await getTransactionDetails(client, hash) + const details = await getTransactionDetails(client, hash, chainid) return details } catch (error) { console.log(error) diff --git a/apps/desktop/src/bun/lib/db/schema.ts b/apps/desktop/src/bun/lib/db/schema.ts index c0e6b28..75c06cb 100644 --- a/apps/desktop/src/bun/lib/db/schema.ts +++ b/apps/desktop/src/bun/lib/db/schema.ts @@ -17,3 +17,57 @@ export const tokenMetadata = sqliteTable( ), }), ) + +export const transactions = sqliteTable( + 'transactions', + { + hash: text().notNull(), + chainId: text().notNull(), + from: text().notNull(), + to: text(), + value: text().notNull(), + blockNumber: text(), + blockHash: text(), + gas: text().notNull(), + gasPrice: text(), + maxFeePerGas: text(), + maxPriorityFeePerGas: text(), + input: text().notNull(), + nonce: int().notNull(), + transactionIndex: int(), + type: text(), + v: text(), + r: text(), + s: text(), + yParity: int(), + }, + (table) => ({ + txUnique: uniqueIndex('tx_unique').on(table.chainId, table.hash), + }), +) + +export const transactionReceipts = sqliteTable( + 'transaction_receipts', + { + transactionHash: text().notNull(), + chainId: text().notNull(), + blockHash: text().notNull(), + blockNumber: text().notNull(), + from: text().notNull(), + to: text(), + cumulativeGasUsed: text().notNull(), + gasUsed: text().notNull(), + status: text().notNull(), + effectiveGasPrice: text(), + contractAddress: text(), + type: text().notNull(), + transactionIndex: int().notNull(), + logsBloom: text(), + }, + (table) => ({ + receiptUnique: uniqueIndex('receipt_unique').on( + table.chainId, + table.transactionHash, + ), + }), +) diff --git a/apps/desktop/src/bun/lib/viem/get-transaction-details.ts b/apps/desktop/src/bun/lib/viem/get-transaction-details.ts index 17fe7f5..edc9dc6 100644 --- a/apps/desktop/src/bun/lib/viem/get-transaction-details.ts +++ b/apps/desktop/src/bun/lib/viem/get-transaction-details.ts @@ -1,10 +1,13 @@ +import { eq, and } from 'drizzle-orm' import { isHex, formatEther, type PublicClient, formatGwei, - decodeFunctionData, } from 'viem' +import { db } from '../db' +import { transactions, transactionReceipts } from '../db/schema' +import { getChainID } from '../alchemy/network' export type TransactionDetails = { hash: string @@ -23,14 +26,123 @@ export type TransactionDetails = { export const getTransactionDetails = async ( client: PublicClient, hash: string, + chainId: string, ): Promise => { if (!isHex(hash)) return null + const id = getChainID(chainId) + if (!id) return null + + const hashLower = hash.toLowerCase() + + const cachedTx = db + .select() + .from(transactions) + .where( + and( + eq(transactions.hash, hashLower), + eq(transactions.chainId, id), + ), + ) + .get() + + const cachedReceipt = db + .select() + .from(transactionReceipts) + .where( + and( + eq(transactionReceipts.transactionHash, hashLower), + eq(transactionReceipts.chainId, id), + ), + ) + .get() + + if (cachedTx && cachedReceipt) { + const value = BigInt(cachedTx.value) + const gasUsed = BigInt(cachedReceipt.gasUsed) + const gasPrice = cachedTx.gasPrice + ? BigInt(cachedTx.gasPrice) + : BigInt(0) + + return { + hash: cachedTx.hash, + from: cachedTx.from, + to: cachedTx.to, + value: formatEther(value), + blockNumber: cachedReceipt.blockNumber, + fee: ( + parseFloat(formatGwei(gasUsed)) * + parseFloat(formatGwei(gasPrice)) + ).toString(), + gasPrice: formatGwei(gasPrice), + status: cachedReceipt.status as 'success' | 'reverted', + type: cachedTx.type ?? 'legacy', + nonce: cachedTx.nonce, + input: cachedTx.input, + } + } + const [tx, receipt] = await Promise.all([ client.getTransaction({ hash }), client.getTransactionReceipt({ hash }), ]) + try { + await db + .insert(transactions) + .values({ + hash: hashLower, + chainId: id, + from: tx.from.toLowerCase(), + to: tx.to?.toLowerCase() ?? null, + value: tx.value.toString(), + blockNumber: tx.blockNumber?.toString() ?? null, + blockHash: tx.blockHash?.toLowerCase() ?? null, + gas: tx.gas.toString(), + gasPrice: tx.gasPrice?.toString() ?? null, + maxFeePerGas: tx.maxFeePerGas?.toString() ?? null, + maxPriorityFeePerGas: + tx.maxPriorityFeePerGas?.toString() ?? null, + input: tx.input, + nonce: tx.nonce, + transactionIndex: tx.transactionIndex ?? null, + type: tx.type ?? null, + v: tx.v?.toString() ?? null, + r: tx.r?.toLowerCase() ?? null, + s: tx.s?.toLowerCase() ?? null, + yParity: tx.yParity ?? null, + }) + .onConflictDoNothing() + } catch (e) { + console.log('Failed to cache transaction', e) + } + + try { + await db + .insert(transactionReceipts) + .values({ + transactionHash: hashLower, + chainId: id, + blockHash: receipt.blockHash.toLowerCase(), + blockNumber: receipt.blockNumber.toString(), + from: receipt.from.toLowerCase(), + to: receipt.to?.toLowerCase() ?? null, + cumulativeGasUsed: receipt.cumulativeGasUsed.toString(), + gasUsed: receipt.gasUsed.toString(), + status: receipt.status, + effectiveGasPrice: + receipt.effectiveGasPrice?.toString() ?? null, + contractAddress: + receipt.contractAddress?.toLowerCase() ?? null, + type: receipt.type, + transactionIndex: receipt.transactionIndex, + logsBloom: receipt.logsBloom?.toLowerCase() ?? null, + }) + .onConflictDoNothing() + } catch (e) { + console.log('Failed to cache transaction receipt', e) + } + return { hash: tx.hash, from: tx.from, diff --git a/apps/desktop/src/routes/tx/[chainid]/[hash].svelte b/apps/desktop/src/routes/tx/[chainid]/[hash].svelte index c829d73..a122227 100644 --- a/apps/desktop/src/routes/tx/[chainid]/[hash].svelte +++ b/apps/desktop/src/routes/tx/[chainid]/[hash].svelte @@ -132,7 +132,6 @@

{details.type}

-