Skip to content

Commit

Permalink
Query StealthKeyChanged events from subgraph (#675)
Browse files Browse the repository at this point in the history
* feat: set the last fetched block as the start block

* feat: handle caching user announcements and latest fetched block

* feat: show user announcements if there are any

* fix: handle watching/loading announcements

* fix: parse out lastFetchedBlock and fix user announcement loading logic

* chore: log

* feat: handle block data caching

* feat: show most recent block data if exists

* fix: type check

* feat: handle user announcements already present and sign language

* feat: only show fetching when no user announcements

* feat: fetching latest from last fetched block component

* feat: fetching latest translation for cn

* feat: clear local storage button and functionality

* fix: start block handling logic

* feat: dedupe user announcements

* fix: logic

* fix: minimize debugging logs on userAnnouncement changes

* feat: handle scanning latest announcements from last fetched block

* feat: sort by timestamp explicitly

* feat: no loading sequence when there are announcements

* fix: need sig lately verbiage

* fix: add need sig lately to cn

* fix: little more mb

* fix: no withdraw verbiage on need-sig-lately

* feat: handle need sig

* Update frontend/src/i18n/locales/en-US.json

Co-authored-by: Gary Ghayrat <61768337+garyghayrat@users.noreply.github.com>

* feat: handle sign button instead of needs sig

* Update frontend/src/i18n/locales/zh-CN.json

Co-authored-by: Gary Ghayrat <61768337+garyghayrat@users.noreply.github.com>

* fix: move local storage clear button above lang

* fix: spacing more uniform

* fix: use computed ref as param, and set setIsInWithdrawFlow to false on mount

* feat: sign and withdraw

* fix: contract periphery tests (#688)

* fix: explicitly sort the tokens by addr

* fix: use vm.computeCreateAddress

* fix: mirror test sender params

* fix: use actual owner

* fix: add back gnosis

* Remove all reference to INFURA_ID (#687)

---------

Co-authored-by: John Feras <jferas@ferasinfotech.com>

* fix: use balanceIndex to ensure that the correct balance is fetched from the stealthBalances array

* fix: dedupe by tx hash and receiver instead of just tx hash

* fix: include receiver to derive isWithdrawn

* fix: img

* Query StealthKeyChanged events from subgraph for user registration block number and stealthkeys

* Added valid non-found in subgraph error (vs schema error) and removed wasteful retries from wallet function getRegisteredStealthKeys

* When getting stealthKeys via subgraph StealthKeyChanged event, save block number of event in storage as starting point of announcements scan

* Remove unneeded 'block: undefined' return values from 'lookupRecipient' function

* Query 10k announcements unless it's Gnosis subgraph

* Update `umbra-js` version and add `.npmignore`

* Changed public key acquisition to query registry contract before subgraph.. umbra-js change

* Fix to return proper error string when public address not found in subgraph

* Update UmbraJs version to 0.2.1

* Use umbra-js@0.2.1 in frontend

---------

Co-authored-by: marcomariscal <marco.a.mariscal@gmail.com>
Co-authored-by: marcomariscal <42938673+marcomariscal@users.noreply.github.com>
Co-authored-by: Gary Ghayrat <61768337+garyghayrat@users.noreply.github.com>
Co-authored-by: garyghayrat <gary.ghayrat@pm.me>
  • Loading branch information
5 people committed Jul 15, 2024
1 parent a13be3a commit f1c3f69
Show file tree
Hide file tree
Showing 10 changed files with 276 additions and 101 deletions.
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "0.2.1",
"@uniswap/token-lists": "^1.0.0-beta.19",
"@unstoppabledomains/resolution": "8.5.0",
"@web3-onboard/coinbase": "2.2.7",
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/pages/AccountReceive.vue
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ function useScan() {
setScanPrivateKey,
scanPrivateKey,
resetScanSettings: resetScanSettingsInSettingsStore,
getRegisteredBlockNumber,
} = useSettingsStore();
const { signer, userAddress: userWalletAddress, isAccountSetup, provider } = useWalletStore();
Expand Down Expand Up @@ -484,6 +485,7 @@ function useScan() {
// Default scan behavior
for await (const announcementsBatch of umbra.value.fetchSomeAnnouncements(
getRegisteredBlockNumber(),
signer.value,
userWalletAddress.value,
overrides
Expand Down
16 changes: 15 additions & 1 deletion frontend/src/store/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const settings = {
language: 'language',
sendHistorySave: 'send-history-save',
UmbraApiVersion: 'umbra-api-version',
registeredBlockNumber: 'registered-block-number',
};


Expand All @@ -29,6 +30,7 @@ const startBlock = ref<number | undefined>(undefined); // block number to start
const endBlock = ref<number | undefined>(undefined); // block number to scan through
const scanPrivateKey = ref<string>(); // private key entered when scanning
const lastWallet = ref<string>(); // name of last wallet used
const registeredBlockNumber = ref<number | undefined>(undefined); // block number of the when the user registered
const params = new URLSearchParams(window.location.search);
const paramLocale = params.get('locale') || undefined;

Expand All @@ -43,7 +45,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
Expand Down Expand Up @@ -140,6 +144,14 @@ 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,
Expand All @@ -162,5 +174,7 @@ export default function useSettingsStore() {
getUmbraApiVersion,
setUmbraApiVersion,
clearUmbraApiVersion,
getRegisteredBlockNumber,
setRegisteredBlockNumber,
};
}
22 changes: 8 additions & 14 deletions frontend/src/store/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -632,19 +632,13 @@ 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
}
}
const { setRegisteredBlockNumber } = useSettingsStore();
try {
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;
}
return null;
}
1 change: 1 addition & 0 deletions umbra-js/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.env
2 changes: 1 addition & 1 deletion umbra-js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@umbracash/umbra-js",
"version": "0.1.6",
"version": "0.2.1",
"description": "Send and receive stealth payments",
"main": "build/src/index.js",
"types": "build/src/index.d.ts",
Expand Down
84 changes: 15 additions & 69 deletions umbra-js/src/classes/Umbra.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@ import {
invalidStealthAddresses,
getEthSweepGasInfo,
lookupRecipient,
getBlockNumberUserRegistered,
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
Expand Down Expand Up @@ -72,7 +73,7 @@ const chainConfigs: Record<number, ChainConfig> = {
* @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');
}
Expand Down Expand Up @@ -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
Expand All @@ -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)) {
Expand All @@ -417,17 +418,23 @@ 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 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<AnnouncementDetail[]> {
const registeredBlockNumber = await getBlockNumberUserRegistered(address, Signer.provider);
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';
Expand All @@ -437,7 +444,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,
Expand Down Expand Up @@ -732,67 +739,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<any[]> {
// 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
Expand Down
15 changes: 15 additions & 0 deletions umbra-js/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -135,4 +149,5 @@ export interface SendData {
export type GraphFilterOverride = {
startBlock?: number | string;
endBlock?: number | string;
registrant?: string;
};
Loading

0 comments on commit f1c3f69

Please sign in to comment.