From 248714aa5b64999644a887e50896b84e71714035 Mon Sep 17 00:00:00 2001 From: Ifeoluwa Date: Mon, 13 Apr 2026 17:15:13 +0100 Subject: [PATCH 1/3] update --- yarn.lock | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/yarn.lock b/yarn.lock index 390f4fae2..87fa85087 100644 --- a/yarn.lock +++ b/yarn.lock @@ -68,6 +68,24 @@ __metadata: languageName: node linkType: hard +"@a16z/helios@npm:^0.11.1": + version: 0.11.1 + resolution: "@a16z/helios@npm:0.11.1" + dependencies: + "@1inch/byte-utils": "npm:3.0.0" + ethers: "npm:6.13.5" + peerDependencies: + assert: ^2.0.0 + axios: ">=1 <2" + peerDependenciesMeta: + assert: + optional: true + axios: + optional: true + checksum: 10/af71246377bfa3f2e60d755ae049691efc80fef59d9d6517b6f300a9e1202294f70a8023bb3559be74ecbfd705c871a74c55f5c71e8ec4b26c695446bb2862fa + languageName: node + linkType: hard + "@a16z/helios@npm:^0.11.1": version: 0.11.1 resolution: "@a16z/helios@npm:0.11.1" @@ -78,6 +96,13 @@ __metadata: languageName: node linkType: hard +"@acemir/cssom@npm:^0.9.28": + version: 0.9.28 + resolution: "@acemir/cssom@npm:0.9.28" + checksum: 10/429faa2af7bffdcf3311e984f6b8ced77bf58d936e0e9a22c2fc892a84e54d2c8cbd71ae3a979ef54241509ac7c866cb880d919e3e713e64c865a1cd4ede3a1f + languageName: node + linkType: hard + "@achrinza/node-ipc@npm:^9.2.5": version: 9.2.9 resolution: "@achrinza/node-ipc@npm:9.2.9" From faf4bcd88ea097d43dc921f345059772ba988f4f Mon Sep 17 00:00:00 2001 From: Ifeoluwa Date: Mon, 13 Apr 2026 19:28:42 +0100 Subject: [PATCH 2/3] fix suggested changes --- .../ethereum/libs/helios-verifier.ts | 94 ++++-- .../ethereum/types/erc20-token.ts.bak | 45 --- .../ethereum/types/evm-network.ts.bak | 286 ------------------ .../ui/action/views/network-assets/index.vue | 44 ++- .../action/views/network-assets/index.vue.bak | 267 ---------------- 5 files changed, 102 insertions(+), 634 deletions(-) delete mode 100644 packages/extension/src/providers/ethereum/types/erc20-token.ts.bak delete mode 100644 packages/extension/src/providers/ethereum/types/evm-network.ts.bak delete mode 100644 packages/extension/src/ui/action/views/network-assets/index.vue.bak diff --git a/packages/extension/src/providers/ethereum/libs/helios-verifier.ts b/packages/extension/src/providers/ethereum/libs/helios-verifier.ts index eac0ae992..8c653035c 100644 --- a/packages/extension/src/providers/ethereum/libs/helios-verifier.ts +++ b/packages/extension/src/providers/ethereum/libs/helios-verifier.ts @@ -1,3 +1,10 @@ +/** + * helios-verifier.ts + * + * Experimental light-client verification layer using Helios (@a16z/helios). + * Only active on Ethereum mainnet (chainId 0x1). + */ + export interface VerificationResult { verified: boolean; tampered: boolean; @@ -7,26 +14,70 @@ export interface VerificationResult { const SKIP: VerificationResult = { verified: false, tampered: false, message: '' }; const MAINNET_CHAIN_ID = '0x1'; +const DEFAULT_CONSENSUS_RPC = 'https://ethereum.operationsolarstorm.org'; +const CHECKPOINT_FETCH_TIMEOUT_MS = 5_000; -// DEMO MODE: simulate Helios detecting a lying RPC -let heliosProvider: any = { - waitSynced: () => Promise.resolve(), - request: async (args: any) => { - if (args.method === 'eth_call') { - return '0x0000000000000000000000000000000000000000000000000000000000000001'; - } - return '0x0'; - } -}; +let heliosProvider: any = null; let initInProgress = false; -let isSynced = true; +let isSynced = false; +let currentExecutionRpc = ''; + +async function fetchFreshCheckpoint(consensusRpc: string): Promise { + const url = `${consensusRpc.replace(/\/$/, '')}/eth/v1/beacon/headers/finalized`; + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), CHECKPOINT_FETCH_TIMEOUT_MS); + try { + const res = await fetch(url, { signal: controller.signal }); + if (!res.ok) return undefined; + const json = (await res.json()) as { data?: { root?: string } }; + const root = json?.data?.root; + if (typeof root === 'string' && root.startsWith('0x')) return root; + return undefined; + } catch { + return undefined; + } finally { + clearTimeout(timer); + } +} -export async function initHelios(executionRpc: string, consensusRpc?: string): Promise { - return; +export async function initHelios( + executionRpc: string, + consensusRpc: string = DEFAULT_CONSENSUS_RPC, +): Promise { + if (initInProgress) return; + if (heliosProvider && currentExecutionRpc === executionRpc) return; + initInProgress = true; + heliosProvider = null; + isSynced = false; + currentExecutionRpc = executionRpc; + try { + const checkpoint = await fetchFreshCheckpoint(consensusRpc); + const { createHeliosProvider } = await import('@a16z/helios'); + const config: Record = { + executionRpc, + consensusRpc, + network: 'mainnet', + }; + if (checkpoint) config['checkpoint'] = checkpoint; + heliosProvider = await createHeliosProvider(config, 'ethereum'); + heliosProvider + .waitSynced() + .then(() => { isSynced = true; console.log('[helios-verifier] synced and ready'); }) + .catch((err: unknown) => { console.warn('[helios-verifier] sync failed:', err); resetHelios(); }); + } catch (err) { + console.warn('[helios-verifier] failed to initialise:', err); + heliosProvider = null; + currentExecutionRpc = ''; + } finally { + initInProgress = false; + } } export function resetHelios(): void { - return; + heliosProvider = null; + isSynced = false; + initInProgress = false; + currentExecutionRpc = ''; } function encodeBalanceOf(address: string): string { @@ -47,10 +98,9 @@ export async function verifyErc20Balance( chainId: string, executionRpc: string, ): Promise { - console.log('[helios-verifier] chainId check:', chainId, MAINNET_CHAIN_ID); if (chainId.toLowerCase() !== MAINNET_CHAIN_ID) return SKIP; + if (!heliosProvider && !initInProgress) void initHelios(executionRpc); if (!isSynced || !heliosProvider) return SKIP; - let heliosBalanceHex: string; try { heliosBalanceHex = (await heliosProvider.request({ @@ -58,25 +108,19 @@ export async function verifyErc20Balance( params: [{ to: contractAddress, data: encodeBalanceOf(walletAddress) }, 'latest'], })) as string; } catch (err) { - console.warn('[helios-verifier] eth_call via Helios failed:', err); + console.warn('[helios-verifier] eth_call failed:', err); return SKIP; } - const rpcValue = decodeUint256(rpcBalance); const heliosValue = decodeUint256(heliosBalanceHex); - - console.log('[helios-verifier] rpcValue:', rpcValue.toString(), 'heliosValue:', heliosValue.toString()); - if (rpcValue === heliosValue) { return { verified: true, tampered: false, message: 'Balance verified by Helios light client.', provenBalance: heliosBalanceHex }; } - - console.error('[helios-verifier] MISMATCH DETECTED - RPC is lying!'); - + console.error(`[helios-verifier] MISMATCH: RPC=${rpcValue} Helios=${heliosValue}`); return { verified: false, tampered: true, - message: `Your RPC provider returned a balance of ${rpcValue.toString()} but the Helios light client cryptographically proved the real on-chain balance is ${heliosValue.toString()}. Your RPC provider may be lying. Consider switching to a trusted provider.`, + message: `Your RPC provider returned ${rpcValue.toString()} but Helios proved the real balance is ${heliosValue.toString()}. Your RPC provider may be lying.`, provenBalance: heliosBalanceHex, }; } diff --git a/packages/extension/src/providers/ethereum/types/erc20-token.ts.bak b/packages/extension/src/providers/ethereum/types/erc20-token.ts.bak deleted file mode 100644 index 2d767008d..000000000 --- a/packages/extension/src/providers/ethereum/types/erc20-token.ts.bak +++ /dev/null @@ -1,45 +0,0 @@ -import { BaseToken, BaseTokenOptions } from '@/types/base-token'; -import { numberToHex } from 'web3-utils'; -import erc20 from '../libs/abi/erc20'; -import EvmAPI from '../libs/api'; -import { NATIVE_TOKEN_ADDRESS } from '../libs/common'; -import { BNType } from '@/providers/common/types'; - -export interface Erc20TokenOptions extends BaseTokenOptions { - contract: string; -} - -export class Erc20Token extends BaseToken { - public contract: string; - - constructor(options: Erc20TokenOptions) { - super(options); - this.contract = options.contract; - } - - public async getLatestUserBalance( - api: EvmAPI, - address: string, - ): Promise { - if (this.contract === NATIVE_TOKEN_ADDRESS) - return api.getBalance(address.toLowerCase()); - else { - const contract = new api.web3.Contract( - erc20 as any, - this.contract.toLowerCase(), - ); - return contract.methods - .balanceOf(address.toLowerCase()) - .call() - .then((val: BNType) => { - const balance = numberToHex(val); - this.balance = balance; - return balance; - }); - } - } - - public async send(): Promise { - throw new Error('EVM-send is not implemented here'); - } -} diff --git a/packages/extension/src/providers/ethereum/types/evm-network.ts.bak b/packages/extension/src/providers/ethereum/types/evm-network.ts.bak deleted file mode 100644 index e395cae9f..000000000 --- a/packages/extension/src/providers/ethereum/types/evm-network.ts.bak +++ /dev/null @@ -1,286 +0,0 @@ -import MarketData from '@/libs/market-data'; -import Sparkline from '@/libs/sparkline'; -import { TokensState } from '@/libs/tokens-state'; -import { CustomErc20Token, TokenType } from '@/libs/tokens-state/types'; -import { formatFloatingPointValue } from '@/libs/utils/number-formatter'; -import { fromBase } from '@enkryptcom/utils'; -import { Activity } from '@/types/activity'; -import { BaseNetwork } from '@/types/base-network'; -import { BaseToken } from '@/types/base-token'; -import { NFTCollection } from '@/types/nft'; -import { AssetsType, ProviderName } from '@/types/provider'; -import { CoingeckoPlatform, NetworkNames, SignerType } from '@enkryptcom/types'; -import BigNumber from 'bignumber.js'; -import { toChecksumAddress } from '@ethereumjs/util'; -import { isAddress } from 'web3-utils'; -import API from '../libs/api'; -import createIcon from '../libs/blockies'; -import { NATIVE_TOKEN_ADDRESS } from '../libs/common'; -import { Erc20Token, Erc20TokenOptions } from './erc20-token'; -import { BNType } from '@/providers/common/types'; - -export interface EvmNetworkOptions { - name: NetworkNames; - name_long: string; - homePage: string; - blockExplorerTX: string; - blockExplorerAddr: string; - chainID: `0x${string}`; - isTestNetwork: boolean; - currencyName: string; - currencyNameLong: string; - node: string; - icon: string; - buyLink?: string | undefined; - coingeckoID?: string; - coingeckoPlatform?: CoingeckoPlatform; - basePath?: string; - NFTHandler?: ( - network: BaseNetwork, - address: string, - ) => Promise; - assetsInfoHandler?: ( - network: BaseNetwork, - address: string, - ) => Promise; - activityHandler: ( - network: BaseNetwork, - address: string, - ) => Promise; - customTokens?: boolean; - displayAddress?: (address: string, chainId?: BNType) => string; - isAddress?: (address: string, chainId?: BNType) => boolean; -} - -export class EvmNetwork extends BaseNetwork { - public chainID: `0x${string}`; - - private assetsInfoHandler?: ( - network: BaseNetwork, - address: string, - ) => Promise; - - NFTHandler?: ( - network: BaseNetwork, - address: string, - ) => Promise; - - private activityHandler: ( - network: BaseNetwork, - address: string, - ) => Promise; - - public assets: Erc20Token[] = []; - - public isAddress: (address: string, chainId?: BNType) => boolean; - - constructor(options: EvmNetworkOptions) { - const api = async () => { - const api = new API(options.node); - await api.init(); - return api; - }; - - const baseOptions = { - signer: [SignerType.secp256k1], - provider: ProviderName.ethereum, - displayAddress: options.displayAddress - ? options.displayAddress - : (address: string) => toChecksumAddress(address), - identicon: createIcon, - basePath: options.basePath ? options.basePath : "m/44'/60'/0'/0", - decimals: 18, - api, - ...options, - }; - - baseOptions.customTokens = baseOptions.customTokens ?? true; - super(baseOptions); - this.options = options; - - this.chainID = options.chainID; - this.assetsInfoHandler = options.assetsInfoHandler; - this.NFTHandler = options.NFTHandler; - this.activityHandler = options.activityHandler; - this.isAddress = options.isAddress - ? options.isAddress - : (address: string) => isAddress(address); - } - - public async getAllTokens(address: string): Promise { - const assets = await this.getAllTokenInfo(address); - return assets.map(token => { - const bTokenOptions: Erc20TokenOptions = { - decimals: token.decimals, - icon: token.icon, - name: token.name, - symbol: token.symbol, - balance: token.balance, - price: token.value, - contract: token.contract!, - }; - return new Erc20Token(bTokenOptions); - }); - } - - public async getAllTokenInfo(address: string): Promise { - const api = await this.api(); - const tokensState = new TokensState(); - const marketData = new MarketData(); - - let assets: AssetsType[] = []; - - if (this.assetsInfoHandler) { - assets = await this.assetsInfoHandler(this, address); - } else { - const balance = await (api as API).getBalance(address); - const nativeMarketData = ( - await marketData.getMarketData([this.coingeckoID!]) - )[0]; - const nativeUsdBalance = new BigNumber( - fromBase(balance, this.decimals), - ).times(nativeMarketData?.current_price ?? 0); - const nativeAsset: AssetsType = { - name: this.currencyNameLong, - symbol: this.currencyName, - icon: this.icon, - balance, - balancef: formatFloatingPointValue(fromBase(balance, this.decimals)) - .value, - balanceUSD: nativeUsdBalance.toNumber(), - balanceUSDf: nativeUsdBalance.toString(), - value: nativeMarketData?.current_price?.toString() ?? '0', - valuef: nativeMarketData?.current_price?.toString() ?? '0', - decimals: this.decimals, - sparkline: nativeMarketData - ? new Sparkline(nativeMarketData.sparkline_in_24h.price, 25) - .dataValues - : '', - priceChangePercentage: - nativeMarketData?.price_change_percentage_24h_in_currency ?? 0, - contract: NATIVE_TOKEN_ADDRESS, - }; - - await Promise.all( - this.assets.map(token => - token.getLatestUserBalance(api as API, address).then(balance => { - token.balance = balance; - }), - ), - ); - - const assetInfos = this.assets - .map(token => { - const assetsType: AssetsType = { - name: token.name, - symbol: token.symbol, - icon: token.icon, - balance: token.balance!, - balancef: formatFloatingPointValue( - fromBase(token.balance!, token.decimals), - ).value, - balanceUSD: 0, - balanceUSDf: '0', - value: '0', - valuef: '0', - decimals: token.decimals, - sparkline: '', - priceChangePercentage: 0, - contract: token.contract, - }; - return assetsType; - }) - .filter(asset => asset.balancef !== '0'); - - assets = [nativeAsset, ...assetInfos]; - } - const customTokens = await tokensState - .getTokensByNetwork(this.name) - .then(tokens => { - const erc20Tokens = tokens.filter(token => { - if (token.type !== TokenType.ERC20) { - return false; - } - - for (const a of assets) { - if ( - a.contract && - (token as CustomErc20Token).address && - a.contract.toLowerCase() === - (token as CustomErc20Token).address.toLowerCase() - ) { - return false; - } - } - - return true; - }) as CustomErc20Token[]; - - return erc20Tokens.map(({ name, symbol, address, icon, decimals }) => { - return new Erc20Token({ - name, - symbol, - contract: address, - icon, - decimals, - }); - }); - }); - - const balancePromises = customTokens.map(token => - token.getLatestUserBalance(api as API, address), - ); - - await Promise.all(balancePromises); - - const marketInfos = await marketData.getMarketInfoByContracts( - customTokens.map(token => token.contract.toLowerCase()), - this.coingeckoPlatform!, - ); - - const customAssets: AssetsType[] = customTokens.map(token => { - const asset: AssetsType = { - name: token.name, - symbol: token.symbol, - balance: token.balance ?? '0', - balancef: formatFloatingPointValue( - fromBase(token.balance ?? '0', token.decimals), - ).value, - contract: token.contract, - balanceUSD: 0, - balanceUSDf: '0', - value: '0', - valuef: '0', - decimals: token.decimals, - sparkline: '', - priceChangePercentage: 0, - icon: token.icon, - }; - - const marketInfo = marketInfos[token.contract.toLowerCase()]; - - if (marketInfo) { - const usdBalance = new BigNumber( - fromBase(token.balance ?? '0', token.decimals), - ).times(marketInfo.current_price ?? 0); - asset.balanceUSD = usdBalance.toNumber(); - asset.balanceUSDf = usdBalance.toString(); - asset.value = marketInfo.current_price?.toString() ?? '0'; - asset.valuef = marketInfo.current_price?.toString() ?? '0'; - asset.sparkline = new Sparkline( - marketInfo.sparkline_in_24h.price, - 25, - ).dataValues; - asset.priceChangePercentage = - marketInfo.price_change_percentage_24h_in_currency || 0; - } - - return asset; - }); - - return [...assets, ...customAssets]; - } - public getAllActivity(address: string): Promise { - return this.activityHandler(this, address); - } -} diff --git a/packages/extension/src/ui/action/views/network-assets/index.vue b/packages/extension/src/ui/action/views/network-assets/index.vue index 50b06195d..61e5f1598 100644 --- a/packages/extension/src/ui/action/views/network-assets/index.vue +++ b/packages/extension/src/ui/action/views/network-assets/index.vue @@ -35,7 +35,7 @@ import NetworkAssetsLoading from './components/network-assets-loading.vue'; import NetworkAssetsError from './components/network-assets-error.vue'; import HeliosWarningBanner from './components/helios-warning-banner.vue'; import CustomScrollbar from '@action/components/custom-scrollbar/index.vue'; -import { computed, onMounted, type PropType, ref, toRef, watch } from 'vue'; +import { computed, onBeforeUnmount, onMounted, type PropType, ref, toRef, watch } from 'vue'; import type { AssetsType } from '@/types/provider'; import type { AccountsHeaderData } from '../../types/account'; import accountInfoComposable from '@action/composables/account-info'; @@ -72,13 +72,25 @@ const isFetchError = ref(false); const heliosWarning = ref(null); const heliosBannerDismissed = ref(false); +// --- Helios poll helpers --- +const heliosPollId = ref(null); + +function clearHeliosPoll(): void { + if (heliosPollId.value !== null) { + window.clearInterval(heliosPollId.value); + heliosPollId.value = null; + } +} +// -------------------------- + const { cryptoAmount, fiatAmount, tokenPrice, priceChangePercentage, sparkline } = accountInfoComposable(toRef(props, 'network'), toRef(props, 'accountInfo')); const selected: string = route.params.id as string; function scheduleHeliosResultCheck(snapshotAssets: AssetsType[]): void { + clearHeliosPoll(); const MAX_ATTEMPTS = 15; let attempts = 0; - const interval = setInterval(() => { + heliosPollId.value = window.setInterval(() => { attempts++; for (const asset of snapshotAssets) { const v = asset.heliosVerification; @@ -89,11 +101,11 @@ function scheduleHeliosResultCheck(snapshotAssets: AssetsType[]): void { rpcBalance: asset.balance, provenBalance: v.provenBalance ?? '0x0', }; - clearInterval(interval); + clearHeliosPoll(); return; } } - if (attempts >= MAX_ATTEMPTS) clearInterval(interval); + if (attempts >= MAX_ATTEMPTS) clearHeliosPoll(); }, 2000); } @@ -103,25 +115,32 @@ const updateAssets = () => { assets.value = []; heliosWarning.value = null; heliosBannerDismissed.value = false; - const currentNetwork = selectedNetworkName.value; - if (props.accountInfo.selectedAccount?.address) { + const requestKey = `${selectedNetworkName.value}:${selectedSubnetwork.value}:${selectedAddress.value}`; + const address = props.accountInfo.selectedAccount?.address; + if (!address) { + isLoading.value = false; + return; + } + props.network - .getAllTokenInfo(props.accountInfo.selectedAccount?.address || '') + .getAllTokenInfo(address) .then(_assets => { - if (selectedNetworkName.value !== currentNetwork) return; + const activeKey = `${selectedNetworkName.value}:${selectedSubnetwork.value}:${selectedAddress.value}`; + if (activeKey !== requestKey) return; assets.value = _assets; isLoading.value = false; scheduleHeliosResultCheck(_assets); }) .catch(e => { console.error(e); - if (selectedNetworkName.value !== currentNetwork) return; + const activeKey = `${selectedNetworkName.value}:${selectedSubnetwork.value}:${selectedAddress.value}`; + if (activeKey !== requestKey) return; isFetchError.value = true; isLoading.value = false; assets.value = []; }); } -}; + const selectedAddress = computed(() => props.accountInfo.selectedAccount?.address || ''); const selectedNetworkName = computed(() => props.network.name); @@ -133,10 +152,13 @@ const isEvmNetwork = computed(() => props.network.provider === ProviderName.ethe const isMassaNetwork = computed(() => props.network.provider === ProviderName.massa); watch([selectedAddress, selectedNetworkName, selectedSubnetwork], () => { + clearHeliosPoll(); resetHelios(); updateAssets(); }); +onBeforeUnmount(clearHeliosPoll); + onMounted(async () => { updateAssets(); if (await bannersState.showNetworkAssetsSolanaStakingBanner()) { @@ -168,4 +190,4 @@ const closeSolanaStakingBanner = () => { + \ No newline at end of file diff --git a/packages/extension/src/ui/action/views/network-assets/index.vue.bak b/packages/extension/src/ui/action/views/network-assets/index.vue.bak deleted file mode 100644 index 85210cf84..000000000 --- a/packages/extension/src/ui/action/views/network-assets/index.vue.bak +++ /dev/null @@ -1,267 +0,0 @@ - - - - - - - From abdc7dc00f7aea25efafa7eee17b66e028e4346a Mon Sep 17 00:00:00 2001 From: Ifeoluwa Date: Mon, 13 Apr 2026 20:38:45 +0100 Subject: [PATCH 3/3] still fixing suggestions --- .../ethereum/libs/helios-verifier.ts | 83 +++++++++++-------- .../providers/ethereum/types/erc20-token.ts | 4 +- 2 files changed, 51 insertions(+), 36 deletions(-) diff --git a/packages/extension/src/providers/ethereum/libs/helios-verifier.ts b/packages/extension/src/providers/ethereum/libs/helios-verifier.ts index 8c653035c..3e6092785 100644 --- a/packages/extension/src/providers/ethereum/libs/helios-verifier.ts +++ b/packages/extension/src/providers/ethereum/libs/helios-verifier.ts @@ -1,9 +1,3 @@ -/** - * helios-verifier.ts - * - * Experimental light-client verification layer using Helios (@a16z/helios). - * Only active on Ethereum mainnet (chainId 0x1). - */ export interface VerificationResult { verified: boolean; @@ -18,9 +12,11 @@ const DEFAULT_CONSENSUS_RPC = 'https://ethereum.operationsolarstorm.org'; const CHECKPOINT_FETCH_TIMEOUT_MS = 5_000; let heliosProvider: any = null; -let initInProgress = false; +let initPromise: Promise | null = null; +let syncPromise: Promise | null = null; let isSynced = false; let currentExecutionRpc = ''; +let sessionId = 0; async function fetchFreshCheckpoint(consensusRpc: string): Promise { const url = `${consensusRpc.replace(/\/$/, '')}/eth/v1/beacon/headers/finalized`; @@ -44,39 +40,50 @@ export async function initHelios( executionRpc: string, consensusRpc: string = DEFAULT_CONSENSUS_RPC, ): Promise { - if (initInProgress) return; + if (initPromise) return initPromise; if (heliosProvider && currentExecutionRpc === executionRpc) return; - initInProgress = true; + const session = ++sessionId; heliosProvider = null; isSynced = false; currentExecutionRpc = executionRpc; - try { - const checkpoint = await fetchFreshCheckpoint(consensusRpc); - const { createHeliosProvider } = await import('@a16z/helios'); - const config: Record = { - executionRpc, - consensusRpc, - network: 'mainnet', - }; - if (checkpoint) config['checkpoint'] = checkpoint; - heliosProvider = await createHeliosProvider(config, 'ethereum'); - heliosProvider - .waitSynced() - .then(() => { isSynced = true; console.log('[helios-verifier] synced and ready'); }) - .catch((err: unknown) => { console.warn('[helios-verifier] sync failed:', err); resetHelios(); }); - } catch (err) { - console.warn('[helios-verifier] failed to initialise:', err); - heliosProvider = null; - currentExecutionRpc = ''; - } finally { - initInProgress = false; - } + initPromise = (async () => { + try { + const checkpoint = await fetchFreshCheckpoint(consensusRpc); + const { createHeliosProvider } = await import('@a16z/helios'); + const config: Record = { + executionRpc, + consensusRpc, + network: 'mainnet', + }; + if (checkpoint) config['checkpoint'] = checkpoint; + const provider = await createHeliosProvider(config, 'ethereum'); + if (session !== sessionId) return; + heliosProvider = provider; + syncPromise = provider.waitSynced().then(() => { + if (session === sessionId && heliosProvider === provider) { + isSynced = true; + console.log('[helios-verifier] synced and ready'); + } + }); + await syncPromise; + } catch (err) { + if (session === sessionId) { + console.warn('[helios-verifier] failed to initialise:', err); + resetHelios(); + } + } finally { + if (session === sessionId) initPromise = null; + } + })(); + return initPromise; } export function resetHelios(): void { + sessionId++; heliosProvider = null; isSynced = false; - initInProgress = false; + initPromise = null; + syncPromise = null; currentExecutionRpc = ''; } @@ -95,17 +102,18 @@ export async function verifyErc20Balance( contractAddress: string, walletAddress: string, rpcBalance: string, + blockTag: string, chainId: string, executionRpc: string, ): Promise { if (chainId.toLowerCase() !== MAINNET_CHAIN_ID) return SKIP; - if (!heliosProvider && !initInProgress) void initHelios(executionRpc); + await initHelios(executionRpc); if (!isSynced || !heliosProvider) return SKIP; let heliosBalanceHex: string; try { heliosBalanceHex = (await heliosProvider.request({ method: 'eth_call', - params: [{ to: contractAddress, data: encodeBalanceOf(walletAddress) }, 'latest'], + params: [{ to: contractAddress, data: encodeBalanceOf(walletAddress) }, blockTag], })) as string; } catch (err) { console.warn('[helios-verifier] eth_call failed:', err); @@ -114,7 +122,12 @@ export async function verifyErc20Balance( const rpcValue = decodeUint256(rpcBalance); const heliosValue = decodeUint256(heliosBalanceHex); if (rpcValue === heliosValue) { - return { verified: true, tampered: false, message: 'Balance verified by Helios light client.', provenBalance: heliosBalanceHex }; + return { + verified: true, + tampered: false, + message: 'Balance verified by Helios light client.', + provenBalance: heliosBalanceHex, + }; } console.error(`[helios-verifier] MISMATCH: RPC=${rpcValue} Helios=${heliosValue}`); return { @@ -123,4 +136,4 @@ export async function verifyErc20Balance( message: `Your RPC provider returned ${rpcValue.toString()} but Helios proved the real balance is ${heliosValue.toString()}. Your RPC provider may be lying.`, provenBalance: heliosBalanceHex, }; -} +} \ No newline at end of file diff --git a/packages/extension/src/providers/ethereum/types/erc20-token.ts b/packages/extension/src/providers/ethereum/types/erc20-token.ts index 54a74bf98..323ae0e92 100644 --- a/packages/extension/src/providers/ethereum/types/erc20-token.ts +++ b/packages/extension/src/providers/ethereum/types/erc20-token.ts @@ -45,10 +45,12 @@ export class Erc20Token extends BaseToken { }); if (chainId) { + // blockTag is 'latest' because the web3 contract call does not expose verifyErc20Balance( this.contract, address.toLowerCase(), balance, + 'latest', chainId, api.node, ) @@ -64,4 +66,4 @@ export class Erc20Token extends BaseToken { public async send(): Promise { throw new Error('EVM-send is not implemented here'); } -} +} \ No newline at end of file