move tx detail caching into sync engine, cache during history sync

- new sync-tx-details.ts: extracts tx+receipt caching from get-details.ts
- sync-tx-history: calls syncTxDetails per transfer hash during sync
- get-details: remove on-the-fly viem fetch + DB write (now handled
  by sync); returns null placeholder
- index: import syncTxDetails; simplify getTransactionDetails handler
This commit is contained in:
2026-05-22 10:18:33 +08:00
parent dd57a72e11
commit 357f707180
5 changed files with 134 additions and 93 deletions
+2 -6
View File
@@ -39,6 +39,7 @@ import {
txHistory, txHistory,
evmWallets, evmWallets,
} from './lib/db/schema' } from './lib/db/schema'
import { syncTxDetails } from './lib/sync/sync-tx-details'
const env = getENV() const env = getENV()
@@ -647,12 +648,7 @@ const rpc = BrowserView.defineRPC<RPC>({
}) })
if (!key) return null if (!key) return null
try { try {
const client = getClient(chainid, key) const details = await getTransactionDetails(hash, chainid)
const details = await getTransactionDetails(
client,
hash,
chainid,
)
return details return details
} catch (error) { } catch (error) {
console.log(error) console.log(error)
@@ -0,0 +1,76 @@
import { getClient, getTransactionDetails } from '../viem'
import { db } from '../db'
import { transactions, transactionReceipts } from '../db/schema'
import { getChainID } from '../alchemy/network'
export const syncTxDetails = async (
chainId: string,
apiKey: string,
hash: string,
) => {
const client = getClient(chainId, apiKey)
const id = getChainID(chainId)
if (!id) return null
const raw = await getTransactionDetails(client, hash)
if (!raw) return null
const hashLower = hash.toLowerCase()
try {
await db
.insert(transactions)
.values({
hash: hashLower,
chainId: id,
from: raw.tx.from.toLowerCase(),
to: raw.tx.to?.toLowerCase() ?? null,
value: raw.tx.value.toString(),
blockNumber: raw.tx.blockNumber?.toString() ?? null,
blockHash: raw.tx.blockHash?.toLowerCase() ?? null,
gas: raw.tx.gas.toString(),
gasPrice: raw.tx.gasPrice?.toString() ?? null,
maxFeePerGas: raw.tx.maxFeePerGas?.toString() ?? null,
maxPriorityFeePerGas:
raw.tx.maxPriorityFeePerGas?.toString() ?? null,
input: raw.tx.input,
nonce: raw.tx.nonce,
transactionIndex: raw.tx.transactionIndex ?? null,
type: raw.tx.type ?? null,
v: raw.tx.v?.toString() ?? null,
r: raw.tx.r?.toLowerCase() ?? null,
s: raw.tx.s?.toLowerCase() ?? null,
yParity: raw.tx.yParity ?? null,
})
.onConflictDoNothing()
} catch (e) {
console.log('Failed to cache transaction', e)
}
try {
await db
.insert(transactionReceipts)
.values({
transactionHash: hashLower,
chainId: id,
blockHash: raw.receipt.blockHash.toLowerCase(),
blockNumber: raw.receipt.blockNumber.toString(),
from: raw.receipt.from.toLowerCase(),
to: raw.receipt.to?.toLowerCase() ?? null,
cumulativeGasUsed: raw.receipt.cumulativeGasUsed.toString(),
gasUsed: raw.receipt.gasUsed.toString(),
status: raw.receipt.status,
effectiveGasPrice:
raw.receipt.effectiveGasPrice?.toString() ?? null,
contractAddress:
raw.receipt.contractAddress?.toLowerCase() ?? null,
type: raw.receipt.type,
transactionIndex: raw.receipt.transactionIndex,
logsBloom: raw.receipt.logsBloom?.toLowerCase() ?? null,
})
.onConflictDoNothing()
} catch (e) {
console.log('Failed to cache transaction receipt', e)
}
}
@@ -3,6 +3,7 @@ import { numberToHex } from 'viem'
import { db } from '../db' import { db } from '../db'
import { txHistory, txSyncStatus } from '../db/schema' import { txHistory, txSyncStatus } from '../db/schema'
import { getTransactionHistory } from '../alchemy/get-transaction-history' import { getTransactionHistory } from '../alchemy/get-transaction-history'
import { syncTxDetails } from './sync-tx-details'
const PAGE_SIZE = 76 const PAGE_SIZE = 76
@@ -28,10 +29,15 @@ export const syncTransactionHistory = async (
? numberToHex(Number(existing.lastSyncBlock) + 1) ? numberToHex(Number(existing.lastSyncBlock) + 1)
: undefined : undefined
const transfers = await getTransactionHistory(key, chainid, address, { const transfers = await getTransactionHistory(
maxCount: 1000, key,
fromBlock, chainid,
}) address,
{
maxCount: 1000,
fromBlock,
},
)
if (!transfers || transfers.length === 0) return if (!transfers || transfers.length === 0) return
@@ -54,12 +60,15 @@ export const syncTransactionHistory = async (
value: t.value?.toString() ?? null, value: t.value?.toString() ?? null,
asset: t.asset ?? null, asset: t.asset ?? null,
category: t.category ?? null, category: t.category ?? null,
rawContractAddress: t.rawContract?.address?.toLowerCase() ?? null, rawContractAddress:
t.rawContract?.address?.toLowerCase() ?? null,
rawContractDecimal: t.rawContract?.decimal ?? null, rawContractDecimal: t.rawContract?.decimal ?? null,
metadataBlockTimestamp: t.metadata?.blockTimestamp ?? null, metadataBlockTimestamp: t.metadata?.blockTimestamp ?? null,
raw: JSON.stringify(t), raw: JSON.stringify(t),
syncedAt: new Date().toISOString(), syncedAt: new Date().toISOString(),
}) })
await syncTxDetails(chainid, key, t.hash)
} }
flush(prepared) flush(prepared)
@@ -87,10 +96,42 @@ export const syncTransactionHistory = async (
function flush(prepared: Array<typeof txHistory.$inferInsert>) { function flush(prepared: Array<typeof txHistory.$inferInsert>) {
while (prepared.length > 0) { while (prepared.length > 0) {
const batch = prepared.splice(0, PAGE_SIZE) const batch = prepared.splice(0, PAGE_SIZE)
const cols = ['chainId', 'hash', 'blockNum', '"from"', '"to"', 'value', 'asset', 'category', 'rawContractAddress', 'rawContractDecimal', 'metadataBlockTimestamp', 'raw', 'syncedAt'] const cols = [
const colKeys = ['chainId', 'hash', 'blockNum', 'from', 'to', 'value', 'asset', 'category', 'rawContractAddress', 'rawContractDecimal', 'metadataBlockTimestamp', 'raw', 'syncedAt'] 'chainId',
const placeholders = batch.map(() => `(${colKeys.map(() => '?').join(',')})`).join(',') 'hash',
const flat = batch.flatMap((r) => colKeys.map((c) => (r as any)[c] ?? null)) 'blockNum',
'"from"',
'"to"',
'value',
'asset',
'category',
'rawContractAddress',
'rawContractDecimal',
'metadataBlockTimestamp',
'raw',
'syncedAt',
]
const colKeys = [
'chainId',
'hash',
'blockNum',
'from',
'to',
'value',
'asset',
'category',
'rawContractAddress',
'rawContractDecimal',
'metadataBlockTimestamp',
'raw',
'syncedAt',
]
const placeholders = batch
.map(() => `(${colKeys.map(() => '?').join(',')})`)
.join(',')
const flat = batch.flatMap((r) =>
colKeys.map((c) => (r as any)[c] ?? null),
)
const sql = `INSERT OR IGNORE INTO tx_history (${cols.join(',')}) VALUES ${placeholders}` const sql = `INSERT OR IGNORE INTO tx_history (${cols.join(',')}) VALUES ${placeholders}`
const dbClient = db.$client const dbClient = db.$client
@@ -3,10 +3,8 @@ import { formatEther, formatGwei, type PublicClient } from 'viem'
import { db } from '../db' import { db } from '../db'
import { transactions, transactionReceipts } from '../db/schema' import { transactions, transactionReceipts } from '../db/schema'
import { getChainID } from '../alchemy/network' import { getChainID } from '../alchemy/network'
import { getTransactionDetails as fetchFromViem } from '../viem/get-transaction-details'
export const getTransactionDetails = async ( export const getTransactionDetails = async (
client: PublicClient,
hash: string, hash: string,
chainId: string, chainId: string,
) => { ) => {
@@ -64,79 +62,5 @@ export const getTransactionDetails = async (
} }
} }
const raw = await fetchFromViem(client, hash) return null
if (!raw) return null
try {
await db
.insert(transactions)
.values({
hash: hashLower,
chainId: id,
from: raw.tx.from.toLowerCase(),
to: raw.tx.to?.toLowerCase() ?? null,
value: raw.tx.value.toString(),
blockNumber: raw.tx.blockNumber?.toString() ?? null,
blockHash: raw.tx.blockHash?.toLowerCase() ?? null,
gas: raw.tx.gas.toString(),
gasPrice: raw.tx.gasPrice?.toString() ?? null,
maxFeePerGas: raw.tx.maxFeePerGas?.toString() ?? null,
maxPriorityFeePerGas:
raw.tx.maxPriorityFeePerGas?.toString() ?? null,
input: raw.tx.input,
nonce: raw.tx.nonce,
transactionIndex: raw.tx.transactionIndex ?? null,
type: raw.tx.type ?? null,
v: raw.tx.v?.toString() ?? null,
r: raw.tx.r?.toLowerCase() ?? null,
s: raw.tx.s?.toLowerCase() ?? null,
yParity: raw.tx.yParity ?? null,
})
.onConflictDoNothing()
} catch (e) {
console.log('Failed to cache transaction', e)
}
try {
await db
.insert(transactionReceipts)
.values({
transactionHash: hashLower,
chainId: id,
blockHash: raw.receipt.blockHash.toLowerCase(),
blockNumber: raw.receipt.blockNumber.toString(),
from: raw.receipt.from.toLowerCase(),
to: raw.receipt.to?.toLowerCase() ?? null,
cumulativeGasUsed: raw.receipt.cumulativeGasUsed.toString(),
gasUsed: raw.receipt.gasUsed.toString(),
status: raw.receipt.status,
effectiveGasPrice:
raw.receipt.effectiveGasPrice?.toString() ?? null,
contractAddress:
raw.receipt.contractAddress?.toLowerCase() ?? null,
type: raw.receipt.type,
transactionIndex: raw.receipt.transactionIndex,
logsBloom: raw.receipt.logsBloom?.toLowerCase() ?? null,
})
.onConflictDoNothing()
} catch (e) {
console.log('Failed to cache transaction receipt', e)
}
return {
hash: raw.tx.hash,
from: raw.tx.from,
to: raw.tx.to,
value: formatEther(raw.tx.value),
blockNumber: raw.receipt.blockNumber?.toString() ?? null,
fee: (
parseFloat(formatGwei(raw.receipt.gasUsed)) *
parseFloat(formatGwei(raw.tx.gasPrice || BigInt(0)))
).toString(),
gasPrice: formatGwei(raw.tx.gasPrice || BigInt(0)),
status: raw.receipt.status === 'success' ? 'success' : 'reverted',
type: raw.tx.type,
nonce: raw.tx.nonce,
input: raw.tx.input,
}
} }
+5 -1
View File
@@ -236,7 +236,11 @@ type RPC = {
response: string[] response: string[]
} }
evmCreateWallet: { evmCreateWallet: {
params: { name: string; phrase: string; passwordHash?: string } params: {
name: string
phrase: string
passwordHash?: string
}
response: { id: string; name: string; createdAt: string } response: { id: string; name: string; createdAt: string }
} }
evmListWallets: { evmListWallets: {