From 53c5cbcfa97da790d2480167c9be70055fdd24c6 Mon Sep 17 00:00:00 2001 From: Trung-Tin Pham <60747384+AtelyPham@users.noreply.github.com> Date: Fri, 21 Apr 2023 13:51:26 +0700 Subject: [PATCH] 8-sided Bridge with DKG Deployment (#1048) --- .../ChainListCardWrapper.tsx | 2 +- .../DepositContainer/DepositContainer.tsx | 8 +- .../TransferConfirmContainer.tsx | 23 +- .../TransferContainer/TransferContainer.tsx | 380 +++++++++++++----- .../containers/TransferContainer/types.d.ts | 3 +- .../WithdrawConfirmContainer.tsx | 25 +- .../WithdrawContainer/WithdrawContainer.tsx | 79 ++-- .../containers/WithdrawContainer/shared.tsx | 6 +- .../containers/WithdrawContainer/types.d.ts | 9 +- apps/bridge-dapp/src/hooks/useMaxFeeInfo.ts | 2 +- apps/bridge-dapp/src/pages/PageBridge.tsx | 2 +- .../src/utils/getAcitveSourceChains.ts | 17 +- .../src/relayer/types.ts | 2 +- .../src/relayer/webb-relayer.ts | 16 +- .../src/vanchor/vanchor-actions.ts | 1 + .../src/WebbProvider.tsx | 36 +- .../src/error/index.ts | 1 + .../src/error/parseError.ts | 37 ++ libs/dapp-config/src/anchors/anchor-config.ts | 54 +-- .../src/chains/chain-config.interface.ts | 11 + libs/dapp-config/src/chains/evm/index.tsx | 133 ++++-- .../src/chains/substrate/index.tsx | 5 + libs/dapp-config/src/gasLimit-config.ts | 4 + .../on-chain-config/evm/on-chain-config.ts | 40 +- .../src/wallets/wallets-config.tsx | 15 +- libs/dapp-types/src/ChainId.ts | 6 + libs/dapp-types/src/EVMChainId.ts | 13 +- libs/icons/src/chains/athena-orbit.svg | 142 +++++++ libs/icons/src/chains/demeter-orbit.svg | 142 +++++++ libs/icons/src/chains/hermes-orbit.svg | 142 +++++++ .../src/ext-provider/web3-provider.ts | 18 +- libs/web3-api-provider/src/webb-provider.ts | 5 +- .../src/webb-provider/vanchor-actions.ts | 50 ++- .../BridgeInputs/RecipientInput.tsx | 52 +-- .../Notification/NotificationItem.tsx | 2 +- .../TokenWithAmount/TokenWithAmount.tsx | 14 +- .../ConfirmationCard/DepositConfirm.tsx | 20 +- .../ConfirmationCard/TransferConfirm.tsx | 42 +- .../ConfirmationCard/WithdrawConfirm.tsx | 21 +- .../containers/DepositCard/DepositCard.tsx | 12 +- .../containers/TransferCard/TransferCard.tsx | 90 ++--- .../src/containers/TransferCard/types.ts | 33 +- .../containers/WithdrawCard/WithdrawCard.tsx | 4 +- .../src/utils/getRoundedAmountString.ts | 22 +- package.json | 9 +- yarn.lock | 56 +-- 46 files changed, 1328 insertions(+), 478 deletions(-) create mode 100644 libs/api-provider-environment/src/error/parseError.ts create mode 100644 libs/icons/src/chains/athena-orbit.svg create mode 100644 libs/icons/src/chains/demeter-orbit.svg create mode 100644 libs/icons/src/chains/hermes-orbit.svg diff --git a/apps/bridge-dapp/src/components/ChainListCardWrapper/ChainListCardWrapper.tsx b/apps/bridge-dapp/src/components/ChainListCardWrapper/ChainListCardWrapper.tsx index 89af63147b..11ce18d555 100644 --- a/apps/bridge-dapp/src/components/ChainListCardWrapper/ChainListCardWrapper.tsx +++ b/apps/bridge-dapp/src/components/ChainListCardWrapper/ChainListCardWrapper.tsx @@ -108,7 +108,7 @@ export const ChainListCardWrapper: FC = ({ { @@ -577,7 +579,7 @@ export const DepositContainer = forwardRef< const tokenListDepositProps = useMemo(() => { return { - className: 'min-w-[550px] h-[700px]', + className: 'min-w-[550px] h-[710px]', title: `Select a token from ${selectedSourceChain?.name}`, popularTokens: [], selectTokens: populatedSelectableWebbTokens, @@ -612,7 +614,7 @@ export const DepositContainer = forwardRef< ); return { - className: 'min-w-[550px] h-[700px]', + className: 'min-w-[550px] h-[710px]', selectTokens: tokens, value: destChainInputValue, title: 'Select a token to Deposit', diff --git a/apps/bridge-dapp/src/containers/TransferContainer/TransferConfirmContainer.tsx b/apps/bridge-dapp/src/containers/TransferContainer/TransferConfirmContainer.tsx index d2d09c77f9..96421fa5b6 100644 --- a/apps/bridge-dapp/src/containers/TransferContainer/TransferConfirmContainer.tsx +++ b/apps/bridge-dapp/src/containers/TransferContainer/TransferConfirmContainer.tsx @@ -10,7 +10,12 @@ import { downloadString } from '@webb-tools/browser-utils'; import { chainsPopulated } from '@webb-tools/dapp-config'; import { useRelayers, useTxQueue, useVAnchor } from '@webb-tools/react-hooks'; import { ChainType, Note, calculateTypedChainId } from '@webb-tools/sdk-core'; -import { TransferConfirm, useWebbUI } from '@webb-tools/webb-ui-components'; +import { + getRoundedAmountString, + TransferConfirm, + useWebbUI, +} from '@webb-tools/webb-ui-components'; +import { BigNumber, ethers } from 'ethers'; import { forwardRef, useCallback, useMemo, useState } from 'react'; import { useLatestTransactionStage, @@ -43,7 +48,7 @@ export const TransferConfirmContainer = forwardRef< transferUtxo, inputNotes, onResetState, - feeAmount, + feeInWei: feeAmount, feeToken, ...props }, @@ -188,6 +193,7 @@ export const TransferConfirmContainer = forwardRef< notes: inputNotes, changeUtxo, transferUtxo, + feeAmount: feeAmount ?? BigNumber.from(0), }; const args = await vAnchorApi.prepareTransaction(tx, txPayload, ''); @@ -236,6 +242,7 @@ export const TransferConfirmContainer = forwardRef< setMainComponent, changeUtxo, transferUtxo, + feeAmount, activeRelayer, noteManager, onResetState, @@ -250,6 +257,16 @@ export const TransferConfirmContainer = forwardRef< return txPayload ? txPayload.txStatus.message?.replace('...', '') : ''; }, [txId, txPayloads]); + const formattedFee = useMemo(() => { + if (!feeAmount) { + return undefined; + } + + const amountNum = Number(ethers.utils.formatEther(feeAmount)); + + return getRoundedAmountString(amountNum, 3, Math.round); + }, [feeAmount]); + return ( setMainComponent(undefined)} checkboxProps={{ diff --git a/apps/bridge-dapp/src/containers/TransferContainer/TransferContainer.tsx b/apps/bridge-dapp/src/containers/TransferContainer/TransferContainer.tsx index 53095df551..4845af3d21 100644 --- a/apps/bridge-dapp/src/containers/TransferContainer/TransferContainer.tsx +++ b/apps/bridge-dapp/src/containers/TransferContainer/TransferContainer.tsx @@ -37,6 +37,7 @@ import { AssetType, ChainType, } from '@webb-tools/webb-ui-components/components/ListCard/types'; +import { TransferCardProps } from '@webb-tools/webb-ui-components/containers/TransferCard/types'; import { BigNumber, ethers } from 'ethers'; import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react'; import { ChainListCardWrapper } from '../../components'; @@ -137,20 +138,17 @@ export const TransferContainer = forwardRef< const { feeInfo, fetchMaxFeeInfo, + fetchMaxFeeInfoFromRelayer, isLoading: isFetchingMaxFeeInfo, + resetMaxFeeInfo, } = useMaxFeeInfo(maxFeeArgs); - const feeValue = useMemo(() => { - if (!feeInfo) { - return undefined; - } - - if (!(feeInfo instanceof BigNumber)) { - console.error('Fee info is not a BigNumber instance'); - return undefined; + const feeInWei = useMemo(() => { + if (!feeInfo || feeInfo instanceof BigNumber) { + return feeInfo; } - return ethers.utils.formatEther(feeInfo); + return feeInfo.estimatedFee; }, [feeInfo]); const currentNativeCurrency = useMemo(() => { @@ -309,7 +307,7 @@ export const TransferContainer = forwardRef< setMainComponent( setMainComponent(undefined)} onChange={(nextRelayer) => { setRelayer( @@ -484,8 +482,24 @@ export const TransferContainer = forwardRef< ); }, [amount, selectedBridgingAsset?.balance]); - // Boolean state for whether the transfer button is disabled - const isTransferButtonDisabled = useMemo(() => { + // The actual amount to be transferred + const receivingAmount = useMemo(() => { + // If no relayer selected, return the amount + if (!activeRelayer) { + return amount; + } + + if (!feeInWei || !amount) { + return amount; + } + + // If relayer selected, return the amount minus the relayer fee + const fee = Number(ethers.utils.formatEther(feeInWei)); + return amount - fee; + }, [activeRelayer, amount, feeInWei]); + + // Boolean indicating whether inputs are valid to transfer + const isValidToTransfer = useMemo(() => { return [ Boolean(fungibleCurrency), // No fungible currency selected Boolean(destChain), // No destination chain selected @@ -503,9 +517,9 @@ export const TransferContainer = forwardRef< isValidRecipient, ]); - // Calculate input notes for current amount - const inputNotes = useMemo(() => { - if (!destChain || !fungibleCurrency || !amount) { + // All available notes + const availableNotes = useMemo(() => { + if (!destChain || !fungibleCurrency) { return []; } @@ -521,29 +535,38 @@ export const TransferContainer = forwardRef< console.error('No anchor address found for chain', typedChainId); return []; } + const resourceId = new ResourceId( vanchorAddr, destChain.chainType, destChain.chainId ); - const avaiNotes = + return ( allNotes .get(resourceId.toString()) ?.filter( (note) => note.note.tokenSymbol === fungibleCurrency.view.symbol - ) ?? []; + ) ?? [] + ); + }, [allNotes, apiConfig, destChain, fungibleCurrency]); + + // Calculate input notes for current amount + const inputNotes = useMemo(() => { + if (!fungibleCurrency) { + return []; + } return ( NoteManager.getNotesFifo( - avaiNotes, + availableNotes, ethers.utils.parseUnits( - amount.toString(), + amount?.toString() ?? '0', fungibleCurrency.view.decimals ) ) ?? [] ); - }, [allNotes, amount, apiConfig, destChain, fungibleCurrency]); + }, [amount, availableNotes, fungibleCurrency]); // Calculate the info for UI display const infoCalculated = useMemo(() => { @@ -562,7 +585,7 @@ export const TransferContainer = forwardRef< : ethers.BigNumber.from(0); const transferAmount = isValidAmount - ? getRoundedAmountString(amount) + ? getRoundedAmountString(receivingAmount) : undefined; const changeAmount = @@ -580,7 +603,7 @@ export const TransferContainer = forwardRef< return { transferAmount, changeAmount, - transferTokenSymbol: selectedBridgingAsset?.symbol, + transferTokenSymbol: selectedBridgingAsset?.symbol ?? '', rawChangeAmount: fungibleCurrency ? +ethers.utils.formatUnits( changeAmountBigNumber, @@ -593,9 +616,24 @@ export const TransferContainer = forwardRef< fungibleCurrency, inputNotes, isValidAmount, + receivingAmount, selectedBridgingAsset?.symbol, ]); + // If no relayer is selected, the fee token symbol should be native + // otherwise, it should be the transfer token symbol + const feeTokenSymbol = useMemo(() => { + if (!activeRelayer) { + return currentNativeCurrency?.symbol ?? ''; + } + + return infoCalculated?.transferTokenSymbol ?? ''; + }, [ + activeRelayer, + currentNativeCurrency?.symbol, + infoCalculated?.transferTokenSymbol, + ]); + const handleSwitchChain = useCallback( async (destChain: Chain) => { if (!activeChain) { @@ -635,7 +673,8 @@ export const TransferContainer = forwardRef< setIsValidRecipient(false); setAmount(undefined); setRelayer(null); - }, [setRelayer]); + resetMaxFeeInfo(); + }, [resetMaxFeeInfo, setRelayer]); // Callback for transfer button clicked const handleTransferClick = useCallback(async () => { @@ -660,15 +699,11 @@ export const TransferContainer = forwardRef< return; } - if ( - !fungibleCurrency || - !destChain || - !api || - !noteManager || - !activeApi?.state?.activeBridge || - !amount || - !currentTypedChainId - ) { + if (!noteManager || !activeApi?.state?.activeBridge || !api) { + throw new Error('No note manager or active bridge'); + } + + if (!fungibleCurrency || !destChain || !amount || !currentTypedChainId) { throw new Error( "Can't transfer without a fungible currency or dest chain" ); @@ -706,10 +741,16 @@ export const TransferContainer = forwardRef< // Setup the recipient's keypair. const recipientKeypair = Keypair.fromString(recipientPubKey); + const fee = feeInWei ?? BigNumber.from(0); + + const utxoAmount = activeRelayer + ? amountBigNumber.sub(fee) + : amountBigNumber; + const transferUtxo = await CircomUtxo.generateUtxo({ curve: 'Bn254', backend: 'Circom', - amount: amountBigNumber.toString(), + amount: utxoAmount.toString(), chainId: destTypedChainId.toString(), keypair: recipientKeypair, originChainId: currentTypedChainId.toString(), @@ -762,9 +803,11 @@ export const TransferContainer = forwardRef< { - if (!fungibleCurrency || !amount || !destChain || !activeChain) { + const isReady = useMemo(() => { + if (!fungibleCurrency || !destChain || !activeChain) { + return false; + } + + if (!amount || !isValidAmount) { return false; } @@ -895,80 +942,199 @@ export const TransferContainer = forwardRef< balancesFromNotes, destChain, fungibleCurrency, + isValidAmount, isValidRecipient, recipientPubKey, ]); useEffect(() => { - if (isReadyToFetchMaxFee) { - fetchMaxFeeInfo(); + if (isReady) { + if (activeRelayer) { + fetchMaxFeeInfoFromRelayer(activeRelayer); + } else { + fetchMaxFeeInfo(); + } + } + }, [activeRelayer, fetchMaxFeeInfo, fetchMaxFeeInfoFromRelayer, isReady]); + + // Transfer card props + const bridgeAssetInputProps = useMemo(() => { + return { + token: selectedBridgingAsset, + onClick: handleBridgingAssetInputClick, + }; + }, [handleBridgingAssetInputClick, selectedBridgingAsset]); + + const destChainInputProps = useMemo(() => { + return { + chain: selectedDestChain, + chainType: 'dest' as const, + onClick: handleDestChainClick, + info: 'Destination chain', + }; + }, [handleDestChainClick, selectedDestChain]); + + const amountInputProps = useMemo(() => { + return { + amount: amount ? amount.toString() : undefined, + onAmountChange, + errorMessage: amountError, + isDisabled: !selectedBridgingAsset || !destChain, + onMaxBtnClick: () => setAmount(selectedBridgingAsset?.balance ?? 0), + }; + }, [amount, amountError, destChain, onAmountChange, selectedBridgingAsset]); + + const relayerInputProps = useMemo(() => { + return { + relayerAddress: activeRelayer?.beneficiary, + iconTheme: activeChain + ? activeChain.chainType === ChainTypeEnum.EVM + ? ('ethereum' as const) + : ('substrate' as const) + : undefined, + onClick: handleRelayerClick, + }; + }, [activeChain, activeRelayer?.beneficiary, handleRelayerClick]); + + const recipientInputProps = useMemo< + TransferCardProps['recipientInputProps'] + >(() => { + return { + isValidSet(valid: boolean) { + setIsValidRecipient(valid); + }, + title: 'Recipient Public Key', + info: 'Public key of the recipient', + errorMessage: recipientError, + value: recipientPubKey, + validate: (value) => isValidPublicKey(value), + onChange: (recipient) => { + setRecipientPubKey(recipient); + }, + overrideInputProps: { + placeholder: 'Enter recipient public key', + }, + }; + }, [recipientError, recipientPubKey]); + + const maxFeeText = useMemo(() => { + if (isFetchingMaxFeeInfo) { + return 'Calculating...'; + } + + if (!feeInWei) { + return '--'; } - }, [fetchMaxFeeInfo, isReadyToFetchMaxFee]); + + return `${getRoundedAmountString( + Number(ethers.utils.formatEther(feeInWei)), + 3, + Math.round + )} ${feeTokenSymbol}`; + }, [feeInWei, feeTokenSymbol, isFetchingMaxFeeInfo]); + + const infoItemProps = useMemo(() => { + const total = availableNotes.reduce((acc, note) => { + const formated = Number( + ethers.utils.formatUnits(note.note.amount, note.note.denomination) + ); + + return acc + formated; + }, 0); + + const { transferAmount, transferTokenSymbol } = infoCalculated; + + const formatedRemainingBalance = getRoundedAmountString( + total - (amount ?? 0), + 3, + Math.round + ); + + return [ + { + leftTextProps: { + title: 'Receiving', + info: 'Receiving', + }, + rightContent: transferAmount + ? `${transferAmount} ${transferTokenSymbol}` + : '--', + }, + { + leftTextProps: { + title: 'Remaining balance', + info: 'Remaining balance', + }, + rightContent: amount + ? `${formatedRemainingBalance} ${transferTokenSymbol}` + : '--', + }, + { + leftTextProps: { + title: 'Max fee', + }, + rightContent: maxFeeText, + }, + ]; + }, [amount, availableNotes, infoCalculated, maxFeeText]); + + // Transfer button props + const buttonDesc = useMemo(() => { + if (!feeInWei || !amount) { + return; + } + + const totalFee = Number(ethers.utils.formatEther(feeInWei)); + const formattedFee = getRoundedAmountString(totalFee, 3, Math.round); + const tkSymbol = infoCalculated?.transferTokenSymbol ?? ''; + const feeText = `${formattedFee} ${tkSymbol}`.trim(); + + if (amount < totalFee) { + return `Insufficient funds. You need more than ${feeText} to cover the fee`; + } + + return; + }, [amount, feeInWei, infoCalculated?.transferTokenSymbol]); + + const isDisabled = useMemo(() => { + return isWalletConnected && hasNoteAccount && isValidToTransfer; + }, [hasNoteAccount, isValidToTransfer, isWalletConnected]); + + const isLoading = useMemo(() => { + return ( + isFetchingMaxFeeInfo || + loading || + walletState === WalletState.CONNECTING + ); + }, [isFetchingMaxFeeInfo, loading, walletState]); + + const loadingText = useMemo(() => { + return isFetchingMaxFeeInfo ? 'Calculating Fee...' : 'Connecting...'; + }, [isFetchingMaxFeeInfo]); + + const transferBtnProps = useMemo(() => { + return { + isDisabled: isDisabled, + isLoading: isLoading, + loadingText: loadingText, + children: buttonText, + onClick: handleTransferClick, + }; + }, [buttonText, handleTransferClick, isDisabled, isLoading, loadingText]); return ( setAmount(selectedBridgingAsset?.balance ?? 0), - }} - relayerInputProps={{ - relayerAddress: activeRelayer?.beneficiary, - iconTheme: activeChain - ? activeChain.chainType === ChainTypeEnum.EVM - ? 'ethereum' - : 'substrate' - : undefined, - onClick: handleRelayerClick, - }} - recipientInputProps={{ - isValidSet(valid: boolean) { - setIsValidRecipient(valid); - }, - title: 'Recipient Public Key', - info: 'Public key of the recipient', - errorMessage: recipientError, - value: recipientPubKey, - validate: (value) => isValidPublicKey(value), - onChange: (recipient) => { - setRecipientPubKey(recipient); - }, - overrideInputProps: { - placeholder: 'Enter recipient public key', - }, - }} - transferBtnProps={{ - isDisabled: - isWalletConnected && hasNoteAccount && isTransferButtonDisabled, - isLoading: - isFetchingMaxFeeInfo || - loading || - walletState === WalletState.CONNECTING, - loadingText: isFetchingMaxFeeInfo - ? 'Calculating Fee...' - : 'Connecting...', - children: buttonText, - onClick: handleTransferClick, - }} - feeAmount={feeValue} - feeToken={currentNativeCurrency?.symbol} - transferAmount={infoCalculated.transferAmount} - transferToken={infoCalculated.transferTokenSymbol} - changeAmount={infoCalculated.changeAmount} + bridgeAssetInputProps={bridgeAssetInputProps} + destChainInputProps={destChainInputProps} + amountInputProps={amountInputProps} + relayerInputProps={relayerInputProps} + recipientInputProps={recipientInputProps} + transferBtnProps={transferBtnProps} + infoItemProps={infoItemProps} + buttonDesc={buttonDesc} + buttonDescVariant="error" /> ); } diff --git a/apps/bridge-dapp/src/containers/TransferContainer/types.d.ts b/apps/bridge-dapp/src/containers/TransferContainer/types.d.ts index b11212288e..53adab75bf 100644 --- a/apps/bridge-dapp/src/containers/TransferContainer/types.d.ts +++ b/apps/bridge-dapp/src/containers/TransferContainer/types.d.ts @@ -5,6 +5,7 @@ import { AssetType } from '@webb-tools/webb-ui-components/components/ListCard/ty import { Chain } from '@webb-tools/dapp-config'; import { Note } from '@webb-tools/sdk-core'; import { TransactionPayload } from '@webb-tools/webb-ui-components'; +import { BigNumber } from 'ethers'; export type CurrencyRecord = Record; @@ -97,7 +98,7 @@ export interface TransferConfirmContainerProps /** * The fee amount */ - feeAmount?: number | string; + feeInWei?: BigNumber | null; /** * The fee token diff --git a/apps/bridge-dapp/src/containers/WithdrawContainer/WithdrawConfirmContainer.tsx b/apps/bridge-dapp/src/containers/WithdrawContainer/WithdrawConfirmContainer.tsx index af5690c05f..bffa0216d1 100644 --- a/apps/bridge-dapp/src/containers/WithdrawContainer/WithdrawConfirmContainer.tsx +++ b/apps/bridge-dapp/src/containers/WithdrawContainer/WithdrawConfirmContainer.tsx @@ -4,10 +4,10 @@ import { chainsPopulated } from '@webb-tools/dapp-config'; import { useRelayers, useTxQueue, useVAnchor } from '@webb-tools/react-hooks'; import { ChainType, Note } from '@webb-tools/sdk-core'; import { - WithdrawConfirm, getRoundedAmountString, useCopyable, useWebbUI, + WithdrawConfirm, } from '@webb-tools/webb-ui-components'; import { forwardRef, useCallback, useMemo, useState } from 'react'; @@ -17,6 +17,7 @@ import { TransactionState, WithdrawTransactionPayloadType, } from '@webb-tools/abstract-api-provider'; +import { BigNumber, ethers } from 'ethers'; import { useLatestTransactionStage, useTransactionProgressValue, @@ -28,8 +29,6 @@ import { getTransactionHash, } from '../../utils'; import { WithdrawConfirmContainerProps } from './types'; -import { ExchangeRateInfo } from './shared'; -import { BigNumber, ethers } from 'ethers'; export const WithdrawConfirmContainer = forwardRef< HTMLDivElement, @@ -52,7 +51,8 @@ export const WithdrawConfirmContainer = forwardRef< recipient, refundAmount, refundToken, - targetChainId, + sourceTypedChainId, + targetTypedChainId, unwrapCurrency: { value: unwrapCurrency } = {}, ...props }, @@ -77,9 +77,9 @@ export const WithdrawConfirmContainer = forwardRef< const { relayersState: { activeRelayer }, } = useRelayers({ - typedChainId: targetChainId, + typedChainId: targetTypedChainId, target: activeApi?.state.activeBridge - ? activeApi.state.activeBridge.targets[targetChainId] + ? activeApi.state.activeBridge.targets[targetTypedChainId] : undefined, }); @@ -127,10 +127,10 @@ export const WithdrawConfirmContainer = forwardRef< }, []); const avatarTheme = useMemo(() => { - return chainsPopulated[targetChainId].chainType === ChainType.EVM + return chainsPopulated[targetTypedChainId].chainType === ChainType.EVM ? 'ethereum' : 'substrate'; - }, [targetChainId]); + }, [targetTypedChainId]); const cardTitle = useMemo(() => { let status = ''; @@ -308,6 +308,7 @@ export const WithdrawConfirmContainer = forwardRef< const txPayload = txPayloads.find((txPayload) => txPayload.id === txId); return txPayload ? txPayload.txStatus.message?.replace('...', '') : ''; }, [txId, txPayloads]); + const formattedFee = useMemo(() => { const feeInEthers = ethers.utils.formatEther(fee); @@ -345,9 +346,13 @@ export const WithdrawConfirmContainer = forwardRef< ref={ref} title={cardTitle} activeChains={activeChains} + sourceChain={{ + name: chainsPopulated[sourceTypedChainId].name, + type: chainsPopulated[sourceTypedChainId].base ?? 'webb-dev', + }} destChain={{ - name: chainsPopulated[targetChainId].name, - type: chainsPopulated[targetChainId].base ?? 'webb-dev', + name: chainsPopulated[targetTypedChainId].name, + type: chainsPopulated[targetTypedChainId].base ?? 'webb-dev', }} actionBtnProps={{ isDisabled: withdrawTxInProgress diff --git a/apps/bridge-dapp/src/containers/WithdrawContainer/WithdrawContainer.tsx b/apps/bridge-dapp/src/containers/WithdrawContainer/WithdrawContainer.tsx index 70f2e8efe8..2d32350bb8 100644 --- a/apps/bridge-dapp/src/containers/WithdrawContainer/WithdrawContainer.tsx +++ b/apps/bridge-dapp/src/containers/WithdrawContainer/WithdrawContainer.tsx @@ -52,7 +52,6 @@ import { WalletState, } from '../../hooks'; import { useEducationCardStep } from '../../hooks/useEducationCardStep'; -import { getErrorMessage } from '../../utils'; import { ExchangeRateInfo, TransactionFeeInfo } from './shared'; import { WithdrawContainerProps } from './types'; import { WithdrawConfirmContainer } from './WithdrawConfirmContainer'; @@ -215,6 +214,7 @@ export const WithdrawContainer = forwardRef< fetchMaxFeeInfo, isLoading: isFetchingFeeInfo, feeInfo: feeInfoOrBigNumber, + resetMaxFeeInfo, } = useMaxFeeInfo(maxFeeArgs); const feeInfo = useMemo(() => { @@ -360,9 +360,8 @@ export const WithdrawContainer = forwardRef< const exchangeRate = Number( ethers.utils.formatEther(feeInfoOrBigNumber.refundExchangeRate) ); - const refundAmountWei = ethers.utils.parseEther( - (refundAmount * exchangeRate).toString() - ); + const converted = refundAmount * exchangeRate; + const refundAmountWei = ethers.utils.parseEther(converted.toFixed(6)); feeWei = feeWei.add(refundAmountWei); } @@ -411,19 +410,12 @@ export const WithdrawContainer = forwardRef< return 'Switch chain to withdraw'; } - // If user selects a relayer, require the fee info to be fetched - if (activeRelayer && !feeInfo) { - return 'Fetch fee info'; - } - if (selectedUnwrapToken && isUnwrap) { return 'Unwrap and Withdraw'; } return 'Withdraw'; }, [ - activeRelayer, - feeInfo, hasNoteAccount, isDisabledWithdraw, isUnwrap, @@ -434,12 +426,14 @@ export const WithdrawContainer = forwardRef< const amountAfterFeeWei = useMemo(() => { const amountWei = ethers.utils.parseEther(amount.toString()); - if (!totalFeeInWei) { + // If no fee or no active relayer, then return the original amount + // as the fee is not deducted from the amount + if (!totalFeeInWei || !activeRelayer) { return amountWei; } return amountWei.sub(totalFeeInWei); - }, [amount, totalFeeInWei]); + }, [activeRelayer, amount, totalFeeInWei]); // Calculate the info for UI display const infoCalculated = useMemo(() => { @@ -448,6 +442,7 @@ export const WithdrawContainer = forwardRef< const receivingAmount = isValidAmount ? getRoundedAmountString(amountAfterFee, 3, Math.round) : undefined; + const remainderAmount = isValidAmount ? getRoundedAmountString(availableAmount - amount) : undefined; @@ -478,7 +473,11 @@ export const WithdrawContainer = forwardRef< () => feeInfo ? ( @@ -498,7 +497,7 @@ export const WithdrawContainer = forwardRef< const refundFee = feeInfo && refundAmount && isRefund ? getRoundedAmountString( - refundAmount / + refundAmount * Number(ethers.utils.formatEther(feeInfo.refundExchangeRate)) ) : undefined; @@ -522,7 +521,8 @@ export const WithdrawContainer = forwardRef< setRelayer(null); setRefundAmount(0); setRefundAmountError(''); - }, [setRelayer]); + resetMaxFeeInfo(); + }, [resetMaxFeeInfo, setRelayer]); const handleSwitchToOtherDestChains = useCallback(async () => { if (otherAvailableChains.length === 0 || !activeWallet) { @@ -692,6 +692,9 @@ export const WithdrawContainer = forwardRef< }); } + // Default source chain is the first source chain in the input notes + const sourceTypedChainId = Number(inputNotes[0].note.sourceChainId); + const fee = feeInfoOrBigNumber instanceof BigNumber ? feeInfoOrBigNumber @@ -703,7 +706,8 @@ export const WithdrawContainer = forwardRef< changeUtxo={changeUtxo} changeNote={changeNote} changeAmount={formattedChangeAmount} - targetChainId={currentTypedChainId} + sourceTypedChainId={sourceTypedChainId} + targetTypedChainId={currentTypedChainId} availableNotes={inputNotes} amount={amount} fee={fee} @@ -779,7 +783,7 @@ export const WithdrawContainer = forwardRef< setMainComponent( { const relayerData = relayer.capabilities.supportedChains[ @@ -969,7 +973,7 @@ export const WithdrawContainer = forwardRef< : undefined, onClick: handleRelayerInputClick, }), - [activeChain, activeRelayer, handleRelayerInputClick] + [activeChain, activeRelayer?.beneficiary, handleRelayerInputClick] ); const recipientInputProps = useMemo< @@ -998,7 +1002,6 @@ export const WithdrawContainer = forwardRef< : isWalletConnected && hasNoteAccount && isDisabledWithdraw, isLoading: loading || walletState === WalletState.CONNECTING || isFetchingFeeInfo, - className: cx('mt-4'), loadingText: isFetchingFeeInfo ? 'Fetching fee info...' : 'Connecting...', children: buttonText, onClick: handleWithdrawButtonClick, @@ -1124,19 +1127,27 @@ export const WithdrawContainer = forwardRef< ? `${formattedRefundAmount} ${nativeCurrencySymbol}` : '--'; - const txFeeContent = isFetchingFeeInfo - ? 'Calculating...' - : totalFeeInWei - ? `${getRoundedAmountString( - Number(ethers.utils.formatEther(totalFeeInWei)), - 3, - Math.round - )} ${fungiCurrencySymbol}` + const feeBN = totalFeeInWei + ? totalFeeInWei : feeInfoOrBigNumber instanceof BigNumber - ? `${ethers.utils.formatEther( - feeInfoOrBigNumber - )} ${nativeCurrencySymbol}` - : '--'; + ? feeInfoOrBigNumber + : undefined; + + let feeText = '--'; + if (feeBN) { + const fee = Number(ethers.utils.formatEther(feeBN)); + + // If feeInfo is instance of BigNumber, it means that the fee is in native currency + // otherwise it's in fungible token + const tokenSymbol = + feeInfoOrBigNumber instanceof BigNumber + ? nativeCurrencySymbol + : fungiCurrencySymbol; + + feeText = `${getRoundedAmountString(fee, 3, Math.round)} ${tokenSymbol}`; + } + + const txFeeContent = isFetchingFeeInfo ? 'Calculating...' : feeText; return [ { diff --git a/apps/bridge-dapp/src/containers/WithdrawContainer/shared.tsx b/apps/bridge-dapp/src/containers/WithdrawContainer/shared.tsx index e73852bc33..cff8e12751 100644 --- a/apps/bridge-dapp/src/containers/WithdrawContainer/shared.tsx +++ b/apps/bridge-dapp/src/containers/WithdrawContainer/shared.tsx @@ -11,7 +11,7 @@ export const ExchangeRateInfo: FC<{ Exchange Rate: - + 1 {nativeTokenSymbol} = {exchangeRate} {fungibleTokenSymbol} @@ -30,7 +30,7 @@ export const TransactionFeeInfo: FC<{ }> = ({ estimatedFee, refundFee, fungibleTokenSymbol }) => { return (
- + Transaction Fee:{' '} {estimatedFee} {fungibleTokenSymbol ?? ''} @@ -38,7 +38,7 @@ export const TransactionFeeInfo: FC<{ {refundFee && ( - + Refund Fee:{' '} {refundFee} {fungibleTokenSymbol ?? ''} diff --git a/apps/bridge-dapp/src/containers/WithdrawContainer/types.d.ts b/apps/bridge-dapp/src/containers/WithdrawContainer/types.d.ts index 40d432b387..5dee47af3f 100644 --- a/apps/bridge-dapp/src/containers/WithdrawContainer/types.d.ts +++ b/apps/bridge-dapp/src/containers/WithdrawContainer/types.d.ts @@ -58,9 +58,14 @@ export interface WithdrawConfirmContainerProps extends PropsOf<'div'> { recipient: string; /** - * The target chain id + * Source typed chain id */ - targetChainId: number; + sourceTypedChainId: number; + + /** + * The typed chain id + */ + targetTypedChainId: number; /** * The token to withdraw diff --git a/apps/bridge-dapp/src/hooks/useMaxFeeInfo.ts b/apps/bridge-dapp/src/hooks/useMaxFeeInfo.ts index 24bebdd260..1bd99f988a 100644 --- a/apps/bridge-dapp/src/hooks/useMaxFeeInfo.ts +++ b/apps/bridge-dapp/src/hooks/useMaxFeeInfo.ts @@ -109,7 +109,7 @@ export const useMaxFeeInfo = ( } if (!opt?.fungibleCurrencyId) { - throw new Error('No fungible currency selected'); + throw new Error('No fungible currency id selected'); } setError(null); diff --git a/apps/bridge-dapp/src/pages/PageBridge.tsx b/apps/bridge-dapp/src/pages/PageBridge.tsx index 27be969690..4656184227 100644 --- a/apps/bridge-dapp/src/pages/PageBridge.tsx +++ b/apps/bridge-dapp/src/pages/PageBridge.tsx @@ -233,7 +233,7 @@ const PageBridge = () => { // The customMainComponent alters the global mainComponent for display. // Therfore, if the customMainComponent exists (input selected) then hide the base component. className={cx( - 'min-w-[550px] min-h-[700px] h-full bg-mono-0 dark:bg-mono-180 p-4 rounded-lg space-y-4 grow', + 'min-w-[550px] min-h-[710px] h-full bg-mono-0 dark:bg-mono-180 p-4 rounded-lg space-y-4 grow', customMainComponent ? 'hidden' : 'block', 'flex flex-col' )} diff --git a/apps/bridge-dapp/src/utils/getAcitveSourceChains.ts b/apps/bridge-dapp/src/utils/getAcitveSourceChains.ts index df6edb7a2b..209f4cb3e9 100644 --- a/apps/bridge-dapp/src/utils/getAcitveSourceChains.ts +++ b/apps/bridge-dapp/src/utils/getAcitveSourceChains.ts @@ -1,13 +1,26 @@ -import { anchorDeploymentBlock, ChainConfig } from '@webb-tools/dapp-config'; +import { + anchorDeploymentBlock, + ChainConfig, + ChainEnvironment, +} from '@webb-tools/dapp-config'; import { calculateTypedChainId } from '@webb-tools/sdk-core'; // Get the all the active source chains from the anchor config and chain config export const getAcitveSourceChains = ( chains: Record ): Array => { + const currentEnv = process.env.NODE_ENV || 'development'; + return Object.values(chains).filter((chain) => { const typedChainId = calculateTypedChainId(chain.chainType, chain.chainId); const anchorConfig = anchorDeploymentBlock[typedChainId]; - return anchorConfig && Object.keys(anchorConfig).length > 0; + + const env = chain.env; + + const isSupported = env + ? env.includes(currentEnv as ChainEnvironment) + : true; + + return anchorConfig && Object.keys(anchorConfig).length > 0 && isSupported; }); }; diff --git a/libs/abstract-api-provider/src/relayer/types.ts b/libs/abstract-api-provider/src/relayer/types.ts index 87ab74b25b..6ef0368d87 100644 --- a/libs/abstract-api-provider/src/relayer/types.ts +++ b/libs/abstract-api-provider/src/relayer/types.ts @@ -153,7 +153,7 @@ export interface Errored { export type RelayerMessage = { withdraw?: Withdraw; error?: string; - network?: string; + network?: string | { failed: { reason: string } }; }; /** diff --git a/libs/abstract-api-provider/src/relayer/webb-relayer.ts b/libs/abstract-api-provider/src/relayer/webb-relayer.ts index c659d832a2..60fe7fd951 100644 --- a/libs/abstract-api-provider/src/relayer/webb-relayer.ts +++ b/libs/abstract-api-provider/src/relayer/webb-relayer.ts @@ -6,6 +6,7 @@ import { ChainType, parseTypedChainId } from '@webb-tools/sdk-core'; import { Observable, Subject } from 'rxjs'; import { filter } from 'rxjs/operators'; +import { LoggerService } from '@webb-tools/browser-utils'; import { chainsPopulated } from '@webb-tools/dapp-config'; import { u8aToHex } from '@webb-tools/utils'; import { @@ -88,6 +89,8 @@ class RelayedWithdraw { private emitter: Subject<[RelayedWithdrawResult, string | undefined]> = new Subject(); + private readonly logger = LoggerService.get('RelayedWithdraw'); + constructor(private ws: WebSocket, private prefix: RelayerCMDKey) { this.watcher = this.emitter.asObservable(); @@ -104,14 +107,23 @@ class RelayedWithdraw { }; ws.onerror = (e) => { - console.log(e); + this.logger.error('Relayer error: ', e); }; } private handleMessage = ( data: RelayerMessage ): [RelayedWithdrawResult, string | undefined] => { - if (data.error || data.withdraw?.errored) { + this.logger.info('Relayer message: ', data); + + if (data.network && typeof data.network !== 'string') { + const { failed } = data.network; + + return [ + RelayedWithdrawResult.Errored, + failed?.reason || 'Relayer network error', + ]; + } else if (data.error || data.withdraw?.errored) { return [ RelayedWithdrawResult.Errored, data.error || data.withdraw?.errored?.reason, diff --git a/libs/abstract-api-provider/src/vanchor/vanchor-actions.ts b/libs/abstract-api-provider/src/vanchor/vanchor-actions.ts index 0e5e2a0c65..133eb5cd30 100644 --- a/libs/abstract-api-provider/src/vanchor/vanchor-actions.ts +++ b/libs/abstract-api-provider/src/vanchor/vanchor-actions.ts @@ -112,6 +112,7 @@ export type TransferTransactionPayloadType = { notes: Note[]; changeUtxo: Utxo; transferUtxo: Utxo; + feeAmount: BigNumber; }; // Union type of all the payloads that can be used in a transaction (Deposit, Transfer, Withdraw) diff --git a/libs/api-provider-environment/src/WebbProvider.tsx b/libs/api-provider-environment/src/WebbProvider.tsx index ed2f599303..6f97c4cb63 100644 --- a/libs/api-provider-environment/src/WebbProvider.tsx +++ b/libs/api-provider-environment/src/WebbProvider.tsx @@ -7,20 +7,20 @@ import { WebbApiProvider, } from '@webb-tools/abstract-api-provider'; import { Bridge } from '@webb-tools/abstract-api-provider/state'; -import { LoggerService } from '@webb-tools/app-util'; +import { LoggerService } from '@webb-tools/browser-utils'; import { - NetworkStorage, keypairStorageFactory, netStorageFactory, + NetworkStorage, noteStorageFactory, resetNoteStorage, } from '@webb-tools/browser-utils/storage'; import { ApiConfig, Chain, - Wallet, chainsConfig, chainsPopulated, + Wallet, walletsConfig, } from '@webb-tools/dapp-config'; import { @@ -41,9 +41,9 @@ import { StoreProvider } from '@webb-tools/react-environment/store'; import { getRelayerManagerFactory } from '@webb-tools/relayer-manager-factory'; import { DimensionsProvider } from '@webb-tools/responsive-utils'; import { + calculateTypedChainId, ChainType, Keypair, - calculateTypedChainId, } from '@webb-tools/sdk-core'; import { Web3Provider, @@ -55,7 +55,7 @@ import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { ethers } from 'ethers'; import { TAppEvent } from './app-event'; -import { unsupportedChain } from './error'; +import { parseError, unsupportedChain } from './error'; import { insufficientApiInterface } from './error/interactive-errors/insufficient-api-interface'; import { useTxApiQueue } from './transaction'; import { WebbContext } from './webb-context'; @@ -558,7 +558,8 @@ export const WebbProvider: FC = ({ children, appEvent }) => { }); } /// get the current active chain from metamask - const chainId = await web3Provider.network; // storage based on network id + const network = await web3Provider.network; + const chainId = network.chainId; const relayerManager = (await relayerManagerFactory.getRelayerManager( @@ -650,8 +651,6 @@ export const WebbProvider: FC = ({ children, appEvent }) => { } } - console.log('bridgeOptions', bridgeOptions); - // set the available bridges of the new chain webbWeb3Provider.state.setBridgeOptions(bridgeOptions); webbWeb3Provider.state.activeBridge = defaultBridge; @@ -688,7 +687,8 @@ export const WebbProvider: FC = ({ children, appEvent }) => { throw new Error('Native token not found'); } - await web3Provider.addChain({ + // Switch to the chain + await web3Provider.switchAndAddChain({ chainId: `0x${chain.chainId.toString(16)}`, chainName: chain.name, rpcUrls: chain.evmRpcUrls ?? [], @@ -701,8 +701,10 @@ export const WebbProvider: FC = ({ children, appEvent }) => { ? [chain.blockExplorerStub] : undefined, }); + // add network will prompt the switch, check evmId again and throw if user rejected - const newChainId = await web3Provider.network; + const newNetwork = await web3Provider.network; + const newChainId = newNetwork.chainId; if (newChainId != chain.chainId) { appEvent.send('walletConnectionState', 'failed'); @@ -741,19 +743,19 @@ export const WebbProvider: FC = ({ children, appEvent }) => { return localActiveApi; } catch (e) { setLoading(false); + logger.error(e); + appEvent.send('walletConnectionState', 'failed'); if (e instanceof WebbError) { /// Catch the errors for the switcher while switching catchWebbError(e); - } - logger.error(e); - LoggerService.get('App').error(e); - - // Notify the error - if (typeof e === 'object' && e && 'toString' in e) { + } else { + // Parse and display error + const parsedError = parseError(e); notificationApi({ variant: 'error', - message: e.toString(), + message: 'Web3: Switch Chain Error', + secondaryMessage: parsedError.message, }); } diff --git a/libs/api-provider-environment/src/error/index.ts b/libs/api-provider-environment/src/error/index.ts index 2aa3e3d7c2..fb4f9fcd11 100644 --- a/libs/api-provider-environment/src/error/index.ts +++ b/libs/api-provider-environment/src/error/index.ts @@ -1 +1,2 @@ export * from './interactive-errors'; +export * from './parseError'; diff --git a/libs/api-provider-environment/src/error/parseError.ts b/libs/api-provider-environment/src/error/parseError.ts new file mode 100644 index 0000000000..ee78cb3715 --- /dev/null +++ b/libs/api-provider-environment/src/error/parseError.ts @@ -0,0 +1,37 @@ +import { serializeError } from 'eth-rpc-errors'; + +const DEFAULT_ERROR_CODE = 4000; +const DEFAULT_ERROR_MESSAGE = 'Unknown error'; + +const DEFAULT_FALLBACK_ERROR = { + code: DEFAULT_ERROR_CODE, + message: DEFAULT_ERROR_MESSAGE, +}; + +export const parseError = ( + error: unknown +): { code: number; message: string; extraInfo?: T } => { + // Maybe metaMask error + const err = serializeError(error, { + fallbackError: DEFAULT_FALLBACK_ERROR, + }); + + // If error is not a MetaMask error, return it as is + if (err.code !== DEFAULT_ERROR_CODE) { + return err; + } + + // Handle other errors + + // If error is an instance of Error, return the error message + // with the default error code + if (error instanceof Error) { + return { + code: DEFAULT_ERROR_CODE, + message: error.message, + extraInfo: error as T, + }; + } + + return DEFAULT_FALLBACK_ERROR; +}; diff --git a/libs/dapp-config/src/anchors/anchor-config.ts b/libs/dapp-config/src/anchors/anchor-config.ts index 68ea8c1a14..4dd59d112a 100644 --- a/libs/dapp-config/src/anchors/anchor-config.ts +++ b/libs/dapp-config/src/anchors/anchor-config.ts @@ -1,62 +1,38 @@ -import { EVMChainId } from '@webb-tools/dapp-types'; -import { calculateTypedChainId, ChainType } from '@webb-tools/sdk-core'; +import { PresetTypedChainId } from '@webb-tools/dapp-types'; -// 0xa1a2b7e08793b3033122b83cbee56726678588b5 - webbWETH - mocked backend // 0x38e7aa90c77f86747fab355eecaa0c2e4c3a463d - webbAlpha - mocked backend -// 0xaa4cd2df238be5c360d2031bac48dc17e6a187d8 - webbStandAlone - DKG backend -// 0xf8c9d24e3bc3e2d3eddde507079b08e82f239fc4 - webbtTNT-standalone +// 0x64Ba293E654992a94f304b00e3cEb8FD0f7AA773 - webbtTNT - DKG backend export const anchorDeploymentBlock: Record> = { - [calculateTypedChainId(ChainType.EVM, EVMChainId.ArbitrumTestnet)]: { - '0xa1a2b7e08793b3033122b83cbee56726678588b5': 8513284, + [PresetTypedChainId.ArbitrumTestnet]: { '0x38e7aa90c77f86747fab355eecaa0c2e4c3a463d': 13062856, - '0xf8c9d24e3bc3e2d3eddde507079b08e82f239fc4': 14922326, - '0xaa4cd2df238be5c360d2031bac48dc17e6a187d8': 15309867, }, - [calculateTypedChainId(ChainType.EVM, EVMChainId.Goerli)]: { - '0xa1a2b7e08793b3033122b83cbee56726678588b5': 8508326, + [PresetTypedChainId.Goerli]: { '0x38e7aa90c77f86747fab355eecaa0c2e4c3a463d': 8703495, - '0xf8c9d24e3bc3e2d3eddde507079b08e82f239fc4': 8768287, - '0xaa4cd2df238be5c360d2031bac48dc17e6a187d8': 8784848, }, - [calculateTypedChainId(ChainType.EVM, EVMChainId.Sepolia)]: { - '0xa1a2b7e08793b3033122b83cbee56726678588b5': 2920599, + [PresetTypedChainId.Sepolia]: { '0x38e7aa90c77f86747fab355eecaa0c2e4c3a463d': 3146553, - '0xf8c9d24e3bc3e2d3eddde507079b08e82f239fc4': 3220705, - '0xaa4cd2df238be5c360d2031bac48dc17e6a187d8': 3239056, }, - [calculateTypedChainId(ChainType.EVM, EVMChainId.OptimismTestnet)]: { - '0xa1a2b7e08793b3033122b83cbee56726678588b5': 5611883, - }, - [calculateTypedChainId(ChainType.EVM, EVMChainId.PolygonTestnet)]: { - '0xa1a2b7e08793b3033122b83cbee56726678588b5': 32139400, + [PresetTypedChainId.PolygonTestnet]: { '0x38e7aa90c77f86747fab355eecaa0c2e4c3a463d': 33462722, - '0xf8c9d24e3bc3e2d3eddde507079b08e82f239fc4': 33927921, - '0xaa4cd2df238be5c360d2031bac48dc17e6a187d8': 34045996, }, - [calculateTypedChainId(ChainType.EVM, EVMChainId.MoonbaseAlpha)]: { - '0xa1a2b7e08793b3033122b83cbee56726678588b5': 3771120, + [PresetTypedChainId.MoonbaseAlpha]: { '0x38e7aa90c77f86747fab355eecaa0c2e4c3a463d': 3996742, - '0xf8c9d24e3bc3e2d3eddde507079b08e82f239fc4': 4074545, - '0xaa4cd2df238be5c360d2031bac48dc17e6a187d8': 4092725, }, - [calculateTypedChainId(ChainType.EVM, EVMChainId.AvalancheFuji)]: { + [PresetTypedChainId.AvalancheFuji]: { '0x38e7aa90c77f86747fab355eecaa0c2e4c3a463d': 20151492, - '0xf8c9d24e3bc3e2d3eddde507079b08e82f239fc4': 20573380, }, - [calculateTypedChainId(ChainType.EVM, EVMChainId.ScrollAlpha)]: { + [PresetTypedChainId.ScrollAlpha]: { '0x38e7aa90c77f86747fab355eecaa0c2e4c3a463d': 666098, - '0xf8c9d24e3bc3e2d3eddde507079b08e82f239fc4': 995373, - '0xaa4cd2df238be5c360d2031bac48dc17e6a187d8': 1079099, }, - [calculateTypedChainId(ChainType.EVM, EVMChainId.HermesLocalnet)]: { - '0xc705034ded85e817b9E56C977E61A2098362898B': 0, + [PresetTypedChainId.HermesOrbit]: { + '0x64Ba293E654992a94f304b00e3cEb8FD0f7AA773': 134, }, - [calculateTypedChainId(ChainType.EVM, EVMChainId.AthenaLocalnet)]: { - '0x91eB86019FD8D7c5a9E31143D422850A13F670A3': 0, + [PresetTypedChainId.AthenaOrbit]: { + '0x64Ba293E654992a94f304b00e3cEb8FD0f7AA773': 150, }, - [calculateTypedChainId(ChainType.EVM, EVMChainId.DemeterLocalnet)]: { - '0x6595b34ED0a270B10a586FC1EA22030A95386f1e': 0, + [PresetTypedChainId.DemeterOrbit]: { + '0x64Ba293E654992a94f304b00e3cEb8FD0f7AA773': 131, }, }; diff --git a/libs/dapp-config/src/chains/chain-config.interface.ts b/libs/dapp-config/src/chains/chain-config.interface.ts index 78de0f4412..950b7ac7e9 100644 --- a/libs/dapp-config/src/chains/chain-config.interface.ts +++ b/libs/dapp-config/src/chains/chain-config.interface.ts @@ -16,6 +16,12 @@ export type ChainBase = | 'scroll' | 'webb-dev'; +export type ChainEnvironment = + | 'development' + | 'test' + | 'staging' + | 'production'; + export interface ChainConfig { chainType: ChainType; name: string; @@ -27,4 +33,9 @@ export interface ChainConfig { evmRpcUrls?: string[]; blockExplorerStub?: string; logo: React.ComponentType | React.ElementType; + + /** + * The supported environments for this chain (defaults to all) + */ + env?: ChainEnvironment[]; } diff --git a/libs/dapp-config/src/chains/evm/index.tsx b/libs/dapp-config/src/chains/evm/index.tsx index 4968f6889c..84a11d3c9b 100644 --- a/libs/dapp-config/src/chains/evm/index.tsx +++ b/libs/dapp-config/src/chains/evm/index.tsx @@ -3,19 +3,20 @@ // The extra evm rpc urls are from https://github.com/DefiLlama/chainlist -import { EVMChainId } from '@webb-tools/dapp-types'; +import { EVMChainId, PresetTypedChainId } from '@webb-tools/dapp-types'; import ArbitrumLogo from '@webb-tools/logos/chains/ArbitrumLogo'; import GanacheLogo from '@webb-tools/logos/chains/GanacheLogo'; import { MoonbeamLogo } from '@webb-tools/logos/chains/MoonbeamLogo'; import OptimismLogo from '@webb-tools/logos/chains/OptimismLogo'; import PolygonLogo from '@webb-tools/logos/chains/PolygonLogo'; import EtherLogo from '@webb-tools/logos/Eth'; -import { calculateTypedChainId, ChainType } from '@webb-tools/sdk-core'; +import { ChainType } from '@webb-tools/sdk-core'; import { ChainConfig } from '../chain-config.interface'; export const chainsConfig: Record = { - [calculateTypedChainId(ChainType.EVM, EVMChainId.Goerli)]: { + // Testnet + [PresetTypedChainId.Goerli]: { chainType: ChainType.EVM, group: 'eth', chainId: EVMChainId.Goerli, @@ -32,7 +33,7 @@ export const chainsConfig: Record = { logo: EtherLogo, tag: 'test', }, - [calculateTypedChainId(ChainType.EVM, EVMChainId.OptimismTestnet)]: { + [PresetTypedChainId.OptimismTestnet]: { chainType: ChainType.EVM, group: 'eth', chainId: EVMChainId.OptimismTestnet, @@ -49,7 +50,7 @@ export const chainsConfig: Record = { logo: OptimismLogo, tag: 'test', }, - [calculateTypedChainId(ChainType.EVM, EVMChainId.ArbitrumTestnet)]: { + [PresetTypedChainId.ArbitrumTestnet]: { chainType: ChainType.EVM, group: 'eth', chainId: EVMChainId.ArbitrumTestnet, @@ -65,7 +66,7 @@ export const chainsConfig: Record = { logo: ArbitrumLogo, tag: 'test', }, - [calculateTypedChainId(ChainType.EVM, EVMChainId.PolygonTestnet)]: { + [PresetTypedChainId.PolygonTestnet]: { chainType: ChainType.EVM, group: 'matic', chainId: EVMChainId.PolygonTestnet, @@ -81,40 +82,7 @@ export const chainsConfig: Record = { blockExplorerStub: 'https://mumbai.polygonscan.com/', logo: PolygonLogo, }, - [calculateTypedChainId(ChainType.EVM, EVMChainId.HermesLocalnet)]: { - chainType: ChainType.EVM, - group: 'eth', - chainId: EVMChainId.HermesLocalnet, - name: 'Hermes', - base: 'webb-dev', - tag: 'dev', - url: 'http://127.0.0.1:5001', - evmRpcUrls: ['http://127.0.0.1:5001'], - logo: GanacheLogo, - }, - [calculateTypedChainId(ChainType.EVM, EVMChainId.AthenaLocalnet)]: { - chainType: ChainType.EVM, - group: 'eth', - chainId: EVMChainId.AthenaLocalnet, - name: 'Athena', - base: 'webb-dev', - tag: 'dev', - url: 'http://127.0.0.1:5002', - evmRpcUrls: ['http://127.0.0.1:5002'], - logo: GanacheLogo, - }, - [calculateTypedChainId(ChainType.EVM, EVMChainId.DemeterLocalnet)]: { - chainType: ChainType.EVM, - group: 'eth', - chainId: EVMChainId.DemeterLocalnet, - name: 'Demeter', - base: 'webb-dev', - tag: 'dev', - url: 'http://127.0.0.1:5003', - evmRpcUrls: ['http://127.0.0.1:5003'], - logo: GanacheLogo, - }, - [calculateTypedChainId(ChainType.EVM, EVMChainId.MoonbaseAlpha)]: { + [PresetTypedChainId.MoonbaseAlpha]: { chainType: ChainType.EVM, group: 'eth', chainId: EVMChainId.MoonbaseAlpha, @@ -130,7 +98,7 @@ export const chainsConfig: Record = { blockExplorerStub: 'https://moonbase.moonscan.io/', logo: MoonbeamLogo, }, - [calculateTypedChainId(ChainType.EVM, EVMChainId.Sepolia)]: { + [PresetTypedChainId.Sepolia]: { chainType: ChainType.EVM, group: 'eth', chainId: EVMChainId.Sepolia, @@ -147,7 +115,7 @@ export const chainsConfig: Record = { ], logo: EtherLogo, }, - [calculateTypedChainId(ChainType.EVM, EVMChainId.AvalancheFuji)]: { + [PresetTypedChainId.AvalancheFuji]: { chainType: ChainType.EVM, group: 'eth', chainId: EVMChainId.AvalancheFuji, @@ -163,7 +131,7 @@ export const chainsConfig: Record = { ], logo: EtherLogo, }, - [calculateTypedChainId(ChainType.EVM, EVMChainId.ScrollAlpha)]: { + [PresetTypedChainId.ScrollAlpha]: { chainType: ChainType.EVM, group: 'eth', chainId: EVMChainId.ScrollAlpha, @@ -175,4 +143,83 @@ export const chainsConfig: Record = { evmRpcUrls: ['https://alpha-rpc.scroll.io/l2'], logo: EtherLogo, }, + + // Self hosted chains + [PresetTypedChainId.HermesOrbit]: { + chainType: ChainType.EVM, + group: 'eth', + chainId: EVMChainId.HermesOrbit, + name: 'Hermes Orbit', + base: 'webb-dev', + tag: 'test', + url: 'https://hermes-testnet.webb.tools', + evmRpcUrls: ['https://hermes-testnet.webb.tools'], + blockExplorerStub: 'https://hermes-explorer.webb.tools', + logo: GanacheLogo, + env: ['development', 'test'], + }, + [PresetTypedChainId.AthenaOrbit]: { + chainType: ChainType.EVM, + group: 'eth', + chainId: EVMChainId.AthenaOrbit, + name: 'Athena Orbit', + base: 'webb-dev', + tag: 'test', + url: 'https://athena-testnet.webb.tools', + evmRpcUrls: ['https://athena-testnet.webb.tools'], + blockExplorerStub: 'https://athena-explorer.webb.tools', + logo: GanacheLogo, + env: ['development', 'test'], + }, + [PresetTypedChainId.DemeterOrbit]: { + chainType: ChainType.EVM, + group: 'eth', + chainId: EVMChainId.DemeterOrbit, + name: 'Demeter Orbit', + base: 'webb-dev', + tag: 'test', + url: 'https://demeter-testnet.webb.tools', + evmRpcUrls: ['https://demeter-testnet.webb.tools'], + blockExplorerStub: 'https://demeter-explorer.webb.tools', + logo: GanacheLogo, + env: ['development', 'test'], + }, + + // Localnet + [PresetTypedChainId.HermesLocalnet]: { + chainType: ChainType.EVM, + group: 'eth', + chainId: EVMChainId.HermesLocalnet, + name: 'Hermes', + base: 'webb-dev', + tag: 'dev', + url: 'http://127.0.0.1:5004', + evmRpcUrls: ['http://127.0.0.1:5004'], + logo: GanacheLogo, + env: ['development'], + }, + [PresetTypedChainId.AthenaLocalnet]: { + chainType: ChainType.EVM, + group: 'eth', + chainId: EVMChainId.AthenaLocalnet, + name: 'Athena', + base: 'webb-dev', + tag: 'dev', + url: 'http://127.0.0.1:5005', + evmRpcUrls: ['http://127.0.0.1:5005'], + logo: GanacheLogo, + env: ['development'], + }, + [PresetTypedChainId.DemeterLocalnet]: { + chainType: ChainType.EVM, + group: 'eth', + chainId: EVMChainId.DemeterLocalnet, + name: 'Demeter', + base: 'webb-dev', + tag: 'dev', + url: 'http://127.0.0.1:5006', + evmRpcUrls: ['http://127.0.0.1:5006'], + logo: GanacheLogo, + env: ['development'], + }, }; diff --git a/libs/dapp-config/src/chains/substrate/index.tsx b/libs/dapp-config/src/chains/substrate/index.tsx index 214eb73852..76ea578f68 100644 --- a/libs/dapp-config/src/chains/substrate/index.tsx +++ b/libs/dapp-config/src/chains/substrate/index.tsx @@ -6,6 +6,7 @@ import { ChainType } from '@webb-tools/sdk-core'; import { ChainConfig } from '../chain-config.interface'; +// All substrate chains temporary use in `development` environment now export const chainsConfig: Record = { [PresetTypedChainId.ProtocolSubstrateStandalone]: { chainType: ChainType.Substrate, @@ -15,6 +16,7 @@ export const chainsConfig: Record = { logo: WEBBLogo, url: 'ws://127.0.0.1:9944', name: 'Substrate', + env: ['development'], }, [PresetTypedChainId.LocalTangleStandalone]: { chainType: ChainType.Substrate, @@ -24,6 +26,7 @@ export const chainsConfig: Record = { logo: WEBBLogo, url: 'wss://standalone.webb.tools', name: 'Tangle', + env: ['development'], }, [PresetTypedChainId.Kusama]: { chainType: ChainType.KusamaRelayChain, @@ -33,6 +36,7 @@ export const chainsConfig: Record = { logo: KSMLogo, url: 'wss://kusama-rpc.polkadot.io', name: 'Kusama', + env: ['development'], }, [PresetTypedChainId.Polkadot]: { chainType: ChainType.PolkadotRelayChain, @@ -42,5 +46,6 @@ export const chainsConfig: Record = { logo: DOTLogo, url: 'wss://rpc.polkadot.io', name: 'Polkadot', + env: ['development'], }, }; diff --git a/libs/dapp-config/src/gasLimit-config.ts b/libs/dapp-config/src/gasLimit-config.ts index bb0eba5399..0c191ab9c0 100644 --- a/libs/dapp-config/src/gasLimit-config.ts +++ b/libs/dapp-config/src/gasLimit-config.ts @@ -14,6 +14,10 @@ const gasLimitConfig: GasLimitConfigType = { [PresetTypedChainId.Goerli]: BigNumber.from(2000000), [PresetTypedChainId.AvalancheFuji]: BigNumber.from(2000000), [PresetTypedChainId.ScrollAlpha]: BigNumber.from(2000000), + + [PresetTypedChainId.HermesOrbit]: BigNumber.from(2000000), // TODO: benchmark gas limit + [PresetTypedChainId.AthenaOrbit]: BigNumber.from(2000000), // TODO: benchmark gas limit + [PresetTypedChainId.DemeterOrbit]: BigNumber.from(2000000), // TODO: benchmark gas limit }; export default gasLimitConfig; diff --git a/libs/dapp-config/src/on-chain-config/evm/on-chain-config.ts b/libs/dapp-config/src/on-chain-config/evm/on-chain-config.ts index 75781c6526..2ffe24c541 100644 --- a/libs/dapp-config/src/on-chain-config/evm/on-chain-config.ts +++ b/libs/dapp-config/src/on-chain-config/evm/on-chain-config.ts @@ -30,7 +30,13 @@ const LOCALNET_CHAIN_IDS = [ EVMChainId.DemeterLocalnet, ]; -const LOCALNET_CURRENCY: ICurrency = { +const SELF_HOSTED_CHAIN_IDS = [ + EVMChainId.HermesOrbit, + EVMChainId.AthenaOrbit, + EVMChainId.DemeterOrbit, +]; + +const DEFAULT_CURRENCY: ICurrency = { name: 'Localnet Ether', symbol: 'ETH', decimals: 18, @@ -75,16 +81,17 @@ export class EVMOnChainConfig extends OnChainConfigBase { // First check if the native currency is already cached const cachedNativeCurrency = this.nativeCurrencyCache.get(typedChainId); if (cachedNativeCurrency) { - return Promise.resolve(cachedNativeCurrency); + return cachedNativeCurrency; } // Validate the chainType is EVM and get the chaindId const { chainId } = this.validateChainType(typedChainId); - // Maybe evn localnet - if (LOCALNET_CHAIN_IDS.includes(chainId)) { - this.nativeCurrencyCache.set(typedChainId, LOCALNET_CURRENCY); - return Promise.resolve(LOCALNET_CURRENCY); + // Maybe evn localnet or self hosted + const customChainIds = LOCALNET_CHAIN_IDS.concat(SELF_HOSTED_CHAIN_IDS); + if (customChainIds.includes(chainId)) { + this.nativeCurrencyCache.set(typedChainId, DEFAULT_CURRENCY); + return DEFAULT_CURRENCY; } if (!chainData.length) { @@ -96,15 +103,20 @@ export class EVMOnChainConfig extends OnChainConfigBase { chainData = await resp.json(); } catch (error) { - console.error('Unable to retrieve native token information', error); - return null; + console.error( + 'Unable to retrieve native token information, fallback to default', + error + ); + return DEFAULT_CURRENCY; } } const chain = chainData.find((chain) => chain.chainId === chainId); if (!chain) { - console.error(`Found unsupported chainId ${chainId} for EVM`); - return Promise.resolve(null); + console.error( + `Found unsupported chainId ${chainId} for EVM, fallback to default` + ); + return DEFAULT_CURRENCY; } // Parse the native currency @@ -116,7 +128,7 @@ export class EVMOnChainConfig extends OnChainConfigBase { // Cache the native currency this.nativeCurrencyCache.set(typedChainId, nativeCurrency); - return Promise.resolve(nativeCurrency); + return nativeCurrency; } async fetchFungibleCurrency( @@ -131,7 +143,7 @@ export class EVMOnChainConfig extends OnChainConfigBase { return null; } - return Promise.resolve(cachedFungibleCurrency); + return cachedFungibleCurrency; } // Validate the chainType is EVM and get the chaindId @@ -178,10 +190,10 @@ export class EVMOnChainConfig extends OnChainConfigBase { const cachedCurrencies = this.wrappableCurrenciesCache.get(typedChainId); if (cachedCurrencies) { if (cachedCurrencies instanceof Error) { - return Promise.resolve([]); + return []; } - return Promise.resolve(cachedCurrencies); + return cachedCurrencies; } // Validate the chainType is EVM and get the chaindId diff --git a/libs/dapp-config/src/wallets/wallets-config.tsx b/libs/dapp-config/src/wallets/wallets-config.tsx index e645d455d2..5a1698748b 100644 --- a/libs/dapp-config/src/wallets/wallets-config.tsx +++ b/libs/dapp-config/src/wallets/wallets-config.tsx @@ -13,6 +13,8 @@ import { WalletConfig } from '.'; const ANY_EVM = [ PresetTypedChainId.EthereumMainNet, + + // Testnet PresetTypedChainId.Goerli, PresetTypedChainId.Sepolia, PresetTypedChainId.HarmonyTestnet1, @@ -21,12 +23,19 @@ const ANY_EVM = [ PresetTypedChainId.OptimismTestnet, PresetTypedChainId.ArbitrumTestnet, PresetTypedChainId.PolygonTestnet, - PresetTypedChainId.HermesLocalnet, - PresetTypedChainId.AthenaLocalnet, - PresetTypedChainId.DemeterLocalnet, PresetTypedChainId.MoonbaseAlpha, PresetTypedChainId.AvalancheFuji, PresetTypedChainId.ScrollAlpha, + + // Self hosted + PresetTypedChainId.HermesOrbit, + PresetTypedChainId.AthenaOrbit, + PresetTypedChainId.DemeterOrbit, + + // Localnet + PresetTypedChainId.HermesLocalnet, + PresetTypedChainId.AthenaLocalnet, + PresetTypedChainId.DemeterLocalnet, ]; const ANY_SUBSTRATE = [ diff --git a/libs/dapp-types/src/ChainId.ts b/libs/dapp-types/src/ChainId.ts index 021b8d6798..b1fff74739 100644 --- a/libs/dapp-types/src/ChainId.ts +++ b/libs/dapp-types/src/ChainId.ts @@ -88,6 +88,12 @@ export enum PresetTypedChainId { ScrollAlpha = calculateTypedChainId(ChainType.EVM, EVMChainId.ScrollAlpha), + // Self hosted chains + HermesOrbit = calculateTypedChainId(ChainType.EVM, EVMChainId.HermesOrbit), + AthenaOrbit = calculateTypedChainId(ChainType.EVM, EVMChainId.AthenaOrbit), + DemeterOrbit = calculateTypedChainId(ChainType.EVM, EVMChainId.DemeterOrbit), + + // Localnets HermesLocalnet = calculateTypedChainId( ChainType.EVM, EVMChainId.HermesLocalnet diff --git a/libs/dapp-types/src/EVMChainId.ts b/libs/dapp-types/src/EVMChainId.ts index 084ef2adee..55289e6b50 100644 --- a/libs/dapp-types/src/EVMChainId.ts +++ b/libs/dapp-types/src/EVMChainId.ts @@ -13,12 +13,19 @@ export enum EVMChainId { OptimismTestnet = 420, ArbitrumTestnet = 421613, PolygonTestnet = 80001, - HermesLocalnet = 5001, - AthenaLocalnet = 5002, - DemeterLocalnet = 5003, MoonbaseAlpha = 1287, AvalancheFuji = 43113, ScrollAlpha = 534353, + + /** Self hosted EVM */ + AthenaOrbit = 5001, + HermesOrbit = 5002, + DemeterOrbit = 5003, + + /** Local EVM */ + HermesLocalnet = 5004, + AthenaLocalnet = 5005, + DemeterLocalnet = 5006, } export default EVMChainId; diff --git a/libs/icons/src/chains/athena-orbit.svg b/libs/icons/src/chains/athena-orbit.svg new file mode 100644 index 0000000000..7bf094d065 --- /dev/null +++ b/libs/icons/src/chains/athena-orbit.svg @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/icons/src/chains/demeter-orbit.svg b/libs/icons/src/chains/demeter-orbit.svg new file mode 100644 index 0000000000..7bf094d065 --- /dev/null +++ b/libs/icons/src/chains/demeter-orbit.svg @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/icons/src/chains/hermes-orbit.svg b/libs/icons/src/chains/hermes-orbit.svg new file mode 100644 index 0000000000..7bf094d065 --- /dev/null +++ b/libs/icons/src/chains/hermes-orbit.svg @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/web3-api-provider/src/ext-provider/web3-provider.ts b/libs/web3-api-provider/src/ext-provider/web3-provider.ts index f1973b0853..3b39125e9b 100644 --- a/libs/web3-api-provider/src/ext-provider/web3-provider.ts +++ b/libs/web3-api-provider/src/ext-provider/web3-provider.ts @@ -136,7 +136,7 @@ export class Web3Provider { } get network() { - return this._inner.eth.net.getId(); + return this.intoEthersProvider().getNetwork(); } get eth() { @@ -191,6 +191,22 @@ export class Web3Provider { }); } + // Switch to the chain or add it if it doesn't exist + async switchAndAddChain( + chainInput: AddEthereumChainParameter + ): Promise { + try { + await this.switchChain({ chainId: chainInput.chainId }); + } catch (error) { + // This error code indicates that the chain has not been added to MetaMask. + if ((error as any)?.code === 4902) { + await this.addChain(chainInput); + } else { + throw error; // Otherwise, throw the error to be handled by the caller. + } + } + } + addToken(addTokenInput: AddToken) { return ( this._inner.currentProvider as unknown as AbstractProvider diff --git a/libs/web3-api-provider/src/webb-provider.ts b/libs/web3-api-provider/src/webb-provider.ts index 4ff6f3dd51..aedb69c33e 100644 --- a/libs/web3-api-provider/src/webb-provider.ts +++ b/libs/web3-api-provider/src/webb-provider.ts @@ -185,9 +185,8 @@ export class WebbWeb3Provider this.ethersProvider = this.web3Provider.intoEthersProvider(); const handler = async () => { - const chainId = await this.web3Provider.network; - - this.emit('providerUpdate', [chainId]); + const network = await this.ethersProvider.getNetwork(); + this.emit('providerUpdate', [network.chainId]); }; // eslint-disable-next-line @typescript-eslint/ban-ts-comment diff --git a/libs/web3-api-provider/src/webb-provider/vanchor-actions.ts b/libs/web3-api-provider/src/webb-provider/vanchor-actions.ts index 4fad3df560..2f65205d73 100644 --- a/libs/web3-api-provider/src/webb-provider/vanchor-actions.ts +++ b/libs/web3-api-provider/src/webb-provider/vanchor-actions.ts @@ -18,7 +18,7 @@ import { bridgeStorageFactory, registrationStorageFactory, } from '@webb-tools/browser-utils/storage'; -import { ERC20__factory } from '@webb-tools/contracts'; +import { ERC20, ERC20__factory } from '@webb-tools/contracts'; import { checkNativeAddress } from '@webb-tools/dapp-types'; import { ChainType, @@ -39,6 +39,7 @@ import { } from '@webb-tools/utils'; import { BigNumber, + BigNumberish, ContractReceipt, ContractTransaction, Overrides, @@ -109,7 +110,9 @@ const isVAnchorTransferPayload = ( 'changeUtxo' in payload && payload['changeUtxo'] instanceof Utxo && 'transferUtxo' in payload && - payload['transferUtxo'] instanceof Utxo + payload['transferUtxo'] instanceof Utxo && + 'feeAmount' in payload && + payload['feeAmount'] instanceof BigNumber ); }; @@ -212,20 +215,23 @@ export class Web3VAnchorActions extends VAnchorActions { leavesMap, // leavesMap ]); } else if (isVAnchorTransferPayload(payload)) { - const { changeUtxo, transferUtxo, notes } = payload; + const { changeUtxo, transferUtxo, notes, feeAmount } = payload; const { inputUtxos, leavesMap } = await this.commitmentsSetup(notes, tx); const relayer = this.inner.relayerManager.activeRelayer?.beneficiary ?? ZERO_ADDRESS; + // If no relayer is set, then the fee is 0, otherwise it is the fee amount + const feeVal = relayer === ZERO_ADDRESS ? BigNumber.from(0) : feeAmount; + // set the anchor to make the transfer on (where the notes are being spent for the transfer) return Promise.resolve([ tx, // tx notes[0].note.targetIdentifyingData, // contractAddress inputUtxos, // inputs [changeUtxo, transferUtxo], // outputs - BigNumber.from(0), // fee + feeVal, // fee BigNumber.from(0), // refund ZERO_ADDRESS, // recipient relayer, // relayer @@ -757,41 +763,45 @@ export class Web3VAnchorActions extends VAnchorActions { ): Promise | never { const { note } = payload; const { amount } = note; + const srcVAnchor = await this.getVAnchor(payload); + const spenderAddress = srcVAnchor.contract.address; const currentWebbToken = await srcVAnchor.getWebbToken(); + const approvalValue = await tokenWrapper.contract.getAmountToWrap(amount); let approvalTransaction: ContractTransaction | undefined; + // If the `wrapUnwrapToken` is different from the `currentWebbToken` address, + // we are wrapping / unwrapping. otherwise, we are depositing / withdrawing. + const isWrapOrUnwrap = wrapUnwrapToken !== currentWebbToken.address; + + const isRequiredApproval = !isWrapOrUnwrap + ? await srcVAnchor.isWebbTokenApprovalRequired(amount) + : await srcVAnchor.isWrappableTokenApprovalRequired( + wrapUnwrapToken, + approvalValue + ); + if (checkNativeAddress(wrapUnwrapToken)) { /// native token no approval needed - } else if ( - wrapUnwrapToken === currentWebbToken.address && - (await srcVAnchor.isWebbTokenApprovalRequired(amount)) - ) { + } else if (!isWrapOrUnwrap && isRequiredApproval) { // approve the token approvalTransaction = await currentWebbToken.approve( - srcVAnchor.contract.address, + spenderAddress, amount, { gasLimit: '0x5B8D80', } ); - } else if ( - await srcVAnchor.isWrappableTokenApprovalRequired( - wrapUnwrapToken, - approvalValue - ) - ) { + } else if (isRequiredApproval) { // approve the wrappable asset const token = ERC20__factory.connect( wrapUnwrapToken, this.inner.getEthersProvider().getSigner() ); - approvalTransaction = await token.approve( - currentWebbToken.address, - approvalValue, - { gasLimit: '0x5B8D80' } - ); + approvalTransaction = await token.approve(spenderAddress, approvalValue, { + gasLimit: '0x5B8D80', + }); } if (approvalTransaction) { diff --git a/libs/webb-ui-components/src/components/BridgeInputs/RecipientInput.tsx b/libs/webb-ui-components/src/components/BridgeInputs/RecipientInput.tsx index 349c058de6..ae36b8c53e 100644 --- a/libs/webb-ui-components/src/components/BridgeInputs/RecipientInput.tsx +++ b/libs/webb-ui-components/src/components/BridgeInputs/RecipientInput.tsx @@ -46,23 +46,15 @@ export const RecipientInput = forwardRef( ) => { const [address, setAddress] = useState(() => value); - const onClick = useCallback(async () => { - try { - const addr = await window.navigator.clipboard.readText(); - - setAddress(addr); - } catch (e) { - notificationApi({ - message: 'Failed to read clipboard', - secondaryMessage: - 'Please change your browser settings to allow clipboard access.', - variant: 'warning', - }); - } - }, [setAddress]); const [recipientError, setRecipientError] = useState( undefined ); + + const error = useMemo( + () => errorMessage || recipientError, + [recipientError, errorMessage] + ); + const onChange = useCallback( (nextVal: string) => { const address = nextVal.trim(); @@ -71,26 +63,34 @@ export const RecipientInput = forwardRef( if (address === '' || (validate ? validate(address) : true)) { setRecipientError(undefined); + isValidSet?.(true); } else { setRecipientError('Invalid wallet address '); + isValidSet?.(false); } }, - [onChangeProp, validate] + [isValidSet, onChangeProp, validate] ); - useEffect(() => { - setAddress(value); - }, [value, setAddress]); + const handlePasteButtonClick = useCallback(async () => { + try { + const addr = await window.navigator.clipboard.readText(); - const error = useMemo( - () => errorMessage || recipientError, - [recipientError, errorMessage] - ); + onChange(addr); + } catch (e) { + notificationApi({ + message: 'Failed to read clipboard', + secondaryMessage: + 'Please change your browser settings to allow clipboard access.', + variant: 'warning', + }); + } + }, [onChange]); + // Effect ensure update the address when the value prop changes useEffect(() => { - const isValid = (error?.trim() ?? '') === ''; - isValidSet?.(isValid); - }, [error, isValidSet]); + setAddress(value); + }, [value]); return ( <> @@ -124,7 +124,7 @@ export const RecipientInput = forwardRef( - -
+ {buttonDesc && ( + + + {buttonDesc} + + )} - - ); } diff --git a/libs/webb-ui-components/src/containers/TransferCard/types.ts b/libs/webb-ui-components/src/containers/TransferCard/types.ts index 6e9ffdfb9f..4c550ce467 100644 --- a/libs/webb-ui-components/src/containers/TransferCard/types.ts +++ b/libs/webb-ui-components/src/containers/TransferCard/types.ts @@ -5,6 +5,7 @@ import { AmountInput, Button, ChainInput, + InfoItem, RecipientInput, RelayerInput, TokenInput, @@ -37,37 +38,23 @@ export interface TransferCardProps extends PropsOf<'div'> { recipientInputProps?: ComponentProps; /** - * The transfer amount + * The info item props to pass to the info item component */ - transferAmount?: number | string; + infoItemProps?: Array>; /** - * The transfer token symbol - */ - transferToken?: string; - - /** - * The fee amount - */ - feeAmount?: number | string; - - /** - * The fee token symbol - */ - feeToken?: string; - - /** - * The fee percentage to display + * The withdraw button props */ - feePercentage?: number; + transferBtnProps?: ComponentProps; /** - * The remainder amount + * The description message display below the withdraw button */ - changeAmount?: number | string; + buttonDesc?: string; /** - * The withdraw button props + * The variant of message display below the withdraw button + * @default 'info' */ - transferBtnProps?: ComponentProps; + buttonDescVariant?: 'info' | 'error'; } diff --git a/libs/webb-ui-components/src/containers/WithdrawCard/WithdrawCard.tsx b/libs/webb-ui-components/src/containers/WithdrawCard/WithdrawCard.tsx index 6774de4cf4..2786a59b97 100644 --- a/libs/webb-ui-components/src/containers/WithdrawCard/WithdrawCard.tsx +++ b/libs/webb-ui-components/src/containers/WithdrawCard/WithdrawCard.tsx @@ -123,11 +123,13 @@ export const WithdrawCard = forwardRef( customAmountInputProps={customAmountInputProps} /> + + {/** Info */} {infoItemProps && (
@@ -138,7 +140,7 @@ export const WithdrawCard = forwardRef( )}
-
+