diff --git a/frontend/src/pages/AccountReceive.vue b/frontend/src/pages/AccountReceive.vue index 93e8c6aa0..981368cdb 100644 --- a/frontend/src/pages/AccountReceive.vue +++ b/frontend/src/pages/AccountReceive.vue @@ -391,6 +391,98 @@ function useScan() { return provider.getBlock('latest'); } + function getPonderNetworkName(chainId: number): string | null { + switch (chainId) { + case 1: + return 'mainnet'; + case 10: + return 'optimism'; + case 137: + return 'polygon'; + case 8453: + return 'base'; + case 42161: + return 'arbitrumOne'; + case 11155111: + return 'sepolia'; + default: + return null; + } + } + + async function getSubgraphHeadBlockNumber(chainId: number, subgraphUrl: string): Promise { + const headers = { 'Content-Type': 'application/json' }; + const ponderNetwork = getPonderNetworkName(chainId); + + if (ponderNetwork) { + try { + const ponderResponse = await fetch(subgraphUrl, { + method: 'POST', + headers, + body: JSON.stringify({ + query: `{ + _meta { + status + } + }`, + }), + }); + + if (!ponderResponse.ok) throw new Error(`Subgraph head request failed with status ${ponderResponse.status}`); + + const ponderPayload = (await ponderResponse.json()) as { + data?: { _meta?: { status?: Record } }; + errors?: Array<{ message: string }>; + }; + + if (ponderPayload.errors?.length) { + throw new Error(ponderPayload.errors.map((error) => error.message).join('; ')); + } + + const ponderHead = ponderPayload.data?._meta?.status?.[ponderNetwork]?.block?.number; + if (typeof ponderHead === 'number' && Number.isFinite(ponderHead)) { + return ponderHead; + } + } catch { + // Legacy endpoints can reject the Ponder query with different HTTP statuses or GraphQL errors. + } + } + + const legacyResponse = await fetch(subgraphUrl, { + method: 'POST', + headers, + body: JSON.stringify({ + query: `{ + _meta { + block { + number + } + } + }`, + }), + }); + + if (!legacyResponse.ok) throw new Error(`Legacy subgraph head request failed with status ${legacyResponse.status}`); + + const legacyPayload = (await legacyResponse.json()) as { + data?: { _meta?: { block?: { number?: number | string } } }; + errors?: Array<{ message: string }>; + }; + if (legacyPayload.errors?.length) { + throw new Error(legacyPayload.errors.map((error) => error.message).join('; ')); + } + + const legacyHead = legacyPayload.data?._meta?.block?.number; + if (typeof legacyHead === 'number' && Number.isFinite(legacyHead)) { + return legacyHead; + } + if (typeof legacyHead === 'string' && legacyHead.length > 0) { + return Number(legacyHead); + } + + throw new Error(`Missing subgraph head block for chain ${chainId}`); + } + async function scan() { // Reset paused state paused.value = false; @@ -409,7 +501,8 @@ function useScan() { window.logger.debug( `Scanning for announcements from ${startBlockLocal.value ?? 'undefined'} to ${endBlockLocal.value ?? 'undefined'}` ); - const overrides = { startBlock: startBlockLocal.value, endBlock: endBlockLocal.value }; + // Cleared number inputs can be empty strings; treat an empty end block as an unrestricted scan. + const overrides = { startBlock: startBlockLocal.value, endBlock: endBlockLocal.value || undefined }; // Scan for funds const spendingPubKey = chooseKey(spendingKeyPair.value?.publicKeyHex); @@ -482,6 +575,19 @@ function useScan() { const latestBlock: Block = await getLastBlock(provider.value!); mostRecentBlockNumber.value = latestBlock.number; mostRecentBlockTimestamp.value = latestBlock.timestamp; + let nextStartBlock: number | undefined; + if (umbra.value.chainConfig.subgraphUrl) { + try { + nextStartBlock = await getSubgraphHeadBlockNumber( + umbra.value.chainConfig.chainId, + umbra.value.chainConfig.subgraphUrl + ); + } catch (error) { + window.logger.warn('Failed to fetch subgraph head block, preserving previous checkpoint', error); + } + } else { + nextStartBlock = latestBlock.number; + } // Default scan behavior for await (const announcementsBatch of umbra.value.fetchSomeAnnouncements( @@ -533,11 +639,12 @@ function useScan() { await filterUserAnnouncementsAsync(spendingPubKey, viewingPrivKey, announcementsQueue); scanStatus.value = 'complete'; - // Save the latest block to localStorage for future scans as the start block - setLastFetchedBlock(latestBlock.number); - - // Update startBlockLocal with the latest block number - startBlockLocal.value = latestBlock.number; + // Cap the checkpoint at the RPC head too, since announcement fetching may fall back to RPC logs. + if (nextStartBlock !== undefined) { + const checkpoint = Math.min(nextStartBlock, latestBlock.number, overrides.endBlock ?? nextStartBlock); + setLastFetchedBlock(checkpoint); + startBlockLocal.value = checkpoint; + } } } catch (e) { scanStatus.value = 'waiting'; // reset to the default state because we were unable to fetch announcements