import React, { createContext, FC, PropsWithChildren, useContext, useCallback, useEffect, useState } from "react"; import { getRecentClaims, INewsFeedProvider, PoolDetails, SupportedChains, useClaim, useClaimedAlt, useGetMemberUBIPools, useMultiClaim } from "@gooddollar/web3sdk-v2"; import { QueryParams, useEthers } from "@usedapp/core"; import { isArray, isEmpty, isUndefined, noop } from "lodash"; import { Lock } from "async-await-mutex-lock"; import { getUnclaimedPools } from "../utils/pools"; import { ClaimContextProps } from "../types"; import { useFormatClaimTransactions } from "../../../hooks"; const ClaimContext = createContext(undefined); export const useClaimContext = () => { const context = useContext(ClaimContext); if (!context) { throw new Error("useClaimContext must be used within a ClaimProvider"); } return context; }; const explorerPollLock = new Lock(); interface ClaimProviderProps { activePoolAddresses?: { [key: string]: string }; explorerEndPoints: { [key in keyof typeof SupportedChains]: string }; supportedChains?: SupportedChains[]; withSignModals: boolean; provider?: any; onNews: () => void; onUpgrade: () => void; onConnect?: () => Promise; onSuccess?: () => Promise; onSendTx?: () => void; onSwitchChain?: (chainId: number) => Promise; withNewsFeed: boolean; newsProps: Omit; } export const ClaimProvider: FC> = ({ activePoolAddresses = { ALT: "0xa6d77aa130e729886c90a48e0ee539d6c6795df7", NG: "0x77253761353271813c1aca275de8eec768b217c5", CO: "0x627dbf00ce1a54067f5a34d6596a217a029c1532" }, children, explorerEndPoints = { MAINNET: "https://api.etherscan.io/v2/api?chainid=1&", CELO: "https://celo.blockscout.com/api?", FUSE: "https://explorer.fuse.io/api?", XDC: "https://explorer.xdc.network/api" }, provider, supportedChains = [SupportedChains.CELO, SupportedChains.FUSE, SupportedChains.XDC], withSignModals, withNewsFeed = true, newsProps, onUpgrade, onConnect, onNews, // onSendTx, onSuccess, onSwitchChain }) => { const { account, chainId, library, switchNetwork } = useEthers(); const [refreshRate, setRefreshRate] = useState(4); const [preClaimPools, setClaimPools] = useState(undefined); const [postClaimPools, setPostClaimPools] = useState(undefined); const activeChain = SupportedChains[chainId as number] ?? "CELO"; const endpoints = explorerEndPoints[activeChain as keyof typeof SupportedChains]; const [error, setError] = useState(undefined); const claimedAlt = useClaimedAlt(chainId); const [txDetails, setTxDetails] = useState({ transaction: undefined, isOpen: false }); const formattedTransactionList = useFormatClaimTransactions( postClaimPools && postClaimPools?.length > 0 ? postClaimPools : preClaimPools ? preClaimPools : [], chainId, account ?? "" ); const claimDetails = useClaim(refreshRate); const { poolsDetails, loading, fetchPools } = useGetMemberUBIPools(); const { errorMessage } = { errorMessage: undefined }; const { poolContracts, startClaiming: onClaim, claimFlowStatus } = useMultiClaim(preClaimPools); const switchChain = useCallback( (chainId: number) => { // 4902: Network is not added, and should be done manually // explanation to user is shown through network modal switchNetwork(chainId).catch((e: any) => { if (e.code === 4902) { // toggleNetworkModal() //todo: add network modal for gooddapp } }); }, [switchNetwork, claimedAlt] ); const onTxDetailsPress = useCallback( (transaction: any) => { setTxDetails({ transaction, isOpen: true }); }, [txDetails] ); const onClaimFailed = useCallback(async () => { setError(errorMessage ?? /*i18n*/ "An unknown error occurred while claiming"); setClaimPools(undefined); }, [errorMessage]); const onReset = useCallback(() => { setClaimPools(undefined); setPostClaimPools(undefined); void fetchPools(); }, [onSuccess, chainId, account]); // should handle what happens after all claims are done (eg. showing a next-task modal) // if nothing is done, it will just silently finish const onClaimSuccess = useCallback(async () => { setClaimPools(undefined); setRefreshRate("everyBlock"); void fetchPools(); await onSuccess?.(); }, [onSuccess]); useEffect(() => { onReset(); }, [/*used*/ chainId, /*used*/ account]); //Handling of claimable pools useEffect(() => { if (isUndefined(poolsDetails || claimDetails.hasClaimed) || !isUndefined(preClaimPools)) { return; } const unclaimedPools = getUnclaimedPools(poolsDetails); if (account && !loading && !isUndefined(claimDetails.hasClaimed)) { let details: any[] = !claimDetails.hasClaimed ? [claimDetails] : []; if (!isEmpty(unclaimedPools) && unclaimedPools) { details.push(...unclaimedPools); } // the claimReceipts are available faster then awaiting useCall and sdk responses if (claimFlowStatus.claimReceipts) { const alreadyClaimed = claimFlowStatus.claimReceipts.filter(Boolean).map(receipt => receipt.to); details = details.filter((pool: PoolDetails) => !alreadyClaimed.includes(pool.address)); } setClaimPools(details); } }, [/*used*/ poolContracts, claimDetails, preClaimPools, poolsDetails, claimFlowStatus.isClaimingDone]); //Handling of post-claim transaction list useEffect(() => { void (async () => { if (explorerPollLock.isAcquired("pollLock") || !isEmpty(postClaimPools) || poolsDetails === undefined) { return; } const noUnclaimedPools = !isEmpty(getUnclaimedPools(poolsDetails)); const hasClaimed = claimDetails.hasClaimed === true; if (noUnclaimedPools) { setPostClaimPools(undefined); return; } if (account && hasClaimed && !noUnclaimedPools && !loading) { await explorerPollLock.acquire("pollLock"); const poolAddresses = Object.values(activePoolAddresses); const poolContracts = [claimDetails.address, ...poolAddresses]; const claimTransactionList = await getRecentClaims( account, endpoints, provider ?? library, poolContracts, isArray(poolsDetails) && poolsDetails?.length > 0 ) .then(res => { return res; }) .catch(e => { console.error("getRecentClaims failed:", e); return []; }) .finally(() => { explorerPollLock.release("pollLock"); }); const details: any = []; details.push(claimTransactionList); setClaimPools([]); setPostClaimPools(details); } })(); }, [postClaimPools, claimDetails, endpoints, poolsDetails]); return ( {children} ); };