diff --git a/packages/core-mobile/app/contexts/BridgeContext.tsx b/packages/core-mobile/app/contexts/BridgeContext.tsx index 5fdff47150..015d0b8a6d 100644 --- a/packages/core-mobile/app/contexts/BridgeContext.tsx +++ b/packages/core-mobile/app/contexts/BridgeContext.tsx @@ -1,40 +1,12 @@ -import React, { - createContext, - ReactNode, - useCallback, - useContext, - useEffect, - useRef -} from 'react' +import React, { ReactNode, useEffect } from 'react' import { BridgeSDKProvider, BridgeTransaction, - getMinimumConfirmations, - trackBridgeTransaction, - TrackerSubscription, useBridgeSDK } from '@avalabs/core-bridge-sdk' -import { useDispatch, useSelector } from 'react-redux' -import { selectActiveAccount } from 'store/account' -import { - addBridgeTransaction, - popBridgeTransaction, - selectBridgeConfig, - selectBridgeTransactions -} from 'store/bridge' -import { - useAvalancheProvider, - useBitcoinProvider, - useEthereumProvider -} from 'hooks/networks/networkProviderHooks' +import { useSelector } from 'react-redux' +import { selectBridgeConfig } from 'store/bridge' import { isEqual } from 'lodash' -import { Network } from '@avalabs/core-chains-sdk' -import Logger from 'utils/Logger' -import { showSnackBarCustom } from 'components/Snackbar' -import TransactionToast, { - TransactionToastType -} from 'components/toast/TransactionToast' -import AnalyticsService from 'services/analytics/AnalyticsService' import BridgeService from 'services/bridge/BridgeService' import { selectIsDeveloperMode } from 'store/settings/advanced' @@ -48,15 +20,6 @@ export type PartialBridgeTransaction = Pick< | 'symbol' > -interface BridgeContext { - createBridgeTransaction( - tx: PartialBridgeTransaction, - network: Network - ): Promise -} - -const bridgeContext = createContext({} as BridgeContext) - export function BridgeProvider({ children }: { @@ -69,27 +32,13 @@ export function BridgeProvider({ ) } -export function useBridgeContext(): BridgeContext { - return useContext(bridgeContext) -} - -const TrackerSubscriptions = new Map() - function LocalBridgeProvider({ children }: { children: ReactNode }): JSX.Element { - const dispatch = useDispatch() const bridgeConfig = useSelector(selectBridgeConfig) - const config = bridgeConfig?.config - const activeAccount = useSelector(selectActiveAccount) - const bridgeTransactions = useSelector(selectBridgeTransactions) - const ethereumProvider = useEthereumProvider() - const bitcoinProvider = useBitcoinProvider() - const avalancheProvider = useAvalancheProvider() const { bridgeConfig: bridgeConfigSDK, setBridgeConfig } = useBridgeSDK() - const isToastVisible = useRef() const isDeveloperMode = useSelector(selectIsDeveloperMode) @@ -97,32 +46,6 @@ function LocalBridgeProvider({ BridgeService.setBridgeEnvironment(isDeveloperMode) }, [isDeveloperMode]) - const removeBridgeTransaction = useCallback( - (tx: BridgeTransaction) => { - dispatch(popBridgeTransaction(tx.sourceTxHash)) - AnalyticsService.capture('BridgeTransferRequestSucceeded') - - if (!isToastVisible.current) { - isToastVisible.current = true - - showSnackBarCustom({ - component: ( - - ), - duration: 'short', - onClose: () => { - isToastVisible.current = false - } - }) - } - }, - [dispatch] - ) - useEffect(() => { // sync bridge config in bridge sdk with ours // this is necessary because: @@ -134,143 +57,5 @@ function LocalBridgeProvider({ } }, [bridgeConfig, bridgeConfigSDK, setBridgeConfig]) - // init tracking updates for txs - const subscribeToTransaction = useCallback( - async (trackedTransaction: BridgeTransaction) => { - if ( - trackedTransaction && - config && - !TrackerSubscriptions.has(trackedTransaction.sourceTxHash) && - avalancheProvider && - ethereumProvider - ) { - // Start transaction tracking process (no need to await) - try { - const subscription = trackBridgeTransaction({ - bridgeTransaction: trackedTransaction, - onBridgeTransactionUpdate: (tx: BridgeTransaction) => { - // Update the transaction, even if it's complete - // (we want to keep the tx up to date, because some Component(i.e. BridgeTransactionStatus) has local state that depends on it) - dispatch(addBridgeTransaction(tx)) - - if (tx.complete) { - removeBridgeTransaction(tx) - } - }, - config, - avalancheProvider, - ethereumProvider, - bitcoinProvider - }) - - TrackerSubscriptions.set( - trackedTransaction.sourceTxHash, - subscription - ) - } catch (e) { - Logger.error('failed to subscribe to transaction', e) - } - } - }, - [ - avalancheProvider, - bitcoinProvider, - config, - dispatch, - ethereumProvider, - removeBridgeTransaction - ] - ) - - /** - * Add a new pending bridge transaction to the background state and start the - * transaction tracking process. - */ - const createBridgeTransaction = useCallback( - async ( - partialBridgeTransaction: PartialBridgeTransaction, - network: Network - ) => { - if (!config || !network || !activeAccount) { - return Promise.reject('Wallet not ready') - } - - const { - sourceChain, - sourceTxHash, - sourceStartedAt, - targetChain, - amount, - symbol - } = partialBridgeTransaction - - const addressC = activeAccount.addressC - const addressBTC = activeAccount.addressBTC - - if (!addressBTC) return { error: 'missing addressBTC' } - if (!addressC) return { error: 'missing addressC' } - if (!sourceChain) return { error: 'missing sourceChain' } - if (!sourceTxHash) return { error: 'missing sourceTxHash' } - if (!sourceStartedAt) return { error: 'missing sourceStartedAt' } - if (!targetChain) return { error: 'missing targetChain' } - if (!amount) return { error: 'missing amount' } - if (!symbol) return { error: 'missing symbol' } - if (!config) return { error: 'missing bridge config' } - if (bridgeTransactions[sourceTxHash]) - return { error: 'bridge tx already exists' } - const requiredConfirmationCount = getMinimumConfirmations( - sourceChain, - config - ) - - const environment = network.isTestnet ? 'test' : 'main' - - const bridgeTransaction: BridgeTransaction = { - /* from params */ - sourceChain, - sourceTxHash, - sourceStartedAt, - targetChain, - amount, - symbol, - /* new fields */ - addressC, - addressBTC, - complete: false, - confirmationCount: 0, - environment, - requiredConfirmationCount - } - - dispatch(addBridgeTransaction(bridgeTransaction)) - subscribeToTransaction(bridgeTransaction) - }, - [ - activeAccount, - bridgeTransactions, - config, - dispatch, - subscribeToTransaction - ] - ) - - // load pending txs from storage - useEffect(() => { - Object.values(bridgeTransactions).forEach(tx => { - if (tx.complete) { - removeBridgeTransaction(tx) - } else { - subscribeToTransaction(tx) - } - }) - }, [bridgeTransactions, subscribeToTransaction, removeBridgeTransaction]) - - return ( - - {children} - - ) + return <>{children} } diff --git a/packages/core-mobile/app/hooks/networks/useNetworkContractTokens.ts b/packages/core-mobile/app/hooks/networks/useNetworkContractTokens.ts index 57a308aaee..f15f3a3687 100644 --- a/packages/core-mobile/app/hooks/networks/useNetworkContractTokens.ts +++ b/packages/core-mobile/app/hooks/networks/useNetworkContractTokens.ts @@ -9,7 +9,7 @@ import { useMemo } from 'react' import { getNetworkContractTokens } from './utils/getNetworkContractTokens' export const useNetworkContractTokens = ( - network: Network + network: Network | undefined ): NetworkContractToken[] => { const allCustomTokens = useSelector(selectAllCustomTokens) const isDeveloperMode = useSelector(selectIsDeveloperMode) diff --git a/packages/core-mobile/app/hooks/networks/utils/getNetworkContractTokens.ts b/packages/core-mobile/app/hooks/networks/utils/getNetworkContractTokens.ts index c3e64ba4cf..399cf6db2e 100644 --- a/packages/core-mobile/app/hooks/networks/utils/getNetworkContractTokens.ts +++ b/packages/core-mobile/app/hooks/networks/utils/getNetworkContractTokens.ts @@ -4,8 +4,10 @@ import ModuleManager from 'vmModule/ModuleManager' import { mapToVmNetwork } from 'vmModule/utils/mapToVmNetwork' export const getNetworkContractTokens = async ( - network: Network + network: Network | undefined ): Promise => { + if (!network) return [] + const module = await ModuleManager.loadModuleByNetwork(network) return module.getTokens(mapToVmNetwork(network)) diff --git a/packages/core-mobile/app/navigation/wallet/BridgeScreenStack.tsx b/packages/core-mobile/app/navigation/wallet/BridgeScreenStack.tsx index 98b6195cd6..fe986dd985 100644 --- a/packages/core-mobile/app/navigation/wallet/BridgeScreenStack.tsx +++ b/packages/core-mobile/app/navigation/wallet/BridgeScreenStack.tsx @@ -16,7 +16,7 @@ import { AssetBalance } from 'screens/bridge/utils/types' import { useSelector } from 'react-redux' import { selectIsBridgeBlocked } from 'store/posthog' import AnalyticsService from 'services/analytics/AnalyticsService' -import BridgeUniversal from 'screens/bridge/BridgeUniversal' +import Bridge from 'screens/bridge/Bridge' export type BridgeStackParamList = { [AppNavigation.Bridge.Bridge]: { initialTokenSymbol: string } | undefined @@ -46,7 +46,7 @@ function BridgeScreenStack(): JSX.Element { ...MainHeaderOptions() }} name={AppNavigation.Bridge.Bridge} - component={BridgeUniversal} + component={Bridge} /> { const { params } = useRoute() const { theme } = useTheme() const dispatch = useDispatch() - const criticalConfig = useSelector(selectBridgeCriticalConfig) - const [selectedAsset, setSelectedAsset] = useState() + const { activeNetwork } = useNetworks() + const selectedCurrency = useSelector(selectSelectedCurrency) const { - sourceBalance, - amount, - setAmount, + assetBalance, + inputAmount, + setInputAmount, assetsWithBalances, networkFee, loading, price, maximum, minimum, - receiveAmount, - wrapStatus, transfer, bridgeFee, - provider, - denomination, - amountBN + bridgeType, + amount, + sourceNetworks, + targetNetworks, + sourceNetwork, + setSourceNetwork, + targetNetwork, + setTargetNetwork, + bridgeAssets, + selectedBridgeAsset, + setSelectedBridgeAsset, + error } = useBridge() - const { - setCurrentAsset: setCurrentAssetSymbol, - currentBlockchain, - targetBlockchain - } = useBridgeSDK() - const { activeNetwork, networks } = useNetworks() const activeAccount = useSelector(selectActiveAccount) - const { getTokenSymbolOnNetwork } = useGetTokenSymbolOnNetwork() const [bridgeError, setBridgeError] = useState('') const [isPending, setIsPending] = useState(false) const tokenInfoData = useTokenInfoContext() @@ -125,51 +113,49 @@ const Bridge: FC = () => { activeAccount?.index ) ) - const selectedAssetSymbol = useMemo( - () => - isUnifiedBridgeAsset(selectedAsset?.asset) - ? selectedAsset?.asset.symbol - : getTokenSymbolOnNetwork( - selectedAsset?.asset.symbol ?? '', - currentBlockchain - ), - [currentBlockchain, getTokenSymbolOnNetwork, selectedAsset?.asset] - ) + const selectedAssetSymbol = selectedBridgeAsset?.symbol - const { bridgeBtcBlocked, bridgeEthBlocked } = usePosthogContext() const { currencyFormatter } = useApplicationContext().appHook - const isAmountTooLow = - amount && !amount.eq(BIG_ZERO) && minimum && amount.lt(minimum) - - const isAmountTooLarge = - amount && !amount.eq(BIG_ZERO) && maximum && amount.gt(maximum) - + const isAmountTooLow = amount !== 0n && minimum && amount < minimum + const isAmountTooLarge = amount !== 0n && maximum && amount > maximum const isNativeBalanceNotEnoughForNetworkFee = Boolean( - amount && - !amount.eq(BIG_ZERO) && - networkFee && - bigintToBig(nativeTokenBalance, activeNetwork.networkToken.decimals).lt( - networkFee - ) + amount !== 0n && networkFee && nativeTokenBalance < networkFee ) - const hasValidAmount = !isAmountTooLow && amount.gt(BIG_ZERO) + const denomination = useMemo(() => { + if (!assetBalance) { + return 0 + } + + return getDenomination(assetBalance.asset) + }, [assetBalance]) + + const hasValidAmount = !isAmountTooLow && amount > 0n + + const receiveAmount = useMemo( + () => (bridgeFee !== undefined ? amount - bridgeFee : undefined), + [amount, bridgeFee] + ) const hasInvalidReceiveAmount = - hasValidAmount && !!receiveAmount && receiveAmount.eq(BIG_ZERO) + hasValidAmount && receiveAmount !== undefined && receiveAmount === 0n const formattedAmountCurrency = hasValidAmount && price - ? currencyFormatter(price.mul(amount).toNumber()) + ? currencyFormatter( + price.mul(bigintToBig(amount, denomination)).toNumber() + ) : NO_AMOUNT const formattedReceiveAmount = hasValidAmount && receiveAmount - ? bigToLocaleString(receiveAmount) + ? bigToLocaleString(bigintToBig(receiveAmount, denomination)) : NO_AMOUNT const formattedReceiveAmountCurrency = hasValidAmount && price && receiveAmount - ? currencyFormatter(price.mul(receiveAmount).toNumber()) + ? currencyFormatter( + price.mul(bigintToBig(receiveAmount, denomination)).toNumber() + ) : NO_AMOUNT const transferDisabled = @@ -178,56 +164,34 @@ const Bridge: FC = () => { isAmountTooLow || isAmountTooLarge || isNativeBalanceNotEnoughForNetworkFee || - BIG_ZERO.eq(amount) || + amount === 0n || hasInvalidReceiveAmount - // Update selected asset for unified bridge whenever currentBlockchain changes useEffect(() => { - if (!selectedAsset) return - - const correspondingAsset = assetsWithBalances?.find(asset => { - // when selected asset is USDC.e and we are switching to Ethereum - // we want to automatically select USDC - // to do that, we need to compare by symbol (USDC) instead of symbolOnNetwork (USDC.e) - if ( - currentBlockchain === Blockchain.ETHEREUM && - selectedAsset.symbolOnNetwork === 'USDC.e' - ) { - return asset.symbol === selectedAsset.symbol - } - - // for all other cases we just simply compare the real symbol on network - return asset.symbolOnNetwork === selectedAsset.symbolOnNetwork - }) + setSourceNetwork(activeNetwork) + }, [activeNetwork, setSourceNetwork]) - // if the found asset is not in the list of new assets with balances, clear the selection - if (!correspondingAsset) { - setSelectedAsset(undefined) - return - } + useEffect(() => { + if (!sourceNetwork) return - // if the found asset is a unified bridge asset and its value is different, set it as the current asset - if ( - isUnifiedBridgeAsset(correspondingAsset.asset) && - JSON.stringify(correspondingAsset.asset) !== - JSON.stringify(selectedAsset.asset) - ) { - setSelectedAsset(correspondingAsset) - } - }, [assetsWithBalances, currentBlockchain, selectedAsset]) + dispatch(setActive(sourceNetwork.chainId)) + // Reset because a denomination change will change its value + setInputAmount(undefined) + }, [sourceNetwork, dispatch, setInputAmount]) const handleSelect = useCallback( (token: AssetBalance): void => { - const symbol = token.symbol + if (!isUnifiedBridgeAsset(token.asset)) { + return + } - setCurrentAssetSymbol(symbol) - setSelectedAsset(token) + setSelectedBridgeAsset(token.asset) }, - [setCurrentAssetSymbol] + [setSelectedBridgeAsset] ) useEffect(() => { - if (params?.initialTokenSymbol && !selectedAsset) { + if (params?.initialTokenSymbol && !selectedBridgeAsset) { const token = assetsWithBalances?.find( tk => (tk.symbolOnNetwork ?? tk.symbol) === params.initialTokenSymbol ) @@ -235,30 +199,22 @@ const Bridge: FC = () => { handleSelect(token) } } - }, [params, assetsWithBalances, handleSelect, selectedAsset]) - - // Remove chains turned off by the feature flags - const filterChains = useCallback( - (chains: Blockchain[]) => - chains.filter(chain => { - switch (chain) { - case Blockchain.BITCOIN: - return !bridgeBtcBlocked - case Blockchain.ETHEREUM: - return !bridgeEthBlocked - default: - return true - } - }), - [bridgeBtcBlocked, bridgeEthBlocked] - ) + }, [params, assetsWithBalances, handleSelect, selectedBridgeAsset]) - /** - * Blockchain array that's fed to dropdown - */ - const availableBlockchains = useMemo( - () => filterChains(sourceBlockchains), - [filterChains] + useEffect(() => { + if (error) { + setBridgeError(error.message) + } + }, [error]) + + const bridgeTokenList = useMemo( + () => + (assetsWithBalances ?? []).filter(asset => + bridgeAssets + .map(bridgeAsset => bridgeAsset.symbol) + .includes(asset.symbolOnNetwork ?? asset.asset.symbol) + ), + [assetsWithBalances, bridgeAssets] ) /** @@ -267,80 +223,117 @@ const Bridge: FC = () => { const navigateToTokenSelector = (): void => { navigation.navigate(AppNavigation.Modal.BridgeSelectToken, { onTokenSelected: handleSelect, - bridgeTokenList: assetsWithBalances ?? [] + bridgeTokenList }) } const handleAmountChanged = useCallback( (value: { bn: bigint; amount: string }) => { - const bigValue = bigintToBig(value.bn, denomination) + const bigValue = value.bn if (bridgeError) { setBridgeError('') } try { - setAmount(bigValue) + setInputAmount(bigValue) } catch (e) { Logger.error('failed to set amount', e) } }, - [bridgeError, denomination, setAmount] + [bridgeError, setInputAmount] ) - const setCurrentBlockchain = (blockchain: Blockchain): void => { - // update network - const blockChainNetwork = blockchainToNetwork( - blockchain, - networks, - criticalConfig - ) - blockChainNetwork && dispatch(setActive(blockChainNetwork.chainId)) - // Reset because a denomination change will change its value - setAmount(BIG_ZERO) - } + const [previousConfig, setPreviousConfig] = useState<{ + sourceNetwork: Network + bridgeAsset: BridgeAsset | undefined + }>() + + useEffect(() => { + if (previousConfig?.bridgeAsset) { + const bridgeAssetSymbol = unwrapAssetSymbol( + previousConfig.bridgeAsset.symbol + ) + + const bridgeAsset = + bridgeAssets.find(asset => asset.symbol === bridgeAssetSymbol) ?? + bridgeAssets.find( + asset => asset.symbol === wrapAssetSymbol(bridgeAssetSymbol) + ) + + if (bridgeAsset) { + setSelectedBridgeAsset(bridgeAsset) + } + } + }, [ + previousConfig?.bridgeAsset, + setSelectedBridgeAsset, + bridgeAssets, + setInputAmount + ]) + + useEffect(() => { + if ( + previousConfig?.sourceNetwork && + targetNetworks.findIndex( + network => network.chainId === previousConfig.sourceNetwork.chainId + ) !== -1 + ) { + setTargetNetwork(previousConfig.sourceNetwork) - const handleBlockchainToggle = (): void => { - if (targetBlockchain) { - setCurrentBlockchain(targetBlockchain) + setPreviousConfig(undefined) + } + }, [ + selectedBridgeAsset, + previousConfig?.sourceNetwork, + targetNetworks, + setTargetNetwork, + setPreviousConfig, + setInputAmount + ]) + + const handleNetworkToggle = (): void => { + if (targetNetwork && sourceNetwork) { + setPreviousConfig({ + sourceNetwork: sourceNetwork, + bridgeAsset: selectedBridgeAsset + }) + setSourceNetwork(targetNetwork) } } - /** - * Handles transfer transaction - */ const handleTransfer = async (): Promise => { - if (BIG_ZERO.eq(amount)) { + if (amount === 0n || !sourceNetwork || !targetNetwork) { return } AnalyticsService.capture('BridgeTransferStarted', { - sourceBlockchain: currentBlockchain, - targetBlockchain + sourceBlockchain: sourceNetwork.chainName, + targetBlockchain: targetNetwork.chainName }) try { setIsPending(true) - const [hash, error] = await resolve(transfer()) + const [hash, transferError] = await resolve(transfer()) setIsPending(false) - if (error || !hash) { + if (transferError || !hash) { // do not show the error when the user denied the transfer - if (isUserRejectedError(error)) { - Logger.error('failed to bridge', error) + if (isUserRejectedError(transferError)) { + Logger.error('failed to bridge', transferError) AnalyticsService.capture('BridgeTransferRequestUserRejectedError', { - sourceBlockchain: currentBlockchain, - targetBlockchain, - fee: bridgeFee.toNumber() + sourceBlockchain: sourceNetwork.chainName, + targetBlockchain: targetNetwork.chainName, + fee: bigintToBig(bridgeFee, denomination).toNumber() }) return } setBridgeError(TRANSFER_ERROR) showTransactionErrorToast({ - message: getJsonRpcErrorMessage(error) + message: getJsonRpcErrorMessage(transferError) }) - Logger.error(TRANSFER_ERROR, error) + Logger.error(TRANSFER_ERROR, transferError) AnalyticsService.capture('BridgeTransferRequestError', { - sourceBlockchain: currentBlockchain, - targetBlockchain + sourceBlockchain: sourceNetwork.chainName, + targetBlockchain: targetNetwork.chainName }) return } @@ -368,55 +361,29 @@ const Bridge: FC = () => { } } - const renderBlockchain = ( - blockchain: Blockchain, + const renderNetworkItem = ( + network: Network | undefined, textVariant: 'buttonLarge' | 'buttonMedium' ): JSX.Element => { - const blockchainTitle = getBlockchainDisplayName(blockchain) - - const symbol = - blockchain === Blockchain.AVALANCHE - ? TokenSymbol.AVAX - : blockchain === Blockchain.ETHEREUM - ? TokenSymbol.ETH - : blockchain === Blockchain.BITCOIN - ? TokenSymbol.BTC - : undefined - return ( <> - + - {blockchainTitle} + {network?.chainName} ) } - const renderToBlockchain = (blockchain: Blockchain): JSX.Element => { - return ( - - {renderBlockchain(blockchain, 'buttonLarge')} - - ) - } - - const renderFromBlockchain = (blockchain: Blockchain): JSX.Element => { + const renderNetwork = (network: Network | undefined): JSX.Element => { return ( { alignItems: 'center', justifyContent: 'flex-end' }}> - {renderBlockchain(blockchain, 'buttonLarge')} + {renderNetworkItem(network, 'buttonLarge')} ) } const renderDropdownItem = ( - blockchain: Blockchain, - selectedBlockchain?: Blockchain + network: Network, + selectedNetwork?: Network ): JSX.Element => { - const isSelected = blockchain === selectedBlockchain + const isSelected = network.chainId === selectedNetwork?.chainId return ( { alignItems: 'center', flex: 1 }}> - {renderBlockchain(blockchain, 'buttonMedium')} + {renderNetworkItem(network, 'buttonMedium')} {isSelected && ( { network.chainId === sourceNetwork?.chainId + )} + onItemSelected={setSourceNetwork} optionsRenderItem={item => - renderDropdownItem(item.item, currentBlockchain) + renderDropdownItem(item.item, sourceNetwork) } - selectionRenderItem={() => renderFromBlockchain(currentBlockchain)} + selectionRenderItem={() => renderNetwork(sourceNetwork)} style={{ top: 22 }} alignment="flex-end" prompt={ - + Select Network } @@ -503,37 +479,48 @@ const Bridge: FC = () => { } const renderBalance = (): JSX.Element => { - const shouldRenderBalance = selectedAsset && sourceBalance?.balance - return ( {`Balance: `} - {shouldRenderBalance - ? `${formatBalance(sourceBalance?.balance)} ${selectedAssetSymbol}` - : selectedAsset && } + {selectedBridgeAsset && assetBalance?.balance !== undefined + ? `${formatBalance( + bigintToBig(assetBalance.balance, denomination) + )} ${selectedAssetSymbol}` + : selectedBridgeAsset && } ) } const renderTokenSelectInput = (): JSX.Element => ( navigateToTokenSelector()}> - - {selectedAsset && ( + + {selectedBridgeAsset && ( <> )} - - {selectedAsset ? selectedAssetSymbol : 'Select Token'} + + {selectedBridgeAsset ? selectedAssetSymbol : 'Select Token'} @@ -545,8 +532,8 @@ const Bridge: FC = () => { return } handleAmountChanged({ - bn: bigToBigInt(maximum, denomination), - amount: bigToLocaleString(maximum, 4) + bn: maximum, + amount: bigToLocaleString(bigintToBig(maximum, denomination), 4) }) }, [denomination, handleAmountChanged, maximum]) @@ -554,7 +541,7 @@ const Bridge: FC = () => { <> { /> )} - {!selectedAsset && ( + {!selectedBridgeAsset && ( { ) const renderError = (): JSX.Element | null => { - if (amount.eq(BIG_ZERO)) return null + if (amount === 0n) return null if (isAmountTooLow) return ( @@ -598,7 +585,10 @@ const Bridge: FC = () => { sx={{ color: '$dangerDark' }}> - {`Amount too low -- minimum is ${minimum?.toFixed(9)}`} + {`Amount too low -- minimum is ${bigintToBig( + minimum, + denomination + )?.toFixed(9)}`} ) @@ -641,13 +631,7 @@ const Bridge: FC = () => { variant="caption" sx={{ color: '$dangerDark' - }}>{`Insufficient balance to cover gas costs.\nPlease add ${ - currentBlockchain === Blockchain.AVALANCHE - ? TokenSymbol.AVAX - : currentBlockchain === Blockchain.ETHEREUM - ? TokenSymbol.ETH - : TokenSymbol.BTC - }.`} + }}>{`Insufficient balance to cover gas costs.\nPlease add ${sourceNetwork?.networkToken.symbol}.`} ) return null @@ -655,15 +639,31 @@ const Bridge: FC = () => { const renderSelectSection = (): JSX.Element => { return ( - + {renderBalance()} - + {renderTokenSelectInput()} {renderAmountInput()} - - {renderError()} + + {renderError()} {/* Amount in currency */} { const renderToggleBtn = (): JSX.Element => { return ( + onPress={handleNetworkToggle} + style={{ + alignSelf: 'center', + marginTop: -20, + borderRadius: 50, + width: 40, + height: 40, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: theme.colors.$white + }}> ) @@ -703,54 +712,98 @@ const Bridge: FC = () => { const renderToSection = (): JSX.Element => { return ( - - - - - - Receive - - Estimated (minus transfer fees) - - - - {/* receive amount */} - - - {formattedReceiveAmount} - - {formattedReceiveAmount !== NO_AMOUNT && ( - {selectedAssetSymbol} + + To + + {targetNetworks.length === 1 ? ( + renderNetwork(targetNetwork) + ) : ( + network.chainId === targetNetwork?.chainId )} - - {/* estimate amount */} - + onItemSelected={setTargetNetwork} + optionsRenderItem={item => + renderDropdownItem(item.item, targetNetwork) + } + selectionRenderItem={() => renderNetwork(targetNetwork)} + style={{ + top: 22 + }} + alignment="flex-end" + prompt={ + + Select Network + + } + /> + )} + + + ) + } + + const renderReceiveSection = (): JSX.Element => { + return ( + + + Receive + + Estimated (minus transfer fees) + + + + {/* receive amount */} + + + {formattedReceiveAmount} + + {formattedReceiveAmount !== NO_AMOUNT && ( + {selectedAssetSymbol} + )} + + {/* estimate amount */} + + + {formattedReceiveAmountCurrency} + + {formattedReceiveAmountCurrency !== NO_AMOUNT && ( - {formattedReceiveAmountCurrency} + sx={{ marginTop: 4, color: '$neutral400' }}> + {selectedCurrency} - {formattedReceiveAmountCurrency !== NO_AMOUNT && ( - - {selectedCurrency} - - )} - - - - + )} + + + ) } @@ -827,19 +880,25 @@ const Bridge: FC = () => { return ( - + Bridge {renderFromSection()} @@ -847,73 +906,17 @@ const Bridge: FC = () => { {renderSelectSection()} {renderToggleBtn()} - {renderToSection()} + + {renderToSection()} + + {renderReceiveSection()} + - - {wrapStatus === WrapStatus.WAITING_FOR_DEPOSIT && ( - - Waiting for deposit confirmation - - )} + {renderTransferBtn()} - {provider === BridgeProvider.UNIFIED && renderCircleBadge()} + {bridgeType === BridgeType.CCTP && renderCircleBadge()} ) } -const styles = StyleSheet.create({ - container: { - flex: 1, - marginHorizontal: 8 - }, - fromContainer: { - flex: 1, - paddingStart: 16, - paddingTop: 16, - paddingBottom: 16 - }, - errorAndPriceRow: { - justifyContent: 'space-between', - paddingEnd: 16, - marginBottom: 16, - minHeight: 16 - }, - errorContainer: { - flex: 1 - }, - tokenSelectContainer: { - justifyContent: 'space-between', - alignItems: 'center' - }, - tokenRow: { - justifyContent: 'center', - alignItems: 'center' - }, - tokenSelectorText: { - justifyContent: 'center', - alignItems: 'center', - marginRight: 8, - fontWeight: '600' - }, - toggleButton: { - alignSelf: 'center', - marginTop: -20, - borderRadius: 50, - width: 40, - height: 40, - justifyContent: 'center', - alignItems: 'center' - }, - receiveRow: { - flex: 1, - padding: 16, - justifyContent: 'space-between' - } -}) - -export default React.memo(Bridge) +export default Bridge diff --git a/packages/core-mobile/app/screens/bridge/BridgeTokenSelector.tsx b/packages/core-mobile/app/screens/bridge/BridgeTokenSelector.tsx index 58031e91e5..bcf34922ff 100644 --- a/packages/core-mobile/app/screens/bridge/BridgeTokenSelector.tsx +++ b/packages/core-mobile/app/screens/bridge/BridgeTokenSelector.tsx @@ -3,7 +3,7 @@ import { ListRenderItemInfo, View } from 'react-native' import { Text } from '@avalabs/k2-mobile' import Loader from 'components/Loader' import { Space } from 'components/Space' -import { Asset, BIG_ZERO, useTokenInfoContext } from '@avalabs/core-bridge-sdk' +import { Asset, useTokenInfoContext } from '@avalabs/core-bridge-sdk' import AvaListItem from 'components/AvaListItem' import Avatar from 'components/Avatar' import { formatTokenAmount } from 'utils/Utils' @@ -12,7 +12,8 @@ import { BridgeAsset } from '@avalabs/bridge-unified' import { BottomSheetFlatList } from '@gorhom/bottom-sheet' import { AssetBalance } from 'screens/bridge/utils/types' import AnalyticsService from 'services/analytics/AnalyticsService' -import { isUnifiedBridgeAsset } from './utils/bridgeUtils' +import { bigintToBig } from 'utils/bigNumbers/bigintToBig' +import { isUnifiedBridgeAsset, unwrapAssetSymbol } from './utils/bridgeUtils' const DEFAULT_HORIZONTAL_MARGIN = 16 @@ -55,11 +56,15 @@ function BridgeTokenSelector({ ) } + const denomination = isUnifiedBridgeAsset(token.asset) + ? token.asset.decimals + : token.asset.denomination + return ( - {formatTokenAmount(token.balance || BIG_ZERO, 6)} {token.symbol} + {formatTokenAmount( + bigintToBig(token.balance || 0n, denomination), + 6 + )}{' '} + {token.symbol} } onPress={() => { diff --git a/packages/core-mobile/app/screens/bridge/BridgeUniversal.tsx b/packages/core-mobile/app/screens/bridge/BridgeUniversal.tsx deleted file mode 100644 index 960c436420..0000000000 --- a/packages/core-mobile/app/screens/bridge/BridgeUniversal.tsx +++ /dev/null @@ -1,936 +0,0 @@ -import React, { FC, useCallback, useEffect, useMemo, useState } from 'react' -import { Alert, Dimensions, Linking, Pressable, StyleSheet } from 'react-native' -import { Space } from 'components/Space' -import AvaButton from 'components/AvaButton' -import BridgeToggleIcon from 'assets/icons/BridgeToggleIcon.svg' -import DropDown from 'components/Dropdown' -import { Row } from 'components/Row' -import Separator from 'components/Separator' -import Avatar from 'components/Avatar' -import CheckmarkSVG from 'components/svg/CheckmarkSVG' -import { - BIG_ZERO, - formatTokenAmount, - useBridgeSDK, - useTokenInfoContext, - WrapStatus -} from '@avalabs/core-bridge-sdk' -import AppNavigation from 'navigation/AppNavigation' -import CarrotSVG from 'components/svg/CarrotSVG' -import useBridge from 'screens/bridge/hooks/useBridge' -import { useNavigation, useRoute } from '@react-navigation/native' -import { useApplicationContext } from 'contexts/ApplicationContext' -import { SafeAreaProvider } from 'react-native-safe-area-context' -import { BridgeScreenProps } from 'navigation/types' -import { setActive } from 'store/network' -import { - bigintToBig, - bigToBigInt, - bigToLocaleString, - resolve -} from '@avalabs/core-utils-sdk' -import Big from 'big.js' -import { ActivityIndicator } from 'components/ActivityIndicator' -import Logger from 'utils/Logger' -import { isUnifiedBridgeAsset } from 'screens/bridge/utils/bridgeUtils' -import { BNInput } from 'components/BNInput' -import { useDispatch, useSelector } from 'react-redux' -import AnalyticsService from 'services/analytics/AnalyticsService' -import { Button, Text, View, useTheme, ScrollView } from '@avalabs/k2-mobile' -import CircleLogo from 'assets/icons/circle_logo.svg' -import { Tooltip } from 'components/Tooltip' -import { DOCS_BRIDGE_FAQS } from 'resources/Constants' -import { selectSelectedCurrency } from 'store/settings/currency/slice' -import { useNetworks } from 'hooks/networks/useNetworks' -import { selectAvailableNativeTokenBalanceForNetworkAndAccount } from 'store/balance/slice' -import { RootState } from 'store' -import { selectActiveAccount } from 'store/account/slice' -import { Audios, audioFeedback } from 'utils/AudioFeedback' -import { showTransactionErrorToast } from 'utils/toast' -import { getJsonRpcErrorMessage } from 'utils/getJsonRpcErrorMessage' -import { isUserRejectedError } from 'store/rpc/providers/walletConnect/utils' -import { Network } from '@avalabs/core-chains-sdk' -import { NetworkLogo } from 'screens/network/NetworkLogo' -import { BridgeAsset } from '@avalabs/bridge-unified' -import { isEthereumNetwork } from 'services/network/utils/isEthereumNetwork' -import { AssetBalance, BridgeProvider } from './utils/types' - -const blockchainTitleMaxWidth = Dimensions.get('window').width * 0.5 -const dropdownWidth = Dimensions.get('window').width * 0.6 - -const TRANSFER_ERROR = 'There was a problem with the transfer.' - -const NO_AMOUNT = '-' - -const formatBalance = (balance: Big | undefined): string | undefined => { - return balance && formatTokenAmount(balance, 6) -} - -type NavigationProps = BridgeScreenProps - -const BridgeUniversal: FC = () => { - const navigation = useNavigation() - const { params } = useRoute() - const { theme } = useTheme() - const dispatch = useDispatch() - const { activeNetwork } = useNetworks() - - const selectedCurrency = useSelector(selectSelectedCurrency) - const { - sourceBalance, - amount, - setAmount, - assetsWithBalances, - networkFee, - loading, - price, - maximum, - minimum, - receiveAmount, - wrapStatus, - transfer, - bridgeFee, - provider, - denomination, - amountBN, - sourceNetworks, - targetNetworks, - sourceNetwork, - setSourceNetwork, - targetNetwork, - setTargetNetwork, - bridgeAssets, - selectedBridgeAsset, - setSelectedBridgeAsset, - error - } = useBridge() - - const { setCurrentAsset: setCurrentAssetSymbol } = useBridgeSDK() - const activeAccount = useSelector(selectActiveAccount) - const [bridgeError, setBridgeError] = useState('') - const [isPending, setIsPending] = useState(false) - const tokenInfoData = useTokenInfoContext() - const nativeTokenBalance = useSelector((state: RootState) => - selectAvailableNativeTokenBalanceForNetworkAndAccount( - state, - activeNetwork.chainId, - activeAccount?.index - ) - ) - const selectedAssetSymbol = selectedBridgeAsset?.symbol - - const { currencyFormatter } = useApplicationContext().appHook - const isAmountTooLow = - amount && !amount.eq(BIG_ZERO) && minimum && amount.lt(minimum) - - const isAmountTooLarge = - amount && !amount.eq(BIG_ZERO) && maximum && amount.gt(maximum) - - const isNativeBalanceNotEnoughForNetworkFee = Boolean( - amount && - !amount.eq(BIG_ZERO) && - networkFee && - bigintToBig(nativeTokenBalance, activeNetwork.networkToken.decimals).lt( - networkFee - ) - ) - - const hasValidAmount = !isAmountTooLow && amount.gt(BIG_ZERO) - - const hasInvalidReceiveAmount = - hasValidAmount && !!receiveAmount && receiveAmount.eq(BIG_ZERO) - - const formattedAmountCurrency = - hasValidAmount && price - ? currencyFormatter(price.mul(amount).toNumber()) - : NO_AMOUNT - - const formattedReceiveAmount = - hasValidAmount && receiveAmount - ? bigToLocaleString(receiveAmount) - : NO_AMOUNT - const formattedReceiveAmountCurrency = - hasValidAmount && price && receiveAmount - ? currencyFormatter(price.mul(receiveAmount).toNumber()) - : NO_AMOUNT - - const transferDisabled = - loading || - isPending || - isAmountTooLow || - isAmountTooLarge || - isNativeBalanceNotEnoughForNetworkFee || - BIG_ZERO.eq(amount) || - hasInvalidReceiveAmount - - useEffect(() => { - setSourceNetwork(activeNetwork) - }, [activeNetwork, setSourceNetwork]) - - useEffect(() => { - if (!sourceNetwork) return - - dispatch(setActive(sourceNetwork.chainId)) - // Reset because a denomination change will change its value - setAmount(undefined) - }, [sourceNetwork, dispatch, setAmount]) - - const handleSelect = useCallback( - (token: AssetBalance): void => { - if (!isUnifiedBridgeAsset(token.asset)) { - return - } - - const symbol = token.symbol - - setCurrentAssetSymbol(symbol) - setSelectedBridgeAsset(token.asset) - }, - [setCurrentAssetSymbol, setSelectedBridgeAsset] - ) - - useEffect(() => { - if (params?.initialTokenSymbol && !selectedBridgeAsset) { - const token = assetsWithBalances?.find( - tk => (tk.symbolOnNetwork ?? tk.symbol) === params.initialTokenSymbol - ) - if (token) { - handleSelect(token) - } - } - }, [params, assetsWithBalances, handleSelect, selectedBridgeAsset]) - - useEffect(() => { - if (error) { - setBridgeError(error.message) - } - }, [error]) - - const bridgeTokenList = useMemo( - () => - (assetsWithBalances ?? []).filter(asset => - bridgeAssets - .map(bridgeAsset => bridgeAsset.symbol) - .includes(asset.symbolOnNetwork ?? asset.asset.symbol) - ), - [assetsWithBalances, bridgeAssets] - ) - - /** - * Opens token selection modal - */ - const navigateToTokenSelector = (): void => { - navigation.navigate(AppNavigation.Modal.BridgeSelectToken, { - onTokenSelected: handleSelect, - bridgeTokenList - }) - } - - const handleAmountChanged = useCallback( - (value: { bn: bigint; amount: string }) => { - const bigValue = bigintToBig(value.bn, denomination) - if (bridgeError) { - setBridgeError('') - } - try { - setAmount(bigValue) - } catch (e) { - Logger.error('failed to set amount', e) - } - }, - [bridgeError, denomination, setAmount] - ) - - const [previousConfig, setPreviousConfig] = useState<{ - sourceNetwork: Network - bridgeAsset: BridgeAsset | undefined - }>() - - useEffect(() => { - if (previousConfig?.bridgeAsset) { - let bridgeAssetSymbol = previousConfig.bridgeAsset.symbol - if ( - sourceNetwork && - isEthereumNetwork(sourceNetwork) && - bridgeAssetSymbol === 'USDC.e' - ) { - bridgeAssetSymbol = 'USDC' - } - - const bridgeAsset = bridgeAssets.find( - asset => asset.symbol === bridgeAssetSymbol - ) - - if (bridgeAsset) { - setSelectedBridgeAsset(bridgeAsset) - } - } - }, [ - previousConfig?.bridgeAsset, - setSelectedBridgeAsset, - bridgeAssets, - setAmount, - sourceNetwork - ]) - - useEffect(() => { - if ( - previousConfig?.sourceNetwork && - targetNetworks.findIndex( - network => network.chainId === previousConfig.sourceNetwork.chainId - ) !== -1 - ) { - setTargetNetwork(previousConfig.sourceNetwork) - - setPreviousConfig(undefined) - setAmount(undefined) - } - }, [ - selectedBridgeAsset, - previousConfig?.sourceNetwork, - targetNetworks, - setTargetNetwork, - setPreviousConfig, - setAmount - ]) - - const handleNetworkToggle = (): void => { - if (targetNetwork && sourceNetwork) { - setPreviousConfig({ - sourceNetwork: sourceNetwork, - bridgeAsset: selectedBridgeAsset - }) - setSourceNetwork(targetNetwork) - } - } - - /** - * Handles transfer transaction - */ - const handleTransfer = async (): Promise => { - if (BIG_ZERO.eq(amount) || !sourceNetwork || !targetNetwork) { - return - } - - AnalyticsService.capture('BridgeTransferStarted', { - sourceBlockchain: sourceNetwork.chainName, - targetBlockchain: targetNetwork.chainName - }) - - try { - setIsPending(true) - const [hash, transferError] = await resolve(transfer()) - setIsPending(false) - - if (transferError || !hash) { - // do not show the error when the user denied the transfer - if (isUserRejectedError(transferError)) { - Logger.error('failed to bridge', transferError) - AnalyticsService.capture('BridgeTransferRequestUserRejectedError', { - sourceBlockchain: sourceNetwork.chainName, - targetBlockchain: targetNetwork.chainName, - fee: bridgeFee.toNumber() - }) - return - } - setBridgeError(TRANSFER_ERROR) - showTransactionErrorToast({ - message: getJsonRpcErrorMessage(transferError) - }) - Logger.error(TRANSFER_ERROR, transferError) - AnalyticsService.capture('BridgeTransferRequestError', { - sourceBlockchain: sourceNetwork.chainName, - targetBlockchain: targetNetwork.chainName - }) - return - } - - audioFeedback(Audios.Send) - - // Navigate to transaction status page - navigation.navigate(AppNavigation.Bridge.BridgeTransactionStatus, { - txHash: hash ?? '' - }) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (e: any) { - const errorMessage = - 'reason' in e - ? e?.reason - : e?.message ?? - 'An unknown error has occurred. Bridging was halted. Please try again later' - Alert.alert('Error Bridging', errorMessage) - AnalyticsService.capture('BridgeTokenSelectError', { - errorMessage - }) - return - } finally { - setIsPending(false) - } - } - - const renderNetworkItem = ( - network: Network | undefined, - textVariant: 'buttonLarge' | 'buttonMedium' - ): JSX.Element => { - return ( - <> - - - - {network?.chainName} - - - ) - } - - const renderNetwork = (network: Network | undefined): JSX.Element => { - return ( - - {renderNetworkItem(network, 'buttonLarge')} - - ) - } - - const renderDropdownItem = ( - network: Network, - selectedNetwork?: Network - ): JSX.Element => { - const isSelected = network.chainId === selectedNetwork?.chainId - - return ( - - - {renderNetworkItem(network, 'buttonMedium')} - - {isSelected && ( - - - - - )} - - ) - } - - const renderFromSection = (): JSX.Element => { - return ( - - From - - network.chainId === sourceNetwork?.chainId - )} - onItemSelected={setSourceNetwork} - optionsRenderItem={item => - renderDropdownItem(item.item, sourceNetwork) - } - selectionRenderItem={() => renderNetwork(sourceNetwork)} - style={{ - top: 22 - }} - alignment="flex-end" - prompt={ - - Select Network - - } - /> - - - ) - } - - const renderBalance = (): JSX.Element => { - const shouldRenderBalance = selectedBridgeAsset && sourceBalance?.balance - - return ( - - {`Balance: `} - {shouldRenderBalance - ? `${formatBalance(sourceBalance?.balance)} ${selectedAssetSymbol}` - : selectedBridgeAsset && } - - ) - } - const renderTokenSelectInput = (): JSX.Element => ( - navigateToTokenSelector()}> - - {selectedBridgeAsset && ( - <> - - - - )} - - {selectedBridgeAsset ? selectedAssetSymbol : 'Select Token'} - - - - - ) - - const handleMax = useCallback(() => { - if (!maximum) { - return - } - handleAmountChanged({ - bn: bigToBigInt(maximum, denomination), - amount: bigToLocaleString(maximum, 4) - }) - }, [denomination, handleAmountChanged, maximum]) - - const renderAmountInput = (): JSX.Element => ( - - <> - - {loading && ( - - )} - - {!selectedBridgeAsset && ( - navigateToTokenSelector()} - /> - )} - - ) - - const renderError = (): JSX.Element | null => { - if (amount.eq(BIG_ZERO)) return null - - if (isAmountTooLow) - return ( - - {`Amount too low -- minimum is ${minimum?.toFixed(9)}`} - - ) - - if (isAmountTooLarge) - return ( - - Insufficient balance - - ) - - if (bridgeError) - return ( - - {bridgeError} - - ) - - if (hasInvalidReceiveAmount) - return ( - - {`Receive amount can't be 0`} - - ) - - if (isNativeBalanceNotEnoughForNetworkFee) - return ( - {`Insufficient balance to cover gas costs.\nPlease add ${sourceNetwork?.networkToken.symbol}.`} - ) - - return null - } - - const renderSelectSection = (): JSX.Element => { - return ( - - {renderBalance()} - - {renderTokenSelectInput()} - {renderAmountInput()} - - - - {renderError()} - - {/* Amount in currency */} - - - - {formattedAmountCurrency} - - {formattedAmountCurrency !== NO_AMOUNT && ( - - {selectedCurrency} - - )} - - - - - ) - } - - const renderToggleBtn = (): JSX.Element => { - return ( - - - - ) - } - - const renderToSection = (): JSX.Element => { - return ( - - To - - {targetNetworks.length === 1 ? ( - renderNetwork(targetNetwork) - ) : ( - network.chainId === targetNetwork?.chainId - )} - onItemSelected={setTargetNetwork} - optionsRenderItem={item => - renderDropdownItem(item.item, targetNetwork) - } - selectionRenderItem={() => renderNetwork(targetNetwork)} - style={{ - top: 22 - }} - alignment="flex-end" - prompt={ - - Select Network - - } - /> - )} - - - ) - } - - const renderReceiveSection = (): JSX.Element => { - return ( - - - Receive - - Estimated (minus transfer fees) - - - - {/* receive amount */} - - - {formattedReceiveAmount} - - {formattedReceiveAmount !== NO_AMOUNT && ( - {selectedAssetSymbol} - )} - - {/* estimate amount */} - - - {formattedReceiveAmountCurrency} - - {formattedReceiveAmountCurrency !== NO_AMOUNT && ( - - {selectedCurrency} - - )} - - - - ) - } - - const renderTransferBtn = (): JSX.Element => { - return ( - - ) - } - - const handleBridgeFaqs = (): void => { - Linking.openURL(DOCS_BRIDGE_FAQS).catch(e => { - Logger.error(DOCS_BRIDGE_FAQS, e) - }) - } - - const renderCCTPPopoverInfoText = (): JSX.Element => ( - - - USDC is routed through Circle's Cross-Chain Transfer Protocol. - - - Bridge FAQs - - - ) - - const renderCircleBadge = (): JSX.Element => { - return ( - - Powered by - - - - ) - } - - return ( - - - - Bridge - - - - - {renderFromSection()} - - {renderSelectSection()} - - {renderToggleBtn()} - - {renderToSection()} - - {renderReceiveSection()} - - - - {wrapStatus === WrapStatus.WAITING_FOR_DEPOSIT && ( - - Waiting for deposit confirmation - - )} - {renderTransferBtn()} - {provider === BridgeProvider.UNIFIED && renderCircleBadge()} - - ) -} - -export default BridgeUniversal diff --git a/packages/core-mobile/app/screens/bridge/components/BridgeTransactionItem.tsx b/packages/core-mobile/app/screens/bridge/components/BridgeTransactionItem.tsx index c8beab7c8e..ca64f8b130 100644 --- a/packages/core-mobile/app/screens/bridge/components/BridgeTransactionItem.tsx +++ b/packages/core-mobile/app/screens/bridge/components/BridgeTransactionItem.tsx @@ -1,6 +1,5 @@ import React, { FC, useMemo } from 'react' import { useApplicationContext } from 'contexts/ApplicationContext' -import { BridgeTransaction } from '@avalabs/core-bridge-sdk' import AvaText from 'components/AvaText' import AvaListItem from 'components/AvaListItem' import BridgeSVG from 'components/svg/BridgeSVG' @@ -19,7 +18,7 @@ import { BridgeTransfer } from '@avalabs/bridge-unified' import { bigintToBig } from '@avalabs/core-utils-sdk' interface BridgeTransactionItemProps { - item: Transaction | BridgeTransaction | BridgeTransfer + item: Transaction | BridgeTransfer onPress: () => void } @@ -38,7 +37,7 @@ const BridgeTransactionItem: FC = ({ return bigintToBig(item.amount, item.asset.decimals).toString() } - return item.amount.toString() + return undefined }, [item, pending]) return ( @@ -65,12 +64,7 @@ const BridgeTransactionItem: FC = ({ rightComponent={ - {amount}{' '} - {pending - ? isUnifiedBridgeTransfer(item) - ? item.asset.symbol - : item.symbol - : item.tokens[0]?.symbol} + {amount} {pending ? item.asset.symbol : item.tokens[0]?.symbol} {'explorerLink' in item && item?.explorerLink && ( <> diff --git a/packages/core-mobile/app/screens/bridge/handlers/getEVMAssetBalances.ts b/packages/core-mobile/app/screens/bridge/handlers/getEVMAssetBalances.ts index 9b6550846f..ac83db86f4 100644 --- a/packages/core-mobile/app/screens/bridge/handlers/getEVMAssetBalances.ts +++ b/packages/core-mobile/app/screens/bridge/handlers/getEVMAssetBalances.ts @@ -1,12 +1,6 @@ -import { - Asset, - BIG_ZERO, - isBtcAsset, - isNativeAsset -} from '@avalabs/core-bridge-sdk' +import { Asset, isBtcAsset, isNativeAsset } from '@avalabs/core-bridge-sdk' import { BridgeAsset, isErc20Asset } from '@avalabs/bridge-unified' import { AssetBalance } from 'screens/bridge/utils/types' -import { bigintToBig } from '@avalabs/core-utils-sdk' import { TokenType, TokenWithBalance } from '@avalabs/vm-module-types' import { isUnifiedBridgeAsset } from '../utils/bridgeUtils' @@ -35,7 +29,9 @@ export function getEVMAssetBalances( const symbol = asset.symbol const token = isUnifiedBridgeAsset(asset) ? erc20TokensByAddress[ - isErc20Asset(asset) ? asset.address?.toLowerCase() : asset.symbol + isErc20Asset(asset) + ? asset.address?.toLowerCase() + : asset.symbol.toLowerCase() ] : isNativeAsset(asset) ? erc20TokensByAddress[asset.symbol.toLowerCase()] @@ -44,11 +40,7 @@ export function getEVMAssetBalances( : erc20TokensByAddress[asset.wrappedContractAddress?.toLowerCase()] || erc20TokensByAddress[asset.nativeContractAddress?.toLowerCase()] - const balance = - (token && - 'decimals' in token && - bigintToBig(token.balance, token.decimals)) || - BIG_ZERO + const balance = (token && token.balance) || 0n return { symbol, asset, balance } }) } diff --git a/packages/core-mobile/app/screens/bridge/hooks/useAssetBalancesEVM.ts b/packages/core-mobile/app/screens/bridge/hooks/useAssetBalancesEVM.ts index 68eab4b845..5b9f4c9d49 100644 --- a/packages/core-mobile/app/screens/bridge/hooks/useAssetBalancesEVM.ts +++ b/packages/core-mobile/app/screens/bridge/hooks/useAssetBalancesEVM.ts @@ -12,7 +12,8 @@ import { AssetBalance } from 'screens/bridge/utils/types' import { useSelector } from 'react-redux' import { selectTokensWithBalance } from 'store/balance/slice' import { uniqBy } from 'lodash' -import { isUnifiedBridgeAsset } from '../utils/bridgeUtils' +import { bigintToBig } from 'utils/bigNumbers/bigintToBig' +import { getDenomination, isUnifiedBridgeAsset } from '../utils/bridgeUtils' import { getEVMAssetBalances } from '../handlers/getEVMAssetBalances' import { useUnifiedBridgeAssets } from './useUnifiedBridgeAssets' @@ -83,9 +84,18 @@ export function useAssetBalancesEVM( [allAssets, chain, getTokenSymbolOnNetwork, tokens] ) - const sortedAssetsWithBalances = assetsWithBalances.sort( - (asset1, asset2) => asset2.balance?.cmp(asset1.balance || 0) || 0 - ) + const sortedAssetsWithBalances = assetsWithBalances.sort((asset1, asset2) => { + const asset1Balance = bigintToBig( + asset1.balance || 0n, + getDenomination(asset1.asset) + ) + const asset2Balance = bigintToBig( + asset2.balance || 0n, + getDenomination(asset2.asset) + ) + + return asset2Balance.cmp(asset1Balance) + }) return { assetsWithBalances: sortedAssetsWithBalances, loading: false } } diff --git a/packages/core-mobile/app/screens/bridge/hooks/useAvalancheBridge.ts b/packages/core-mobile/app/screens/bridge/hooks/useAvalancheBridge.ts deleted file mode 100644 index 0b785da94d..0000000000 --- a/packages/core-mobile/app/screens/bridge/hooks/useAvalancheBridge.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { BIG_ZERO, Blockchain, useBridgeSDK } from '@avalabs/core-bridge-sdk' -import { BridgeAdapter } from 'screens/bridge/hooks/useBridge' -import { useBridgeContext } from 'contexts/BridgeContext' -import { useCallback, useMemo } from 'react' -import { useAssetBalancesEVM } from 'screens/bridge/hooks/useAssetBalancesEVM' -import Big from 'big.js' -import { useSelector } from 'react-redux' -import AnalyticsService from 'services/analytics/AnalyticsService' -import { selectActiveAccount } from 'store/account' -import { useNetworks } from 'hooks/networks/useNetworks' -import Logger from 'utils/Logger' -import { noop } from '@avalabs/core-utils-sdk' -import { useTransferAssetEVM } from './useTransferAssetEVM' - -/** - * Hook for transferring assets from Avalanche to Ethereum (unwrapping) - */ -export function useAvalancheBridge({ - amount, - bridgeFee, - minimum -}: { - amount: Big - bridgeFee: Big - minimum: Big -}): BridgeAdapter { - const { activeNetwork } = useNetworks() - const { targetBlockchain, currentAssetData } = useBridgeSDK() - const { createBridgeTransaction } = useBridgeContext() - const { transfer: transferEVM } = useTransferAssetEVM() - const activeAccount = useSelector(selectActiveAccount) - - const { assetsWithBalances, loading } = useAssetBalancesEVM( - Blockchain.AVALANCHE - ) - - const sourceBalance = useMemo( - () => - assetsWithBalances.find( - ({ asset }) => asset.symbol === currentAssetData?.symbol - ), - [assetsWithBalances, currentAssetData?.symbol] - ) - - const maximum = sourceBalance?.balance || BIG_ZERO - const receiveAmount = amount.gt(minimum) ? amount.minus(bridgeFee) : BIG_ZERO - - const transfer = useCallback(async () => { - if (!currentAssetData) return Promise.reject('Asset not found') - - const timestamp = Date.now() - - const transactionHash = await transferEVM({ - amount, - asset: currentAssetData, - onStatusChange: noop, - onTxHashChange: noop - }) - - if (!transactionHash) return Promise.reject('Failed to transfer') - - AnalyticsService.captureWithEncryption('BridgeTransactionStarted', { - chainId: activeNetwork.chainId, - sourceTxHash: transactionHash, - fromAddress: activeAccount?.addressC - }) - - createBridgeTransaction( - { - sourceChain: Blockchain.AVALANCHE, - sourceTxHash: transactionHash, - sourceStartedAt: timestamp, - targetChain: targetBlockchain, - amount, - symbol: currentAssetData.symbol - }, - activeNetwork - ).catch(Logger.error) - - return transactionHash - }, [ - currentAssetData, - transferEVM, - amount, - activeNetwork, - activeAccount?.addressC, - createBridgeTransaction, - targetBlockchain - ]) - - return { - sourceBalance, - assetsWithBalances, - loading, - receiveAmount, - maximum, - transfer - } -} diff --git a/packages/core-mobile/app/screens/bridge/hooks/useBridge.ts b/packages/core-mobile/app/screens/bridge/hooks/useBridge.ts index 73aee89406..5eb088e4bc 100644 --- a/packages/core-mobile/app/screens/bridge/hooks/useBridge.ts +++ b/packages/core-mobile/app/screens/bridge/hooks/useBridge.ts @@ -1,54 +1,34 @@ -import { - Blockchain, - useBridgeFeeEstimate, - useBridgeSDK, - useMinimumTransferAmount, - WrapStatus -} from '@avalabs/core-bridge-sdk' +import { Blockchain } from '@avalabs/core-bridge-sdk' import { useEffect, useMemo, useState } from 'react' -import { VsCurrencyType } from '@avalabs/core-coingecko-sdk' -import { useBtcBridge } from 'screens/bridge/hooks/useBtcBridge' -import { AssetBalance, BridgeProvider } from 'screens/bridge/utils/types' +import { AssetBalance } from 'screens/bridge/utils/types' import Big from 'big.js' -import { useSelector } from 'react-redux' -import { selectSelectedCurrency } from 'store/settings/currency' -import { selectActiveAccount } from 'store/account' -import { BIG_ZERO, bigToBigInt } from '@avalabs/core-utils-sdk' import Logger from 'utils/Logger' -import BridgeService from 'services/bridge/BridgeService' -import { selectIsDeveloperMode } from 'store/settings/advanced' -import { selectBridgeAppConfig } from 'store/bridge' -import UnifiedBridgeService from 'services/bridge/UnifiedBridgeService' -import { useNetworks } from 'hooks/networks/useNetworks' -import { useNetworkFee } from 'hooks/useNetworkFee' -import { bigintToBig } from 'utils/bigNumbers/bigintToBig' -import { useCoinGeckoId } from 'hooks/useCoinGeckoId' -import { useSimplePrice } from 'hooks/useSimplePrice' import { Network } from '@avalabs/core-chains-sdk' -import { BridgeAsset } from '@avalabs/bridge-unified' +import { BridgeAsset, BridgeType } from '@avalabs/bridge-unified' +import { isEthereumNetwork } from 'services/network/utils/isEthereumNetwork' +import UnifiedBridgeService from 'services/bridge/UnifiedBridgeService' +import { getAssetBalance, unwrapAssetSymbol } from '../utils/bridgeUtils' +import { useUnifiedBridgeAssets } from './useUnifiedBridgeAssets' +import { useAssetBalancesEVM } from './useAssetBalancesEVM' +import { useGetBridgeFees } from './useGetBridgeFees' +import { useBridgeAssetPrice } from './useBridgeAssetPrice' +import { useBridgeType } from './useBridgeType' +import { useBridgeTransfer } from './useBridgeTransfer' import { - getOriginalSymbol, - isUnifiedBridgeAsset, - networkToBlockchain -} from '../utils/bridgeUtils' -import { useUnifiedBridge } from './useUnifiedBridge/useUnifiedBridge' -import { getTargetChainId } from './useUnifiedBridge/utils' + useBridgeSourceNetworks, + useBridgeTargetNetworks +} from './useBridgeNetworks' -export interface BridgeAdapter { - address?: string - sourceBalance?: AssetBalance - targetBalance?: AssetBalance +interface Bridge { + assetBalance?: AssetBalance assetsWithBalances?: AssetBalance[] - networkFee?: Big + networkFee?: bigint loading?: boolean - bridgeFee?: Big - /** Amount minus network and bridge fees */ - receiveAmount?: Big + bridgeFee: bigint /** Maximum transfer amount */ - maximum?: Big + maximum?: bigint /** Minimum transfer amount */ - minimum?: Big - wrapStatus?: WrapStatus + minimum?: bigint /** * Transfer funds to the target network * @returns the transaction hash @@ -65,199 +45,160 @@ export interface BridgeAdapter { selectedBridgeAsset?: BridgeAsset setSelectedBridgeAsset: (asset: BridgeAsset) => void error: Error | undefined -} + bridgeType: BridgeType | undefined -interface Bridge extends BridgeAdapter { - amount: Big - setAmount: (amount: Big | undefined) => void + inputAmount: bigint | undefined + setInputAmount: (amount: bigint | undefined) => void + amount: bigint price: Big | undefined - provider: BridgeProvider - bridgeFee: Big - denomination: number - amountBN: bigint | undefined } // eslint-disable-next-line sonarjs/cognitive-complexity export default function useBridge(): Bridge { - const { activeNetwork, networks, getNetwork } = useNetworks() - const config = useSelector(selectBridgeAppConfig) - const isTestnet = useSelector(selectIsDeveloperMode) - const currency = useSelector(selectSelectedCurrency) - const activeAccount = useSelector(selectActiveAccount) - const [sourceBalance, setSourceBalance] = useState() - const { - currentBlockchain, - setCurrentBlockchain, - currentAssetData, - setCurrentAsset, - targetBlockchain - } = useBridgeSDK() - - const targetChainId = useMemo( - () => getTargetChainId(isTestnet, targetBlockchain), - [isTestnet, targetBlockchain] + const [sourceNetwork, setSourceNetwork] = useState() + const [targetNetwork, setTargetNetwork] = useState() + const [selectedBridgeAsset, setSelectedBridgeAsset] = useState() + const { bridgeAssets } = useUnifiedBridgeAssets() + const [bridgeError, setBridgeError] = useState() + const [minimum, setMinimum] = useState() + const [bridgeFee, setBridgeFee] = useState(0n) + const [inputAmount, setInputAmount] = useState() + const amount = useMemo(() => inputAmount ?? 0n, [inputAmount]) + const { assetsWithBalances, loading } = useAssetBalancesEVM( + sourceNetwork && isEthereumNetwork(sourceNetwork) + ? Blockchain.ETHEREUM + : Blockchain.AVALANCHE ) - const targetNetwork = getNetwork(targetChainId) - - // reset current asset when unmounting - useEffect(() => { - return () => { - setCurrentAsset('') - } - }, [setCurrentAsset]) + const assetBalance = useMemo( + () => getAssetBalance(selectedBridgeAsset?.symbol, assetsWithBalances), + [selectedBridgeAsset, assetsWithBalances] + ) - const [inputAmount, setAmount] = useState() - const [networkFee, setNetworkFee] = useState() + const sourceNetworks = useBridgeSourceNetworks() + const targetNetworks = useBridgeTargetNetworks(selectedBridgeAsset) - const amount = inputAmount || BIG_ZERO + const [networkFee, setNetworkFee] = useState() - const bridgeFee = useBridgeFeeEstimate(amount) || BIG_ZERO - const minimum = useMinimumTransferAmount(amount) - const { data: networkFeeRate } = useNetworkFee() + const price = useBridgeAssetPrice(selectedBridgeAsset) - const btc = useBtcBridge({ amount, bridgeFee, minimum }) - const unified = useUnifiedBridge(amount) + const transfer = useBridgeTransfer({ + amount, + bridgeAsset: selectedBridgeAsset, + sourceNetwork, + targetNetwork + }) - const assetSymbol = unified.selectedBridgeAsset?.symbol - ? getOriginalSymbol(unified.selectedBridgeAsset.symbol) - : undefined + const bridgeType = useBridgeType(selectedBridgeAsset, targetNetwork?.chainId) - const coingeckoId = useCoinGeckoId(assetSymbol) + const { getBridgeFee, getNetworkFee } = useGetBridgeFees({ + amount, + bridgeAsset: selectedBridgeAsset, + sourceNetwork, + targetNetwork + }) - const price = useSimplePrice( - coingeckoId, - currency.toLowerCase() as VsCurrencyType - ) + useEffect(() => { + getNetworkFee() + .then(fee => { + if (fee) { + setNetworkFee(fee) + } + }) + .catch(e => { + Logger.error('Failed to get network fee', e) + }) + }, [getNetworkFee]) - const denomination = useMemo(() => { - if (!sourceBalance) { - return 0 - } + useEffect(() => { + getBridgeFee() + .then(fee => { + if (fee !== undefined) { + setBridgeFee(fee) + } + }) + .catch(error => { + Logger.error(error) + setBridgeError(error) + }) + }, [getBridgeFee, amount]) - if (isUnifiedBridgeAsset(sourceBalance.asset)) { - return sourceBalance.asset.decimals + useEffect(() => { + if (!selectedBridgeAsset || !sourceNetwork || !targetNetwork) { + return } - return sourceBalance.asset.denomination - }, [sourceBalance]) - - const amountBN = useMemo( - () => - inputAmount !== undefined - ? bigToBigInt(inputAmount, denomination) - : undefined, - [inputAmount, denomination] - ) + UnifiedBridgeService.getMinimumTransferAmount({ + amount, + asset: selectedBridgeAsset, + sourceNetwork, + targetNetwork + }) + .then(minimumAmount => setMinimum(minimumAmount)) + .catch(error => { + Logger.error('Failed to get minimum transfer amount', error) + setMinimum(undefined) + }) + }, [selectedBridgeAsset, sourceNetwork, targetNetwork, amount]) useEffect(() => { - if (unified.isAssetSupported) { - setSourceBalance(unified.sourceBalance) - } else if (currentBlockchain === Blockchain.BITCOIN) { - setSourceBalance(btc.sourceBalance) - } else { - setSourceBalance(undefined) + if (targetNetworks.length === 0) { + return } - }, [btc, currentBlockchain, unified.isAssetSupported, unified.sourceBalance]) + + if ( + !targetNetwork || + !targetNetworks.find(network => network.chainId === targetNetwork.chainId) + ) { + setTargetNetwork(targetNetworks[0]) + } + }, [targetNetworks, targetNetwork]) useEffect(() => { - const getNetworkFee = async (): Promise => { - if ( - !networkFeeRate || - !activeAccount || - !unified.selectedBridgeAsset || - !currentAssetData || - amount.eq(BIG_ZERO) - ) - return + if (!selectedBridgeAsset) return - let gasLimit + const bridgeAssetSymbol = selectedBridgeAsset.symbol - if (unified.isAssetSupported) { - gasLimit = await UnifiedBridgeService.estimateGas({ - asset: unified.selectedBridgeAsset, - amount, - activeAccount, - sourceNetwork: activeNetwork, - targetNetwork - }) - } else { - gasLimit = await BridgeService.estimateGas({ - currentBlockchain, - amount, - activeAccount, - activeNetwork, - currency, - allNetworks: networks, - asset: currentAssetData, - isTestnet, - config - }) - } + const bridgeAsset = + bridgeAssets.find(asset => asset.symbol === bridgeAssetSymbol) ?? + bridgeAssets.find( + asset => asset.symbol === unwrapAssetSymbol(bridgeAssetSymbol) + ) - if (gasLimit) { - setNetworkFee( - bigintToBig( - networkFeeRate.low.maxFeePerGas.mul(gasLimit).toSubUnit(), - activeNetwork.networkToken.decimals - ) - ) - } + if (bridgeAsset) { + setSelectedBridgeAsset(bridgeAsset) + } else { + setSelectedBridgeAsset(undefined) } + }, [sourceNetwork, bridgeAssets, selectedBridgeAsset]) - getNetworkFee().catch(e => { - Logger.error('Failed to get network fee', e) - }) - }, [ - activeAccount, - activeNetwork, - networks, - amount, - config, - currency, - currentAssetData, - currentBlockchain, - isTestnet, - targetNetwork, - networkFeeRate, - unified - ]) - - // Derive bridge Blockchain from active network useEffect(() => { - const networkBlockchain = networkToBlockchain(activeNetwork) - if (currentBlockchain !== networkBlockchain) { - setCurrentBlockchain(networkBlockchain) - } - }, [activeNetwork, currentBlockchain, setCurrentBlockchain]) + setInputAmount(undefined) + }, [selectedBridgeAsset]) - const defaults = { - amount, - setAmount, - networkFee, + return { + assetBalance, + loading, + assetsWithBalances, bridgeFee, - price, + maximum: assetBalance?.balance, minimum, - provider: BridgeProvider.LEGACY, - denomination, - amountBN - } - - if (unified.isAssetSupported) { - return { - ...defaults, - ...unified, - provider: BridgeProvider.UNIFIED - } - } else if (currentBlockchain === Blockchain.BITCOIN) { - return { - ...defaults, - ...btc - } - } else { - return { - ...defaults, - transfer: () => Promise.reject('invalid bridge') - } + transfer, + sourceNetworks, + targetNetworks, + sourceNetwork, + setSourceNetwork, + targetNetwork, + setTargetNetwork, + bridgeAssets, + selectedBridgeAsset, + setSelectedBridgeAsset, + error: bridgeError, + bridgeType, + inputAmount, + setInputAmount, + amount, + networkFee, + price } } diff --git a/packages/core-mobile/app/screens/bridge/hooks/useBridgeAssetPrice.ts b/packages/core-mobile/app/screens/bridge/hooks/useBridgeAssetPrice.ts new file mode 100644 index 0000000000..101018d880 --- /dev/null +++ b/packages/core-mobile/app/screens/bridge/hooks/useBridgeAssetPrice.ts @@ -0,0 +1,20 @@ +import { BridgeAsset } from '@avalabs/bridge-unified' +import { useCoinGeckoId } from 'hooks/useCoinGeckoId' +import { useSelector } from 'react-redux' +import { selectSelectedCurrency } from 'store/settings/currency' +import { useSimplePrice } from 'hooks/useSimplePrice' +import { VsCurrencyType } from '@avalabs/core-coingecko-sdk' +import { getOriginalSymbol } from '../utils/bridgeUtils' + +export const useBridgeAssetPrice = ( + bridgeAsset: BridgeAsset | undefined +): Big | undefined => { + const assetSymbol = bridgeAsset?.symbol + ? getOriginalSymbol(bridgeAsset.symbol) + : undefined + + const coingeckoId = useCoinGeckoId(assetSymbol) + const currency = useSelector(selectSelectedCurrency) + + return useSimplePrice(coingeckoId, currency.toLowerCase() as VsCurrencyType) +} diff --git a/packages/core-mobile/app/screens/bridge/hooks/useBridgeNetworks.ts b/packages/core-mobile/app/screens/bridge/hooks/useBridgeNetworks.ts new file mode 100644 index 0000000000..1746c749f7 --- /dev/null +++ b/packages/core-mobile/app/screens/bridge/hooks/useBridgeNetworks.ts @@ -0,0 +1,18 @@ +import { useNetworksFromCaip2ChainIds } from 'temp/caip2ChainIds' +import { Network } from '@avalabs/core-chains-sdk' +import { BridgeAsset } from '@avalabs/bridge-unified' +import { useUnifiedBridgeAssets } from './useUnifiedBridgeAssets' + +export const useBridgeSourceNetworks = (): Network[] => { + const { chainAssetMap } = useUnifiedBridgeAssets() + + return useNetworksFromCaip2ChainIds(Object.keys(chainAssetMap ?? [])) +} + +export const useBridgeTargetNetworks = ( + selectedBridgeAsset: BridgeAsset | undefined +): Network[] => { + return useNetworksFromCaip2ChainIds( + Object.keys(selectedBridgeAsset?.destinations ?? []) + ) +} diff --git a/packages/core-mobile/app/screens/bridge/hooks/useBridgeTransfer.ts b/packages/core-mobile/app/screens/bridge/hooks/useBridgeTransfer.ts new file mode 100644 index 0000000000..555287c80f --- /dev/null +++ b/packages/core-mobile/app/screens/bridge/hooks/useBridgeTransfer.ts @@ -0,0 +1,85 @@ +import { BridgeAsset } from '@avalabs/bridge-unified' +import { Network } from '@avalabs/core-chains-sdk' +import { useInAppRequest } from 'hooks/useInAppRequest' +import { useCallback } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { selectActiveAccount } from 'store/account' +import UnifiedBridgeService from 'services/bridge/UnifiedBridgeService' +import { setPendingTransfer } from 'store/unifiedBridge' +import AnalyticsService from 'services/analytics/AnalyticsService' +import { isUnifiedBridgeAsset } from '../utils/bridgeUtils' + +export const useBridgeTransfer = ({ + amount, + bridgeAsset, + sourceNetwork, + targetNetwork +}: { + amount: bigint + bridgeAsset: BridgeAsset | undefined + sourceNetwork: Network | undefined + targetNetwork: Network | undefined +}): (() => Promise) => { + const activeAccount = useSelector(selectActiveAccount) + const dispatch = useDispatch() + const { request } = useInAppRequest() + + return useCallback(async () => { + if (!bridgeAsset) { + throw new Error('No asset chosen') + } + + if (!isUnifiedBridgeAsset(bridgeAsset)) { + throw new Error('Asset is not supported ') + } + + if (!sourceNetwork) { + throw new Error('Invalid source network') + } + + if (!targetNetwork) { + throw new Error('Invalid target network') + } + + if (!activeAccount) { + throw new Error('No active account') + } + + const pendingTransfer = await UnifiedBridgeService.transfer({ + asset: bridgeAsset, + amount, + targetNetwork, + sourceNetwork, + activeAccount, + updateListener: updatedTransfer => { + dispatch(setPendingTransfer(updatedTransfer)) + }, + request + }) + + AnalyticsService.capture('UnifedBridgeTransferStarted', { + bridgeType: 'CCTP', + activeChainId: sourceNetwork.chainId, + targetChainId: targetNetwork.chainId + }) + + AnalyticsService.captureWithEncryption('BridgeTransactionStarted', { + chainId: sourceNetwork.chainId, + sourceTxHash: pendingTransfer.sourceTxHash, + fromAddress: pendingTransfer.fromAddress, + toAddress: pendingTransfer.toAddress + }) + + dispatch(setPendingTransfer(pendingTransfer)) + + return pendingTransfer.sourceTxHash + }, [ + bridgeAsset, + targetNetwork, + activeAccount, + amount, + sourceNetwork, + dispatch, + request + ]) +} diff --git a/packages/core-mobile/app/screens/bridge/hooks/useBridgeType.ts b/packages/core-mobile/app/screens/bridge/hooks/useBridgeType.ts new file mode 100644 index 0000000000..0a85bd8c43 --- /dev/null +++ b/packages/core-mobile/app/screens/bridge/hooks/useBridgeType.ts @@ -0,0 +1,16 @@ +import { BridgeAsset, BridgeType } from '@avalabs/bridge-unified' +import { useMemo } from 'react' +import { getEvmCaip2ChainId } from 'temp/caip2ChainIds' + +export const useBridgeType = ( + bridgeAsset: BridgeAsset | undefined, + targetChainId: number | undefined +): BridgeType | undefined => { + return useMemo(() => { + if (!bridgeAsset || !targetChainId) { + return undefined + } + + return bridgeAsset.destinations[getEvmCaip2ChainId(targetChainId)]?.[0] + }, [bridgeAsset, targetChainId]) +} diff --git a/packages/core-mobile/app/screens/bridge/hooks/useBtcBridge.ts b/packages/core-mobile/app/screens/bridge/hooks/useBtcBridge.ts deleted file mode 100644 index a9cb5d4327..0000000000 --- a/packages/core-mobile/app/screens/bridge/hooks/useBtcBridge.ts +++ /dev/null @@ -1,231 +0,0 @@ -import Big from 'big.js' -import { BridgeAdapter } from 'screens/bridge/hooks/useBridge' -import { - BIG_ZERO, - BitcoinConfigAsset, - Blockchain, - btcToSatoshi, - getBtcAsset, - satoshiToBtc, - useBridgeSDK -} from '@avalabs/core-bridge-sdk' -import { useBridgeContext } from 'contexts/BridgeContext' -import { useCallback, useEffect, useMemo, useState } from 'react' -import { getBtcBalance } from 'screens/bridge/hooks/getBtcBalance' -import { AssetBalance } from 'screens/bridge/utils/types' -import { - BitcoinInputUTXOWithOptionalScript, - getMaxTransferAmount -} from '@avalabs/core-wallets-sdk' -import { useSelector } from 'react-redux' -import { selectTokensWithBalanceByNetwork } from 'store/balance/slice' -import { selectSelectedCurrency } from 'store/settings/currency' -import { VsCurrencyType } from '@avalabs/core-coingecko-sdk' -import Logger from 'utils/Logger' -import { selectBridgeAppConfig } from 'store/bridge' -import { selectActiveAccount } from 'store/account' -import { getBitcoinNetwork } from 'services/network/utils/providerUtils' -import AnalyticsService from 'services/analytics/AnalyticsService' -import { useNetworks } from 'hooks/networks/useNetworks' -import { useNetworkFee } from 'hooks/useNetworkFee' -import useCChainNetwork from 'hooks/earn/useCChainNetwork' -import { useTransferAssetBTC } from './useTransferAssetBTC' - -// eslint-disable-next-line sonarjs/cognitive-complexity -export function useBtcBridge({ - amount, - bridgeFee, - minimum -}: { - amount: Big - bridgeFee: Big - minimum: Big -}): BridgeAdapter { - const { activeNetwork } = useNetworks() - const activeAccount = useSelector(selectActiveAccount) - const currency = useSelector(selectSelectedCurrency) - const bridgeConfig = useSelector(selectBridgeAppConfig) - const { createBridgeTransaction } = useBridgeContext() - const { transfer: transferBTC } = useTransferAssetBTC() - const { currentAsset, currentBlockchain, targetBlockchain } = useBridgeSDK() - const btcAddress = activeAccount?.addressBTC - const avalancheNetwork = useCChainNetwork() - const avalancheTokens = useSelector( - selectTokensWithBalanceByNetwork(avalancheNetwork) - ) - const isBitcoinBridge = getIsBitcoinBridge( - currentBlockchain, - targetBlockchain - ) - const [btcBalance, setBtcBalance] = useState() - const [btcBalanceAvalanche, setBtcBalanceAvalanche] = useState() - const [utxos, setUtxos] = useState() - const loading = !btcBalance || !btcBalanceAvalanche - - const { data: networkFee } = useNetworkFee(activeNetwork) - - const [feeRate, setFeeRate] = useState(0) - - const amountInSatoshis = btcToSatoshi(amount) - const btcAsset = bridgeConfig && getBtcAsset(bridgeConfig) - const assetsWithBalances = getBtcAssetWithBalances(btcAsset, btcBalance) - - const maximum = useMemo(() => { - if (!bridgeConfig || !activeAccount) return Big(0) - const maxAmt = getMaxTransferAmount( - utxos ?? [], - bridgeConfig.criticalBitcoin.walletAddresses.btc, - activeAccount.addressBTC, - feeRate - ) - return satoshiToBtc(maxAmt) - }, [utxos, bridgeConfig, feeRate, activeAccount]) - - const receiveAmount = amount.gt(minimum) ? amount.minus(bridgeFee) : BIG_ZERO - - // setting feeRate to lowest network fee to calculate max amount - useEffect(() => { - if (!networkFee) return - - setFeeRate(Number(networkFee.low.maxFeePerGas.toSubUnit())) - }, [networkFee]) - - useEffect(() => { - async function loadBalances(): Promise { - if (isBitcoinBridge && btcAsset && btcAddress) { - const token = await getBtcBalance( - !activeNetwork.isTestnet, - btcAddress, - currency as VsCurrencyType - ) - - if (token) { - setUtxos(token.utxos) - setBtcBalance({ - symbol: btcAsset.symbol, - asset: btcAsset, - balance: satoshiToBtc(Number(token.balance)), - logoUri: token.logoUri, - priceInCurrency: token.priceInCurrency - }) - } - - const btcAvalancheBalance = avalancheTokens.find( - tk => tk.symbol === 'BTC.b' - ) - - setBtcBalanceAvalanche({ - symbol: btcAsset.symbol, - asset: btcAsset, - balance: satoshiToBtc(Number(btcAvalancheBalance?.balance ?? 0)), - logoUri: btcAvalancheBalance?.logoUri, - priceInCurrency: btcAvalancheBalance?.priceInCurrency - }) - } - } - - loadBalances().catch(Logger.error) - }, [ - btcAddress, - isBitcoinBridge, - activeNetwork, - avalancheTokens, - btcAsset, - currency - ]) - - const transfer = useCallback(async () => { - if (!btcAddress) return Promise.reject('Invalid address') - - if (!activeNetwork) return Promise.reject('Network not found') - - if (!bridgeConfig) return Promise.reject('Bridge config not found') - - if (!isBitcoinBridge) return Promise.reject('Invalid bridge') - - if (!amountInSatoshis || amountInSatoshis === 0) - return Promise.reject('Invalid amount') - - if (!feeRate || feeRate === 0) return Promise.reject('Invalid fee rate') - - const bitcoinNetwork = getBitcoinNetwork(activeNetwork.isTestnet) - - const timestamp = Date.now() - const symbol = currentAsset || '' - - const transactionHash = await transferBTC({ - amount: amountInSatoshis, - feeRate - }) - - if (!transactionHash) return Promise.reject('Failed to transfer') - - AnalyticsService.captureWithEncryption('BridgeTransactionStarted', { - chainId: bitcoinNetwork.chainId, - sourceTxHash: transactionHash, - fromAddress: btcAddress - }) - - createBridgeTransaction( - { - sourceChain: Blockchain.BITCOIN, - sourceTxHash: transactionHash, - sourceStartedAt: timestamp, - targetChain: Blockchain.AVALANCHE, - amount, - symbol - }, - activeNetwork - ).catch(Logger.error) - - return transactionHash - }, [ - btcAddress, - activeNetwork, - bridgeConfig, - isBitcoinBridge, - amountInSatoshis, - feeRate, - currentAsset, - transferBTC, - createBridgeTransaction, - amount - ]) - - return { - address: btcAddress, - sourceBalance: btcBalance, - targetBalance: btcBalanceAvalanche, - assetsWithBalances, - loading, - receiveAmount, - maximum, - transfer - } -} - -const getIsBitcoinBridge = ( - currentBlockchain: Blockchain, - targetBlockchain: Blockchain -): boolean => { - return ( - currentBlockchain === Blockchain.BITCOIN || - targetBlockchain === Blockchain.BITCOIN - ) -} - -const getBtcAssetWithBalances = ( - btcAsset?: BitcoinConfigAsset, - btcBalance?: AssetBalance -): AssetBalance[] => { - if (!btcAsset) { - return [] - } - return [ - { - symbol: btcAsset.symbol, - asset: btcAsset, - balance: btcBalance?.balance - } - ] -} diff --git a/packages/core-mobile/app/screens/bridge/hooks/useEthBridge.ts b/packages/core-mobile/app/screens/bridge/hooks/useEthBridge.ts deleted file mode 100644 index dfc9caa1e2..0000000000 --- a/packages/core-mobile/app/screens/bridge/hooks/useEthBridge.ts +++ /dev/null @@ -1,122 +0,0 @@ -import Big from 'big.js' -import { - BIG_ZERO, - Blockchain, - isNativeAsset, - useBridgeSDK, - useMaxTransferAmount, - WrapStatus -} from '@avalabs/core-bridge-sdk' -import { BridgeAdapter } from 'screens/bridge/hooks/useBridge' -import { useBridgeContext } from 'contexts/BridgeContext' -import { useAssetBalancesEVM } from 'screens/bridge/hooks/useAssetBalancesEVM' -import { useCallback, useMemo, useState } from 'react' -import { useSelector } from 'react-redux' -import { selectActiveAccount } from 'store/account' -import { useEthereumProvider } from 'hooks/networks/networkProviderHooks' -import AnalyticsService from 'services/analytics/AnalyticsService' -import { useNetworks } from 'hooks/networks/useNetworks' -import Logger from 'utils/Logger' -import { noop } from '@avalabs/core-utils-sdk' -import { useTransferAssetEVM } from './useTransferAssetEVM' - -/** - * Hook for transferring assets from Ethereum to Avalanche (wrapping) - */ -export function useEthBridge({ - amount, - bridgeFee, - minimum -}: { - amount: Big - bridgeFee: Big - minimum: Big -}): BridgeAdapter { - const { currentAssetData } = useBridgeSDK() - - const { createBridgeTransaction } = useBridgeContext() - const { transfer: transferEVM } = useTransferAssetEVM() - const { assetsWithBalances, loading } = useAssetBalancesEVM( - Blockchain.ETHEREUM - ) - - const sourceBalance = useMemo( - () => - assetsWithBalances.find( - ({ asset }) => asset.symbol === currentAssetData?.symbol - ), - [assetsWithBalances, currentAssetData?.symbol] - ) - - const { activeNetwork } = useNetworks() - const activeAccount = useSelector(selectActiveAccount) - const ethereumProvider = useEthereumProvider() - const [wrapStatus, setWrapStatus] = useState(WrapStatus.INITIAL) - - const maximum = - useMaxTransferAmount( - sourceBalance?.balance, - activeAccount?.addressC, - ethereumProvider - ) || undefined - - const receiveAmount = amount.gt(minimum) ? amount.minus(bridgeFee) : BIG_ZERO - - const transfer = useCallback(async () => { - if (!activeAccount) return Promise.reject('Active account not found') - - if (!currentAssetData) return Promise.reject('Asset not found') - - const timestamp = Date.now() - const symbol = isNativeAsset(currentAssetData) - ? currentAssetData.wrappedAssetSymbol - : currentAssetData.symbol - - // this transfer is part of the Bridge context - const transactionHash = await transferEVM({ - amount, - asset: currentAssetData, - onStatusChange: setWrapStatus, - onTxHashChange: noop - }) - - if (!transactionHash) return Promise.reject('Failed to transfer') - - AnalyticsService.captureWithEncryption('BridgeTransactionStarted', { - chainId: activeNetwork.chainId, - sourceTxHash: transactionHash, - fromAddress: activeAccount?.addressC - }) - - createBridgeTransaction( - { - sourceChain: Blockchain.ETHEREUM, - sourceTxHash: transactionHash, - sourceStartedAt: timestamp, - targetChain: Blockchain.AVALANCHE, - amount, - symbol - }, - activeNetwork - ).catch(Logger.error) - - return transactionHash - }, [ - activeAccount, - currentAssetData, - transferEVM, - amount, - activeNetwork, - createBridgeTransaction - ]) - - return { - sourceBalance, - assetsWithBalances, - loading, - receiveAmount, - maximum, - wrapStatus, - transfer - } -} diff --git a/packages/core-mobile/app/screens/bridge/hooks/useGetBridgeFees.ts b/packages/core-mobile/app/screens/bridge/hooks/useGetBridgeFees.ts new file mode 100644 index 0000000000..e95437964f --- /dev/null +++ b/packages/core-mobile/app/screens/bridge/hooks/useGetBridgeFees.ts @@ -0,0 +1,95 @@ +import { BridgeAsset } from '@avalabs/bridge-unified' +import { Network } from '@avalabs/core-chains-sdk' +import { useNetworkFee } from 'hooks/useNetworkFee' +import { useCallback } from 'react' +import { useSelector } from 'react-redux' +import UnifiedBridgeService from 'services/bridge/UnifiedBridgeService' +import { selectActiveAccount } from 'store/account' + +export const useGetBridgeFees = ({ + amount, + bridgeAsset, + sourceNetwork, + targetNetwork +}: { + amount: bigint + bridgeAsset: BridgeAsset | undefined + sourceNetwork: Network | undefined + targetNetwork: Network | undefined +}): { + getBridgeFee: () => Promise + getNetworkFee: () => Promise +} => { + const { data: networkFeeRate } = useNetworkFee() + const activeAccount = useSelector(selectActiveAccount) + + const getBridgeFee = useCallback(async () => { + if (!bridgeAsset || !targetNetwork || !sourceNetwork || amount === 0n) { + return undefined + } + + return await UnifiedBridgeService.getFee({ + asset: bridgeAsset, + amount, + sourceNetwork, + targetNetwork + }) + }, [amount, bridgeAsset, sourceNetwork, targetNetwork]) + + const getNetworkFee = useCallback(async () => { + if ( + !networkFeeRate || + !activeAccount || + !bridgeAsset || + !sourceNetwork || + amount === 0n + ) + return + + const gasLimit = await UnifiedBridgeService.estimateGas({ + asset: bridgeAsset, + amount, + activeAccount, + sourceNetwork, + targetNetwork + }) + + if (gasLimit) { + return networkFeeRate.low.maxFeePerGas.mul(gasLimit).toSubUnit() + } + }, [ + activeAccount, + amount, + bridgeAsset, + networkFeeRate, + sourceNetwork, + targetNetwork + ]) + + return { getBridgeFee, getNetworkFee } +} + +export const useGetMinimumTransferAmount = ({ + amount, + bridgeAsset, + sourceNetwork, + targetNetwork +}: { + amount: bigint + bridgeAsset: BridgeAsset | undefined + sourceNetwork: Network | undefined + targetNetwork: Network | undefined +}): (() => Promise) => { + return useCallback(async () => { + if (!bridgeAsset || !targetNetwork || !sourceNetwork) { + return undefined + } + + return await UnifiedBridgeService.getMinimumTransferAmount({ + asset: bridgeAsset, + amount, + sourceNetwork, + targetNetwork + }) + }, [amount, bridgeAsset, sourceNetwork, targetNetwork]) +} diff --git a/packages/core-mobile/app/screens/bridge/hooks/usePendingBridgeTransactions.ts b/packages/core-mobile/app/screens/bridge/hooks/usePendingBridgeTransactions.ts index b139ae7efe..0a811613a2 100644 --- a/packages/core-mobile/app/screens/bridge/hooks/usePendingBridgeTransactions.ts +++ b/packages/core-mobile/app/screens/bridge/hooks/usePendingBridgeTransactions.ts @@ -1,47 +1,13 @@ -import { BridgeTransaction } from '@avalabs/core-bridge-sdk' import { BridgeTransfer } from '@avalabs/bridge-unified' import { Network } from '@avalabs/core-chains-sdk' import { useMemo } from 'react' import { useSelector } from 'react-redux' -import { isAvalancheNetwork } from 'services/network/utils/isAvalancheNetwork' -import { isEthereumNetwork } from 'services/network/utils/isEthereumNetwork' -import { selectBridgeTransactions } from 'store/bridge' import { selectPendingTransfers } from 'store/unifiedBridge/slice' import { caipToChainId } from 'utils/data/caip' -import { isBitcoinNetwork } from 'utils/network/isBitcoinNetwork' -const usePendingLegacyBridgeTransactions = ( - network?: Network -): BridgeTransaction[] => { - const pendingBridgeByTxId = useSelector(selectBridgeTransactions) - - return useMemo(() => { - if (!network) { - return Object.values(pendingBridgeByTxId) - } - - const networkNameToCheck = isBitcoinNetwork(network) - ? BridgeNetwork.BITCOIN - : isAvalancheNetwork(network) - ? BridgeNetwork.AVALANCHE - : isEthereumNetwork(network) - ? BridgeNetwork.ETHEREUM - : null - - return [ - ...Object.values(pendingBridgeByTxId).filter( - tx => - (tx.sourceChain.valueOf() === networkNameToCheck || - tx.targetChain.valueOf() === networkNameToCheck) && - tx.environment === (network.isTestnet ? 'test' : 'main') - ) - ] - }, [network, pendingBridgeByTxId]) -} - -const usePendingUnifiedBridgeTransactions = ( +const usePendingBridgeTransactions = ( network?: Network -): BridgeTransfer[] => { +): Array => { const pendingTransfer = useSelector(selectPendingTransfers) return useMemo(() => { @@ -56,24 +22,4 @@ const usePendingUnifiedBridgeTransactions = ( }, [pendingTransfer, network?.chainId]) } -const usePendingBridgeTransactions = ( - network?: Network -): Array => { - const legacyBridgeTransfers = usePendingLegacyBridgeTransactions(network) - const unifiedBridgeTransfers = usePendingUnifiedBridgeTransactions(network) - - return useMemo(() => { - return [ - ...Object.values(legacyBridgeTransfers), - ...Object.values(unifiedBridgeTransfers) - ] - }, [unifiedBridgeTransfers, legacyBridgeTransfers]) -} - -enum BridgeNetwork { - AVALANCHE = 'avalanche', - BITCOIN = 'bitcoin', - ETHEREUM = 'ethereum' -} - export default usePendingBridgeTransactions diff --git a/packages/core-mobile/app/screens/bridge/hooks/useTokenForBridgeTransaction.ts b/packages/core-mobile/app/screens/bridge/hooks/useTokenForBridgeTransaction.ts index 08b7cdac82..e0cd479fea 100644 --- a/packages/core-mobile/app/screens/bridge/hooks/useTokenForBridgeTransaction.ts +++ b/packages/core-mobile/app/screens/bridge/hooks/useTokenForBridgeTransaction.ts @@ -1,4 +1,3 @@ -import { Blockchain, BridgeTransaction } from '@avalabs/core-bridge-sdk' import { BridgeTransfer } from '@avalabs/bridge-unified' import { BITCOIN_NETWORK, @@ -9,37 +8,38 @@ import { NetworkContractToken } from '@avalabs/vm-module-types' import { useNetworkContractTokens } from 'hooks/networks/useNetworkContractTokens' import { useNetworks } from 'hooks/networks/useNetworks' import { useMemo } from 'react' -import { isUnifiedBridgeTransfer } from '../utils/bridgeUtils' +import { isBitcoinChainId } from 'utils/network/isBitcoinNetwork' +import { getChainIdFromCaip2 } from 'temp/caip2ChainIds' +import { isEthereumChainId } from 'services/network/utils/isEthereumNetwork' export function useTokenForBridgeTransaction( - bridgeTransaction: BridgeTransaction | BridgeTransfer | undefined, + bridgeTransaction: BridgeTransfer | undefined, isTestnet: boolean ): NetworkContractToken | NetworkToken | undefined { const chainId = useMemo(() => { - switch (bridgeTransaction?.sourceChain) { - case Blockchain.BITCOIN: - return isTestnet ? ChainId.BITCOIN_TESTNET : ChainId.BITCOIN - case Blockchain.ETHEREUM: - // ETHEREUM_SEPOLIA doesn't have contract tokens, so always use ETHEREUM_HOMESTEAD chainid. - return ChainId.ETHEREUM_HOMESTEAD - case Blockchain.AVALANCHE: - default: - return isTestnet - ? ChainId.AVALANCHE_TESTNET_ID - : ChainId.AVALANCHE_MAINNET_ID + const sourceChainId = bridgeTransaction?.sourceChain.chainId + ? getChainIdFromCaip2(bridgeTransaction.sourceChain.chainId) + : undefined + + if (!sourceChainId) return undefined + + if (isBitcoinChainId(sourceChainId)) { + return isTestnet ? ChainId.BITCOIN_TESTNET : ChainId.BITCOIN + } + + if (isEthereumChainId(sourceChainId)) { + return ChainId.ETHEREUM_HOMESTEAD } + + return isTestnet + ? ChainId.AVALANCHE_TESTNET_ID + : ChainId.AVALANCHE_MAINNET_ID }, [bridgeTransaction, isTestnet]) const { getNetwork } = useNetworks() const network = getNetwork(chainId) - if (!network) { - throw new Error(`Network not found for chainId: ${chainId}`) - } - const tokens = useNetworkContractTokens(network) - const symbol = isUnifiedBridgeTransfer(bridgeTransaction) - ? bridgeTransaction.asset.symbol - : bridgeTransaction?.symbol + const symbol = bridgeTransaction?.asset.symbol return useMemo(() => { const token = tokens.find(t => t.symbol === symbol) @@ -48,8 +48,14 @@ export function useTokenForBridgeTransaction( if (symbol === BITCOIN_NETWORK.networkToken.symbol) { return BITCOIN_NETWORK.networkToken + } else if ( + chainId && + isEthereumChainId(chainId) && + network?.networkToken.symbol === symbol + ) { + return network?.networkToken } return undefined - }, [tokens, symbol]) + }, [tokens, symbol, network, chainId]) } diff --git a/packages/core-mobile/app/screens/bridge/hooks/useTransferAssetBTC.ts b/packages/core-mobile/app/screens/bridge/hooks/useTransferAssetBTC.ts deleted file mode 100644 index f34d8cf127..0000000000 --- a/packages/core-mobile/app/screens/bridge/hooks/useTransferAssetBTC.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { useSelector } from 'react-redux' -import { useCallback } from 'react' -import { Blockchain, useBridgeSDK } from '@avalabs/core-bridge-sdk' -import { selectBridgeAppConfig } from 'store/bridge' -import { useInAppRequest } from 'hooks/useInAppRequest' -import BridgeService from 'services/bridge/BridgeService' -import { selectIsDeveloperMode } from 'store/settings/advanced/slice' -import { selectActiveAccount } from 'store/account/slice' - -type TransferParams = { - amount: number - feeRate: number -} - -/** - * Hook for transferring assets from Bitcoin to Avalanche - */ -export function useTransferAssetBTC(): { - transfer: ({ amount, feeRate }: TransferParams) => Promise -} { - const config = useSelector(selectBridgeAppConfig) - const { currentBlockchain } = useBridgeSDK() - const isDeveloperMode = useSelector(selectIsDeveloperMode) - const activeAccount = useSelector(selectActiveAccount) - const { request } = useInAppRequest() - - const transfer = useCallback( - async ({ amount, feeRate }: TransferParams) => { - if (currentBlockchain !== Blockchain.BITCOIN) { - return Promise.reject('Invalid blockchain') - } - - if (!config) { - return Promise.reject('Wallet not ready') - } - - if (!activeAccount) { - return Promise.reject('No active account') - } - - return BridgeService.transferBTC({ - fromAccount: activeAccount.addressBTC, - amount, - feeRate, - config, - isMainnet: !isDeveloperMode, - request - }) - }, - [currentBlockchain, config, activeAccount, isDeveloperMode, request] - ) - - return { - transfer - } -} diff --git a/packages/core-mobile/app/screens/bridge/hooks/useTransferAssetEVM.ts b/packages/core-mobile/app/screens/bridge/hooks/useTransferAssetEVM.ts deleted file mode 100644 index 76ad577e4b..0000000000 --- a/packages/core-mobile/app/screens/bridge/hooks/useTransferAssetEVM.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { - Asset, - Blockchain, - useBridgeSDK, - WrapStatus -} from '@avalabs/core-bridge-sdk' -import Big from 'big.js' -import { useSelector } from 'react-redux' -import { selectActiveAccount } from 'store/account' -import { useCallback } from 'react' -import { selectBridgeAppConfig } from 'store/bridge' -import { useNetworks } from 'hooks/networks/useNetworks' -import { useInAppRequest } from 'hooks/useInAppRequest' -import BridgeService from 'services/bridge/BridgeService' -import { selectIsDeveloperMode } from 'store/settings/advanced/slice' - -type TransferParams = { - amount: Big - asset: Asset - onStatusChange: (status: WrapStatus) => void - onTxHashChange: (txHash: string) => void -} - -/** - * Hook for transferring assets between Avalanche and Ethereum - */ -export function useTransferAssetEVM(): { - transfer: ({ - amount, - asset, - onStatusChange, - onTxHashChange - }: TransferParams) => Promise -} { - const { networks } = useNetworks() - const activeAccount = useSelector(selectActiveAccount) - const config = useSelector(selectBridgeAppConfig) - const isDeveloperMode = useSelector(selectIsDeveloperMode) - const { currentBlockchain } = useBridgeSDK() - const { request } = useInAppRequest() - - const transfer = useCallback( - async ({ - amount, - asset, - onStatusChange, - onTxHashChange - }: TransferParams) => { - if ( - currentBlockchain !== Blockchain.ETHEREUM && - currentBlockchain !== Blockchain.AVALANCHE - ) { - return Promise.reject('Invalid blockchain') - } - - if (!config || !activeAccount) { - return Promise.reject('Wallet not ready') - } - - return BridgeService.transferEVM({ - currentBlockchain, - amount: amount.toString(), - asset, - config, - activeAccount, - allNetworks: networks, - isTestnet: isDeveloperMode, - onStatusChange, - onTxHashChange, - request - }) - }, - [ - currentBlockchain, - config, - activeAccount, - isDeveloperMode, - networks, - request - ] - ) - - return { - transfer - } -} diff --git a/packages/core-mobile/app/screens/bridge/hooks/useUnifiedBridge/useUnifiedBridge.ts b/packages/core-mobile/app/screens/bridge/hooks/useUnifiedBridge/useUnifiedBridge.ts deleted file mode 100644 index e2de1547b2..0000000000 --- a/packages/core-mobile/app/screens/bridge/hooks/useUnifiedBridge/useUnifiedBridge.ts +++ /dev/null @@ -1,187 +0,0 @@ -import Big from 'big.js' -import { BIG_ZERO, bigToBigInt, bigintToBig } from '@avalabs/core-utils-sdk' -import { useCallback, useEffect, useMemo, useState } from 'react' -import { Blockchain } from '@avalabs/core-bridge-sdk' -import { useDispatch, useSelector } from 'react-redux' -import UnifiedBridgeService from 'services/bridge/UnifiedBridgeService' -import Logger from 'utils/Logger' -import { selectActiveAccount } from 'store/account/slice' -import { setPendingTransfer } from 'store/unifiedBridge/slice' -import AnalyticsService from 'services/analytics/AnalyticsService' -import { useInAppRequest } from 'hooks/useInAppRequest' -import { useNetworksFromCaip2ChainIds } from 'temp/caip2ChainIds' -import { Network } from '@avalabs/core-chains-sdk' -import { isEthereumNetwork } from 'services/network/utils/isEthereumNetwork' -import { BridgeAsset } from '@avalabs/bridge-unified' -import { isUnifiedBridgeAsset } from '../../utils/bridgeUtils' -import { useUnifiedBridgeAssets } from '../useUnifiedBridgeAssets' -import { useAssetBalancesEVM } from '../useAssetBalancesEVM' -import { BridgeAdapter } from '../useBridge' -import { getSourceBalance } from './utils' -interface UnifiedBridge extends BridgeAdapter { - isAssetSupported: boolean -} - -/** - * Hook for when the Unified Bridge SDK can handle the transfer - */ -// eslint-disable-next-line sonarjs/cognitive-complexity -export const useUnifiedBridge = (amount: Big): UnifiedBridge => { - const { request } = useInAppRequest() - const dispatch = useDispatch() - const [sourceNetwork, setSourceNetwork] = useState() - const [targetNetwork, setTargetNetwork] = useState() - const [selectedBridgeAsset, setSelectedBridgeAsset] = useState() - const activeAccount = useSelector(selectActiveAccount) - const { bridgeAssets, chainAssetMap } = useUnifiedBridgeAssets() - const [bridgeError, setBridgeError] = useState() - - const [receiveAmount, setReceiveAmount] = useState() - const [minimum, setMinimum] = useState() - const [bridgeFee, setBridgeFee] = useState() - - const { assetsWithBalances, loading } = useAssetBalancesEVM( - sourceNetwork && isEthereumNetwork(sourceNetwork) - ? Blockchain.ETHEREUM - : Blockchain.AVALANCHE - ) - - const sourceBalance = useMemo( - () => getSourceBalance(selectedBridgeAsset, assetsWithBalances), - [selectedBridgeAsset, assetsWithBalances] - ) - - const sourceNetworks = useNetworksFromCaip2ChainIds( - Object.keys(chainAssetMap ?? []) - ) - - const targetNetworks = useNetworksFromCaip2ChainIds( - Object.keys(selectedBridgeAsset?.destinations ?? []) - ) - - useEffect(() => { - const getFee = async (): Promise => { - const hasAmount = amount && !amount.eq(BIG_ZERO) - - if (selectedBridgeAsset && hasAmount && targetNetwork && sourceNetwork) { - const fee = await UnifiedBridgeService.getFee({ - asset: selectedBridgeAsset, - amount: bigToBigInt(amount, selectedBridgeAsset.decimals), - sourceNetwork: sourceNetwork, - targetNetwork: targetNetwork - }) - - const feeBig = bigintToBig(fee, selectedBridgeAsset.decimals) - - setBridgeFee(feeBig) - setMinimum(feeBig) - setReceiveAmount(amount.sub(feeBig)) - } - } - - getFee().catch(error => { - Logger.error(error) - setBridgeError(error) - }) - }, [amount, sourceBalance, selectedBridgeAsset, targetNetwork, sourceNetwork]) - - const transfer = useCallback(async () => { - if (!selectedBridgeAsset) { - throw new Error('No asset chosen') - } - - if (!isUnifiedBridgeAsset(selectedBridgeAsset)) { - throw new Error('Asset is not supported ') - } - - if (!sourceNetwork) { - throw new Error('Invalid source network') - } - - if (!targetNetwork) { - throw new Error('Invalid target network') - } - - if (!activeAccount) { - throw new Error('No active account') - } - - const pendingTransfer = await UnifiedBridgeService.transfer({ - asset: selectedBridgeAsset, - amount: bigToBigInt(amount, selectedBridgeAsset.decimals), - targetNetwork, - sourceNetwork, - activeAccount, - updateListener: updatedTransfer => { - dispatch(setPendingTransfer(updatedTransfer)) - }, - request - }) - - AnalyticsService.capture('UnifedBridgeTransferStarted', { - bridgeType: 'CCTP', - activeChainId: sourceNetwork.chainId, - targetChainId: targetNetwork.chainId - }) - - AnalyticsService.captureWithEncryption('BridgeTransactionStarted', { - chainId: sourceNetwork.chainId, - sourceTxHash: pendingTransfer.sourceTxHash, - fromAddress: pendingTransfer.fromAddress, - toAddress: pendingTransfer.toAddress - }) - - dispatch(setPendingTransfer(pendingTransfer)) - - return pendingTransfer.sourceTxHash - }, [ - selectedBridgeAsset, - targetNetwork, - activeAccount, - amount, - sourceNetwork, - dispatch, - request - ]) - - useEffect(() => { - if (!selectedBridgeAsset) { - setSelectedBridgeAsset(bridgeAssets[0]) - } - }, [selectedBridgeAsset, bridgeAssets]) - - useEffect(() => { - if (targetNetworks.length === 0) { - return - } - - if ( - !targetNetwork || - !targetNetworks.find(network => network.chainId === targetNetwork.chainId) - ) { - setTargetNetwork(targetNetworks[0]) - } - }, [targetNetworks, targetNetwork]) - - return { - sourceBalance, - loading, - assetsWithBalances, - receiveAmount, - bridgeFee, - maximum: sourceBalance?.balance, - minimum, - transfer, - isAssetSupported: true, - sourceNetworks, - targetNetworks, - sourceNetwork, - setSourceNetwork, - targetNetwork, - setTargetNetwork, - bridgeAssets, - selectedBridgeAsset, - setSelectedBridgeAsset, - error: bridgeError - } -} diff --git a/packages/core-mobile/app/screens/bridge/hooks/useUnifiedBridge/utils.ts b/packages/core-mobile/app/screens/bridge/hooks/useUnifiedBridge/utils.ts deleted file mode 100644 index 317cf998fb..0000000000 --- a/packages/core-mobile/app/screens/bridge/hooks/useUnifiedBridge/utils.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Blockchain } from '@avalabs/core-bridge-sdk' -import { chainIdToCaip } from 'utils/data/caip' -import { ChainId } from '@avalabs/core-chains-sdk' -import { - BridgeAsset, - isErc20Asset, - isNativeAsset -} from '@avalabs/bridge-unified' -import { isUnifiedBridgeAsset } from '../../utils/bridgeUtils' -import { AssetBalance } from '../../utils/types' - -export const getTargetChainId = ( - isDeveloperMode: boolean, - targetBlockchain: Blockchain -): number => { - switch (targetBlockchain) { - case Blockchain.AVALANCHE: - return isDeveloperMode - ? ChainId.AVALANCHE_TESTNET_ID - : ChainId.AVALANCHE_MAINNET_ID - - case Blockchain.BITCOIN: - return isDeveloperMode ? ChainId.BITCOIN_TESTNET : ChainId.BITCOIN - case Blockchain.ETHEREUM: - default: - // NOTE: this will only happen for Ethereum and is safe for now, - // since we're only using this piece of code for Unified Bridge (CCTP). - // Needs revisiting when we migrate Avalanche Bridge to @avalabs/bridge-unified package. - return isDeveloperMode - ? ChainId.ETHEREUM_TEST_SEPOLIA - : ChainId.ETHEREUM_HOMESTEAD - } -} - -export const getIsAssetSupported = ( - selectedAsset: AssetBalance | undefined, - assets: BridgeAsset[], - targetChainId: number -): boolean => { - if (!selectedAsset || !isUnifiedBridgeAsset(selectedAsset.asset)) return false - - if (isNativeAsset(selectedAsset.asset)) { - return true - } - - const lookupAddress = selectedAsset.asset.address ?? '' - - const asset = assets.find(_asset => { - return isErc20Asset(_asset) && lookupAddress === _asset.address - }) - - if (!asset) { - return false - } - - return chainIdToCaip(targetChainId) in asset.destinations -} - -export const getSourceBalance = ( - selectedAsset: BridgeAsset | undefined, - assetsWithBalances: AssetBalance[] -): AssetBalance | undefined => { - if (!selectedAsset) { - return undefined - } - - return assetsWithBalances.find(({ asset }) => { - return asset.symbol === selectedAsset.symbol - }) -} diff --git a/packages/core-mobile/app/screens/bridge/utils/bridgeUtils.ts b/packages/core-mobile/app/screens/bridge/utils/bridgeUtils.ts index af3375e994..5b70b156aa 100644 --- a/packages/core-mobile/app/screens/bridge/utils/bridgeUtils.ts +++ b/packages/core-mobile/app/screens/bridge/utils/bridgeUtils.ts @@ -1,4 +1,5 @@ import { + Asset, Blockchain, BridgeTransaction, CriticalConfig, @@ -12,6 +13,7 @@ import { Transaction as InternalTransaction, TxToken } from '@avalabs/vm-module-types' +import { AssetBalance } from './types' export const NULL_ADDRESS = '0x0000000000000000000000000000000000000000' @@ -82,8 +84,8 @@ export const isBridgeTransactionBTC = ( } export function isPendingBridgeTransaction( - item: Transaction | BridgeTransaction | BridgeTransfer -): item is BridgeTransaction | BridgeTransfer { + item: Transaction | BridgeTransfer +): item is BridgeTransfer { return 'addressBTC' in item || 'sourceChain' in item } @@ -166,3 +168,31 @@ export const getNativeTokenSymbol = (chain: Blockchain | Chain): string => { export function getOriginalSymbol(symbol: string): string { return symbol.replace(/\.(e|b|p)$/i, '') } + +export const getAssetBalance = ( + symbol: string | undefined, + assetsWithBalances: AssetBalance[] +): AssetBalance | undefined => { + if (!symbol) { + return undefined + } + + return assetsWithBalances.find(({ asset }) => { + return asset.symbol === symbol + }) +} + +export const getDenomination = (asset: BridgeAsset | Asset): number => + isUnifiedBridgeAsset(asset) ? asset.decimals : asset.denomination + +export const unwrapAssetSymbol = (symbol: string): string => { + if (symbol.endsWith('.e')) { + return symbol.slice(0, -2) // remove .e + } + + return symbol +} + +export const wrapAssetSymbol = (symbol: string): string => { + return `${symbol}.e` // add .e +} diff --git a/packages/core-mobile/app/screens/bridge/utils/types.ts b/packages/core-mobile/app/screens/bridge/utils/types.ts index 2945329246..ad0fe0b4fb 100644 --- a/packages/core-mobile/app/screens/bridge/utils/types.ts +++ b/packages/core-mobile/app/screens/bridge/utils/types.ts @@ -1,17 +1,11 @@ import { Asset } from '@avalabs/core-bridge-sdk' import { BridgeAsset } from '@avalabs/bridge-unified' -import Big from 'big.js' export interface AssetBalance { symbol: string asset: Asset | BridgeAsset - balance: Big | undefined + balance: bigint | undefined symbolOnNetwork?: string logoUri?: string priceInCurrency?: number } - -export enum BridgeProvider { - LEGACY = 'legacy', - UNIFIED = 'unified' -} diff --git a/packages/core-mobile/app/screens/shared/ActivityList/Transactions.tsx b/packages/core-mobile/app/screens/shared/ActivityList/Transactions.tsx index eaaae93ac7..6b7b00418a 100644 --- a/packages/core-mobile/app/screens/shared/ActivityList/Transactions.tsx +++ b/packages/core-mobile/app/screens/shared/ActivityList/Transactions.tsx @@ -14,7 +14,6 @@ import { BridgeTransactionStatusParams } from 'navigation/types' import useInAppBrowser from 'hooks/useInAppBrowser' import { Transaction } from 'store/transaction' import ZeroState from 'components/ZeroState' -import { BridgeTransaction } from '@avalabs/core-bridge-sdk' import { UI, useIsUIDisabled } from 'hooks/useIsUIDisabled' import { RefreshControl } from 'components/RefreshControl' import FlashList from 'components/FlashList' @@ -30,10 +29,10 @@ const BOTTOM_PADDING = SCREEN_WIDTH * 0.3 type Section = { title: string - data: Transaction[] | Array + data: Transaction[] | Array } -type Item = string | Transaction | BridgeTransaction | BridgeTransfer +type Item = string | Transaction | BridgeTransfer interface Props { isRefreshing: boolean @@ -110,9 +109,7 @@ const Transactions: FC = ({ return flatListData }, [bridgeDisabled, data, pendingBridgeTxs]) - const renderPendingBridgeTransaction = ( - tx: BridgeTransaction | BridgeTransfer - ): JSX.Element => { + const renderPendingBridgeTransaction = (tx: BridgeTransfer): JSX.Element => { return ( = ({ } const keyExtractor = ( - item: string | Transaction | BridgeTransaction | BridgeTransfer + item: string | Transaction | BridgeTransfer ): string => { if (typeof item === 'string') return item diff --git a/packages/core-mobile/app/screens/shared/BridgeTransactionStatus.tsx b/packages/core-mobile/app/screens/shared/BridgeTransactionStatus.tsx index 702e4c91be..043e97f4c7 100644 --- a/packages/core-mobile/app/screens/shared/BridgeTransactionStatus.tsx +++ b/packages/core-mobile/app/screens/shared/BridgeTransactionStatus.tsx @@ -2,7 +2,7 @@ import React, { FC, useEffect, useLayoutEffect, useState } from 'react' import { StyleSheet, View } from 'react-native' import { useApplicationContext } from 'contexts/ApplicationContext' import AvaText from 'components/AvaText' -import { Blockchain, BridgeTransaction } from '@avalabs/core-bridge-sdk' +import { Blockchain } from '@avalabs/core-bridge-sdk' import DotSVG from 'components/svg/DotSVG' import Avatar from 'components/Avatar' import AvaListItem from 'components/AvaListItem' @@ -14,11 +14,7 @@ import BridgeConfirmations from 'screens/bridge/components/BridgeConfirmations' import { VsCurrencyType } from '@avalabs/core-coingecko-sdk' import { useNavigation } from '@react-navigation/native' import Logger from 'utils/Logger' -import { - getBlockchainDisplayName, - getNativeTokenSymbol, - isUnifiedBridgeTransfer -} from 'screens/bridge/utils/bridgeUtils' +import { getNativeTokenSymbol } from 'screens/bridge/utils/bridgeUtils' import AvaButton from 'components/AvaButton' import AppNavigation from 'navigation/AppNavigation' import { useTokenForBridgeTransaction } from 'screens/bridge/hooks/useTokenForBridgeTransaction' @@ -37,9 +33,7 @@ type Props = { } const BridgeTransactionStatus: FC = ({ txHash, showHideButton }) => { - const [bridgeTransaction, setBridgeTransaction] = useState< - BridgeTransaction | BridgeTransfer - >() + const [bridgeTransaction, setBridgeTransaction] = useState() const { activeNetwork } = useNetworks() const tokenInfo = useTokenForBridgeTransaction( bridgeTransaction, @@ -52,9 +46,8 @@ const BridgeTransactionStatus: FC = ({ txHash, showHideButton }) => { const { selectedCurrency, currencyFormatter } = appHook const { navigate, getParent, dispatch, setOptions } = useNavigation() - const symbol = isUnifiedBridgeTransfer(bridgeTransaction) - ? bridgeTransaction.asset.symbol - : bridgeTransaction?.symbol + const symbol = bridgeTransaction?.asset.symbol + const coingeckoId = useCoinGeckoId(symbol) const assetPrice = useSimplePrice( @@ -132,18 +125,22 @@ const BridgeTransactionStatus: FC = ({ txHash, showHideButton }) => { [bridgeTransaction, isComplete, sourceCurrentConfirmations] ) - const tokenLogo = ( - - - + const renderTokenLogo = (): JSX.Element | undefined => { + if (!bridgeTransaction) return undefined + + return ( + + + + + - - - ) + ) + } const renderNetworkFeeRightComponent = (): React.JSX.Element => { if (sourceNetworkFee === undefined) { @@ -170,7 +167,7 @@ const BridgeTransactionStatus: FC = ({ txHash, showHideButton }) => { return ( - {tokenLogo} + {renderTokenLogo()} {bridgeTransaction && ( = ({ txHash, showHideButton }) => { - {isUnifiedBridgeTransfer(bridgeTransaction) - ? amount?.toNumber().toFixed(6) - : bridgeTransaction.amount.toNumber().toFixed(6)} + {amount?.toNumber().toFixed(6)} {' ' + symbol} - - {amount && assetPrice.mul(amount).toNumber()} - + {assetPrice !== undefined && ( + + {amount && assetPrice.mul(amount).toNumber()} + + )} } /> @@ -204,11 +201,11 @@ const BridgeTransactionStatus: FC = ({ txHash, showHideButton }) => { title={From} titleAlignment="flex-start" rightComponent={ - - {isUnifiedBridgeTransfer(bridgeTransaction) - ? humanize(bridgeTransaction.sourceChain.chainName) - : getBlockchainDisplayName(bridgeTransaction?.sourceChain)} - + bridgeTransaction && ( + + {humanize(bridgeTransaction.sourceChain.chainName)} + + ) } /> @@ -234,11 +231,11 @@ const BridgeTransactionStatus: FC = ({ txHash, showHideButton }) => { title={To} titleAlignment="flex-start" rightComponent={ - - {isUnifiedBridgeTransfer(bridgeTransaction) - ? humanize(bridgeTransaction.targetChain.chainName) - : getBlockchainDisplayName(bridgeTransaction?.targetChain)} - + bridgeTransaction && ( + + {humanize(bridgeTransaction.targetChain.chainName)} + + ) } /> diff --git a/packages/core-mobile/app/services/bridge/BridgeService.ts b/packages/core-mobile/app/services/bridge/BridgeService.ts index f837e454a5..dd4d14d129 100644 --- a/packages/core-mobile/app/services/bridge/BridgeService.ts +++ b/packages/core-mobile/app/services/bridge/BridgeService.ts @@ -1,14 +1,10 @@ import { AppConfig, Asset, - BitcoinConfigAsset, Blockchain, BridgeConfig, - btcToSatoshi, Environment, - estimateGas, fetchConfig, - getBtcTransactionDetails, setBridgeEnvironment, transferAssetBTC, TransferAssetBTCParams, @@ -16,15 +12,12 @@ import { TransferAssetEVMParams, WrapStatus } from '@avalabs/core-bridge-sdk' -import Big from 'big.js' import { Account } from 'store/account/types' -import { Network } from '@avalabs/core-chains-sdk' import { getAvalancheEvmProvider, getEthereumProvider } from 'services/network/utils/providerUtils' import { Networks } from 'store/network/types' -import { getBtcBalance } from 'screens/bridge/hooks/getBtcBalance' import { blockchainToNetwork } from 'screens/bridge/utils/bridgeUtils' import { Request } from 'store/rpc/utils/createInAppRequest' import { RpcMethod } from 'store/rpc/types' @@ -65,80 +58,6 @@ export class BridgeService { return fetchConfig() } - async estimateGas({ - currentBlockchain, - amount, - asset, - allNetworks, - isTestnet, - config, - activeNetwork, - activeAccount, - currency - }: { - currentBlockchain: Blockchain - amount: Big - asset: Asset - allNetworks: Networks - isTestnet: boolean - activeNetwork: Network - activeAccount?: Account - config?: AppConfig - currency: string - }): Promise { - if (!config) { - throw new Error('missing bridge config') - } - if (!activeAccount) { - throw new Error('no active account found') - } - - if (currentBlockchain === Blockchain.BITCOIN) { - if (btcToSatoshi(amount) === 0) { - throw new Error(`Amount can't be 0`) - } - const token = await getBtcBalance( - !activeNetwork.isTestnet, - activeAccount?.addressBTC, - currency - ) - - // Bitcoin's formula for fee is `transactionByteLength * feeRate`. - // By setting the feeRate here to 1, we'll receive the transaction's byte length, - // which is what we need to have the dynamic fee calculations in the UI. - // Think of the byteLength as gasLimit for EVM transactions. - const feeRate = 1 - const { fee: byteLength } = getBtcTransactionDetails( - config, - activeAccount.addressBTC, - token?.utxos ?? [], - btcToSatoshi(amount), - feeRate - ) - - return BigInt(byteLength) - } else { - const avalancheProvider = getAvalancheEvmProvider(allNetworks, isTestnet) - const ethereumProvider = getEthereumProvider(allNetworks, isTestnet) - - if (!avalancheProvider || !ethereumProvider) { - throw new Error('no providers available') - } - - return estimateGas( - amount, - activeAccount.addressC, - asset as Exclude, - { - ethereum: ethereumProvider, - avalanche: avalancheProvider - }, - config, - currentBlockchain - ) - } - } - async transferBTC({ fromAccount, amount, diff --git a/packages/core-mobile/app/services/bridge/UnifiedBridgeService.ts b/packages/core-mobile/app/services/bridge/UnifiedBridgeService.ts index 331ba19267..5241c3ffff 100644 --- a/packages/core-mobile/app/services/bridge/UnifiedBridgeService.ts +++ b/packages/core-mobile/app/services/bridge/UnifiedBridgeService.ts @@ -9,7 +9,8 @@ import { TokenType, Signer, getEnabledBridgeServices, - isErc20Asset + isErc20Asset, + isNativeAsset } from '@avalabs/bridge-unified' import { Network } from '@avalabs/core-chains-sdk' import { rpcErrors } from '@metamask/rpc-errors' @@ -18,10 +19,9 @@ import { Account } from 'store/account/types' import { isBitcoinNetwork } from 'utils/network/isBitcoinNetwork' import { assertNotUndefined } from 'utils/assertions' import Logger from 'utils/Logger' -import { bigToBigInt, noop } from '@avalabs/core-utils-sdk' +import { noop } from '@avalabs/core-utils-sdk' import { isUnifiedBridgeAsset } from 'screens/bridge/utils/bridgeUtils' import { Asset } from '@avalabs/core-bridge-sdk' -import Big from 'big.js' import { Request } from 'store/rpc/utils/createInAppRequest' import { RpcMethod } from 'store/rpc/types' import { TransactionParams } from '@avalabs/evm-module' @@ -98,7 +98,10 @@ export class UnifiedBridgeService { sourceChain: await this.buildChain(sourceNetwork) }) - if (isErc20Asset(asset) && asset.address) { + if (isNativeAsset(asset)) { + // todo: handle this properly + return 0n + } else if (isErc20Asset(asset) && asset.address) { const address = asset.address.toLowerCase() as `0x${string}` const fee = feeMap[address] @@ -193,7 +196,7 @@ export class UnifiedBridgeService { targetNetwork }: { asset: Asset | BridgeAsset - amount: Big + amount: bigint activeAccount: Account sourceNetwork: Network targetNetwork?: Network @@ -226,7 +229,29 @@ export class UnifiedBridgeService { return await this.service.estimateGas({ asset, fromAddress, - amount: bigToBigInt(amount, asset.decimals), + amount, + sourceChain, + targetChain + }) + } + + async getMinimumTransferAmount({ + asset, + amount, + sourceNetwork, + targetNetwork + }: { + asset: BridgeAsset + amount: bigint + sourceNetwork: Network + targetNetwork: Network + }): Promise { + const sourceChain = await this.buildChain(sourceNetwork) + const targetChain = await this.buildChain(targetNetwork) + + return this.service.getMinimumTransferAmount({ + asset, + amount, sourceChain, targetChain }) diff --git a/packages/core-mobile/app/store/bridge/slice.test.ts b/packages/core-mobile/app/store/bridge/slice.test.ts index a1b8b754b6..d75a5dd4e7 100644 --- a/packages/core-mobile/app/store/bridge/slice.test.ts +++ b/packages/core-mobile/app/store/bridge/slice.test.ts @@ -3,7 +3,6 @@ import Big from 'big.js' import { Blockchain, BridgeTransaction } from '@avalabs/core-bridge-sdk' import { assertNotUndefined } from 'utils/assertions' import { - addBridgeTransaction, bridgeReducer as reducer, popBridgeTransaction, setConfig @@ -76,47 +75,6 @@ describe('bridge - reducer', () => { }) }) - describe('addBridgeTransaction', () => { - it('should save new transaction', () => { - const currentState = { - bridgeTransactions: { - [bridgeTx1.sourceTxHash]: bridgeTx1 - }, - config: undefined - } - const state = reducer(currentState, addBridgeTransaction(bridgeTx2)) - - expect(state).toStrictEqual({ - bridgeTransactions: { - [bridgeTx1.sourceTxHash]: bridgeTx1, - [bridgeTx2.sourceTxHash]: bridgeTx2 - }, - config: undefined - }) - }) - - it('should update existing transaction', () => { - const newBridgeTx = { - ...bridgeTx1, - confirmationCount: 55 - } - const currentState = { - bridgeTransactions: { - [bridgeTx1.sourceTxHash]: bridgeTx1 - }, - config: undefined - } - const state = reducer(currentState, addBridgeTransaction(newBridgeTx)) - - expect(state).toStrictEqual({ - bridgeTransactions: { - [bridgeTx1.sourceTxHash]: newBridgeTx - }, - config: undefined - }) - }) - }) - describe('popBridgeTransaction', () => { it('should remove transaction if exists', () => { const currentState = { diff --git a/packages/core-mobile/app/store/bridge/slice.ts b/packages/core-mobile/app/store/bridge/slice.ts index 12ec91e4e1..cdde3652de 100644 --- a/packages/core-mobile/app/store/bridge/slice.ts +++ b/packages/core-mobile/app/store/bridge/slice.ts @@ -3,11 +3,9 @@ import { RootState } from 'store' import { AppConfig, BridgeConfig, - BridgeTransaction, CriticalConfig } from '@avalabs/core-bridge-sdk' -import { BridgeState, initialState } from 'store/bridge/types' -import { selectActiveNetwork } from 'store/network' +import { initialState } from 'store/bridge/types' export const reducerName = 'bridge' @@ -15,10 +13,6 @@ export const bridgeSlice = createSlice({ name: reducerName, initialState, reducers: { - addBridgeTransaction: (state, action: PayloadAction) => { - const bridgeTx = action.payload - state.bridgeTransactions[bridgeTx.sourceTxHash] = bridgeTx - }, popBridgeTransaction: (state, action: PayloadAction) => { const sourceTxHash = action.payload delete state.bridgeTransactions[sourceTxHash] @@ -29,12 +23,6 @@ export const bridgeSlice = createSlice({ } }) -const selectTransactions = ( - state: RootState -): { - [key: string]: BridgeTransaction -} => state.bridge.bridgeTransactions - export const selectBridgeConfig = ( state: RootState ): BridgeConfig | undefined => state.bridge.config @@ -54,25 +42,6 @@ export const selectBridgeCriticalConfig = ( } } -export const selectBridgeTransactions = ( - state: RootState -): { [key: string]: BridgeTransaction } => { - const activeNetwork = selectActiveNetwork(state) - const bridgeTransactions = selectTransactions(state) - return Object.values(bridgeTransactions).reduce< - BridgeState['bridgeTransactions'] - >((txs, btx) => { - const isMainnet = !activeNetwork.isTestnet - // go figure - const bridgeTx = btx as BridgeTransaction - if (bridgeTx.environment === (isMainnet ? 'main' : 'test')) { - txs[bridgeTx.sourceTxHash] = bridgeTx - } - return txs - }, {}) -} - -export const { addBridgeTransaction, popBridgeTransaction, setConfig } = - bridgeSlice.actions +export const { popBridgeTransaction, setConfig } = bridgeSlice.actions export const bridgeReducer = bridgeSlice.reducer