diff --git a/packages/apps/job-launcher/client/src/components/Jobs/Create/CryptoPayForm.tsx b/packages/apps/job-launcher/client/src/components/Jobs/Create/CryptoPayForm.tsx index f9fce014bb..73dc6542df 100644 --- a/packages/apps/job-launcher/client/src/components/Jobs/Create/CryptoPayForm.tsx +++ b/packages/apps/job-launcher/client/src/components/Jobs/Create/CryptoPayForm.tsx @@ -7,6 +7,7 @@ import { Box, Button, Checkbox, + CircularProgress, FormControl, FormControlLabel, Grid, @@ -19,12 +20,8 @@ import { Decimal } from 'decimal.js'; import { ethers } from 'ethers'; import { useEffect, useMemo, useState } from 'react'; import { Address } from 'viem'; -import { - useAccount, - useReadContract, - useWalletClient, - usePublicClient, -} from 'wagmi'; +import { readContract } from 'viem/actions'; +import { useAccount, useWalletClient, usePublicClient } from 'wagmi'; import { TokenSelect } from '../../../components/TokenSelect'; import { useCreateJobPageUI } from '../../../providers/CreateJobPageUIProvider'; import * as jobService from '../../../services/job'; @@ -51,6 +48,11 @@ export const CryptoPayForm = ({ const [amount, setAmount] = useState(); const [isLoading, setIsLoading] = useState(false); const [jobLauncherAddress, setJobLauncherAddress] = useState(); + const [jobLauncherFee, setJobLauncherFee] = useState(); + const [configurationError, setConfigurationError] = useState(); + const [configurationRetry, setConfigurationRetry] = useState(0); + const [feeError, setFeeError] = useState(); + const [feeRetry, setFeeRetry] = useState(0); const [minFee, setMinFee] = useState(0.01); const { data: signer } = useWalletClient(); const publicClient = usePublicClient(); @@ -62,14 +64,21 @@ export const CryptoPayForm = ({ useEffect(() => { const fetchJobLauncherData = async () => { - const address = await paymentService.getOperatorAddress(); - const fee = await paymentService.getFee(); - setJobLauncherAddress(address); - setMinFee(fee); + setConfigurationError(undefined); + try { + const address = await paymentService.getOperatorAddress(); + const fee = await paymentService.getFee(); + setJobLauncherAddress(address); + setMinFee(fee); + } catch { + setConfigurationError( + 'Unable to load the payment configuration. Please try again.', + ); + } }; fetchJobLauncherData(); - }, []); + }, [configurationRetry]); useEffect(() => { const fetchRates = async () => { @@ -106,17 +115,46 @@ export const CryptoPayForm = ({ setDecimals(Math.min(tokenDecimals, 6)); }, [tokenDecimals]); - const { data: jobLauncherFee } = useReadContract({ - address: NETWORKS[jobRequest.chainId!]?.kvstoreAddress as Address, - abi: KVStoreABI, - functionName: 'get', - args: jobLauncherAddress - ? [jobLauncherAddress, KVStoreKeys.fee] - : undefined, - query: { - enabled: !!jobLauncherAddress, - }, - }); + useEffect(() => { + if (!signer || !publicClient || !jobLauncherAddress || !jobRequest.chainId) + return; + + let ignore = false; + + const fetchJobLauncherFee = async () => { + setFeeError(undefined); + setJobLauncherFee(undefined); + try { + const parameters = { + address: NETWORKS[jobRequest.chainId!]?.kvstoreAddress as Address, + abi: KVStoreABI, + functionName: 'get', + args: [jobLauncherAddress, KVStoreKeys.fee], + } as const; + + let fee: string; + try { + fee = (await readContract(signer, parameters)) as string; + } catch { + fee = (await readContract(publicClient, parameters)) as string; + } + + if (!ignore) { + setJobLauncherFee(fee); + } + } catch { + if (!ignore) { + setFeeError('Unable to load the job launcher fee. Please try again.'); + } + } + }; + + fetchJobLauncherFee(); + + return () => { + ignore = true; + }; + }, [feeRetry, jobLauncherAddress, jobRequest.chainId, publicClient, signer]); const minFeeToken = useMemo(() => { if (minFee && paymentTokenRate) @@ -125,7 +163,7 @@ export const CryptoPayForm = ({ }, [minFee, paymentTokenRate]); const feeAmount = useMemo(() => { - if (!amount) return 0; + if (!amount || jobLauncherFee == null) return 0; const amountDecimal = new Decimal(amount); const feeDecimal = new Decimal(jobLauncherFee as string).div(100); return Number( @@ -244,7 +282,7 @@ export const CryptoPayForm = ({ } }; - if (!chain || chain.id !== jobRequest.chainId) + if (isConnected && (!chain || chain.id !== jobRequest.chainId)) return ( @@ -257,6 +295,43 @@ export const CryptoPayForm = ({ ); + if (configurationError || feeError) + return ( + + + {configurationError || feeError} + + + + ); + + if ( + !jobLauncherAddress || + (isConnected && (!signer || jobLauncherFee == null)) + ) + return ( + + + + ); + return ( @@ -389,7 +464,7 @@ export const CryptoPayForm = ({ > Fee - ({Number(jobLauncherFee)}%){' '} + ({jobLauncherFee == null ? '—' : Number(jobLauncherFee)}%){' '} {paymentTokenSymbol ? `${Number(feeAmount.toFixed(6))} ${paymentTokenSymbol?.toUpperCase()}` : ''} @@ -476,6 +551,7 @@ export const CryptoPayForm = ({ !paymentTokenAddress || !fundTokenSymbol || !amount || + jobLauncherFee == null || jobRequest.chainId !== chain?.id } loading={isLoading} diff --git a/packages/apps/job-launcher/client/src/components/Jobs/Create/FiatPayForm.tsx b/packages/apps/job-launcher/client/src/components/Jobs/Create/FiatPayForm.tsx index 82502c9b9b..b6164da092 100644 --- a/packages/apps/job-launcher/client/src/components/Jobs/Create/FiatPayForm.tsx +++ b/packages/apps/job-launcher/client/src/components/Jobs/Create/FiatPayForm.tsx @@ -2,6 +2,7 @@ import KVStoreABI from '@human-protocol/core/abis/KVStore.json'; import { KVStoreKeys, NETWORKS } from '@human-protocol/sdk'; import { LoadingButton } from '@mui/lab'; import { + Alert, Box, Button, Checkbox, @@ -142,6 +143,8 @@ export const FiatPayForm = ({ data: jobLauncherFee, error, isError, + isFetching: isFeeFetching, + refetch: refetchJobLauncherFee, } = useReadContract({ address: NETWORKS[jobRequest.chainId!]?.kvstoreAddress as Address, abi: KVStoreABI, @@ -276,7 +279,9 @@ export const FiatPayForm = ({ return ( - {loadingInitialData ? ( + {loadingInitialData || + !jobLauncherAddress || + (!isError && (isFeeFetching || jobLauncherFee == null)) ? ( + ) : isError ? ( + + + Unable to load the job launcher fee from MetaMask or the public + network provider. + + + ) : ( @@ -522,6 +537,8 @@ export const FiatPayForm = ({ !amount || (!payWithAccountBalance && !selectedCard) || hasError || + isFeeFetching || + jobLauncherFee == null || !tokenSymbol } > diff --git a/packages/apps/job-launcher/client/src/providers/WagmiProvider.tsx b/packages/apps/job-launcher/client/src/providers/WagmiProvider.tsx index ad9308ed9e..f63c653823 100644 --- a/packages/apps/job-launcher/client/src/providers/WagmiProvider.tsx +++ b/packages/apps/job-launcher/client/src/providers/WagmiProvider.tsx @@ -4,10 +4,11 @@ import { WagmiProvider as WWagmiProvider, fallback, http, + injected, unstable_connector, } from 'wagmi'; import * as wagmiChains from 'wagmi/chains'; -import { coinbaseWallet, walletConnect, metaMask } from 'wagmi/connectors'; +import { coinbaseWallet, walletConnect } from 'wagmi/connectors'; import { LOCALHOST } from '../constants/chains'; @@ -37,41 +38,41 @@ export const wagmiConfig = createConfig({ coinbaseWallet({ appName: 'human-job-launcher', }), - metaMask(), + injected({ target: 'metaMask' }), ], transports: { - [wagmiChains.mainnet.id]: fallback([unstable_connector(metaMask), http()]), - [wagmiChains.sepolia.id]: fallback([unstable_connector(metaMask), http()]), - [wagmiChains.bsc.id]: fallback([unstable_connector(metaMask), http()]), + [wagmiChains.mainnet.id]: fallback([unstable_connector(injected), http()]), + [wagmiChains.sepolia.id]: fallback([unstable_connector(injected), http()]), + [wagmiChains.bsc.id]: fallback([unstable_connector(injected), http()]), [wagmiChains.bscTestnet.id]: fallback([ - unstable_connector(metaMask), + unstable_connector(injected), http(), ]), - [wagmiChains.polygon.id]: fallback([unstable_connector(metaMask), http()]), + [wagmiChains.polygon.id]: fallback([unstable_connector(injected), http()]), [wagmiChains.polygonAmoy.id]: fallback([ - unstable_connector(metaMask), + unstable_connector(injected), http(), ]), - [wagmiChains.moonbeam.id]: fallback([unstable_connector(metaMask), http()]), + [wagmiChains.moonbeam.id]: fallback([unstable_connector(injected), http()]), [wagmiChains.moonbaseAlpha.id]: fallback([ - unstable_connector(metaMask), + unstable_connector(injected), http(), ]), [wagmiChains.avalanche.id]: fallback([ - unstable_connector(metaMask), + unstable_connector(injected), http(), ]), [wagmiChains.avalancheFuji.id]: fallback([ - unstable_connector(metaMask), + unstable_connector(injected), http(), ]), - [wagmiChains.xLayer.id]: fallback([unstable_connector(metaMask), http()]), + [wagmiChains.xLayer.id]: fallback([unstable_connector(injected), http()]), [wagmiChains.xLayerTestnet.id]: fallback([ - unstable_connector(metaMask), + unstable_connector(injected), http(), ]), [LOCALHOST.id]: fallback([ - unstable_connector(metaMask), + unstable_connector(injected), http(LOCALHOST.rpcUrls.default.http[0]), ]), },