move transaction history to alchemy lib

This commit is contained in:
2026-05-14 01:50:53 +08:00
parent f47491ad86
commit 90a3086d52
7 changed files with 170 additions and 86 deletions
+16 -46
View File
@@ -10,6 +10,7 @@ import {
import { inspect } from 'util' import { inspect } from 'util'
import { getTokenMetadata, getTokensBalances } from './lib/alchemy' import { getTokenMetadata, getTokensBalances } from './lib/alchemy'
import { tryCatch } from '@koins/utils' import { tryCatch } from '@koins/utils'
import { getTransactionHistory } from './lib/alchemy/get-transaction-history'
const DEV_SERVER_PORT = 5173 const DEV_SERVER_PORT = 5173
const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}` const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`
@@ -94,7 +95,7 @@ type RPC = {
service: string service: string
name: string name: string
} }
response: Promise<string | null> response: string | null
} }
setSecret: { setSecret: {
params: { params: {
@@ -102,27 +103,27 @@ type RPC = {
name: string name: string
value: string value: string
} }
response: Promise<void> response: void
} }
fetchTxHistory: { fetchTxHistory: {
params: { params: {
address: string address: string
chainid: string chainid: string
} }
response: Promise<TxEntry[]> response: TxEntry[]
} }
fetchTokenBalances: { fetchTokenBalances: {
params: { params: {
address: string address: string
chainid: string chainid: string
} }
response: Promise<TokenBalanceResult[]> response: TokenBalanceResult[]
} }
openExternal: { openExternal: {
params: { params: {
url: string url: string
} }
response: Promise<void> response: void
} }
} }
messages: {} messages: {}
@@ -138,47 +139,16 @@ async function fetchAlchemyTransfers(
chainid: string, chainid: string,
address: string, address: string,
): Promise<any[]> { ): Promise<any[]> {
const network = ALCHEMY_NETWORKS[chainid] const transactions = await getTransactionHistory(
if (!network) return [] key,
const url = `https://${network}.g.alchemy.com/v2/${key}` chainid,
address,
{ maxCount: 10 },
)
const body = (fromAddress?: string, toAddress?: string) => ({ if (!transactions) return []
jsonrpc: '2.0',
id: 0,
method: 'alchemy_getAssetTransfers',
params: [
{
...(fromAddress ? { fromAddress } : {}),
...(toAddress ? { toAddress } : {}),
category: ['external', 'erc20'],
withMetadata: true,
maxCount: '0x3E8',
},
],
})
const [outRes, inRes] = await Promise.all([ return transactions
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body(address, undefined)),
}).then((r) => r.json()),
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body(undefined, address)),
}).then((r) => r.json()),
])
const outgoing: any[] = outRes.result?.transfers ?? []
const incoming: any[] = inRes.result?.transfers ?? []
const seen = new Set<string>()
return [...outgoing, ...incoming].filter((t) => {
if (seen.has(t.hash)) return false
seen.add(t.hash)
return true
})
} }
function formatUnits(balance: string, decimals: number): string { function formatUnits(balance: string, decimals: number): string {
@@ -261,7 +231,7 @@ function mapTransfer(t: any): TxEntry {
} }
const rpc = BrowserView.defineRPC<RPC>({ const rpc = BrowserView.defineRPC<RPC>({
maxRequestTime: 200000, maxRequestTime: 10000,
handlers: { handlers: {
requests: { requests: {
getSecret: async ({ name, service }) => { getSecret: async ({ name, service }) => {
@@ -389,4 +359,4 @@ const win = new BrowserWindow({
}, },
}) })
console.log('Svelte app started!') console.log('Koins app started!')
+3 -2
View File
@@ -1,5 +1,7 @@
import { tryCatch } from '@koins/utils' import { tryCatch } from '@koins/utils'
const TIMEOUT = 15_000
export const post = async <T>( export const post = async <T>(
url: string, url: string,
body: any, body: any,
@@ -9,6 +11,7 @@ export const post = async <T>(
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body), body: JSON.stringify(body),
signal: AbortSignal.timeout(TIMEOUT),
}), }),
) )
@@ -17,8 +20,6 @@ export const post = async <T>(
throw error throw error
} }
console.log('data', await response.text())
const [data, dataError] = await tryCatch(response.json()) const [data, dataError] = await tryCatch(response.json())
if (dataError) { if (dataError) {
@@ -28,7 +28,7 @@ export const getTokensBalances = async (
params: [address, 'erc20'], params: [address, 'erc20'],
}) })
console.log(inspect(balRes, { depth: null, colors: true })) // console.log(inspect(balRes, { depth: null, colors: true }))
return balRes return balRes
} }
@@ -0,0 +1,59 @@
import { post } from './call'
import { getChainID, getURL } from './network'
import { inspect } from 'util'
import type { Transaction } from './types'
import { numberToHex } from 'viem'
export const getTransactionHistory = async (
key: string,
chainid: string,
address: string,
{ maxCount = 20 }: { maxCount: number },
) => {
const chainId = getChainID(chainid)
if (!chainId) return
const url = getURL({
chainId,
key,
})
const body = (fromAddress?: string, toAddress?: string) => ({
jsonrpc: '2.0',
id: 0,
method: 'alchemy_getAssetTransfers',
params: [
{
...(fromAddress ? { fromAddress } : {}),
...(toAddress ? { toAddress } : {}),
category: ['external', 'internal', 'erc20'],
withMetadata: true,
maxCount: numberToHex(maxCount),
},
],
})
const outRequest = post<{
transfers: Transaction[]
}>(url, body(address, undefined))
const inRequest = post<{
transfers: Transaction[]
}>(url, body(address, undefined))
const outRes = await outRequest
const inRes = await inRequest
console.log(inspect(outRes, { colors: true, depth: null }))
const outgoing = outRes.result?.transfers ?? []
const incoming = inRes.result?.transfers ?? []
const seen = new Set<string>()
return [...outgoing, ...incoming].filter((t) => {
if (!t.hash) return false
if (seen.has(t.hash)) return false
seen.add(t.hash)
return true
})
}
+32
View File
@@ -1,3 +1,35 @@
export type Token = {} export type Token = {}
export type TokenBalance = {} export type TokenBalance = {}
export type Transaction = {
category?:
| 'external'
| 'internal'
| 'token'
| 'erc20'
| 'erc721'
| 'erc1155'
| 'specialnft'
blockNum?: string
from?: string
to?: string
value?: number
erc721TokenId?: string
erc1155Metadata?: {
tokenId: string
value: string
}[]
tokenId?: string
asset?: string
uniqueId?: string
hash?: string
rawContract?: {
value?: string
address?: string
decimal?: string
}
metadata?: {
blockTimestamp?: string
}
}
+6 -5
View File
@@ -29,7 +29,7 @@ type RPC = {
service: string service: string
name: string name: string
} }
response: Promise<string | null> response: string | null
} }
setSecret: { setSecret: {
params: { params: {
@@ -37,27 +37,27 @@ type RPC = {
name: string name: string
value: string value: string
} }
response: Promise<void> response: void
} }
fetchTxHistory: { fetchTxHistory: {
params: { params: {
address: string address: string
chainid: string chainid: string
} }
response: Promise<TxEntry[]> response: TxEntry[]
} }
fetchTokenBalances: { fetchTokenBalances: {
params: { params: {
address: string address: string
chainid: string chainid: string
} }
response: Promise<TokenBalanceResult[]> response: TokenBalanceResult[]
} }
openExternal: { openExternal: {
params: { params: {
url: string url: string
} }
response: Promise<void> response: void
} }
} }
messages: {} messages: {}
@@ -69,6 +69,7 @@ type RPC = {
} }
const rpc = Electroview.defineRPC<RPC>({ const rpc = Electroview.defineRPC<RPC>({
maxRequestTime: 10000,
handlers: { handlers: {
requests: {}, requests: {},
messages: {}, messages: {},
+41 -20
View File
@@ -1,9 +1,6 @@
import { import { JsonRpcProvider, HDNodeWallet, formatEther } from 'ethers'
JsonRpcProvider,
HDNodeWallet,
formatEther,
} from 'ethers'
import { electrobun, type TxEntry } from '$lib/electrobun' import { electrobun, type TxEntry } from '$lib/electrobun'
import { tryCatch } from '@koins/utils'
export const networks = [ export const networks = [
{ {
@@ -57,21 +54,36 @@ export const Wallet = () => {
const net = () => networks.find((n) => n.id === network)! const net = () => networks.find((n) => n.id === network)!
const init = async () => { const init = async () => {
try { if (!electrobun.rpc) return
const raw = await electrobun.rpc?.request.getSecret({ const [raw, rawError] = await tryCatch(
electrobun.rpc.request.getSecret({
service: 'koins', service: 'koins',
name: 'vault', name: 'vault',
}) }),
)
if (rawError) {
console.log('Raw Error:', rawError)
}
vaultExists = raw !== null && raw !== undefined vaultExists = raw !== null && raw !== undefined
const key = await electrobun.rpc?.request.getSecret({ const [key, keyError] = await tryCatch(
electrobun.rpc?.request.getSecret({
service: 'koins', service: 'koins',
name: 'alchemy_key', name: 'alchemy_key',
}) }),
)
if (keyError) {
console.log('key error', keyError)
}
if (key) apiKey = key if (key) apiKey = key
if (raw) { if (raw) {
seed = raw seed = raw
await refresh() await refresh()
} }
try {
} catch (e) { } catch (e) {
console.log(e) console.log(e)
error = error =
@@ -116,31 +128,40 @@ export const Wallet = () => {
} }
const refresh = async () => { const refresh = async () => {
if (!seed) return if (!seed || !electrobun.rpc) return
loading = true loading = true
error = '' error = ''
tokenBalances = [] tokenBalances = []
try {
const wallet = HDNodeWallet.fromPhrase(seed) const wallet = HDNodeWallet.fromPhrase(seed)
address = wallet.address address = wallet.address
const provider = new JsonRpcProvider(net().rpc) const provider = new JsonRpcProvider(net().rpc)
const nativeBal = await provider.getBalance(wallet.address) const [nativeBal, nativeBalError] = await tryCatch(
provider.getBalance(wallet.address),
)
if (nativeBalError) {
loading = false
console.log('Native Balance Error', nativeBalError)
return
}
balance = formatEther(nativeBal) balance = formatEther(nativeBal)
const bals = await electrobun.rpc?.request.fetchTokenBalances({ const [bals, balsError] = await tryCatch(
electrobun.rpc.request.fetchTokenBalances({
address: wallet.address, address: wallet.address,
chainid: net().chainid, chainid: net().chainid,
}) }),
)
if (balsError) {
console.log('Bals Error', balsError)
error = balsError.message
}
tokenBalances = bals ?? [] tokenBalances = bals ?? []
fetchTxHistory() fetchTxHistory()
} catch (e) {
error =
e instanceof Error ? e.message : 'Failed to fetch balance'
} finally {
loading = false loading = false
} }
}
const saveApiKey = async (key: string) => { const saveApiKey = async (key: string) => {
await electrobun.rpc?.request.setSecret({ await electrobun.rpc?.request.setSecret({