Refactor: split RPC handlers into rpc/evm and rpc/monero; split RPC types into GeneralRpcRequests, EvmRpcRequests, MoneroRpcRequests
This commit is contained in:
+19
-748
@@ -6,45 +6,17 @@ import {
|
|||||||
Updater,
|
Updater,
|
||||||
Utils,
|
Utils,
|
||||||
} from 'electrobun/bun'
|
} from 'electrobun/bun'
|
||||||
import { getTokenMetadata } from './lib/tokens'
|
|
||||||
import { getTokensBalances, type Transaction } from './lib/alchemy'
|
|
||||||
import { tryCatch } from '@koins/utils'
|
|
||||||
import {
|
|
||||||
getTransactionHistory,
|
|
||||||
getTransactionDetails,
|
|
||||||
getCachedTransactionHistory,
|
|
||||||
} from './lib/transactions'
|
|
||||||
import { syncTransactionHistory } from './lib/sync'
|
|
||||||
import { migrate } from 'drizzle-orm/bun-sqlite/migrator'
|
import { migrate } from 'drizzle-orm/bun-sqlite/migrator'
|
||||||
import { eq, and } from 'drizzle-orm'
|
|
||||||
import { db } from './lib/db'
|
import { db } from './lib/db'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { getENV } from './lib/get-env'
|
import { getENV } from './lib/get-env'
|
||||||
import { getClient } from './lib/viem'
|
import { createEvmHandlers } from './rpc/evm'
|
||||||
import { formatGwei } from 'viem'
|
import { createMoneroHandlers } from './rpc/monero'
|
||||||
import {
|
import { MoneroWalletManager } from './lib/monero'
|
||||||
isBinaryInstalled,
|
|
||||||
downloadBinary,
|
|
||||||
getBinaryPath,
|
|
||||||
getBinaryDir,
|
|
||||||
MoneroWalletManager,
|
|
||||||
} from './lib/monero'
|
|
||||||
import { canPromptTouchID, promptTouchID } from './lib/biometric'
|
import { canPromptTouchID, promptTouchID } from './lib/biometric'
|
||||||
import { sqlite } from './lib/sqlite'
|
import { sqlite } from './lib/sqlite'
|
||||||
import {
|
import { evmWallets } from './lib/db/schema'
|
||||||
tokenMetadata,
|
import type { RPC } from '../lib/rpc-schema'
|
||||||
transactions,
|
|
||||||
transactionReceipts,
|
|
||||||
txHistory,
|
|
||||||
evmWallets,
|
|
||||||
} from './lib/db/schema'
|
|
||||||
import { syncTxDetails } from './lib/sync/sync-tx-details'
|
|
||||||
import type {
|
|
||||||
RPC,
|
|
||||||
TokenBalanceResult,
|
|
||||||
TokenPriceEntry,
|
|
||||||
TxEntry,
|
|
||||||
} from '../lib/rpc-schema'
|
|
||||||
|
|
||||||
const env = getENV()
|
const env = getENV()
|
||||||
|
|
||||||
@@ -104,235 +76,24 @@ ApplicationMenu.setApplicationMenu([
|
|||||||
|
|
||||||
const url = await getMainViewUrl()
|
const url = await getMainViewUrl()
|
||||||
|
|
||||||
const ALCHEMY_NETWORKS: Record<string, string> = {
|
const moneroState = {
|
||||||
'1': 'eth-mainnet',
|
manager: null as MoneroWalletManager | null,
|
||||||
'137': 'polygon-mainnet',
|
downloading: false,
|
||||||
'56': 'bnb-mainnet',
|
|
||||||
}
|
}
|
||||||
|
const syncState = {
|
||||||
const NATIVE_SYMBOLS: Record<string, string> = {
|
target: null as { address: string; chainid: string } | null,
|
||||||
'1': 'ETH',
|
timer: null as ReturnType<typeof setInterval> | null,
|
||||||
'137': 'POL',
|
|
||||||
'56': 'BNB',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchAlchemyTransfers(
|
|
||||||
key: string,
|
|
||||||
chainid: string,
|
|
||||||
address: string,
|
|
||||||
) {
|
|
||||||
const transactions = await getTransactionHistory(
|
|
||||||
key,
|
|
||||||
chainid,
|
|
||||||
address,
|
|
||||||
{ maxCount: 10 },
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!transactions) return []
|
|
||||||
|
|
||||||
return transactions
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatUnits(balance: string, decimals: number): string {
|
|
||||||
const bal = BigInt(balance)
|
|
||||||
if (bal === 0n) return '0'
|
|
||||||
const divisor = 10n ** BigInt(decimals)
|
|
||||||
const intPart = bal / divisor
|
|
||||||
const fracPart = (bal % divisor)
|
|
||||||
.toString()
|
|
||||||
.padStart(decimals, '0')
|
|
||||||
.replace(/0+$/, '')
|
|
||||||
return fracPart ? `${intPart}.${fracPart}` : `${intPart}`
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchAlchemyTokenBalances(
|
|
||||||
key: string,
|
|
||||||
chainid: string,
|
|
||||||
address: string,
|
|
||||||
): Promise<TokenBalanceResult[]> {
|
|
||||||
const [balRes, balResError] = await tryCatch(
|
|
||||||
getTokensBalances(key, chainid, address),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (balResError) {
|
|
||||||
console.log('Get Tokens Balance Error', balResError)
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!balRes) return []
|
|
||||||
|
|
||||||
const tokens = balRes.result?.tokenBalances ?? []
|
|
||||||
const nonZero = tokens.filter((t) => t.tokenBalance !== '0x0')
|
|
||||||
|
|
||||||
const metas = await Promise.allSettled(
|
|
||||||
nonZero.map((t) =>
|
|
||||||
getTokenMetadata(key, chainid, t.contractAddress),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if (!metas) return []
|
|
||||||
|
|
||||||
return nonZero
|
|
||||||
.map((t: any, i: number) => {
|
|
||||||
const meta =
|
|
||||||
metas[i].status === 'fulfilled' && metas[i].value
|
|
||||||
? metas[i].value.result
|
|
||||||
: null
|
|
||||||
return {
|
|
||||||
symbol: meta?.symbol ?? 'Unknown',
|
|
||||||
decimals: meta?.decimals ?? 0,
|
|
||||||
balance: formatUnits(t.tokenBalance, meta?.decimals ?? 18),
|
|
||||||
contractAddress: t.contractAddress,
|
|
||||||
logo: meta?.logo ?? undefined,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter((t) => t.logo && t.decimals !== 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapTransfer(t: any): TxEntry {
|
|
||||||
const isExternal = t.category === 'external'
|
|
||||||
return {
|
|
||||||
hash: t.hash,
|
|
||||||
timeStamp: String(
|
|
||||||
Math.floor(
|
|
||||||
new Date(t.metadata?.blockTimestamp).getTime() / 1000,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
from: t.from,
|
|
||||||
to: t.to,
|
|
||||||
value: String(t.value),
|
|
||||||
...(isExternal
|
|
||||||
? {}
|
|
||||||
: {
|
|
||||||
tokenSymbol: t.asset,
|
|
||||||
tokenDecimal: t.rawContract?.decimal
|
|
||||||
? String(parseInt(t.rawContract.decimal, 16))
|
|
||||||
: undefined,
|
|
||||||
contractAddress: t.rawContract?.address ?? undefined,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pairTransfers(
|
|
||||||
transfers: any[],
|
|
||||||
address: string,
|
|
||||||
key?: string,
|
|
||||||
chainid?: string,
|
|
||||||
): Promise<TxEntry[]> {
|
|
||||||
const addrLower = address.toLowerCase()
|
|
||||||
const byHash = new Map<string, any[]>()
|
|
||||||
for (const t of transfers) {
|
|
||||||
if (!t.hash) continue
|
|
||||||
const h = t.hash
|
|
||||||
if (!byHash.has(h)) byHash.set(h, [])
|
|
||||||
byHash.get(h)!.push(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
const combined: TxEntry[] = []
|
|
||||||
|
|
||||||
for (const [, group] of byHash) {
|
|
||||||
const native = group.filter(
|
|
||||||
(t: any) =>
|
|
||||||
t.category === 'external' || t.category === 'internal',
|
|
||||||
)
|
|
||||||
const tokens = group.filter((t: any) => t.category === 'erc20')
|
|
||||||
|
|
||||||
const extOut = native.filter(
|
|
||||||
(t: any) => t.from?.toLowerCase() === addrLower,
|
|
||||||
)
|
|
||||||
const extIn = native.filter(
|
|
||||||
(t: any) => t.to?.toLowerCase() === addrLower,
|
|
||||||
)
|
|
||||||
const tokIn = tokens.filter(
|
|
||||||
(t: any) => t.to?.toLowerCase() === addrLower,
|
|
||||||
)
|
|
||||||
const tokOut = tokens.filter(
|
|
||||||
(t: any) => t.from?.toLowerCase() === addrLower,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (extOut.length > 0 && tokIn.length > 0) {
|
|
||||||
combined.push({
|
|
||||||
...mapTransfer(extOut[0]),
|
|
||||||
tokenSymbol: tokIn[0].asset,
|
|
||||||
tokenDecimal: tokIn[0].rawContract?.decimal
|
|
||||||
? String(parseInt(tokIn[0].rawContract.decimal, 16))
|
|
||||||
: undefined,
|
|
||||||
contractAddress: tokIn[0].rawContract?.address ?? undefined,
|
|
||||||
pairedValue: String(tokIn[0].value),
|
|
||||||
pairedSymbol: tokIn[0].asset,
|
|
||||||
pairedDecimals: tokIn[0].rawContract?.decimal
|
|
||||||
? String(parseInt(tokIn[0].rawContract.decimal, 16))
|
|
||||||
: undefined,
|
|
||||||
pairedContractAddress:
|
|
||||||
tokIn[0].rawContract?.address ?? undefined,
|
|
||||||
})
|
|
||||||
} else if (extOut.length > 0) {
|
|
||||||
combined.push(mapTransfer(extOut[0]))
|
|
||||||
} else if (extIn.length > 0) {
|
|
||||||
combined.push(mapTransfer(extIn[0]))
|
|
||||||
} else if (tokOut.length > 0 && tokIn.length > 0) {
|
|
||||||
combined.push({
|
|
||||||
...mapTransfer(tokOut[0]),
|
|
||||||
pairedValue: String(tokIn[0].value),
|
|
||||||
pairedSymbol: tokIn[0].asset,
|
|
||||||
pairedDecimals: tokIn[0].rawContract?.decimal
|
|
||||||
? String(parseInt(tokIn[0].rawContract.decimal, 16))
|
|
||||||
: undefined,
|
|
||||||
pairedContractAddress:
|
|
||||||
tokIn[0].rawContract?.address ?? undefined,
|
|
||||||
})
|
|
||||||
} else if (tokOut.length > 0 && extIn.length > 0) {
|
|
||||||
combined.push({
|
|
||||||
...mapTransfer(tokOut[0]),
|
|
||||||
pairedValue: String(extIn[0].value),
|
|
||||||
pairedSymbol: chainid ? NATIVE_SYMBOLS[chainid] : undefined,
|
|
||||||
})
|
|
||||||
} else if (tokOut.length > 0) {
|
|
||||||
combined.push(mapTransfer(tokOut[0]))
|
|
||||||
} else if (tokIn.length > 0) {
|
|
||||||
combined.push(mapTransfer(tokIn[0]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key && chainid) {
|
|
||||||
const addrSet = new Set<string>()
|
|
||||||
for (const entry of combined) {
|
|
||||||
if (entry.contractAddress)
|
|
||||||
addrSet.add(entry.contractAddress.toLowerCase())
|
|
||||||
if (entry.pairedContractAddress)
|
|
||||||
addrSet.add(entry.pairedContractAddress.toLowerCase())
|
|
||||||
}
|
|
||||||
const logoMap = new Map<string, string>()
|
|
||||||
await Promise.all(
|
|
||||||
Array.from(addrSet).map(async (addr) => {
|
|
||||||
const meta = await getTokenMetadata(key, chainid, addr)
|
|
||||||
if (meta?.result?.logo) logoMap.set(addr, meta.result.logo)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
for (const entry of combined) {
|
|
||||||
if (entry.contractAddress)
|
|
||||||
entry.logo = logoMap.get(entry.contractAddress.toLowerCase())
|
|
||||||
if (entry.pairedContractAddress)
|
|
||||||
entry.pairedLogo = logoMap.get(
|
|
||||||
entry.pairedContractAddress.toLowerCase(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
combined.sort((a, b) => Number(b.timeStamp) - Number(a.timeStamp))
|
|
||||||
|
|
||||||
return combined
|
|
||||||
}
|
|
||||||
|
|
||||||
let moneroManager: MoneroWalletManager | null = null
|
|
||||||
let moneroDownloading = false
|
|
||||||
|
|
||||||
let autoSyncTarget: { address: string; chainid: string } | null = null
|
|
||||||
let autoSyncTimer: ReturnType<typeof setInterval> | null = null
|
|
||||||
|
|
||||||
const rpc = BrowserView.defineRPC<RPC>({
|
const rpc = BrowserView.defineRPC<RPC>({
|
||||||
maxRequestTime: 120000,
|
maxRequestTime: 120000,
|
||||||
handlers: {
|
handlers: {
|
||||||
requests: {
|
requests: {},
|
||||||
|
messages: {},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
rpc.setRequestHandler({
|
||||||
resetApp: async () => {
|
resetApp: async () => {
|
||||||
try {
|
try {
|
||||||
const wallets = db.select().from(evmWallets).all()
|
const wallets = db.select().from(evmWallets).all()
|
||||||
@@ -398,498 +159,8 @@ const rpc = BrowserView.defineRPC<RPC>({
|
|||||||
margin: 1,
|
margin: 1,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
fetchTxHistory: async ({ address, chainid }) => {
|
...createEvmHandlers(rpc, syncState),
|
||||||
const key = await Bun.secrets.get({
|
...createMoneroHandlers(moneroState),
|
||||||
service: 'koins',
|
|
||||||
name: 'alchemy_key',
|
|
||||||
})
|
|
||||||
if (!key) return []
|
|
||||||
|
|
||||||
const [all, allError] = await tryCatch(
|
|
||||||
fetchAlchemyTransfers(key, chainid, address),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (allError) {
|
|
||||||
console.log(allError)
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
return pairTransfers(all, address, key, chainid)
|
|
||||||
},
|
|
||||||
fetchCachedTxHistory: async ({ address, chainid }) => {
|
|
||||||
const transfers = await getCachedTransactionHistory(
|
|
||||||
chainid,
|
|
||||||
address,
|
|
||||||
)
|
|
||||||
return pairTransfers(transfers, address)
|
|
||||||
},
|
|
||||||
syncTxHistory: async ({ address, chainid }) => {
|
|
||||||
const key = await Bun.secrets.get({
|
|
||||||
service: 'koins',
|
|
||||||
name: 'alchemy_key',
|
|
||||||
})
|
|
||||||
if (!key) return
|
|
||||||
try {
|
|
||||||
const count = await syncTransactionHistory(
|
|
||||||
key,
|
|
||||||
chainid,
|
|
||||||
address,
|
|
||||||
)
|
|
||||||
rpc.send.transfersUpdate({ count, chainid, address })
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[sync] error:', e)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setAutoSync: async (target) => {
|
|
||||||
if (autoSyncTimer) clearInterval(autoSyncTimer)
|
|
||||||
autoSyncTimer = null
|
|
||||||
autoSyncTarget = null
|
|
||||||
if (!target) return
|
|
||||||
autoSyncTarget = target
|
|
||||||
autoSyncTimer = setInterval(async () => {
|
|
||||||
if (!autoSyncTarget) return
|
|
||||||
const key = await Bun.secrets.get({
|
|
||||||
service: 'koins',
|
|
||||||
name: 'alchemy_key',
|
|
||||||
})
|
|
||||||
if (!key) return
|
|
||||||
try {
|
|
||||||
const count = await syncTransactionHistory(
|
|
||||||
key,
|
|
||||||
autoSyncTarget.chainid,
|
|
||||||
autoSyncTarget.address,
|
|
||||||
)
|
|
||||||
rpc.send.transfersUpdate({
|
|
||||||
count,
|
|
||||||
chainid: autoSyncTarget.chainid,
|
|
||||||
address: autoSyncTarget.address,
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[auto-sync] error:', e)
|
|
||||||
}
|
|
||||||
}, 30000)
|
|
||||||
},
|
|
||||||
flushTxCache: async () => {
|
|
||||||
sqlite.run('DELETE FROM tx_history')
|
|
||||||
sqlite.run('DELETE FROM tx_sync_status')
|
|
||||||
console.log('[rpc] flushTxCache complete')
|
|
||||||
},
|
|
||||||
fetchTokenBalances: async ({ address, chainid }) => {
|
|
||||||
const key = await Bun.secrets.get({
|
|
||||||
service: 'koins',
|
|
||||||
name: 'alchemy_key',
|
|
||||||
})
|
|
||||||
if (!key) return []
|
|
||||||
try {
|
|
||||||
const result = await fetchAlchemyTokenBalances(
|
|
||||||
key,
|
|
||||||
chainid,
|
|
||||||
address,
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fetchTokenPrices: async ({ symbols, addresses }) => {
|
|
||||||
const key = await Bun.secrets.get({
|
|
||||||
service: 'koins',
|
|
||||||
name: 'alchemy_key',
|
|
||||||
})
|
|
||||||
if (!key) return []
|
|
||||||
try {
|
|
||||||
const results: TokenPriceEntry[] = []
|
|
||||||
|
|
||||||
if (symbols && symbols.length > 0) {
|
|
||||||
const url = `https://api.g.alchemy.com/prices/v1/${key}/tokens/by-symbol?${symbols.map((s) => `symbols=${encodeURIComponent(s)}`).join('&')}`
|
|
||||||
const res = await fetch(url, {
|
|
||||||
signal: AbortSignal.timeout(10000),
|
|
||||||
})
|
|
||||||
if (res.ok) {
|
|
||||||
const body = await res.json()
|
|
||||||
const data = (body.data ?? []) as any[]
|
|
||||||
for (const d of data) {
|
|
||||||
for (const p of d.prices ?? []) {
|
|
||||||
results.push({
|
|
||||||
symbol: d.symbol,
|
|
||||||
currency: p.currency,
|
|
||||||
value: p.value,
|
|
||||||
lastUpdatedAt: p.lastUpdatedAt,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addresses && addresses.length > 0) {
|
|
||||||
const url = `https://api.g.alchemy.com/prices/v1/${key}/tokens/by-address`
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ addresses }),
|
|
||||||
signal: AbortSignal.timeout(10000),
|
|
||||||
})
|
|
||||||
if (res.ok) {
|
|
||||||
const body = await res.json()
|
|
||||||
const data = (body.data ?? []) as any[]
|
|
||||||
for (const d of data) {
|
|
||||||
for (const p of d.prices ?? []) {
|
|
||||||
results.push({
|
|
||||||
symbol: d.symbol,
|
|
||||||
currency: p.currency,
|
|
||||||
value: p.value,
|
|
||||||
lastUpdatedAt: p.lastUpdatedAt,
|
|
||||||
network: d.network,
|
|
||||||
address: d.address,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[prices] results:', results.length)
|
|
||||||
return results
|
|
||||||
} catch (error) {
|
|
||||||
console.log('[prices] error:', error)
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fetchGasPrice: async ({ chainid }) => {
|
|
||||||
const key = await Bun.secrets.get({
|
|
||||||
service: 'koins',
|
|
||||||
name: 'alchemy_key',
|
|
||||||
})
|
|
||||||
if (!key) {
|
|
||||||
console.log('[gas] no key')
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const client = getClient(chainid, key)
|
|
||||||
const gasPrice = await client.getGasPrice()
|
|
||||||
console.log(
|
|
||||||
'[gas] price for chain',
|
|
||||||
chainid,
|
|
||||||
':',
|
|
||||||
gasPrice.toString(),
|
|
||||||
)
|
|
||||||
return formatGwei(gasPrice)
|
|
||||||
} catch (error) {
|
|
||||||
console.log('[gas] error:', error)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fetchTransactionDetails: async ({ hash, chainid, address }) => {
|
|
||||||
const key = await Bun.secrets.get({
|
|
||||||
service: 'koins',
|
|
||||||
name: 'alchemy_key',
|
|
||||||
})
|
|
||||||
if (!key) return null
|
|
||||||
try {
|
|
||||||
const details = await getTransactionDetails(hash, chainid)
|
|
||||||
if (!details) return null
|
|
||||||
|
|
||||||
if (address) {
|
|
||||||
const transfers = db
|
|
||||||
.select()
|
|
||||||
.from(txHistory)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(txHistory.chainId, chainid),
|
|
||||||
eq(txHistory.hash, hash.toLowerCase()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.all()
|
|
||||||
|
|
||||||
if (transfers.length > 0) {
|
|
||||||
const paired = await pairTransfers(transfers, address)
|
|
||||||
if (paired.length > 0 && paired[0].pairedValue) {
|
|
||||||
details.pairedValue = paired[0].pairedValue
|
|
||||||
details.pairedSymbol = paired[0].pairedSymbol
|
|
||||||
details.pairedDecimals = paired[0].pairedDecimals
|
|
||||||
details.pairedContractAddress =
|
|
||||||
paired[0].pairedContractAddress
|
|
||||||
details.pairedLogo = paired[0].pairedLogo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return details
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
moneroBinaryStatus: async () => {
|
|
||||||
const status = {
|
|
||||||
installed: isBinaryInstalled(),
|
|
||||||
downloading: moneroDownloading,
|
|
||||||
}
|
|
||||||
console.log('[rpc] moneroBinaryStatus:', status)
|
|
||||||
return status
|
|
||||||
},
|
|
||||||
moneroDownloadBinary: async () => {
|
|
||||||
console.log(
|
|
||||||
'[rpc] moneroDownloadBinary: starting download...',
|
|
||||||
)
|
|
||||||
moneroDownloading = true
|
|
||||||
try {
|
|
||||||
await downloadBinary()
|
|
||||||
console.log('[rpc] moneroDownloadBinary: complete')
|
|
||||||
} catch (e) {
|
|
||||||
console.log('[rpc] moneroDownloadBinary failed:', e)
|
|
||||||
throw e
|
|
||||||
} finally {
|
|
||||||
moneroDownloading = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
moneroStart: async ({ daemonAddress }) => {
|
|
||||||
console.log('[rpc] moneroStart:', { daemonAddress })
|
|
||||||
if (!moneroManager) {
|
|
||||||
moneroManager = new MoneroWalletManager()
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await moneroManager.start(daemonAddress)
|
|
||||||
} catch (e) {
|
|
||||||
console.log('[rpc] moneroStart error:', e)
|
|
||||||
return {
|
|
||||||
running: false,
|
|
||||||
walletOpen: false,
|
|
||||||
connected: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const connected = await moneroManager.isConnected()
|
|
||||||
console.log(
|
|
||||||
'[rpc] moneroStart complete, connected:',
|
|
||||||
connected,
|
|
||||||
)
|
|
||||||
return { running: true, walletOpen: false, connected }
|
|
||||||
},
|
|
||||||
moneroStop: async () => {
|
|
||||||
console.log('[rpc] moneroStop')
|
|
||||||
if (moneroManager) {
|
|
||||||
await moneroManager.stop()
|
|
||||||
moneroManager = null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
moneroCreateWallet: async ({ name, password }) => {
|
|
||||||
console.log('[rpc] moneroCreateWallet:', name)
|
|
||||||
if (!moneroManager)
|
|
||||||
throw new Error('Monero wallet RPC not started')
|
|
||||||
const result = await moneroManager.createWallet(
|
|
||||||
name,
|
|
||||||
password,
|
|
||||||
)
|
|
||||||
console.log('[rpc] moneroCreateWallet complete')
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
moneroRestoreWallet: async ({
|
|
||||||
name,
|
|
||||||
password,
|
|
||||||
mnemonic,
|
|
||||||
restoreHeight,
|
|
||||||
}) => {
|
|
||||||
console.log('[rpc] moneroRestoreWallet:', name)
|
|
||||||
if (!moneroManager)
|
|
||||||
throw new Error('Monero wallet RPC not started')
|
|
||||||
await moneroManager.restoreWallet(
|
|
||||||
name,
|
|
||||||
password,
|
|
||||||
mnemonic,
|
|
||||||
restoreHeight,
|
|
||||||
)
|
|
||||||
const address = await moneroManager.getAddress()
|
|
||||||
console.log(
|
|
||||||
'[rpc] moneroRestoreWallet complete, address:',
|
|
||||||
address,
|
|
||||||
)
|
|
||||||
return { address }
|
|
||||||
},
|
|
||||||
moneroOpenWallet: async ({ name, password }) => {
|
|
||||||
console.log('[rpc] moneroOpenWallet:', name)
|
|
||||||
if (!moneroManager)
|
|
||||||
throw new Error('Monero wallet RPC not started')
|
|
||||||
await moneroManager.openWallet(name, password)
|
|
||||||
},
|
|
||||||
moneroGetBalance: async () => {
|
|
||||||
console.log('[rpc] moneroGetBalance')
|
|
||||||
if (!moneroManager)
|
|
||||||
throw new Error('Monero wallet RPC not started')
|
|
||||||
const { balance, unlocked } = await moneroManager.getBalance()
|
|
||||||
const address = await moneroManager.getAddress()
|
|
||||||
const height = await moneroManager.getHeight()
|
|
||||||
const daemonHeight = await moneroManager.getDaemonHeight()
|
|
||||||
console.log('[rpc] balance:', {
|
|
||||||
balance: balance.toString(),
|
|
||||||
unlocked: unlocked.toString(),
|
|
||||||
address,
|
|
||||||
height,
|
|
||||||
daemonHeight,
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
balance: balance.toString(),
|
|
||||||
unlocked: unlocked.toString(),
|
|
||||||
address,
|
|
||||||
height,
|
|
||||||
daemonHeight,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
moneroGetTransactions: async ({ accountIndex }) => {
|
|
||||||
console.log('[rpc] moneroGetTransactions', { accountIndex })
|
|
||||||
if (!moneroManager)
|
|
||||||
throw new Error('Monero wallet RPC not started')
|
|
||||||
const txs = await moneroManager.getTransactions(accountIndex)
|
|
||||||
console.log(`[rpc] moneroGetTransactions: ${txs.length} txs`)
|
|
||||||
return txs
|
|
||||||
},
|
|
||||||
moneroWalletStatus: async () => {
|
|
||||||
if (!moneroManager) {
|
|
||||||
console.log('[rpc] moneroWalletStatus: not running')
|
|
||||||
return {
|
|
||||||
running: false,
|
|
||||||
walletOpen: false,
|
|
||||||
connected: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const connected = await moneroManager.isConnected()
|
|
||||||
const walletOpen = await moneroManager.isWalletOpen()
|
|
||||||
console.log(
|
|
||||||
'[rpc] moneroWalletStatus: running, walletOpen:',
|
|
||||||
walletOpen,
|
|
||||||
'connected:',
|
|
||||||
connected,
|
|
||||||
)
|
|
||||||
return { running: true, walletOpen, connected }
|
|
||||||
},
|
|
||||||
moneroGetAccounts: async () => {
|
|
||||||
console.log('[rpc] moneroGetAccounts')
|
|
||||||
if (!moneroManager)
|
|
||||||
throw new Error('Monero wallet RPC not started')
|
|
||||||
const accounts = await moneroManager.getAccounts()
|
|
||||||
console.log(
|
|
||||||
`[rpc] moneroGetAccounts: ${accounts.length} accounts`,
|
|
||||||
)
|
|
||||||
return accounts
|
|
||||||
},
|
|
||||||
moneroListWallets: async () => {
|
|
||||||
console.log('[rpc] moneroListWallets')
|
|
||||||
if (!moneroManager)
|
|
||||||
throw new Error('Monero wallet RPC not started')
|
|
||||||
const wallets = moneroManager.listWallets()
|
|
||||||
console.log(
|
|
||||||
`[rpc] moneroListWallets: ${wallets.join(', ') || 'none'}`,
|
|
||||||
)
|
|
||||||
return wallets
|
|
||||||
},
|
|
||||||
moneroTransfer: async ({
|
|
||||||
address,
|
|
||||||
amount,
|
|
||||||
priority,
|
|
||||||
accountIndex,
|
|
||||||
}) => {
|
|
||||||
console.log('[rpc] moneroTransfer:', {
|
|
||||||
address,
|
|
||||||
amount,
|
|
||||||
priority,
|
|
||||||
})
|
|
||||||
if (!moneroManager)
|
|
||||||
throw new Error('Monero wallet RPC not started')
|
|
||||||
const result = await moneroManager.transfer(
|
|
||||||
address,
|
|
||||||
BigInt(amount),
|
|
||||||
priority ?? 0,
|
|
||||||
accountIndex ?? 0,
|
|
||||||
)
|
|
||||||
console.log('[rpc] moneroTransfer complete:', result.txHash)
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
moneroGetTransferDetails: async ({ txid }) => {
|
|
||||||
console.log('[rpc] moneroGetTransferDetails:', txid)
|
|
||||||
if (!moneroManager)
|
|
||||||
throw new Error('Monero wallet RPC not started')
|
|
||||||
const details = await moneroManager.getTransferDetails(txid)
|
|
||||||
console.log(
|
|
||||||
'[rpc] moneroGetTransferDetails complete:',
|
|
||||||
details?.hash,
|
|
||||||
)
|
|
||||||
return details
|
|
||||||
},
|
|
||||||
moneroGetFeeEstimate: async () => {
|
|
||||||
console.log('[rpc] moneroGetFeeEstimate')
|
|
||||||
if (!moneroManager)
|
|
||||||
throw new Error('Monero wallet RPC not started')
|
|
||||||
try {
|
|
||||||
const fee = await moneroManager.getFeeEstimate()
|
|
||||||
console.log('[monero][gas][fee]', fee)
|
|
||||||
return fee
|
|
||||||
} catch (error) {
|
|
||||||
console.log('[monero][gas][error]', error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
},
|
|
||||||
evmCreateWallet: async ({ name, phrase, passwordHash }) => {
|
|
||||||
console.log('[rpc] evmCreateWallet:', name)
|
|
||||||
const id = crypto.randomUUID()
|
|
||||||
const vaultKey = `evm_seed_${id}`
|
|
||||||
await Bun.secrets.set({
|
|
||||||
service: 'koins',
|
|
||||||
name: vaultKey,
|
|
||||||
value: phrase,
|
|
||||||
})
|
|
||||||
await db.insert(evmWallets).values({
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
passwordHash: passwordHash ?? null,
|
|
||||||
vaultKey,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
console.log('[rpc] evmCreateWallet complete:', id)
|
|
||||||
return { id, name, createdAt: new Date().toISOString() }
|
|
||||||
},
|
|
||||||
evmListWallets: async () => {
|
|
||||||
console.log('[rpc] evmListWallets')
|
|
||||||
const wallets = db.select().from(evmWallets).all()
|
|
||||||
return wallets.map((w) => ({
|
|
||||||
id: w.id,
|
|
||||||
name: w.name,
|
|
||||||
hasPassword: !!w.passwordHash,
|
|
||||||
vaultKey: w.vaultKey,
|
|
||||||
createdAt: w.createdAt,
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
evmGetSeed: async ({ vaultKey }) => {
|
|
||||||
console.log('[rpc] evmGetSeed:', vaultKey)
|
|
||||||
const seed = await Bun.secrets.get({
|
|
||||||
service: 'koins',
|
|
||||||
name: vaultKey,
|
|
||||||
})
|
|
||||||
if (!seed) throw new Error('Seed not found in keychain')
|
|
||||||
return seed
|
|
||||||
},
|
|
||||||
evmDeleteWallet: async ({ id }) => {
|
|
||||||
console.log('[rpc] evmDeleteWallet:', id)
|
|
||||||
const wallet = db
|
|
||||||
.select()
|
|
||||||
.from(evmWallets)
|
|
||||||
.where(eq(evmWallets.id, id))
|
|
||||||
.get()
|
|
||||||
if (!wallet) throw new Error('Wallet not found')
|
|
||||||
await Promise.all([
|
|
||||||
Bun.secrets.delete({
|
|
||||||
service: 'koins',
|
|
||||||
name: wallet.vaultKey,
|
|
||||||
}),
|
|
||||||
Bun.secrets.delete({
|
|
||||||
service: 'koins',
|
|
||||||
name: `evm_auth_${id}`,
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
db.delete(evmWallets).where(eq(evmWallets.id, id)).run()
|
|
||||||
console.log('[rpc] evmDeleteWallet complete')
|
|
||||||
},
|
|
||||||
},
|
|
||||||
messages: {},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const win = new BrowserWindow({
|
const win = new BrowserWindow({
|
||||||
|
|||||||
@@ -0,0 +1,570 @@
|
|||||||
|
import { getTokenMetadata } from '../../lib/tokens'
|
||||||
|
import { getTokensBalances } from '../../lib/alchemy'
|
||||||
|
import { tryCatch } from '@koins/utils'
|
||||||
|
import {
|
||||||
|
getTransactionHistory,
|
||||||
|
getTransactionDetails,
|
||||||
|
getCachedTransactionHistory,
|
||||||
|
} from '../../lib/transactions'
|
||||||
|
import { syncTransactionHistory } from '../../lib/sync'
|
||||||
|
import { eq, and } from 'drizzle-orm'
|
||||||
|
import { db } from '../../lib/db'
|
||||||
|
import { getClient } from '../../lib/viem'
|
||||||
|
import { formatGwei } from 'viem'
|
||||||
|
import { sqlite } from '../../lib/sqlite'
|
||||||
|
import { txHistory, evmWallets } from '../../lib/db/schema'
|
||||||
|
import type {
|
||||||
|
TokenBalanceResult,
|
||||||
|
TokenPriceEntry,
|
||||||
|
TxEntry,
|
||||||
|
} from '../../../lib/rpc-schema'
|
||||||
|
|
||||||
|
export function createEvmHandlers(
|
||||||
|
rpc: {
|
||||||
|
send: {
|
||||||
|
transfersUpdate: (p: {
|
||||||
|
count: number
|
||||||
|
chainid: string
|
||||||
|
address: string
|
||||||
|
}) => void
|
||||||
|
}
|
||||||
|
},
|
||||||
|
syncState: {
|
||||||
|
target: { address: string; chainid: string } | null
|
||||||
|
timer: ReturnType<typeof setInterval> | null
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const ALCHEMY_NETWORKS: Record<string, string> = {
|
||||||
|
'1': 'eth-mainnet',
|
||||||
|
'137': 'polygon-mainnet',
|
||||||
|
'56': 'bnb-mainnet',
|
||||||
|
}
|
||||||
|
|
||||||
|
const NATIVE_SYMBOLS: Record<string, string> = {
|
||||||
|
'1': 'ETH',
|
||||||
|
'137': 'POL',
|
||||||
|
'56': 'BNB',
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchAlchemyTransfers(
|
||||||
|
key: string,
|
||||||
|
chainid: string,
|
||||||
|
address: string,
|
||||||
|
) {
|
||||||
|
const transactions = await getTransactionHistory(
|
||||||
|
key,
|
||||||
|
chainid,
|
||||||
|
address,
|
||||||
|
{ maxCount: 10 },
|
||||||
|
)
|
||||||
|
if (!transactions) return []
|
||||||
|
return transactions
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUnits(balance: string, decimals: number): string {
|
||||||
|
const bal = BigInt(balance)
|
||||||
|
if (bal === 0n) return '0'
|
||||||
|
const divisor = 10n ** BigInt(decimals)
|
||||||
|
const intPart = bal / divisor
|
||||||
|
const fracPart = (bal % divisor)
|
||||||
|
.toString()
|
||||||
|
.padStart(decimals, '0')
|
||||||
|
.replace(/0+$/, '')
|
||||||
|
return fracPart ? `${intPart}.${fracPart}` : `${intPart}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchAlchemyTokenBalances(
|
||||||
|
key: string,
|
||||||
|
chainid: string,
|
||||||
|
address: string,
|
||||||
|
): Promise<TokenBalanceResult[]> {
|
||||||
|
const [balRes, balResError] = await tryCatch(
|
||||||
|
getTokensBalances(key, chainid, address),
|
||||||
|
)
|
||||||
|
if (balResError) {
|
||||||
|
console.log('Get Tokens Balance Error', balResError)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
if (!balRes) return []
|
||||||
|
const tokens = balRes.result?.tokenBalances ?? []
|
||||||
|
const nonZero = tokens.filter(
|
||||||
|
(t: any) => t.tokenBalance !== '0x0',
|
||||||
|
)
|
||||||
|
const metas = await Promise.allSettled(
|
||||||
|
nonZero.map((t: any) =>
|
||||||
|
getTokenMetadata(key, chainid, t.contractAddress),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if (!metas) return []
|
||||||
|
return nonZero
|
||||||
|
.map((t: any, i: number) => {
|
||||||
|
const meta =
|
||||||
|
metas[i].status === 'fulfilled' && metas[i].value
|
||||||
|
? metas[i].value.result
|
||||||
|
: null
|
||||||
|
return {
|
||||||
|
symbol: meta?.symbol ?? 'Unknown',
|
||||||
|
decimals: meta?.decimals ?? 0,
|
||||||
|
balance: formatUnits(t.tokenBalance, meta?.decimals ?? 18),
|
||||||
|
contractAddress: t.contractAddress,
|
||||||
|
logo: meta?.logo ?? undefined,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((t) => t.logo && t.decimals !== 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapTransfer(t: any): TxEntry {
|
||||||
|
const isExternal = t.category === 'external'
|
||||||
|
return {
|
||||||
|
hash: t.hash,
|
||||||
|
timeStamp: String(
|
||||||
|
Math.floor(
|
||||||
|
new Date(t.metadata?.blockTimestamp).getTime() / 1000,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
from: t.from,
|
||||||
|
to: t.to,
|
||||||
|
value: String(t.value),
|
||||||
|
...(isExternal
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
tokenSymbol: t.asset,
|
||||||
|
tokenDecimal: t.rawContract?.decimal
|
||||||
|
? String(parseInt(t.rawContract.decimal, 16))
|
||||||
|
: undefined,
|
||||||
|
contractAddress: t.rawContract?.address ?? undefined,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pairTransfers(
|
||||||
|
transfers: any[],
|
||||||
|
address: string,
|
||||||
|
key?: string,
|
||||||
|
chainid?: string,
|
||||||
|
): Promise<TxEntry[]> {
|
||||||
|
const addrLower = address.toLowerCase()
|
||||||
|
const byHash = new Map<string, any[]>()
|
||||||
|
for (const t of transfers) {
|
||||||
|
if (!t.hash) continue
|
||||||
|
const h = t.hash
|
||||||
|
if (!byHash.has(h)) byHash.set(h, [])
|
||||||
|
byHash.get(h)!.push(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
const combined: TxEntry[] = []
|
||||||
|
|
||||||
|
for (const [, group] of byHash) {
|
||||||
|
const native = group.filter(
|
||||||
|
(t: any) =>
|
||||||
|
t.category === 'external' || t.category === 'internal',
|
||||||
|
)
|
||||||
|
const tokens = group.filter((t: any) => t.category === 'erc20')
|
||||||
|
|
||||||
|
const extOut = native.filter(
|
||||||
|
(t: any) => t.from?.toLowerCase() === addrLower,
|
||||||
|
)
|
||||||
|
const extIn = native.filter(
|
||||||
|
(t: any) => t.to?.toLowerCase() === addrLower,
|
||||||
|
)
|
||||||
|
const tokIn = tokens.filter(
|
||||||
|
(t: any) => t.to?.toLowerCase() === addrLower,
|
||||||
|
)
|
||||||
|
const tokOut = tokens.filter(
|
||||||
|
(t: any) => t.from?.toLowerCase() === addrLower,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (extOut.length > 0 && tokIn.length > 0) {
|
||||||
|
combined.push({
|
||||||
|
...mapTransfer(extOut[0]),
|
||||||
|
tokenSymbol: tokIn[0].asset,
|
||||||
|
tokenDecimal: tokIn[0].rawContract?.decimal
|
||||||
|
? String(parseInt(tokIn[0].rawContract.decimal, 16))
|
||||||
|
: undefined,
|
||||||
|
contractAddress: tokIn[0].rawContract?.address ?? undefined,
|
||||||
|
pairedValue: String(tokIn[0].value),
|
||||||
|
pairedSymbol: tokIn[0].asset,
|
||||||
|
pairedDecimals: tokIn[0].rawContract?.decimal
|
||||||
|
? String(parseInt(tokIn[0].rawContract.decimal, 16))
|
||||||
|
: undefined,
|
||||||
|
pairedContractAddress:
|
||||||
|
tokIn[0].rawContract?.address ?? undefined,
|
||||||
|
})
|
||||||
|
} else if (extOut.length > 0) {
|
||||||
|
combined.push(mapTransfer(extOut[0]))
|
||||||
|
} else if (extIn.length > 0) {
|
||||||
|
combined.push(mapTransfer(extIn[0]))
|
||||||
|
} else if (tokOut.length > 0 && tokIn.length > 0) {
|
||||||
|
combined.push({
|
||||||
|
...mapTransfer(tokOut[0]),
|
||||||
|
pairedValue: String(tokIn[0].value),
|
||||||
|
pairedSymbol: tokIn[0].asset,
|
||||||
|
pairedDecimals: tokIn[0].rawContract?.decimal
|
||||||
|
? String(parseInt(tokIn[0].rawContract.decimal, 16))
|
||||||
|
: undefined,
|
||||||
|
pairedContractAddress:
|
||||||
|
tokIn[0].rawContract?.address ?? undefined,
|
||||||
|
})
|
||||||
|
} else if (tokOut.length > 0 && extIn.length > 0) {
|
||||||
|
combined.push({
|
||||||
|
...mapTransfer(tokOut[0]),
|
||||||
|
pairedValue: String(extIn[0].value),
|
||||||
|
pairedSymbol: chainid ? NATIVE_SYMBOLS[chainid] : undefined,
|
||||||
|
})
|
||||||
|
} else if (tokOut.length > 0) {
|
||||||
|
combined.push(mapTransfer(tokOut[0]))
|
||||||
|
} else if (tokIn.length > 0) {
|
||||||
|
combined.push(mapTransfer(tokIn[0]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key && chainid) {
|
||||||
|
const addrSet = new Set<string>()
|
||||||
|
for (const entry of combined) {
|
||||||
|
if (entry.contractAddress)
|
||||||
|
addrSet.add(entry.contractAddress.toLowerCase())
|
||||||
|
if (entry.pairedContractAddress)
|
||||||
|
addrSet.add(entry.pairedContractAddress.toLowerCase())
|
||||||
|
}
|
||||||
|
const logoMap = new Map<string, string>()
|
||||||
|
await Promise.all(
|
||||||
|
Array.from(addrSet).map(async (addr) => {
|
||||||
|
const meta = await getTokenMetadata(key, chainid, addr)
|
||||||
|
if (meta?.result?.logo) logoMap.set(addr, meta.result.logo)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
for (const entry of combined) {
|
||||||
|
if (entry.contractAddress)
|
||||||
|
entry.logo = logoMap.get(
|
||||||
|
entry.contractAddress.toLowerCase(),
|
||||||
|
)
|
||||||
|
if (entry.pairedContractAddress)
|
||||||
|
entry.pairedLogo = logoMap.get(
|
||||||
|
entry.pairedContractAddress.toLowerCase(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
combined.sort((a, b) => Number(b.timeStamp) - Number(a.timeStamp))
|
||||||
|
return combined
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
fetchTxHistory: async ({
|
||||||
|
address,
|
||||||
|
chainid,
|
||||||
|
}: {
|
||||||
|
address: string
|
||||||
|
chainid: string
|
||||||
|
}) => {
|
||||||
|
const key = await Bun.secrets.get({
|
||||||
|
service: 'koins',
|
||||||
|
name: 'alchemy_key',
|
||||||
|
})
|
||||||
|
if (!key) return []
|
||||||
|
const [all, allError] = await tryCatch(
|
||||||
|
fetchAlchemyTransfers(key, chainid, address),
|
||||||
|
)
|
||||||
|
if (allError) {
|
||||||
|
console.log(allError)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return pairTransfers(all, address, key, chainid)
|
||||||
|
},
|
||||||
|
fetchCachedTxHistory: async ({
|
||||||
|
address,
|
||||||
|
chainid,
|
||||||
|
}: {
|
||||||
|
address: string
|
||||||
|
chainid: string
|
||||||
|
}) => {
|
||||||
|
const transfers = await getCachedTransactionHistory(
|
||||||
|
chainid,
|
||||||
|
address,
|
||||||
|
)
|
||||||
|
return pairTransfers(transfers, address)
|
||||||
|
},
|
||||||
|
syncTxHistory: async ({
|
||||||
|
address,
|
||||||
|
chainid,
|
||||||
|
}: {
|
||||||
|
address: string
|
||||||
|
chainid: string
|
||||||
|
}) => {
|
||||||
|
const key = await Bun.secrets.get({
|
||||||
|
service: 'koins',
|
||||||
|
name: 'alchemy_key',
|
||||||
|
})
|
||||||
|
if (!key) return
|
||||||
|
try {
|
||||||
|
const count = await syncTransactionHistory(
|
||||||
|
key,
|
||||||
|
chainid,
|
||||||
|
address,
|
||||||
|
)
|
||||||
|
rpc.send.transfersUpdate({ count, chainid, address })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[sync] error:', e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setAutoSync: async (
|
||||||
|
target: { address: string; chainid: string } | null,
|
||||||
|
) => {
|
||||||
|
if (syncState.timer) clearInterval(syncState.timer)
|
||||||
|
syncState.timer = null
|
||||||
|
syncState.target = null
|
||||||
|
if (!target) return
|
||||||
|
syncState.target = target
|
||||||
|
syncState.timer = setInterval(async () => {
|
||||||
|
if (!syncState.target) return
|
||||||
|
const key = await Bun.secrets.get({
|
||||||
|
service: 'koins',
|
||||||
|
name: 'alchemy_key',
|
||||||
|
})
|
||||||
|
if (!key) return
|
||||||
|
try {
|
||||||
|
const count = await syncTransactionHistory(
|
||||||
|
key,
|
||||||
|
syncState.target.chainid,
|
||||||
|
syncState.target.address,
|
||||||
|
)
|
||||||
|
rpc.send.transfersUpdate({
|
||||||
|
count,
|
||||||
|
chainid: syncState.target.chainid,
|
||||||
|
address: syncState.target.address,
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[auto-sync] error:', e)
|
||||||
|
}
|
||||||
|
}, 30000)
|
||||||
|
},
|
||||||
|
flushTxCache: async () => {
|
||||||
|
sqlite.run('DELETE FROM tx_history')
|
||||||
|
sqlite.run('DELETE FROM tx_sync_status')
|
||||||
|
console.log('[rpc] flushTxCache complete')
|
||||||
|
},
|
||||||
|
fetchTokenBalances: async ({
|
||||||
|
address,
|
||||||
|
chainid,
|
||||||
|
}: {
|
||||||
|
address: string
|
||||||
|
chainid: string
|
||||||
|
}) => {
|
||||||
|
const key = await Bun.secrets.get({
|
||||||
|
service: 'koins',
|
||||||
|
name: 'alchemy_key',
|
||||||
|
})
|
||||||
|
if (!key) return []
|
||||||
|
try {
|
||||||
|
return await fetchAlchemyTokenBalances(key, chainid, address)
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fetchTokenPrices: async ({
|
||||||
|
symbols,
|
||||||
|
addresses,
|
||||||
|
}: {
|
||||||
|
symbols?: string[]
|
||||||
|
addresses?: { network: string; address: string }[]
|
||||||
|
}) => {
|
||||||
|
const key = await Bun.secrets.get({
|
||||||
|
service: 'koins',
|
||||||
|
name: 'alchemy_key',
|
||||||
|
})
|
||||||
|
if (!key) return []
|
||||||
|
try {
|
||||||
|
const results: TokenPriceEntry[] = []
|
||||||
|
if (symbols && symbols.length > 0) {
|
||||||
|
const url = `https://api.g.alchemy.com/prices/v1/${key}/tokens/by-symbol?${symbols.map((s) => `symbols=${encodeURIComponent(s)}`).join('&')}`
|
||||||
|
const res = await fetch(url, {
|
||||||
|
signal: AbortSignal.timeout(10000),
|
||||||
|
})
|
||||||
|
if (res.ok) {
|
||||||
|
const body = await res.json()
|
||||||
|
for (const d of body.data ?? []) {
|
||||||
|
for (const p of d.prices ?? []) {
|
||||||
|
results.push({
|
||||||
|
symbol: d.symbol,
|
||||||
|
currency: p.currency,
|
||||||
|
value: p.value,
|
||||||
|
lastUpdatedAt: p.lastUpdatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (addresses && addresses.length > 0) {
|
||||||
|
const url = `https://api.g.alchemy.com/prices/v1/${key}/tokens/by-address`
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ addresses }),
|
||||||
|
signal: AbortSignal.timeout(10000),
|
||||||
|
})
|
||||||
|
if (res.ok) {
|
||||||
|
const body = await res.json()
|
||||||
|
for (const d of body.data ?? []) {
|
||||||
|
for (const p of d.prices ?? []) {
|
||||||
|
results.push({
|
||||||
|
symbol: d.symbol,
|
||||||
|
currency: p.currency,
|
||||||
|
value: p.value,
|
||||||
|
lastUpdatedAt: p.lastUpdatedAt,
|
||||||
|
network: d.network,
|
||||||
|
address: d.address,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('[prices] results:', results.length)
|
||||||
|
return results
|
||||||
|
} catch (error) {
|
||||||
|
console.log('[prices] error:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fetchGasPrice: async ({ chainid }: { chainid: string }) => {
|
||||||
|
const key = await Bun.secrets.get({
|
||||||
|
service: 'koins',
|
||||||
|
name: 'alchemy_key',
|
||||||
|
})
|
||||||
|
if (!key) {
|
||||||
|
console.log('[gas] no key')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const client = getClient(chainid, key)
|
||||||
|
const gasPrice = await client.getGasPrice()
|
||||||
|
console.log(
|
||||||
|
'[gas] price for chain',
|
||||||
|
chainid,
|
||||||
|
':',
|
||||||
|
gasPrice.toString(),
|
||||||
|
)
|
||||||
|
return formatGwei(gasPrice)
|
||||||
|
} catch (error) {
|
||||||
|
console.log('[gas] error:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fetchTransactionDetails: async ({
|
||||||
|
hash,
|
||||||
|
chainid,
|
||||||
|
address,
|
||||||
|
}: {
|
||||||
|
hash: string
|
||||||
|
chainid: string
|
||||||
|
address?: string
|
||||||
|
}) => {
|
||||||
|
const key = await Bun.secrets.get({
|
||||||
|
service: 'koins',
|
||||||
|
name: 'alchemy_key',
|
||||||
|
})
|
||||||
|
if (!key) return null
|
||||||
|
try {
|
||||||
|
const details = await getTransactionDetails(hash, chainid)
|
||||||
|
if (!details) return null
|
||||||
|
if (address) {
|
||||||
|
const transfers = db
|
||||||
|
.select()
|
||||||
|
.from(txHistory)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(txHistory.chainId, chainid),
|
||||||
|
eq(txHistory.hash, hash.toLowerCase()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
if (transfers.length > 0) {
|
||||||
|
const paired = await pairTransfers(transfers, address)
|
||||||
|
if (paired.length > 0 && paired[0].pairedValue) {
|
||||||
|
details.pairedValue = paired[0].pairedValue
|
||||||
|
details.pairedSymbol = paired[0].pairedSymbol
|
||||||
|
details.pairedDecimals = paired[0].pairedDecimals
|
||||||
|
details.pairedContractAddress =
|
||||||
|
paired[0].pairedContractAddress
|
||||||
|
details.pairedLogo = paired[0].pairedLogo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return details
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
evmCreateWallet: async ({
|
||||||
|
name,
|
||||||
|
phrase,
|
||||||
|
passwordHash,
|
||||||
|
}: {
|
||||||
|
name: string
|
||||||
|
phrase: string
|
||||||
|
passwordHash?: string
|
||||||
|
}) => {
|
||||||
|
console.log('[rpc] evmCreateWallet:', name)
|
||||||
|
const id = crypto.randomUUID()
|
||||||
|
const vaultKey = `evm_seed_${id}`
|
||||||
|
await Bun.secrets.set({
|
||||||
|
service: 'koins',
|
||||||
|
name: vaultKey,
|
||||||
|
value: phrase,
|
||||||
|
})
|
||||||
|
await db
|
||||||
|
.insert(evmWallets)
|
||||||
|
.values({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
passwordHash: passwordHash ?? null,
|
||||||
|
vaultKey,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
console.log('[rpc] evmCreateWallet complete:', id)
|
||||||
|
return { id, name, createdAt: new Date().toISOString() }
|
||||||
|
},
|
||||||
|
evmListWallets: async () => {
|
||||||
|
console.log('[rpc] evmListWallets')
|
||||||
|
const wallets = db.select().from(evmWallets).all()
|
||||||
|
return wallets.map((w: any) => ({
|
||||||
|
id: w.id,
|
||||||
|
name: w.name,
|
||||||
|
hasPassword: !!w.passwordHash,
|
||||||
|
vaultKey: w.vaultKey,
|
||||||
|
createdAt: w.createdAt,
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
evmGetSeed: async ({ vaultKey }: { vaultKey: string }) => {
|
||||||
|
console.log('[rpc] evmGetSeed:', vaultKey)
|
||||||
|
const seed = await Bun.secrets.get({
|
||||||
|
service: 'koins',
|
||||||
|
name: vaultKey,
|
||||||
|
})
|
||||||
|
if (!seed) throw new Error('Seed not found in keychain')
|
||||||
|
return seed
|
||||||
|
},
|
||||||
|
evmDeleteWallet: async ({ id }: { id: string }) => {
|
||||||
|
console.log('[rpc] evmDeleteWallet:', id)
|
||||||
|
const wallet = db
|
||||||
|
.select()
|
||||||
|
.from(evmWallets)
|
||||||
|
.where(eq(evmWallets.id, id))
|
||||||
|
.get()
|
||||||
|
if (!wallet) throw new Error('Wallet not found')
|
||||||
|
await Promise.all([
|
||||||
|
Bun.secrets.delete({
|
||||||
|
service: 'koins',
|
||||||
|
name: wallet.vaultKey,
|
||||||
|
}),
|
||||||
|
Bun.secrets.delete({
|
||||||
|
service: 'koins',
|
||||||
|
name: `evm_auth_${id}`,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
db.delete(evmWallets).where(eq(evmWallets.id, id)).run()
|
||||||
|
console.log('[rpc] evmDeleteWallet complete')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
import {
|
||||||
|
isBinaryInstalled,
|
||||||
|
downloadBinary,
|
||||||
|
MoneroWalletManager,
|
||||||
|
} from '../../lib/monero'
|
||||||
|
|
||||||
|
export function createMoneroHandlers(state: {
|
||||||
|
manager: MoneroWalletManager | null
|
||||||
|
downloading: boolean
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
moneroBinaryStatus: async () => {
|
||||||
|
const status = {
|
||||||
|
installed: isBinaryInstalled(),
|
||||||
|
downloading: state.downloading,
|
||||||
|
error: undefined,
|
||||||
|
}
|
||||||
|
console.log('[rpc] moneroBinaryStatus:', status)
|
||||||
|
return status
|
||||||
|
},
|
||||||
|
moneroDownloadBinary: async () => {
|
||||||
|
console.log('[rpc] moneroDownloadBinary: starting download...')
|
||||||
|
state.downloading = true
|
||||||
|
try {
|
||||||
|
await downloadBinary()
|
||||||
|
console.log('[rpc] moneroDownloadBinary: complete')
|
||||||
|
} catch (e) {
|
||||||
|
console.log('[rpc] moneroDownloadBinary failed:', e)
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
state.downloading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
moneroStart: async ({
|
||||||
|
daemonAddress,
|
||||||
|
}: {
|
||||||
|
daemonAddress?: string
|
||||||
|
}) => {
|
||||||
|
console.log('[rpc] moneroStart:', { daemonAddress })
|
||||||
|
if (!state.manager) {
|
||||||
|
state.manager = new MoneroWalletManager()
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await state.manager.start(daemonAddress)
|
||||||
|
} catch (e) {
|
||||||
|
console.log('[rpc] moneroStart error:', e)
|
||||||
|
return { running: false, walletOpen: false, connected: false }
|
||||||
|
}
|
||||||
|
const connected = await state.manager.isConnected()
|
||||||
|
console.log('[rpc] moneroStart complete, connected:', connected)
|
||||||
|
return { running: true, walletOpen: false, connected }
|
||||||
|
},
|
||||||
|
moneroStop: async () => {
|
||||||
|
console.log('[rpc] moneroStop')
|
||||||
|
if (state.manager) {
|
||||||
|
await state.manager.stop()
|
||||||
|
state.manager = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
moneroCreateWallet: async ({
|
||||||
|
name,
|
||||||
|
password,
|
||||||
|
}: {
|
||||||
|
name: string
|
||||||
|
password: string
|
||||||
|
}) => {
|
||||||
|
console.log('[rpc] moneroCreateWallet:', name)
|
||||||
|
if (!state.manager)
|
||||||
|
throw new Error('Monero wallet RPC not started')
|
||||||
|
const result = await state.manager.createWallet(name, password)
|
||||||
|
console.log('[rpc] moneroCreateWallet complete')
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
moneroRestoreWallet: async ({
|
||||||
|
name,
|
||||||
|
password,
|
||||||
|
mnemonic,
|
||||||
|
restoreHeight,
|
||||||
|
}: {
|
||||||
|
name: string
|
||||||
|
password: string
|
||||||
|
mnemonic: string
|
||||||
|
restoreHeight?: number
|
||||||
|
}) => {
|
||||||
|
console.log('[rpc] moneroRestoreWallet:', name)
|
||||||
|
if (!state.manager)
|
||||||
|
throw new Error('Monero wallet RPC not started')
|
||||||
|
await state.manager.restoreWallet(
|
||||||
|
name,
|
||||||
|
password,
|
||||||
|
mnemonic,
|
||||||
|
restoreHeight,
|
||||||
|
)
|
||||||
|
const address = await state.manager.getAddress()
|
||||||
|
console.log(
|
||||||
|
'[rpc] moneroRestoreWallet complete, address:',
|
||||||
|
address,
|
||||||
|
)
|
||||||
|
return { address }
|
||||||
|
},
|
||||||
|
moneroOpenWallet: async ({
|
||||||
|
name,
|
||||||
|
password,
|
||||||
|
}: {
|
||||||
|
name: string
|
||||||
|
password: string
|
||||||
|
}) => {
|
||||||
|
console.log('[rpc] moneroOpenWallet:', name)
|
||||||
|
if (!state.manager)
|
||||||
|
throw new Error('Monero wallet RPC not started')
|
||||||
|
await state.manager.openWallet(name, password)
|
||||||
|
},
|
||||||
|
moneroGetBalance: async () => {
|
||||||
|
console.log('[rpc] moneroGetBalance')
|
||||||
|
if (!state.manager)
|
||||||
|
throw new Error('Monero wallet RPC not started')
|
||||||
|
const { balance, unlocked } = await state.manager.getBalance()
|
||||||
|
const address = await state.manager.getAddress()
|
||||||
|
const height = await state.manager.getHeight()
|
||||||
|
const daemonHeight = await state.manager.getDaemonHeight()
|
||||||
|
console.log('[rpc] balance:', {
|
||||||
|
balance: balance.toString(),
|
||||||
|
unlocked: unlocked.toString(),
|
||||||
|
address,
|
||||||
|
height,
|
||||||
|
daemonHeight,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
balance: balance.toString(),
|
||||||
|
unlocked: unlocked.toString(),
|
||||||
|
address,
|
||||||
|
height,
|
||||||
|
daemonHeight,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
moneroGetTransactions: async ({
|
||||||
|
accountIndex,
|
||||||
|
}: {
|
||||||
|
accountIndex?: number
|
||||||
|
}) => {
|
||||||
|
console.log('[rpc] moneroGetTransactions', { accountIndex })
|
||||||
|
if (!state.manager)
|
||||||
|
throw new Error('Monero wallet RPC not started')
|
||||||
|
const txs = await state.manager.getTransactions(accountIndex)
|
||||||
|
console.log(`[rpc] moneroGetTransactions: ${txs.length} txs`)
|
||||||
|
return txs
|
||||||
|
},
|
||||||
|
moneroWalletStatus: async () => {
|
||||||
|
if (!state.manager) {
|
||||||
|
console.log('[rpc] moneroWalletStatus: not running')
|
||||||
|
return { running: false, walletOpen: false, connected: false }
|
||||||
|
}
|
||||||
|
const connected = await state.manager.isConnected()
|
||||||
|
const walletOpen = await state.manager.isWalletOpen()
|
||||||
|
console.log(
|
||||||
|
'[rpc] moneroWalletStatus: running, walletOpen:',
|
||||||
|
walletOpen,
|
||||||
|
'connected:',
|
||||||
|
connected,
|
||||||
|
)
|
||||||
|
return { running: true, walletOpen, connected }
|
||||||
|
},
|
||||||
|
moneroGetAccounts: async () => {
|
||||||
|
console.log('[rpc] moneroGetAccounts')
|
||||||
|
if (!state.manager)
|
||||||
|
throw new Error('Monero wallet RPC not started')
|
||||||
|
const accounts = await state.manager.getAccounts()
|
||||||
|
console.log(
|
||||||
|
`[rpc] moneroGetAccounts: ${accounts.length} accounts`,
|
||||||
|
)
|
||||||
|
return accounts
|
||||||
|
},
|
||||||
|
moneroListWallets: async () => {
|
||||||
|
console.log('[rpc] moneroListWallets')
|
||||||
|
if (!state.manager)
|
||||||
|
throw new Error('Monero wallet RPC not started')
|
||||||
|
const wallets = state.manager.listWallets()
|
||||||
|
console.log(
|
||||||
|
`[rpc] moneroListWallets: ${wallets.join(', ') || 'none'}`,
|
||||||
|
)
|
||||||
|
return wallets
|
||||||
|
},
|
||||||
|
moneroTransfer: async ({
|
||||||
|
address,
|
||||||
|
amount,
|
||||||
|
priority,
|
||||||
|
accountIndex,
|
||||||
|
}: {
|
||||||
|
address: string
|
||||||
|
amount: string
|
||||||
|
priority?: number
|
||||||
|
accountIndex?: number
|
||||||
|
}) => {
|
||||||
|
console.log('[rpc] moneroTransfer:', {
|
||||||
|
address,
|
||||||
|
amount,
|
||||||
|
priority,
|
||||||
|
})
|
||||||
|
if (!state.manager)
|
||||||
|
throw new Error('Monero wallet RPC not started')
|
||||||
|
const result = await state.manager.transfer(
|
||||||
|
address,
|
||||||
|
BigInt(amount),
|
||||||
|
priority ?? 0,
|
||||||
|
accountIndex ?? 0,
|
||||||
|
)
|
||||||
|
console.log('[rpc] moneroTransfer complete:', result.txHash)
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
moneroGetTransferDetails: async ({ txid }: { txid: string }) => {
|
||||||
|
console.log('[rpc] moneroGetTransferDetails:', txid)
|
||||||
|
if (!state.manager)
|
||||||
|
throw new Error('Monero wallet RPC not started')
|
||||||
|
const details = await state.manager.getTransferDetails(txid)
|
||||||
|
console.log(
|
||||||
|
'[rpc] moneroGetTransferDetails complete:',
|
||||||
|
details?.hash,
|
||||||
|
)
|
||||||
|
return details
|
||||||
|
},
|
||||||
|
moneroGetFeeEstimate: async () => {
|
||||||
|
console.log('[rpc] moneroGetFeeEstimate')
|
||||||
|
if (!state.manager)
|
||||||
|
throw new Error('Monero wallet RPC not started')
|
||||||
|
try {
|
||||||
|
const fee = await state.manager.getFeeEstimate()
|
||||||
|
console.log('[monero][gas][fee]', fee)
|
||||||
|
return fee
|
||||||
|
} catch (error) {
|
||||||
|
console.log('[monero][gas][error]', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -139,14 +139,17 @@ export type EvmWalletInfo = {
|
|||||||
createdAt: string
|
createdAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RPC = {
|
export type GeneralRpcRequests = {
|
||||||
bun: {
|
|
||||||
requests: {
|
|
||||||
resetApp: { params: {}; response: boolean }
|
resetApp: { params: {}; response: boolean }
|
||||||
biometricCanAuth: { params: {}; response: boolean }
|
biometricCanAuth: { params: {}; response: boolean }
|
||||||
biometricAuth: { params: { reason: string }; response: boolean }
|
biometricAuth: { params: { reason: string }; response: boolean }
|
||||||
getSecret: { params: { service: string; name: string }; response: string | null }
|
getSecret: { params: { service: string; name: string }; response: string | null }
|
||||||
setSecret: { params: { service: string; name: string; value: string }; response: void }
|
setSecret: { params: { service: string; name: string; value: string }; response: void }
|
||||||
|
openExternal: { params: { url: string }; response: void }
|
||||||
|
generateQrCode: { params: { text: string; size?: number }; response: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EvmRpcRequests = {
|
||||||
fetchTxHistory: { params: { address: string; chainid: string }; response: TxEntry[] }
|
fetchTxHistory: { params: { address: string; chainid: string }; response: TxEntry[] }
|
||||||
fetchCachedTxHistory: { params: { address: string; chainid: string }; response: TxEntry[] }
|
fetchCachedTxHistory: { params: { address: string; chainid: string }; response: TxEntry[] }
|
||||||
syncTxHistory: { params: { address: string; chainid: string }; response: void }
|
syncTxHistory: { params: { address: string; chainid: string }; response: void }
|
||||||
@@ -154,8 +157,15 @@ export type RPC = {
|
|||||||
setAutoSync: { params: { address: string; chainid: string } | null; response: void }
|
setAutoSync: { params: { address: string; chainid: string } | null; response: void }
|
||||||
fetchTokenBalances: { params: { address: string; chainid: string }; response: TokenBalanceResult[] }
|
fetchTokenBalances: { params: { address: string; chainid: string }; response: TokenBalanceResult[] }
|
||||||
fetchTransactionDetails: { params: { hash: string; chainid: string; address?: string }; response: TransactionDetails | null }
|
fetchTransactionDetails: { params: { hash: string; chainid: string; address?: string }; response: TransactionDetails | null }
|
||||||
openExternal: { params: { url: string }; response: void }
|
fetchTokenPrices: { params: { symbols?: string[]; addresses?: { network: string; address: string }[] }; response: TokenPriceEntry[] }
|
||||||
generateQrCode: { params: { text: string; size?: number }; response: string }
|
fetchGasPrice: { params: { chainid: string }; response: string | null }
|
||||||
|
evmCreateWallet: { params: { name: string; phrase: string; passwordHash?: string }; response: { id: string; name: string; createdAt: string } }
|
||||||
|
evmListWallets: { params: {}; response: EvmWalletInfo[] }
|
||||||
|
evmGetSeed: { params: { vaultKey: string }; response: string }
|
||||||
|
evmDeleteWallet: { params: { id: string }; response: void }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MoneroRpcRequests = {
|
||||||
moneroBinaryStatus: { params: {}; response: MoneroBinaryStatus }
|
moneroBinaryStatus: { params: {}; response: MoneroBinaryStatus }
|
||||||
moneroDownloadBinary: { params: {}; response: void }
|
moneroDownloadBinary: { params: {}; response: void }
|
||||||
moneroStart: { params: { daemonAddress?: string }; response: MoneroWalletStatus }
|
moneroStart: { params: { daemonAddress?: string }; response: MoneroWalletStatus }
|
||||||
@@ -171,13 +181,11 @@ export type RPC = {
|
|||||||
moneroTransfer: { params: { address: string; amount: string; priority?: number; accountIndex?: number }; response: MoneroSendResult }
|
moneroTransfer: { params: { address: string; amount: string; priority?: number; accountIndex?: number }; response: MoneroSendResult }
|
||||||
moneroGetTransferDetails: { params: { txid: string }; response: MoneroTransferDetails | null }
|
moneroGetTransferDetails: { params: { txid: string }; response: MoneroTransferDetails | null }
|
||||||
moneroGetFeeEstimate: { params: {}; response: MoneroFeeEstimate | null }
|
moneroGetFeeEstimate: { params: {}; response: MoneroFeeEstimate | null }
|
||||||
evmCreateWallet: { params: { name: string; phrase: string; passwordHash?: string }; response: { id: string; name: string; createdAt: string } }
|
|
||||||
evmListWallets: { params: {}; response: EvmWalletInfo[] }
|
|
||||||
evmGetSeed: { params: { vaultKey: string }; response: string }
|
|
||||||
evmDeleteWallet: { params: { id: string }; response: void }
|
|
||||||
fetchTokenPrices: { params: { symbols?: string[]; addresses?: { network: string; address: string }[] }; response: TokenPriceEntry[] }
|
|
||||||
fetchGasPrice: { params: { chainid: string }; response: string | null }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RPC = {
|
||||||
|
bun: {
|
||||||
|
requests: GeneralRpcRequests & EvmRpcRequests & MoneroRpcRequests
|
||||||
messages: {}
|
messages: {}
|
||||||
}
|
}
|
||||||
webview: {
|
webview: {
|
||||||
|
|||||||
Reference in New Issue
Block a user