split data access into API-only (alchemy/viem) and DB-first wrappers (tokens/transactions)

alchemy/get-token-metadata: stripped DB logic, now pure API call
viem/get-transaction-details: stripped DB logic, now pure viem call
tokens/get-metadata: new DB-first wrapper for token metadata
transactions/get-details: new DB-first wrapper for tx details
transactions/get-history: stores individual Alchemy transfers in tx_history
  table with blockNum, from, to columns for searchable ordering
This commit is contained in:
2026-05-18 03:03:34 +08:00
parent 135d651622
commit 9e76bd54ce
10 changed files with 466 additions and 235 deletions
@@ -1,162 +1,50 @@
import { eq, and } from 'drizzle-orm'
import {
isHex,
formatEther,
type PublicClient,
formatGwei,
} from 'viem'
import { db } from '../db'
import { transactions, transactionReceipts } from '../db/schema'
import { getChainID } from '../alchemy/network'
export type TransactionDetails = {
hash: string
from: string
to: string | null
value: string
blockNumber: string | null
fee: string
gasPrice: string
status: 'success' | 'reverted'
type: string
nonce: number
input: string
}
import { isHex, type PublicClient } from 'viem'
export const getTransactionDetails = async (
client: PublicClient,
hash: string,
chainId: string,
): Promise<TransactionDetails | null> => {
) => {
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,
to: tx.to,
value: formatEther(tx.value),
blockNumber: receipt.blockNumber?.toString() ?? null,
fee: (
parseFloat(formatGwei(receipt.gasUsed)) *
parseFloat(formatGwei(tx.gasPrice || BigInt(0)))
).toString(),
gasPrice: formatGwei(tx.gasPrice || BigInt(0)),
status: receipt.status === 'success' ? 'success' : 'reverted',
type: tx.type,
nonce: tx.nonce,
input: tx.input,
tx: {
hash: tx.hash,
from: tx.from,
to: tx.to ?? null,
value: tx.value,
blockNumber: tx.blockNumber ?? null,
gasPrice: tx.gasPrice ?? null,
gas: tx.gas,
input: tx.input,
nonce: tx.nonce,
type: tx.type ?? 'legacy',
blockHash: tx.blockHash ?? null,
maxFeePerGas: tx.maxFeePerGas ?? null,
maxPriorityFeePerGas: tx.maxPriorityFeePerGas ?? null,
transactionIndex: tx.transactionIndex ?? null,
v: tx.v ?? null,
r: tx.r?.toString() ?? null,
s: tx.s?.toString() ?? null,
yParity: tx.yParity ?? null,
},
receipt: {
blockHash: receipt.blockHash,
blockNumber: receipt.blockNumber,
from: receipt.from,
to: receipt.to ?? null,
cumulativeGasUsed: receipt.cumulativeGasUsed,
gasUsed: receipt.gasUsed,
status: receipt.status,
effectiveGasPrice: receipt.effectiveGasPrice ?? null,
contractAddress: receipt.contractAddress ?? null,
type: receipt.type,
transactionIndex: receipt.transactionIndex,
logsBloom: receipt.logsBloom?.toString() ?? null,
},
}
}