diff --git a/src/services/chain-adapter/evm/asset-service.ts b/src/services/chain-adapter/evm/asset-service.ts index 06700fa0f..05e69b372 100644 --- a/src/services/chain-adapter/evm/asset-service.ts +++ b/src/services/chain-adapter/evm/asset-service.ts @@ -1,7 +1,7 @@ /** * EVM Asset Service - * - * Provides balance queries for EVM chains via walletapi. + * + * Provides balance queries for EVM chains via public JSON-RPC endpoints. */ import type { ChainConfig } from '@/services/chain-config' @@ -9,23 +9,39 @@ import type { IAssetService, Address, Balance, TokenMetadata } from '../types' import { Amount } from '@/types/amount' import { ChainServiceError, ChainErrorCodes } from '../types' +/** Default public RPC endpoints for EVM chains */ +const DEFAULT_RPC_URLS: Record = { + ethereum: 'https://ethereum-rpc.publicnode.com', + binance: 'https://bsc-rpc.publicnode.com', +} + +interface JsonRpcResponse { + jsonrpc: string + id: number + result?: T + error?: { code: number; message: string } +} + export class EvmAssetService implements IAssetService { private readonly config: ChainConfig - private readonly apiUrl: string - private readonly apiPath: string + private readonly rpcUrl: string constructor(config: ChainConfig) { this.config = config - this.apiUrl = config.api?.url ?? 'https://walletapi.bfmeta.info' - this.apiPath = config.api?.path ?? config.id + this.rpcUrl = DEFAULT_RPC_URLS[config.id] ?? config.api?.url ?? DEFAULT_RPC_URLS['ethereum']! } - private async fetch(endpoint: string, body?: unknown): Promise { - const url = `${this.apiUrl}/wallet/${this.apiPath}${endpoint}` - const init: RequestInit = body - ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) } - : { method: 'GET' } - const response = await fetch(url, init) + private async rpc(method: string, params: unknown[]): Promise { + const response = await fetch(this.rpcUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + method, + params, + id: 1, + }), + }) if (!response.ok) { throw new ChainServiceError( @@ -34,12 +50,9 @@ export class EvmAssetService implements IAssetService { ) } - const json = await response.json() as { success: boolean; result?: T; error?: { message: string } } - if (!json.success) { - throw new ChainServiceError( - ChainErrorCodes.NETWORK_ERROR, - json.error?.message ?? 'API request failed', - ) + const json = (await response.json()) as JsonRpcResponse + if (json.error) { + throw new ChainServiceError(ChainErrorCodes.NETWORK_ERROR, json.error.message) } return json.result as T @@ -47,13 +60,13 @@ export class EvmAssetService implements IAssetService { async getNativeBalance(address: Address): Promise { try { - const result = await this.fetch<{ balance: string }>('/address/balance', { address }) + const hexBalance = await this.rpc('eth_getBalance', [address, 'latest']) + const balance = BigInt(hexBalance).toString() return { - amount: Amount.fromRaw(result.balance, this.config.decimals, this.config.symbol), + amount: Amount.fromRaw(balance, this.config.decimals, this.config.symbol), symbol: this.config.symbol, } - } catch (error) { - if (error instanceof ChainServiceError) throw error + } catch { return { amount: Amount.fromRaw('0', this.config.decimals, this.config.symbol), symbol: this.config.symbol, @@ -63,13 +76,13 @@ export class EvmAssetService implements IAssetService { async getTokenBalance(address: Address, tokenAddress: Address): Promise { try { - const result = await this.fetch<{ balance: string; decimals: number; symbol: string }>( - '/token/balance', - { address, tokenAddress }, - ) + // ERC20 balanceOf(address) selector: 0x70a08231 + const data = `0x70a08231000000000000000000000000${address.slice(2).toLowerCase()}` + const hexBalance = await this.rpc('eth_call', [{ to: tokenAddress, data }, 'latest']) + const balance = BigInt(hexBalance).toString() return { - amount: Amount.fromRaw(result.balance, result.decimals, result.symbol), - symbol: result.symbol, + amount: Amount.fromRaw(balance, 18, 'TOKEN'), + symbol: 'TOKEN', } } catch { return { @@ -80,30 +93,18 @@ export class EvmAssetService implements IAssetService { } async getTokenBalances(address: Address): Promise { - // Return only native balance for now const nativeBalance = await this.getNativeBalance(address) return [nativeBalance] } async getTokenMetadata(tokenAddress: Address): Promise { - try { - const result = await this.fetch<{ name: string; symbol: string; decimals: number }>( - '/token/info', - { tokenAddress }, - ) - return { - address: tokenAddress, - name: result.name, - symbol: result.symbol, - decimals: result.decimals, - } - } catch { - return { - address: tokenAddress, - name: 'Unknown Token', - symbol: 'UNKNOWN', - decimals: 18, - } + // ERC20 metadata queries would require multiple eth_call requests + // Return minimal info for now + return { + address: tokenAddress, + name: 'Unknown Token', + symbol: 'UNKNOWN', + decimals: 18, } } } diff --git a/src/services/chain-adapter/evm/chain-service.ts b/src/services/chain-adapter/evm/chain-service.ts index c9ff1eb22..4505c1270 100644 --- a/src/services/chain-adapter/evm/chain-service.ts +++ b/src/services/chain-adapter/evm/chain-service.ts @@ -1,5 +1,7 @@ /** * EVM Chain Service + * + * Provides chain info and gas price queries via public JSON-RPC endpoints. */ import type { ChainConfig } from '@/services/chain-config' @@ -7,20 +9,39 @@ import type { IChainService, ChainInfo, GasPrice, HealthStatus } from '../types' import { Amount } from '@/types/amount' import { ChainServiceError, ChainErrorCodes } from '../types' +/** Default public RPC endpoints for EVM chains */ +const DEFAULT_RPC_URLS: Record = { + ethereum: 'https://ethereum-rpc.publicnode.com', + binance: 'https://bsc-rpc.publicnode.com', +} + +interface JsonRpcResponse { + jsonrpc: string + id: number + result?: T + error?: { code: number; message: string } +} + export class EvmChainService implements IChainService { private readonly config: ChainConfig - private readonly apiUrl: string - private readonly apiPath: string + private readonly rpcUrl: string constructor(config: ChainConfig) { this.config = config - this.apiUrl = config.api?.url ?? 'https://walletapi.bfmeta.info' - this.apiPath = config.api?.path ?? config.id + this.rpcUrl = DEFAULT_RPC_URLS[config.id] ?? config.api?.url ?? DEFAULT_RPC_URLS['ethereum']! } - private async fetch(endpoint: string): Promise { - const url = `${this.apiUrl}/wallet/${this.apiPath}${endpoint}` - const response = await fetch(url) + private async rpc(method: string, params: unknown[] = []): Promise { + const response = await fetch(this.rpcUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + method, + params, + id: 1, + }), + }) if (!response.ok) { throw new ChainServiceError( @@ -29,12 +50,9 @@ export class EvmChainService implements IChainService { ) } - const json = await response.json() as { success: boolean; result?: T; error?: { message: string } } - if (!json.success) { - throw new ChainServiceError( - ChainErrorCodes.NETWORK_ERROR, - json.error?.message ?? 'API request failed', - ) + const json = (await response.json()) as JsonRpcResponse + if (json.error) { + throw new ChainServiceError(ChainErrorCodes.NETWORK_ERROR, json.error.message) } return json.result as T @@ -54,8 +72,8 @@ export class EvmChainService implements IChainService { async getBlockHeight(): Promise { try { - const result = await this.fetch<{ height: number }>('/lastblock') - return BigInt(result.height) + const hexHeight = await this.rpc('eth_blockNumber') + return BigInt(hexHeight) } catch { return 0n } @@ -63,14 +81,13 @@ export class EvmChainService implements IChainService { async getGasPrice(): Promise { try { - const result = await this.fetch<{ gasPrice: string; baseFee?: string }>('/gasprice') - const gasPrice = Amount.fromRaw(result.gasPrice, 9, 'Gwei') // Gas price in Gwei + const hexGasPrice = await this.rpc('eth_gasPrice') + const gasPrice = Amount.fromRaw(BigInt(hexGasPrice).toString(), 9, 'Gwei') return { slow: gasPrice, standard: gasPrice, fast: gasPrice, - baseFee: result.baseFee ? Amount.fromRaw(result.baseFee, 9, 'Gwei') : undefined, lastUpdated: Date.now(), } } catch {