From a2a6a3d5df7b82d8a8808c8be8a5a0dfeb8cd812 Mon Sep 17 00:00:00 2001 From: John Feras Date: Thu, 2 May 2024 15:38:20 -0400 Subject: [PATCH 1/7] Query StealthKeyChanged events from subgraph for user registration block number and stealthkeys --- frontend/package.json | 2 +- umbra-js/package.json | 2 +- umbra-js/src/classes/Umbra.ts | 76 ++---------- umbra-js/src/types.ts | 15 +++ umbra-js/src/utils/utils.ts | 217 ++++++++++++++++++++++++++++++++-- 5 files changed, 229 insertions(+), 83 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index aa2fb67ae..b33017575 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,7 +20,7 @@ "@adraffy/ens-normalize": "1.9.2", "@metamask/jazzicon": "^2.0.0", "@quasar/extras": "^1.15.8", - "@umbracash/umbra-js": "0.1.6", + "@umbracash/umbra-js": "file:../umbra-js", "@uniswap/token-lists": "^1.0.0-beta.19", "@unstoppabledomains/resolution": "8.5.0", "@web3-onboard/coinbase": "2.2.7", diff --git a/umbra-js/package.json b/umbra-js/package.json index a82d9e589..f5566eeeb 100644 --- a/umbra-js/package.json +++ b/umbra-js/package.json @@ -1,6 +1,6 @@ { "name": "@umbracash/umbra-js", - "version": "0.1.6", + "version": "0.2.0", "description": "Send and receive stealth payments", "main": "build/src/index.js", "types": "build/src/index.d.ts", diff --git a/umbra-js/src/classes/Umbra.ts b/umbra-js/src/classes/Umbra.ts index 7271dfab8..1deba5e60 100644 --- a/umbra-js/src/classes/Umbra.ts +++ b/umbra-js/src/classes/Umbra.ts @@ -33,10 +33,11 @@ import { assertSupportedAddress, checkSupportedAddresses, getBlockNumberUserRegistered, + recursiveGraphFetch, } from '../utils/utils'; import { Umbra as UmbraContract, Umbra__factory, ERC20__factory } from '../typechain'; import { ETH_ADDRESS, UMBRA_BATCH_SEND_ABI } from '../utils/constants'; -import type { Announcement, ChainConfig, EthersProvider, GraphFilterOverride, ScanOverrides, SendOverrides, SubgraphAnnouncement, UserAnnouncement, AnnouncementDetail, SendBatch, SendData} from '../types'; // prettier-ignore +import type { Announcement, ChainConfig, EthersProvider, ScanOverrides, SendOverrides, SubgraphAnnouncement, UserAnnouncement, AnnouncementDetail, SendBatch, SendData} from '../types'; // prettier-ignore // Mapping from chainId to contract information const umbraAddress = '0xFb2dc580Eed955B528407b4d36FfaFe3da685401'; // same on all supported networks @@ -72,7 +73,7 @@ const chainConfigs: Record = { * @notice Helper method to parse chainConfig input and return a valid chain configuration * @param chainConfig Supported chainID as number, or custom ChainConfig */ -const parseChainConfig = (chainConfig: ChainConfig | number) => { +export const parseChainConfig = (chainConfig: ChainConfig | number) => { if (!chainConfig) { throw new Error('chainConfig not provided'); } @@ -372,7 +373,7 @@ export class Umbra { } /** - * @notice Fetches all Umbra event logs using Goldsky, if available, falling back to RPC if not + * @notice Fetches all Umbra event logs using a subgraph, if available, falling back to RPC if not * @param overrides Override the start and end block used for scanning; * @returns A list of Announcement events supplemented with additional metadata, such as the sender, block, * timestamp, and txhash @@ -397,7 +398,7 @@ export class Umbra { return filtered.filter((i) => i !== null) as AnnouncementDetail[]; }; - // Try querying events using Goldsky, fallback to querying logs. + // Try querying events using a subgraph, fallback to querying logs. if (this.chainConfig.subgraphUrl) { try { for await (const subgraphAnnouncements of this.fetchAllAnnouncementsFromSubgraph(startBlock, endBlock)) { @@ -417,7 +418,7 @@ export class Umbra { /** * @notice Fetches Umbra event logs starting from the block user registered their stealth keys in using - * Goldsky, if available, falling back to RPC if not + * a subgraph, if available, falling back to RPC if not * @param overrides Override the start and end block used for scanning; * @returns A list of Announcement events supplemented with additional metadata, such as the sender, block, * timestamp, and txhash @@ -427,7 +428,7 @@ export class Umbra { address: string, overrides: ScanOverrides = {} ): AsyncGenerator { - const registeredBlockNumber = await getBlockNumberUserRegistered(address, Signer.provider); + const registeredBlockNumber = await getBlockNumberUserRegistered(address, Signer.provider, this.chainConfig); // Get start and end blocks to scan events for const startBlock = overrides.startBlock || registeredBlockNumber || this.chainConfig.startBlock; const endBlock = overrides.endBlock || 'latest'; @@ -437,7 +438,7 @@ export class Umbra { } /** - * @notice Fetches all Umbra event logs using Goldsky + * @notice Fetches all Umbra event logs using a subgraph * @param startBlock Scanning start block * @param endBlock Scannding end block * @returns A list of Announcement events supplemented with additional metadata, such as the sender, block, @@ -732,67 +733,6 @@ export class Umbra { // ============================== PRIVATE, FUNCTIONAL HELPER METHODS ============================== -/** - * @notice Generic method to recursively grab every 'page' of results - * @dev NOTE: the query MUST return the ID field - * @dev Modifies from: https://github.com/dcgtc/dgrants/blob/f5a783524d0b56eea12c127b2146fba8fb9273b4/app/src/utils/utils.ts#L443 - * @dev Relevant docs: https://thegraph.com/docs/developer/graphql-api#example-3 - * @dev Lives outside of the class instance because user's should not need access to this method - * @dev TODO support node.js by replacing reliance on browser's fetch module with https://github.com/paulmillr/micro-ftch - * @param url the url we will recursively fetch from - * @param key the key in the response object which holds results - * @param query a function which will return the query string (with the page in place) - * @param before the current array of objects - */ -async function* recursiveGraphFetch( - url: string, - key: string, - query: (filter: string) => string, - before: any[] = [], - overrides?: GraphFilterOverride -): AsyncGenerator { - // retrieve the last ID we collected to use as the starting point for this query - const fromId = before.length ? (before[before.length - 1].id as string | number) : false; - let startBlockFilter = ''; - let endBlockFilter = ''; - const startBlock = overrides?.startBlock ? overrides.startBlock.toString() : ''; - const endBlock = overrides?.endBlock ? overrides?.endBlock.toString() : ''; - - if (startBlock) { - startBlockFilter = `block_gte: "${startBlock}",`; - } - - if (endBlock && endBlock !== 'latest') { - endBlockFilter = `block_lte: "${endBlock}",`; - } - // Fetch this 'page' of results - please note that the query MUST return an ID - const res = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - query: query(` - first: 1000, - orderBy: id, - orderDirection: desc, - where: { - ${fromId ? `id_lt: "${fromId}",` : ''} - ${startBlockFilter} - ${endBlockFilter} - } - `), - }), - }); - - // Resolve the json - const json = await res.json(); - - // If there were results on this page yield the results then query the next page, otherwise do nothing. - if (json.data[key].length) { - yield json.data[key]; // yield the data for this page - yield* recursiveGraphFetch(url, key, query, [...before, ...json.data[key]], overrides); // yield the data for the next pages - } -} - /** * @notice Tries withdrawing ETH from a stealth address on behalf of a user * @dev Attempts multiple retries before returning an error. Retries only occur if there was an diff --git a/umbra-js/src/types.ts b/umbra-js/src/types.ts index b50285e0b..351b3b197 100644 --- a/umbra-js/src/types.ts +++ b/umbra-js/src/types.ts @@ -118,6 +118,20 @@ export interface UserAnnouncement { txHash: string; } +// StealthKeyChanged event data received from subgraph queries +export interface SubgraphStealthKeyChangedEvent { + block: string; + from: string; + id: string; // the subgraph uses an ID of `timestamp-logIndex` + registrant: string; + spendingPubKeyPrefix: BigNumber; + spendingPubKey: BigNumber; + timestamp: string; + txHash: string; + viewingPubKeyPrefix: BigNumber; + viewingPubKey: BigNumber; +} + export interface SendBatch { token: string; amount: BigNumberish; @@ -135,4 +149,5 @@ export interface SendData { export type GraphFilterOverride = { startBlock?: number | string; endBlock?: number | string; + registrant?: string; }; diff --git a/umbra-js/src/utils/utils.ts b/umbra-js/src/utils/utils.ts index 617d0ce88..ce4347c7b 100644 --- a/umbra-js/src/utils/utils.ts +++ b/umbra-js/src/utils/utils.ts @@ -24,8 +24,17 @@ import { ens, cns } from '..'; import { default as Resolution } from '@unstoppabledomains/resolution'; import { StealthKeyRegistry } from '../classes/StealthKeyRegistry'; import { TxHistoryProvider } from '../classes/TxHistoryProvider'; -import { EthersProvider, TransactionResponseExtended } from '../types'; +import { KeyPair } from '../classes/KeyPair'; +import { + EthersProvider, + TransactionResponseExtended, + GraphFilterOverride, + ChainConfig, + SubgraphStealthKeyChangedEvent, + ScanOverrides, +} from '../types'; import { StealthKeyChangedEvent } from 'src/typechain/contracts/StealthKeyRegistry'; +import { parseChainConfig } from '../classes/Umbra'; // Lengths of various properties when represented as full hex strings export const lengths = { @@ -216,6 +225,7 @@ export async function lookupRecipient( supportTxHash, }: { advanced?: boolean; supportPubKey?: boolean; supportTxHash?: boolean } = {} ) { + const chainId = (await provider.getNetwork()).chainId; // Check if identifier is a public key. If so we just return that directly const isPublicKey = id.length === 132 && isHexString(id); if (supportPubKey && isPublicKey) { @@ -235,10 +245,31 @@ export async function lookupRecipient( // ENS name, CNS name, or address, so we resolve it to an address const address = await toAddress(id, provider); // throws if an invalid address is provided - // If we're not using advanced mode, use the StealthKeyRegistry + // If we're not using advanced mode, use the StealthKeyRegistry events if (!advanced) { - const registry = new StealthKeyRegistry(provider); - return registry.getStealthKeys(address); + // Fetch the stealth key registry event from the subgraph and fall back to the registry contract if the subgraph returns an error + try { + const chainConfig = parseChainConfig(chainId); + const stealthKeyChangedEvent = await getMostRecentSubgraphStealthKeyChangedEventFromAddress(address, chainConfig); + const spendingPublicKey = KeyPair.getUncompressedFromX( + stealthKeyChangedEvent.spendingPubKey, + stealthKeyChangedEvent.spendingPubKeyPrefix.toString() + ); + const viewingPublicKey = KeyPair.getUncompressedFromX( + stealthKeyChangedEvent.viewingPubKey, + stealthKeyChangedEvent.viewingPubKeyPrefix.toString() + ); + return { spendingPublicKey: spendingPublicKey, viewingPublicKey: viewingPublicKey }; + } catch (error) { + if (error instanceof Error) { + console.log('Public key subgraph fetch error: ', error.message); + } else { + console.log('An unknown error occurred: ', error); + } + console.log('Error using subgraph to lookup receipient stealth keys, will query registry contract'); + const registry = new StealthKeyRegistry(provider); + return registry.getStealthKeys(address); + } } // Otherwise, get public key based on the most recent transaction sent by that address @@ -249,21 +280,181 @@ export async function lookupRecipient( return { spendingPublicKey: publicKey, viewingPublicKey: publicKey }; } -export async function getBlockNumberUserRegistered(address: string, provider: StaticJsonRpcProvider) { +export async function getBlockNumberUserRegistered( + address: string, + provider: StaticJsonRpcProvider, + chainConfig: ChainConfig +) { + // Fetch the stealth key registry event from the subgraph and fall back to the registry contract if the subgraph returns an error address = getAddress(address); // address input validation - const registry = new StealthKeyRegistry(provider); - const filter = registry._registry.filters.StealthKeyChanged(address, null, null, null, null); try { - const timeout = (ms: number) => new Promise((reject) => setTimeout(() => reject(new Error('timeout')), ms)); - const stealthKeyLogsPromise = registry._registry.queryFilter(filter); - const stealthKeyLogs = (await Promise.race([stealthKeyLogsPromise, timeout(3000)])) as StealthKeyChangedEvent[]; - const registryBlock = sortStealthKeyLogs(stealthKeyLogs)[0]?.blockNumber || undefined; - return registryBlock; + console.log('Using subgraph to get block number when user registered'); + const stealthKeyChangedEvent = await getMostRecentSubgraphStealthKeyChangedEventFromAddress(address, chainConfig); + console.log(`stealthKeyChangedEvent.block: ${stealthKeyChangedEvent.block}`); + return stealthKeyChangedEvent.block; } catch { - return undefined; + console.log('Error using subgraph to get block number when user registered, will query registry contract'); + const registry = new StealthKeyRegistry(provider); + const filter = registry._registry.filters.StealthKeyChanged(address, null, null, null, null); + try { + const timeout = (ms: number) => new Promise((reject) => setTimeout(() => reject(new Error('timeout')), ms)); + const stealthKeyLogsPromise = registry._registry.queryFilter(filter); + const stealthKeyLogs = (await Promise.race([stealthKeyLogsPromise, timeout(3000)])) as StealthKeyChangedEvent[]; + const registryBlock = sortStealthKeyLogs(stealthKeyLogs)[0]?.blockNumber || undefined; + return registryBlock; + } catch { + return undefined; + } + } +} + +export async function getMostRecentSubgraphStealthKeyChangedEventFromAddress( + address: string, + chainConfig: ChainConfig, + overrides: ScanOverrides = {} +): Promise { + const startBlock = overrides.startBlock || chainConfig.startBlock; + const endBlock = overrides.endBlock || 'latest'; + + // Fetch stealth key changed events from the subgraph + const stealthKeyChangedEvents = fetchAllStealthKeyChangedEventsForRecipientAddressFromSubgraph( + startBlock, + endBlock, + address, + chainConfig + ); + let theEvent: SubgraphStealthKeyChangedEvent | undefined; + for await (const event of stealthKeyChangedEvents) { + for (let i = 0; i < event.length; i++) { + if (theEvent) { + console.log( + `We found a previous StealthKeyChangedEvent for address ${address} in the subgraph at block ${event[i].block} with transaction hash ${event[i].txHash}` + ); + } else { + theEvent = event[i]; + console.log( + `We found a StealthKeyChangedEvent for address ${address} in the subgraph at block ${event[i].block} with transaction hash ${event[i].txHash}` + ); + } + } } + + if (!theEvent) { + throw new Error('No stealthKeyChangedEvents found matching address in subgraph'); + } + return theEvent; } +/** + * @notice Fetches all Umbra event logs using a subgraph + * @param startBlock Scanning start block + * @param endBlock Scannding end block + * @returns A list of StealthKeyChanged events supplemented with additional metadata, such as the sender, block, + * timestamp, txhash, and the subgraph identifier + */ +async function* fetchAllStealthKeyChangedEventsForRecipientAddressFromSubgraph( + startBlock: string | number, + endBlock: string | number, + registrant: string, + chainConfig: ChainConfig +): AsyncGenerator { + if (!chainConfig.subgraphUrl) { + console.log('throwing error because subgraphUrl is not defined'); + throw new Error('Subgraph URL must be defined to fetch via subgraph'); + } + + // Query subgraph + for await (const stealthKeyChangedEvents of recursiveGraphFetch( + chainConfig.subgraphUrl, + 'stealthKeyChangedEntities', + (filter: string) => `{ + stealthKeyChangedEntities(${filter}) { + block + from + id + registrant + spendingPubKey + spendingPubKeyPrefix + timestamp + txHash + viewingPubKey + viewingPubKeyPrefix + } + }`, + [], + { + startBlock, + endBlock, + registrant, + } + )) { + yield stealthKeyChangedEvents; + } +} + +/** + * @notice Generic method to recursively grab every 'page' of results + * @dev NOTE: the query MUST return the ID field + * @dev Modifies from: https://github.com/dcgtc/dgrants/blob/f5a783524d0b56eea12c127b2146fba8fb9273b4/app/src/utils/utils.ts#L443 + * @dev Relevant docs: https://thegraph.com/docs/developer/graphql-api#example-3 + * @dev Lives outside of the class instance because user's should not need access to this method + * @dev TODO support node.js by replacing reliance on browser's fetch module with https://github.com/paulmillr/micro-ftch + * @param url the url we will recursively fetch from + * @param key the key in the response object which holds results + * @param query a function which will return the query string (with the page in place) + * @param before the current array of objects + */ +export async function* recursiveGraphFetch( + url: string, + key: string, + query: (filter: string) => string, + before: any[] = [], + overrides?: GraphFilterOverride +): AsyncGenerator { + // retrieve the last ID we collected to use as the starting point for this query + const fromId = before.length ? (before[before.length - 1].id as string | number) : false; + let startBlockFilter = ''; + let endBlockFilter = ''; + const startBlock = overrides?.startBlock ? overrides.startBlock.toString() : ''; + const endBlock = overrides?.endBlock ? overrides?.endBlock.toString() : ''; + const registrantFilter = overrides?.registrant ? 'registrant: "' + overrides.registrant.toLowerCase() + '"' : ''; + + if (startBlock) { + startBlockFilter = `block_gte: "${startBlock}",`; + } + + if (endBlock && endBlock !== 'latest') { + endBlockFilter = `block_lte: "${endBlock}",`; + } + + // Fetch this 'page' of results - please note that the query MUST return an ID + const res = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + query: query(` + first: 1000, + orderBy: id, + orderDirection: desc, + where: { + ${fromId ? `id_lt: "${fromId}",` : ''} + ${startBlockFilter} + ${endBlockFilter} + ${registrantFilter} + } + `), + }), + }); + + // Resolve the json + const json = await res.json(); + + // If there were results on this page yield the results then query the next page, otherwise do nothing. + if (json.data[key].length) { + yield json.data[key]; // yield the data for this page + yield* recursiveGraphFetch(url, key, query, [...before, ...json.data[key]], overrides); // yield the data for the next pages + } +} // Sorts stealth key logs in ascending order by block number export function sortStealthKeyLogs(stealthKeyLogs: Event[]) { return stealthKeyLogs.sort(function (a, b) { From 7e164d740793c91b499a9d83d1772137cf0cc6a7 Mon Sep 17 00:00:00 2001 From: John Feras Date: Thu, 2 May 2024 16:04:37 -0400 Subject: [PATCH 2/7] Added valid non-found in subgraph error (vs schema error) and removed wasteful retries from wallet function getRegisteredStealthKeys --- frontend/src/store/wallet.ts | 20 ++++++-------------- umbra-js/src/utils/utils.ts | 1 + 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/frontend/src/store/wallet.ts b/frontend/src/store/wallet.ts index 5fbb3f6db..484daad14 100644 --- a/frontend/src/store/wallet.ts +++ b/frontend/src/store/wallet.ts @@ -631,19 +631,11 @@ const hasSetPublicKeysLegacy = async (name: string, provider: Provider) => { // Helper method to check if user has registered public keys in the StealthKeyRegistry async function getRegisteredStealthKeys(account: string, provider: Provider) { - let retryCounter = 0; - while (retryCounter < 3) { - try { - console.log(`getting stealth keys for ${account}, try ${retryCounter + 1} of 3`); - const stealthPubKeys = await utils.lookupRecipient(account, provider); // throws if no keys found - return stealthPubKeys; - } catch (err) { - window.logger.warn(err); - retryCounter++; - if (retryCounter < 3) { - await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait for 2 seconds - } - } + try { + const stealthPubKeys = await utils.lookupRecipient(account, provider); // throws if no keys found + return stealthPubKeys; + } catch (err) { + window.logger.warn(err); + return null; } - return null; } diff --git a/umbra-js/src/utils/utils.ts b/umbra-js/src/utils/utils.ts index ce4347c7b..42dd6d455 100644 --- a/umbra-js/src/utils/utils.ts +++ b/umbra-js/src/utils/utils.ts @@ -340,6 +340,7 @@ export async function getMostRecentSubgraphStealthKeyChangedEventFromAddress( } if (!theEvent) { + console.log(`Searched the subgraph, but found no StealthKeyChangedEvents for address ${address}`); throw new Error('No stealthKeyChangedEvents found matching address in subgraph'); } return theEvent; From 2cdc6bdadcdb41d5d5c92051de101a9e2193b4d4 Mon Sep 17 00:00:00 2001 From: John Feras Date: Mon, 6 May 2024 14:47:39 -0400 Subject: [PATCH 3/7] When getting stealthKeys via subgraph StealthKeyChanged event, save block number of event in storage as starting point of announcements scan --- frontend/src/pages/AccountReceive.vue | 13 +++++++++++-- frontend/src/store/settings.ts | 16 ++++++++++++++++ frontend/src/store/wallet.ts | 6 ++++-- umbra-js/src/classes/Umbra.ts | 10 ++++++++-- umbra-js/src/utils/utils.ts | 19 ++++++++++++------- 5 files changed, 51 insertions(+), 13 deletions(-) diff --git a/frontend/src/pages/AccountReceive.vue b/frontend/src/pages/AccountReceive.vue index 63442477d..d74723316 100644 --- a/frontend/src/pages/AccountReceive.vue +++ b/frontend/src/pages/AccountReceive.vue @@ -153,8 +153,16 @@ function useScan() { const mostRecentBlockNumber = ref(0); // Start and end blocks for advanced mode settings - const { advancedMode, startBlock, endBlock, setScanBlocks, setScanPrivateKey, scanPrivateKey, resetScanSettings } = - useSettingsStore(); + const { + advancedMode, + startBlock, + endBlock, + setScanBlocks, + setScanPrivateKey, + scanPrivateKey, + resetScanSettings, + getRegisteredBlockNumber, + } = useSettingsStore(); const { signer, userAddress: userWalletAddress, isAccountSetup, provider } = useWalletStore(); const startBlockLocal = ref(); const endBlockLocal = ref(); @@ -320,6 +328,7 @@ function useScan() { mostRecentBlockTimestamp.value = latestBlock.timestamp; // Default scan behavior for await (const announcementsBatch of umbra.value.fetchSomeAnnouncements( + getRegisteredBlockNumber(), signer.value, userWalletAddress.value, overrides diff --git a/frontend/src/store/settings.ts b/frontend/src/store/settings.ts index 308bf265d..8f8f551df 100644 --- a/frontend/src/store/settings.ts +++ b/frontend/src/store/settings.ts @@ -12,6 +12,7 @@ const settings = { language: 'language', sendHistorySave: 'send-history-save', UmbraApiVersion: 'umbra-api-version', + registeredBlockNumber: 'registered-block-number', }; // Shared state between instances @@ -27,6 +28,7 @@ const startBlock = ref(undefined); // block number to start const endBlock = ref(undefined); // block number to scan through const scanPrivateKey = ref(); // private key entered when scanning const lastWallet = ref(); // name of last wallet used +const registeredBlockNumber = ref(undefined); // block number of the when the user registered const params = new URLSearchParams(window.location.search); const paramLocale = params.get('locale') || undefined; @@ -41,6 +43,9 @@ export default function useSettingsStore() { lastWallet.value = LocalStorage.getItem(settings.lastWallet) ? String(LocalStorage.getItem(settings.lastWallet)) : undefined; + registeredBlockNumber.value = LocalStorage.getItem(settings.registeredBlockNumber) + ? Number(LocalStorage.getItem(settings.registeredBlockNumber)) + : undefined; }); setLanguage( paramLocale @@ -137,6 +142,15 @@ export default function useSettingsStore() { LocalStorage.remove(settings.UmbraApiVersion); } + function getRegisteredBlockNumber() { + return registeredBlockNumber.value; + } + + function setRegisteredBlockNumber(blockNumber: number) { + registeredBlockNumber.value = blockNumber; + LocalStorage.set(settings.registeredBlockNumber, blockNumber); + } + return { toggleDarkMode, toggleAdvancedMode, @@ -158,5 +172,7 @@ export default function useSettingsStore() { getUmbraApiVersion, setUmbraApiVersion, clearUmbraApiVersion, + getRegisteredBlockNumber, + setRegisteredBlockNumber, }; } diff --git a/frontend/src/store/wallet.ts b/frontend/src/store/wallet.ts index 484daad14..e7afd5ffc 100644 --- a/frontend/src/store/wallet.ts +++ b/frontend/src/store/wallet.ts @@ -631,9 +631,11 @@ const hasSetPublicKeysLegacy = async (name: string, provider: Provider) => { // Helper method to check if user has registered public keys in the StealthKeyRegistry async function getRegisteredStealthKeys(account: string, provider: Provider) { + const { setRegisteredBlockNumber } = useSettingsStore(); try { - const stealthPubKeys = await utils.lookupRecipient(account, provider); // throws if no keys found - return stealthPubKeys; + const registrationInfo = await utils.lookupRecipient(account, provider); // throws if no keys found + setRegisteredBlockNumber(Number(registrationInfo.block)); + return registrationInfo; } catch (err) { window.logger.warn(err); return null; diff --git a/umbra-js/src/classes/Umbra.ts b/umbra-js/src/classes/Umbra.ts index 1deba5e60..31ef4e446 100644 --- a/umbra-js/src/classes/Umbra.ts +++ b/umbra-js/src/classes/Umbra.ts @@ -30,9 +30,9 @@ import { invalidStealthAddresses, getEthSweepGasInfo, lookupRecipient, + getBlockNumberUserRegistered, assertSupportedAddress, checkSupportedAddresses, - getBlockNumberUserRegistered, recursiveGraphFetch, } from '../utils/utils'; import { Umbra as UmbraContract, Umbra__factory, ERC20__factory } from '../typechain'; @@ -419,16 +419,22 @@ export class Umbra { /** * @notice Fetches Umbra event logs starting from the block user registered their stealth keys in using * a subgraph, if available, falling back to RPC if not + * @param possibleRegisteredBlockNumber Block number when user registered their stealth keys (if known) + * @param Signer Signer with provider to use for fetching the block number (if not known) from the StealthKeyRegistry contract + * @param address Address of the user for fetching the block number (if not known) from the subgraph or StealthKeyRegistry contract * @param overrides Override the start and end block used for scanning; * @returns A list of Announcement events supplemented with additional metadata, such as the sender, block, * timestamp, and txhash + * @dev If the registered block number is not known, it will be fetched from the subgraph or the StealthKeyRegistry contract */ async *fetchSomeAnnouncements( + possibleRegisteredBlockNumber: number | undefined, Signer: JsonRpcSigner, address: string, overrides: ScanOverrides = {} ): AsyncGenerator { - const registeredBlockNumber = await getBlockNumberUserRegistered(address, Signer.provider, this.chainConfig); + const registeredBlockNumber = + possibleRegisteredBlockNumber || (await getBlockNumberUserRegistered(address, Signer.provider, this.chainConfig)); // Get start and end blocks to scan events for const startBlock = overrides.startBlock || registeredBlockNumber || this.chainConfig.startBlock; const endBlock = overrides.endBlock || 'latest'; diff --git a/umbra-js/src/utils/utils.ts b/umbra-js/src/utils/utils.ts index 42dd6d455..da95eb38f 100644 --- a/umbra-js/src/utils/utils.ts +++ b/umbra-js/src/utils/utils.ts @@ -207,8 +207,8 @@ export async function toAddress(name: string, provider: EthersProvider) { /** * @notice Returns public keys from the recipientId * @dev When providing a public key, transaction hash, or address with advanced mode, the spending and viewing - * public keys will be the same. Only keys retrieved from the StealthKeyRegistry will have different spending - * and viewing keys + * public keys will be the same. Only keys retrieved from the StealthKeyRegistry (or the subgraph) will have different spending + * and viewing keys. Additionally, the block number when the user registered will be returned. * @param id Recipient identifier, must be an ENS name, CNS name, address, transaction hash, or public key * @param provider ethers provider to use * @param options Object containing lookup options: @@ -230,7 +230,7 @@ export async function lookupRecipient( const isPublicKey = id.length === 132 && isHexString(id); if (supportPubKey && isPublicKey) { assertValidPoint(id); - return { spendingPublicKey: id, viewingPublicKey: id }; + return { spendingPublicKey: id, viewingPublicKey: id, block: undefined }; } // Check if identifier is a transaction hash. If so, we recover the sender's public keys from the transaction @@ -238,7 +238,7 @@ export async function lookupRecipient( if (supportTxHash && isTxHash) { const publicKey = await recoverPublicKeyFromTransaction(id, provider); assertValidPoint(publicKey); - return { spendingPublicKey: publicKey, viewingPublicKey: publicKey }; + return { spendingPublicKey: publicKey, viewingPublicKey: publicKey, block: undefined }; } // The remaining checks are dependent on the advanced mode option. The provided identifier is now either an @@ -259,7 +259,11 @@ export async function lookupRecipient( stealthKeyChangedEvent.viewingPubKey, stealthKeyChangedEvent.viewingPubKeyPrefix.toString() ); - return { spendingPublicKey: spendingPublicKey, viewingPublicKey: viewingPublicKey }; + return { + spendingPublicKey: spendingPublicKey, + viewingPublicKey: viewingPublicKey, + block: stealthKeyChangedEvent.block, + }; } catch (error) { if (error instanceof Error) { console.log('Public key subgraph fetch error: ', error.message); @@ -268,7 +272,8 @@ export async function lookupRecipient( } console.log('Error using subgraph to lookup receipient stealth keys, will query registry contract'); const registry = new StealthKeyRegistry(provider); - return registry.getStealthKeys(address); + const { spendingPublicKey, viewingPublicKey } = await registry.getStealthKeys(address); + return { spendingPublicKey, viewingPublicKey, block: undefined }; } } @@ -277,7 +282,7 @@ export async function lookupRecipient( if (!txHash) throw new Error('Could not get public key because the provided account has not sent any transactions'); const publicKey = await recoverPublicKeyFromTransaction(txHash, provider); assertValidPoint(publicKey); - return { spendingPublicKey: publicKey, viewingPublicKey: publicKey }; + return { spendingPublicKey: publicKey, viewingPublicKey: publicKey, block: undefined }; } export async function getBlockNumberUserRegistered( From 8a8027d8c9e4a8d779d4d647e1fe211e28b0f0ff Mon Sep 17 00:00:00 2001 From: John Feras Date: Wed, 8 May 2024 14:45:51 -0400 Subject: [PATCH 4/7] Remove unneeded 'block: undefined' return values from 'lookupRecipient' function --- umbra-js/src/utils/utils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/umbra-js/src/utils/utils.ts b/umbra-js/src/utils/utils.ts index da95eb38f..eeae62e9c 100644 --- a/umbra-js/src/utils/utils.ts +++ b/umbra-js/src/utils/utils.ts @@ -230,7 +230,7 @@ export async function lookupRecipient( const isPublicKey = id.length === 132 && isHexString(id); if (supportPubKey && isPublicKey) { assertValidPoint(id); - return { spendingPublicKey: id, viewingPublicKey: id, block: undefined }; + return { spendingPublicKey: id, viewingPublicKey: id }; } // Check if identifier is a transaction hash. If so, we recover the sender's public keys from the transaction @@ -238,7 +238,7 @@ export async function lookupRecipient( if (supportTxHash && isTxHash) { const publicKey = await recoverPublicKeyFromTransaction(id, provider); assertValidPoint(publicKey); - return { spendingPublicKey: publicKey, viewingPublicKey: publicKey, block: undefined }; + return { spendingPublicKey: publicKey, viewingPublicKey: publicKey }; } // The remaining checks are dependent on the advanced mode option. The provided identifier is now either an @@ -273,7 +273,7 @@ export async function lookupRecipient( console.log('Error using subgraph to lookup receipient stealth keys, will query registry contract'); const registry = new StealthKeyRegistry(provider); const { spendingPublicKey, viewingPublicKey } = await registry.getStealthKeys(address); - return { spendingPublicKey, viewingPublicKey, block: undefined }; + return { spendingPublicKey, viewingPublicKey }; } } @@ -282,7 +282,7 @@ export async function lookupRecipient( if (!txHash) throw new Error('Could not get public key because the provided account has not sent any transactions'); const publicKey = await recoverPublicKeyFromTransaction(txHash, provider); assertValidPoint(publicKey); - return { spendingPublicKey: publicKey, viewingPublicKey: publicKey, block: undefined }; + return { spendingPublicKey: publicKey, viewingPublicKey: publicKey }; } export async function getBlockNumberUserRegistered( From 9caa3d605cae4084d7651adeb5c26c675b608576 Mon Sep 17 00:00:00 2001 From: garyghayrat Date: Tue, 21 May 2024 16:55:59 -0400 Subject: [PATCH 5/7] Query 10k announcements unless it's Gnosis subgraph --- umbra-js/src/utils/utils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/umbra-js/src/utils/utils.ts b/umbra-js/src/utils/utils.ts index eeae62e9c..f99d5d994 100644 --- a/umbra-js/src/utils/utils.ts +++ b/umbra-js/src/utils/utils.ts @@ -438,8 +438,9 @@ export async function* recursiveGraphFetch( method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ + // Gnosis chain is using Goldsky subgraph, which only supports 1000 blocks per query. query: query(` - first: 1000, + first: ${url.includes('umbra-xdai') ? 1000 : 10000}, orderBy: id, orderDirection: desc, where: { From a2764fd9967a0b9e87d3c4f3beab30c1a6924b10 Mon Sep 17 00:00:00 2001 From: John Feras Date: Mon, 20 May 2024 13:44:43 -0400 Subject: [PATCH 6/7] Put announcement info on tooltip --- .../src/components/AccountReceiveTable.vue | 41 ++++++++++++++----- frontend/src/i18n/locales/en-US.json | 7 +++- frontend/src/pages/AccountReceive.vue | 26 +++++++----- 3 files changed, 51 insertions(+), 23 deletions(-) diff --git a/frontend/src/components/AccountReceiveTable.vue b/frontend/src/components/AccountReceiveTable.vue index b1147bdd3..ce8510176 100644 --- a/frontend/src/components/AccountReceiveTable.vue +++ b/frontend/src/components/AccountReceiveTable.vue @@ -60,18 +60,39 @@ >. +
+ {{ $t('AccountReceiveTable.funds-question') }} + + + {{ $t('AccountReceiveTable.most-recent-announcement') }} +
+ {{ $t('AccountReceiveTable.most-recent-block') }} {{ mostRecentAnnouncementBlockNumber }} +
+ {{ $t('AccountReceiveTable.most-recent-time') }} + {{ formatDate(mostRecentAnnouncementTimestamp * 1000) }} + {{ formatTime(mostRecentAnnouncementTimestamp * 1000) }} +
+
+ {{ $t('AccountReceiveTable.most-recent-mined') }} +
+ {{ $t('AccountReceiveTable.most-recent-block') }} {{ mostRecentBlockNumber }} +
+ {{ $t('AccountReceiveTable.most-recent-time') }} + {{ formatDate(mostRecentBlockTimestamp * 1000) }} + {{ formatTime(mostRecentBlockTimestamp * 1000) }} +
+
+ + {{ $t('AccountReceiveTable.learn-more') }} + +
+
- {{ $t('AccountReceiveTable.most-recent-announcement') }} - {{ mostRecentAnnouncementBlockNumber }} / - {{ formatDate(mostRecentAnnouncementTimestamp * 1000) }} - {{ formatTime(mostRecentAnnouncementTimestamp * 1000) }} -
- {{ $t('AccountReceiveTable.most-recent-mined') }} - {{ mostRecentBlockNumber }} / - {{ formatDate(mostRecentBlockTimestamp * 1000) }} - {{ formatTime(mostRecentBlockTimestamp * 1000) }} -
{{ scanDescriptionString }}. diff --git a/frontend/src/i18n/locales/en-US.json b/frontend/src/i18n/locales/en-US.json index 175468855..53c88932f 100644 --- a/frontend/src/i18n/locales/en-US.json +++ b/frontend/src/i18n/locales/en-US.json @@ -279,8 +279,11 @@ "loss-warning": "It looks like you're trying to withdraw your funds to a token contract. It is very likely this is not what you intend, and proceeding will likely result in a loss of funds. Do not proceed unless you know exactly what you are doing.", "i-know-what": "I know what I am doing", "danger": "Danger", - "most-recent-announcement": "Most recent announcement block / timestamp:", - "most-recent-mined": "Most recent mined block / timestamp:" + "most-recent-announcement": "Most recent announcement", + "most-recent-block": "Block Num:", + "most-recent-time": "TimeStamp:", + "most-recent-mined": "Most recent mined:", + "funds-question": "Where are my funds" }, "AccountReceiveTableWarning": { "withdrawal-warning": "You are withdrawing to {0}, which has the following warnings:", diff --git a/frontend/src/pages/AccountReceive.vue b/frontend/src/pages/AccountReceive.vue index d74723316..6cf8728ee 100644 --- a/frontend/src/pages/AccountReceive.vue +++ b/frontend/src/pages/AccountReceive.vue @@ -242,6 +242,19 @@ function useScan() { return provider.getBlock('latest'); } + function updateMostRecentAnnouncementInfo(announcementsBatch: AnnouncementDetail[]) { + announcementsBatch.forEach((announcement) => { + const thisTimestamp = parseInt(announcement.timestamp); + if (thisTimestamp > mostRecentAnnouncementTimestamp.value) { + mostRecentAnnouncementTimestamp.value = thisTimestamp; + } + const thisBlock = parseInt(announcement.block); + if (thisBlock > mostRecentAnnouncementBlockNumber.value) { + mostRecentAnnouncementBlockNumber.value = thisBlock; + } + }); + } + async function scan() { // Reset paused state paused.value = false; @@ -302,6 +315,7 @@ function useScan() { if (advancedMode.value && scanPrivateKey.value) { for await (const announcementsBatch of umbra.value.fetchAllAnnouncements(overrides)) { announcementsCount += announcementsBatch.length; // Increment count + updateMostRecentAnnouncementInfo(announcementsBatch); announcementsQueue = [...announcementsQueue, ...announcementsBatch]; if (announcementsCount == 10000) { scanStatus.value = 'scanning latest'; @@ -338,17 +352,7 @@ function useScan() { } announcementsCount += announcementsBatch.length; // Increment count - announcementsBatch.forEach((announcement) => { - const thisTimestamp = parseInt(announcement.timestamp); - if (thisTimestamp > mostRecentAnnouncementTimestamp.value) { - mostRecentAnnouncementTimestamp.value = thisTimestamp; - } - const thisBlock = parseInt(announcement.block); - if (thisBlock > mostRecentAnnouncementBlockNumber.value) { - mostRecentAnnouncementBlockNumber.value = thisBlock; - } - }); - + updateMostRecentAnnouncementInfo(announcementsBatch); announcementsQueue = [...announcementsQueue, ...announcementsBatch]; if (announcementsCount == 10000) { scanStatus.value = 'scanning latest'; From 8bf0bb9eef8fcf074c9729de80d3090dd71f75af Mon Sep 17 00:00:00 2001 From: John Feras Date: Thu, 18 Jul 2024 09:50:25 -0400 Subject: [PATCH 7/7] Update tooltip logic since caching merge --- frontend/src/pages/AccountReceive.vue | 6 +++--- umbra-js/src/utils/utils.ts | 6 ------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/frontend/src/pages/AccountReceive.vue b/frontend/src/pages/AccountReceive.vue index a1e66f3d5..9b85a2e2e 100644 --- a/frontend/src/pages/AccountReceive.vue +++ b/frontend/src/pages/AccountReceive.vue @@ -164,8 +164,8 @@ function useScan() { : null ); const userAnnouncements = ref([]); - const mostRecentAnnouncementTimestamp = ref(); - const mostRecentAnnouncementBlockNumber = ref(); + const mostRecentAnnouncementTimestamp = ref(0); + const mostRecentAnnouncementBlockNumber = ref(0); const mostRecentBlockTimestamp = ref(0); const mostRecentBlockNumber = ref(0); @@ -391,7 +391,7 @@ function useScan() { return provider.getBlock('latest'); } - function(announcementsBatch: AnnouncementDetail[]) { + function updateMostRecentAnnouncementInfo(announcementsBatch: AnnouncementDetail[]) { announcementsBatch.forEach((announcement) => { const thisTimestamp = parseInt(announcement.timestamp); if (thisTimestamp > mostRecentAnnouncementTimestamp.value) { diff --git a/umbra-js/src/utils/utils.ts b/umbra-js/src/utils/utils.ts index cc63d18da..8ccbd26ab 100644 --- a/umbra-js/src/utils/utils.ts +++ b/umbra-js/src/utils/utils.ts @@ -400,12 +400,6 @@ async function* fetchAllStealthKeyChangedEventsForRecipientAddressFromSubgraph( )) { yield stealthKeyChangedEvents; } - - if (!theEvent) { - console.log(`Searched the subgraph, but found no StealthKeyChangedEvents for address ${address}`); - throw new Error('No stealthKeyChangedEvents found matching address in subgraph'); - } - return theEvent; } /**