diff --git a/src/components/claim/ClaimModal.tsx b/src/components/claim/ClaimModal.tsx index 6bf00c533..718a93b0b 100644 --- a/src/components/claim/ClaimModal.tsx +++ b/src/components/claim/ClaimModal.tsx @@ -12,7 +12,7 @@ import { useActiveWeb3React } from '../../hooks/web3' import { useModalOpen, useToggleSelfClaimModal } from '../../state/application/hooks' import { ApplicationModal } from '../../state/application/reducer' import { useClaimCallback, useUserClaimData, useUserUnclaimedAmount } from 'state/claim/hooks' -import { useUserHasSubmittedClaim } from '../../state/transactions/hooks' +import { useUserHasSubmittedClaim } from 'state/transactions/hooks' import { CloseIcon, CustomLightSpinner, ExternalLink, TYPE, UniTokenAnimated } from '../../theme' import { ExplorerDataType, getExplorerLink } from '../../utils/getExplorerLink' import { ButtonPrimary } from '../Button' diff --git a/src/custom/pages/Claim/ClaimSummary.tsx b/src/custom/pages/Claim/ClaimSummary.tsx index dd849a6b4..be8c3018e 100644 --- a/src/custom/pages/Claim/ClaimSummary.tsx +++ b/src/custom/pages/Claim/ClaimSummary.tsx @@ -31,7 +31,7 @@ export default function ClaimSummary({ hasClaims, unclaimedAmount }: ClaimSummar
Total available to claim -

{formatSmart(unclaimedAmount)} vCOW

+

{formatSmart(unclaimedAmount) ?? 0} vCOW

)} diff --git a/src/custom/pages/Claim/index.tsx b/src/custom/pages/Claim/index.tsx index fe77558fb..ab580bb50 100644 --- a/src/custom/pages/Claim/index.tsx +++ b/src/custom/pages/Claim/index.tsx @@ -32,6 +32,7 @@ import { StepIndicator, Steps, TokenLogo, + ClaimRow, } from 'pages/Claim/styled' import EligibleBanner from './EligibleBanner' import { @@ -56,6 +57,7 @@ import ClaimAddress from './ClaimAddress' import CanUserClaimMessage from './CanUserClaimMessage' import { useClaimDispatchers, useClaimState } from 'state/claim/hooks' import { ClaimStatus } from 'state/claim/actions' +import { useAllClaimingTransactionIndices } from 'state/enhancedTransactions/hooks' export default function Claim() { const { account, chainId } = useActiveWeb3React() @@ -117,6 +119,9 @@ export default function Claim() { const hasClaims = useMemo(() => userClaimData.length > 0, [userClaimData]) const isAirdropOnly = useMemo(() => !hasPaidClaim(userClaimData), [userClaimData]) + // get current pending claims set in activities + const indicesSet = useAllClaimingTransactionIndices() + // claim type to currency and price map const typeToCurrencyMap = useMemo(() => getTypeToCurrencyMap(chainId), [chainId]) const typeToPriceMap = useMemo(() => getTypeToPriceMap(), []) @@ -332,33 +337,43 @@ export default function Claim() { {sortedClaimData.map(({ index, type, amount }) => { const isFree = isFreeClaim(type) const currency = typeToCurrencyMap[type] || '' - const vCowPrice = typeToPriceMap[type] + const vCowPrice = typeToPriceMap.get(type) const parsedAmount = parseClaimAmount(amount, chainId) - const cost = vCowPrice * Number(parsedAmount?.toSignificant(6)) + const cost = vCowPrice && vCowPrice * Number(parsedAmount?.toSignificant(6)) + const isPendingClaim = indicesSet.has(index) return ( - + console.log('Claim::Opening Orders panel') : undefined} + > {' '} - + {/* User has on going pending claiming transactions? Show the loader */} + {isPendingClaim ? ( + + ) : ( + + )} - {isFree ? type : `Buy vCOW with ${currency}`} + {isFree ? ClaimType[type] : `Buy vCOW with ${currency}`} {parsedAmount?.toFixed(0, { groupSeparator: ',' })} vCOW - {isFree ? '-' : `${vCowPrice} vCoW per ${currency}`} + {isFree || !vCowPrice ? '-' : `${vCowPrice} vCoW per ${currency}`} {isFree ? Free! : `${cost} ${currency}`} {type === ClaimType.Airdrop ? 'No' : '4 years (linear)'} 28 days, 10h, 50m - + ) })} diff --git a/src/custom/pages/Claim/styled.ts b/src/custom/pages/Claim/styled.ts index 308168be7..763bf4708 100644 --- a/src/custom/pages/Claim/styled.ts +++ b/src/custom/pages/Claim/styled.ts @@ -179,6 +179,11 @@ export const ClaimTable = styled.div` } th { + &:first-child { + display: flex; + align-items: center; + } + position: sticky; top: 0; background: transparent; @@ -194,18 +199,35 @@ export const ClaimTable = styled.div` } td { + display: flex; + align-items: center; + padding-top: 10px; padding-bottom: 10px; color: white; word-break: break-word; + background: var(--color-container-bg); } tr > td { - background: var(--color-container-bg); margin: 0 0 12px; } ` +export const ClaimRow = styled.tr<{ isPending?: boolean }>` + > td { + background-color: ${({ isPending }) => (isPending ? '#221954' : 'rgb(255 255 255 / 6%)')}; + cursor: ${({ isPending }) => (isPending ? 'pointer' : 'initial')}; + + &:first-child { + border-radius: 8px 0 0 8px; + } + &:last-child { + border-radius: 0 8px 8px 0; + } + } +` + export const ClaimAccount = styled.div` display: flex; flex-flow: row nowrap; diff --git a/src/custom/state/claim/hooks/index.ts b/src/custom/state/claim/hooks/index.ts index 268ec9f05..036cf6960 100644 --- a/src/custom/state/claim/hooks/index.ts +++ b/src/custom/state/claim/hooks/index.ts @@ -20,6 +20,10 @@ import { isAddress } from 'utils' import { getClaimKey, getClaimsRepoPath, transformRepoClaimsToUserClaims } from 'state/claim/hooks/utils' import { SupportedChainId } from 'constants/chains' +import { registerOnWindow } from 'utils/misc' +import mockData, { MOCK_INDICES } from './mocks/claimData' +import { getIndexes } from './utils' +import { useAllClaimingTransactionIndices } from 'state/enhancedTransactions/hooks' export { useUserClaimData } from '@src/state/claim/hooks' @@ -68,6 +72,15 @@ export enum ClaimType { Advisor, // free, with vesting, only on mainnet } +export type TypeToPriceMapper = Map + +// Hardcoded values +export const ClaimTypePriceMap: TypeToPriceMapper = new Map([ + [ClaimType.GnoOption, 16.66], + [ClaimType.Investor, 26.66], + [ClaimType.UserOption, 36.66], +]) + type RepoClaimType = keyof typeof ClaimType export const FREE_CLAIM_TYPES: ClaimType[] = [ClaimType.Airdrop, ClaimType.Team, ClaimType.Advisor] @@ -182,13 +195,18 @@ export function useUserHasAvailableClaim(account: Account): boolean { export function useUserUnclaimedAmount(account: string | null | undefined): CurrencyAmount | undefined { const { chainId } = useActiveWeb3React() const claims = useUserAvailableClaims(account) + const pendingIndices = useAllClaimingTransactionIndices() const vCow = chainId ? V_COW[chainId] : undefined if (!vCow) return undefined if (!claims || claims.length === 0) { return CurrencyAmount.fromRawAmount(vCow, JSBI.BigInt(0)) } + const totalAmount = claims.reduce((acc, claim) => { + // don't add pending + if (pendingIndices.has(claim.index)) return acc + return JSBI.add(acc, JSBI.BigInt(claim.amount)) }, JSBI.BigInt('0')) @@ -235,6 +253,14 @@ export function useUserClaims(account: Account): UserClaims | null { return claimKey ? claimInfo[claimKey] : null } +// TODO: remove +const createMockTx = (data: number[]) => ({ + hash: '0x' + Math.round(Math.random() * 10).toString() + 'AxAFjAhG89G89AfnLK3CCxAfnLKQffQ782G89AfnLK3CCxxx123FF', + summary: `Claimed ${Math.random() * 3337} vCOW`, + claim: { recipient: '0x97EC4fcD5F78cA6f6E4E1EAC6c0Ec8421bA518B7' }, + data, // add the claim indices to state +}) + /** * Fetches from contract the deployment timestamp in ms * @@ -330,6 +356,20 @@ export function useClaimCallback(account: string | null | undefined): { const addTransaction = useTransactionAdder() const vCowToken = chainId ? V_COW[chainId] : undefined + // TODO: remove + registerOnWindow({ + addMockClaimTransactions: (data?: number[]) => { + let finalData: number[] | undefined = data + + if (!finalData) { + const mockDataIndices = connectedAccount ? getIndexes(mockData[connectedAccount] || []) : [] + finalData = mockDataIndices?.length > 0 ? mockDataIndices : MOCK_INDICES + } + + return addTransaction(createMockTx(finalData)) + }, + }) + const claimCallback = useCallback( async function (claimInput: ClaimInput[]) { if ( @@ -356,17 +396,17 @@ export function useClaimCallback(account: string | null | undefined): { return vCowContract.estimateGas['claimMany'](...args).then((estimatedGas) => { // Last item in the array contains the call overrides - args[args.length - 1] = { - ...args[args.length - 1], // add back whatever is already there + const extendedArgs = _extendFinalArg(args, { from: connectedAccount, // add the `from` as the connected account gasLimit: calculateGasMargin(chainId, estimatedGas), // add the estimated gas limit - } + }) - return vCowContract.claimMany(...args).then((response: TransactionResponse) => { + return vCowContract.claimMany(...extendedArgs).then((response: TransactionResponse) => { addTransaction({ hash: response.hash, summary: `Claimed ${formatSmart(vCowAmount)} vCOW`, claim: { recipient: account }, + data: args[0], // add the claim indices to state }) return response.hash }) @@ -652,3 +692,16 @@ export function useClaimDispatchers() { export function useClaimState() { return useSelector((state: AppState) => state.claim) } + +/** + * Extend the Payable optional param + */ +function _extendFinalArg(args: ClaimManyFnArgs, extendedArg: Record) { + const lastArg = args.pop() + args.push({ + ...lastArg, // add back whatever is already there + ...extendedArg, + }) + + return args +} diff --git a/src/custom/state/claim/hooks/mocks/claimData.ts b/src/custom/state/claim/hooks/mocks/claimData.ts index ccd05e95e..089fdfe75 100644 --- a/src/custom/state/claim/hooks/mocks/claimData.ts +++ b/src/custom/state/claim/hooks/mocks/claimData.ts @@ -1,4 +1,6 @@ -const mockData: any = { +import { RepoClaimData } from '..' + +const mockData: Record = { // airdrops + investments '0xf17aFe5237D982868B8A97424dD79a4A50c36412': [ { @@ -145,5 +147,7 @@ const mockData: any = { // no claims '0x7C842Bf74359911aEe49bA021014B05265f951c6': [], } +const mockKeys = Object.keys(mockData) +export const MOCK_INDICES = mockData[mockKeys[0]].map(({ index }) => index) export default mockData diff --git a/src/custom/state/claim/hooks/utils.ts b/src/custom/state/claim/hooks/utils.ts index f4184306e..9bb747d27 100644 --- a/src/custom/state/claim/hooks/utils.ts +++ b/src/custom/state/claim/hooks/utils.ts @@ -4,9 +4,11 @@ import { V_COW } from 'constants/tokens' import { CLAIMS_REPO, ClaimType, + ClaimTypePriceMap, FREE_CLAIM_TYPES, PAID_CLAIM_TYPES, RepoClaims, + TypeToPriceMapper, UserClaims, } from 'state/claim/hooks/index' @@ -95,24 +97,13 @@ export function getTypeToCurrencyMap(chainId: number | undefined): TypeToCurrenc return map } -export type TypeToPriceMapper = { - [key: string]: number -} - /** * Helper function to get vCow price based on claim type and chainId * * @param type */ export function getTypeToPriceMap(): TypeToPriceMapper { - // Hardcoded values - const map: TypeToPriceMapper = { - [ClaimType.GnoOption]: 16.66, - [ClaimType.Investor]: 26.66, - [ClaimType.UserOption]: 36.66, - } - - return map + return ClaimTypePriceMap } /** @@ -129,7 +120,7 @@ export function isFreeClaim(type: ClaimType): boolean { * * @param type */ -export function getIndexes(data: UserClaims): number[] { +export function getIndexes(data: RepoClaims | UserClaims): number[] { return data.map(({ index }) => index) } diff --git a/src/custom/state/enhancedTransactions/actions.ts b/src/custom/state/enhancedTransactions/actions.ts index 397a195c4..00bd2c90d 100644 --- a/src/custom/state/enhancedTransactions/actions.ts +++ b/src/custom/state/enhancedTransactions/actions.ts @@ -4,8 +4,10 @@ import { SerializableTransactionReceipt } from '@src/state/transactions/actions' import { EnhancedTransactionDetails } from './reducer' type WithChainId = { chainId: number } +type WithData = { data?: any } export type AddTransactionParams = WithChainId & + WithData & Pick< EnhancedTransactionDetails, 'hash' | 'hashType' | 'from' | 'approval' | 'presign' | 'claim' | 'summary' | 'safeTransaction' diff --git a/src/custom/state/enhancedTransactions/hooks/index.ts b/src/custom/state/enhancedTransactions/hooks/index.ts index 888c95afe..2b73f03f4 100644 --- a/src/custom/state/enhancedTransactions/hooks/index.ts +++ b/src/custom/state/enhancedTransactions/hooks/index.ts @@ -24,12 +24,25 @@ export function useTransactionAdder(): TransactionAdder { (addTransactionParams: AddTransactionHookParams) => { if (!account || !chainId) return - const { hash, summary, approval, presign, safeTransaction } = addTransactionParams + const { hash, summary, data, claim, approval, presign, safeTransaction } = addTransactionParams const hashType = isGnosisSafeWallet ? HashType.GNOSIS_SAFE_TX : HashType.ETHEREUM_TX if (!hash) { throw Error('No transaction hash found') } - dispatch(addTransaction({ hash, hashType, from: account, chainId, approval, summary, presign, safeTransaction })) + dispatch( + addTransaction({ + hash, + hashType, + from: account, + chainId, + approval, + summary, + claim, + data, + presign, + safeTransaction, + }) + ) }, [dispatch, chainId, account, isGnosisSafeWallet] ) @@ -105,3 +118,26 @@ export function useTransactionsByHash({ hashes }: { hashes: string[] }): Enhance }, {}) }, [allTxs, hashes]) } + +export function useAllClaimingTransactions() { + const transactionsMap = useAllTransactions() + const transactions = Object.values(transactionsMap) + + return useMemo(() => { + return transactions.filter((tx) => !!tx.claim) + }, [transactions]) +} + +export function useAllClaimingTransactionIndices() { + const claimingTransactions = useAllClaimingTransactions() + return useMemo(() => { + const flattenedClaimingTransactions = claimingTransactions.reduce((acc, { data }) => { + if (data) { + acc.push(...data) + } + return acc + }, []) + + return new Set(flattenedClaimingTransactions) + }, [claimingTransactions]) +} diff --git a/src/custom/state/enhancedTransactions/reducer.ts b/src/custom/state/enhancedTransactions/reducer.ts index dd0e1151f..fa5877ce4 100644 --- a/src/custom/state/enhancedTransactions/reducer.ts +++ b/src/custom/state/enhancedTransactions/reducer.ts @@ -30,6 +30,7 @@ export interface EnhancedTransactionDetails { summary?: string confirmedTime?: number receipt?: SerializableTransactionReceipt // Ethereum transaction receipt + data?: any // any attached data type // Operations approval?: { tokenAddress: string; spender: string } @@ -63,7 +64,10 @@ export default createReducer(initialState, (builder) => builder .addCase( addTransaction, - (transactions, { payload: { chainId, from, hash, hashType, approval, summary, presign, safeTransaction } }) => { + ( + transactions, + { payload: { chainId, from, hash, hashType, approval, summary, presign, safeTransaction, claim, data } } + ) => { if (transactions[chainId]?.[hash]) { console.warn('[state::enhancedTransactions] Attempted to add existing transaction', hash) // Unknown transaction. Do nothing! @@ -77,11 +81,13 @@ export default createReducer(initialState, (builder) => addedTime: now(), from, summary, + data, // Operations approval, presign, safeTransaction, + claim, } transactions[chainId] = txs } diff --git a/src/custom/state/transactions/hooks.ts b/src/custom/state/transactions/hooks.ts new file mode 100644 index 000000000..c0c578b16 --- /dev/null +++ b/src/custom/state/transactions/hooks.ts @@ -0,0 +1,22 @@ +import { useMemo } from 'react' +import { useAllClaimingTransactions } from 'state/enhancedTransactions/hooks' +import { EnhancedTransactionDetails } from 'state/enhancedTransactions/reducer' + +export * from '@src/state/transactions/hooks' + +// watch for submissions to claim +// return null if not done loading, return undefined if not found +export function useUserHasSubmittedClaim(account?: string): { + claimSubmitted: boolean + claimTxn: EnhancedTransactionDetails | undefined +} { + const pendingClaims = useAllClaimingTransactions() + const claimTxn = useMemo( + () => + // find one that is both the user's claim, AND not mined + pendingClaims.find((claim) => claim.claim?.recipient === account), + [account, pendingClaims] + ) + + return { claimSubmitted: !!claimTxn, claimTxn } +}