From 85d8b1e5140b04764111f9c042d2e982d08f9091 Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Mon, 24 May 2021 18:15:12 -0500 Subject: [PATCH] fix: improve privacy of the claim popup (#1698) * fix: improve privacy of the claim popup fixes https://github.com/Uniswap/uniswap-interface/issues/1337 * fix unhandled rejection * clean up the claim code a bit more * working claim fetch * working claim fetch * trigger some events on the claim popup --- src/components/Popups/ClaimPopup.tsx | 16 +++- src/state/claim/hooks.ts | 113 ++++++++++++++++++++------- 2 files changed, 98 insertions(+), 31 deletions(-) diff --git a/src/components/Popups/ClaimPopup.tsx b/src/components/Popups/ClaimPopup.tsx index f1ca2b0edc9..0f9b604ae5c 100644 --- a/src/components/Popups/ClaimPopup.tsx +++ b/src/components/Popups/ClaimPopup.tsx @@ -1,5 +1,6 @@ import { CurrencyAmount, Token } from '@uniswap/sdk-core' -import React, { useEffect } from 'react' +import React, { useCallback, useEffect } from 'react' +import ReactGA from 'react-ga' import { X } from 'react-feather' import styled, { keyframes } from 'styled-components' import tokenLogo from '../../assets/images/token-logo.png' @@ -62,6 +63,13 @@ export default function ClaimPopup() { // toggle for showing this modal const showClaimModal = useModalOpen(ApplicationModal.SELF_CLAIM) const toggleSelfClaimModal = useToggleSelfClaimModal() + const handleToggleSelfClaimModal = useCallback(() => { + ReactGA.event({ + category: 'MerkleDrop', + action: 'Toggle self claim modal', + }) + toggleSelfClaimModal() + }, [toggleSelfClaimModal]) // const userHasAvailableclaim = useUserHasAvailableClaim() const userHasAvailableclaim: boolean = useUserHasAvailableClaim(account) @@ -70,6 +78,10 @@ export default function ClaimPopup() { // listen for available claim and show popup if needed useEffect(() => { if (userHasAvailableclaim) { + ReactGA.event({ + category: 'MerkleDrop', + action: 'Show claim popup', + }) toggleShowClaimPopup() } // the toggleShowClaimPopup function changes every time the popup changes, so this will cause an infinite loop. @@ -102,7 +114,7 @@ export default function ClaimPopup() { - + Claim your UNI tokens diff --git a/src/state/claim/hooks.ts b/src/state/claim/hooks.ts index d67521978b7..369b7a331f1 100644 --- a/src/state/claim/hooks.ts +++ b/src/state/claim/hooks.ts @@ -21,49 +21,104 @@ interface UserClaimData { } } -const CLAIM_PROMISES: { [key: string]: Promise } = {} +type LastAddress = string +type ClaimAddressMapping = { [firstAddress: string]: LastAddress } +let FETCH_CLAIM_MAPPING_PROMISE: Promise | null = null +function fetchClaimMapping(): Promise { + return ( + FETCH_CLAIM_MAPPING_PROMISE ?? + (FETCH_CLAIM_MAPPING_PROMISE = fetch( + `https://raw.githubusercontent.com/Uniswap/mrkl-drop-data-chunks/final/chunks/mapping.json` + ) + .then((res) => res.json()) + .catch((error) => { + console.error('Failed to get claims mapping', error) + FETCH_CLAIM_MAPPING_PROMISE = null + })) + ) +} +const FETCH_CLAIM_FILE_PROMISES: { [startingAddress: string]: Promise<{ [address: string]: UserClaimData }> } = {} +function fetchClaimFile(key: string): Promise<{ [address: string]: UserClaimData }> { + return ( + FETCH_CLAIM_FILE_PROMISES[key] ?? + (FETCH_CLAIM_FILE_PROMISES[key] = fetch( + `https://raw.githubusercontent.com/Uniswap/mrkl-drop-data-chunks/final/chunks/${key}.json` + ) + .then((res) => res.json()) + .catch((error) => { + console.error(`Failed to get claim file mapping for starting address ${key}`, error) + delete FETCH_CLAIM_FILE_PROMISES[key] + })) + ) +} + +const FETCH_CLAIM_PROMISES: { [key: string]: Promise } = {} // returns the claim for the given address, or null if not valid -function fetchClaim(account: string, chainId: number): Promise { +function fetchClaim(account: string): Promise { const formatted = isAddress(account) if (!formatted) return Promise.reject(new Error('Invalid address')) - const key = `${chainId}:${account}` - - return (CLAIM_PROMISES[key] = - CLAIM_PROMISES[key] ?? - fetch('https://merkle-drop-1.uniswap.workers.dev/', { - body: JSON.stringify({ chainId, address: formatted }), - headers: { - 'Content-Type': 'application/json', - 'Referrer-Policy': 'no-referrer', - }, - method: 'POST', - }) - .then((res) => (res.ok ? res.json() : console.log(`No claim for account ${formatted} on chain ID ${chainId}`))) - .catch((error) => console.error('Failed to get claim data', error))) + + return ( + FETCH_CLAIM_PROMISES[account] ?? + (FETCH_CLAIM_PROMISES[account] = fetchClaimMapping() + .then((mapping) => { + const sorted = Object.keys(mapping).sort((a, b) => (a.toLowerCase() < b.toLowerCase() ? -1 : 1)) + + for (const startingAddress of sorted) { + const lastAddress = mapping[startingAddress] + if (startingAddress.toLowerCase() <= formatted.toLowerCase()) { + if (formatted.toLowerCase() <= lastAddress.toLowerCase()) { + return startingAddress + } + } else { + throw new Error(`Claim for ${formatted} was not found in partial search`) + } + } + throw new Error(`Claim for ${formatted} was not found after searching all mappings`) + }) + .then(fetchClaimFile) + .then((result) => { + if (result[formatted]) return result[formatted] + throw new Error(`Claim for ${formatted} was not found in claim file!`) + }) + .catch((error) => { + console.debug('Claim fetch failed', error) + throw error + })) + ) } // parse distributorContract blob and detect if user has claim data // null means we know it does not -export function useUserClaimData(account: string | null | undefined): UserClaimData | null | undefined { +export function useUserClaimData(account: string | null | undefined): UserClaimData | null { const { chainId } = useActiveWeb3React() - const key = `${chainId}:${account}` - const [claimInfo, setClaimInfo] = useState<{ [key: string]: UserClaimData | null }>({}) + const [claimInfo, setClaimInfo] = useState<{ [account: string]: UserClaimData | null }>({}) useEffect(() => { - if (!account || !chainId) return - fetchClaim(account, chainId).then((accountClaimInfo) => - setClaimInfo((claimInfo) => { - return { - ...claimInfo, - [key]: accountClaimInfo, - } + if (!account || chainId !== 1) return + + fetchClaim(account) + .then((accountClaimInfo) => + setClaimInfo((claimInfo) => { + return { + ...claimInfo, + [account]: accountClaimInfo, + } + }) + ) + .catch(() => { + setClaimInfo((claimInfo) => { + return { + ...claimInfo, + [account]: null, + } + }) }) - ) - }, [account, chainId, key]) + }, [account, chainId]) - return account && chainId ? claimInfo[key] : undefined + return account && chainId === 1 ? claimInfo[account] : null } // check if user is in blob and has not yet claimed UNI