diff --git a/package.json b/package.json index 4fa3ba4d6..ffb97e871 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@avalabs/avalanche-module": "0.11.2", "@avalabs/avalanchejs": "4.1.0-alpha.7", "@avalabs/bitcoin-module": "0.11.2", - "@avalabs/bridge-unified": "0.0.0-feat-ictt-configs-20241009072139", + "@avalabs/bridge-unified": "0.0.0-CP-8544-BTC-bridge-2-20241024221835", "@avalabs/core-bridge-sdk": "3.1.0-alpha.10", "@avalabs/core-chains-sdk": "3.1.0-alpha.10", "@avalabs/core-coingecko-sdk": "3.1.0-alpha.10", diff --git a/src/components/common/TokenEllipsis.tsx b/src/components/common/TokenEllipsis.tsx index 71ba1a965..5f07353eb 100644 --- a/src/components/common/TokenEllipsis.tsx +++ b/src/components/common/TokenEllipsis.tsx @@ -1,11 +1,12 @@ import { PropsWithChildren } from 'react'; -import { Tooltip } from '@avalabs/core-k2-components'; +import { SxProps, Tooltip } from '@avalabs/core-k2-components'; import { truncateAddress } from '@avalabs/core-utils-sdk'; interface TokenEllipsisProps { maxLength: number; text: string; className?: string; + sx?: SxProps; } function isTruncated(maxLength, text) { @@ -16,6 +17,7 @@ export function TokenEllipsis({ maxLength, text, className, + sx, }: PropsWithChildren) { const name = text.length <= maxLength ? text : truncateAddress(text, maxLength / 2); @@ -26,7 +28,7 @@ export function TokenEllipsis({ title={text} disableHoverListener={!isTruncated(maxLength, text)} disableFocusListener={!isTruncated(maxLength, text)} - sx={{ cursor: 'pointer' }} + sx={sx ?? { cursor: 'pointer' }} > <>{name} diff --git a/src/components/common/TokenSelect.tsx b/src/components/common/TokenSelect.tsx index 78873009e..8b0812322 100644 --- a/src/components/common/TokenSelect.tsx +++ b/src/components/common/TokenSelect.tsx @@ -9,7 +9,6 @@ import { } from 'react'; import { useSettingsContext } from '@src/contexts/SettingsProvider'; import { ContainedDropdown } from '@src/components/common/ContainedDropdown'; -import { AssetBalance } from '@src/pages/Bridge/models'; import EthLogo from '@src/images/tokens/eth.png'; import { hasUnconfirmedBTCBalance, @@ -45,7 +44,6 @@ const InputContainer = styled(Card)` align-items: center; padding: 8px 16px; background: ${({ theme }) => theme.palette.grey[850]}; - cursor: pointer; display: flex; `; @@ -71,7 +69,7 @@ const StyledDropdownMenuItem = styled(DropdownItem)` interface TokenSelectProps { selectedToken?: TokenWithBalance | null; - onTokenChange(token: TokenWithBalance | AssetBalance): void; + onTokenChange(token: TokenWithBalance): void; maxAmount?: bigint; inputAmount?: bigint; onInputAmountChange?(data: { amount: string; bigint: bigint }): void; @@ -83,7 +81,6 @@ interface TokenSelectProps { label?: string; selectorLabel?: string; tokensList?: TokenWithBalance[]; - bridgeTokensList?: AssetBalance[]; isValueLoading?: boolean; hideErrorMessage?: boolean; skipHandleMaxAmount?: boolean; @@ -107,7 +104,6 @@ export function TokenSelect({ isValueLoading, hideErrorMessage, skipHandleMaxAmount, - bridgeTokensList, setIsOpen, containerRef, withMaxButton = true, @@ -140,13 +136,10 @@ export function TokenSelect({ }, [onInputAmountChange, maxAmountString] ); - const hideTokenDropdown = - (bridgeTokensList && bridgeTokensList.length < 2) || - (tokensList && tokensList.length < 2); + const hideTokenDropdown = tokensList && tokensList.length < 2; const displayTokenList = useDisplaytokenlist({ tokensList, - bridgeTokensList, searchQuery, }); @@ -187,9 +180,8 @@ export function TokenSelect({ useEffect(() => { // when only one token is present, auto select it - const tokens = bridgeTokensList ?? tokensList; - const hasOnlyOneToken = tokens?.length === 1; - const theOnlyToken = hasOnlyOneToken ? tokens[0] : undefined; + const hasOnlyOneToken = tokensList?.length === 1; + const theOnlyToken = hasOnlyOneToken ? tokensList[0] : undefined; const isOnlyTokenNotSelected = theOnlyToken && theOnlyToken?.symbol !== selectedToken?.symbol; @@ -198,17 +190,16 @@ export function TokenSelect({ return; } // when selected token is not supported, clear it - const supportedSymbols = - tokens?.flatMap((tok) => [tok.symbol, tok.symbolOnNetwork]) ?? []; + const supportedSymbols = tokensList?.flatMap((tok) => tok.symbol) ?? []; if ( selectedToken && - tokens?.[0] && + tokensList?.[0] && !supportedSymbols.includes(selectedToken.symbol) ) { - onTokenChange(tokens[0]); + onTokenChange(tokensList[0]); } - }, [bridgeTokensList, tokensList, onTokenChange, selectedToken]); + }, [tokensList, onTokenChange, selectedToken]); const rowRenderer = useCallback( ({ key, index, style }) => { diff --git a/src/components/common/TokenSelector.tsx b/src/components/common/TokenSelector.tsx index b0fd8f61e..dfbfe200b 100644 --- a/src/components/common/TokenSelector.tsx +++ b/src/components/common/TokenSelector.tsx @@ -30,12 +30,12 @@ export function TokenSelector({ const { t } = useTranslation(); return ( @@ -51,7 +51,11 @@ export function TokenSelector({ <> {token.icon} - + {!hideCaretIcon ? ( isOpen ? ( diff --git a/src/contexts/UnifiedBridgeProvider.tsx b/src/contexts/UnifiedBridgeProvider.tsx index 27112e659..786a3db51 100644 --- a/src/contexts/UnifiedBridgeProvider.tsx +++ b/src/contexts/UnifiedBridgeProvider.tsx @@ -73,9 +73,13 @@ export interface UnifiedBridgeContext { getErrorMessage(errorCode: UnifiedBridgeErrorCode): string; transferableAssets: BridgeAsset[]; state: UnifiedBridgeState; + assets: ChainAssetMap; + availableChainIds: string[]; + isReady: boolean; } const DEFAULT_STATE = { + assets: {}, state: UNIFIED_BRIDGE_DEFAULT_STATE, estimateTransferGas() { throw new Error('Bridge not ready'); @@ -99,6 +103,8 @@ const DEFAULT_STATE = { throw new Error('Bridge not ready'); }, transferableAssets: [], + availableChainIds: [], + isReady: false, }; const UnifiedBridgeContext = createContext(DEFAULT_STATE); @@ -129,19 +135,26 @@ export function UnifiedBridgeProvider({ const [state, setState] = useState( UNIFIED_BRIDGE_DEFAULT_STATE ); + const [isReady, setIsReady] = useState(false); const { featureFlags } = useFeatureFlagContext(); const isCCTPDisabled = !featureFlags[FeatureGates.UNIFIED_BRIDGE_CCTP]; - const disabledBridgeTypes = useMemo( - () => - isCCTPDisabled - ? [ - BridgeType.CCTP, - BridgeType.ICTT_ERC20_ERC20, - BridgeType.AVALANCHE_EVM, - ] - : [BridgeType.AVALANCHE_EVM, BridgeType.ICTT_ERC20_ERC20], - [isCCTPDisabled] - ); + const isICTTDisabled = false; // TODO: feature flag it + const isABDisabled = false; // TODO: feature flag it + const disabledBridgeTypes = useMemo(() => { + const disabledBridges: BridgeType[] = []; + + if (isCCTPDisabled) { + disabledBridges.push(BridgeType.CCTP); + } + if (isICTTDisabled) { + disabledBridges.push(BridgeType.ICTT_ERC20_ERC20); + } + if (isABDisabled) { + disabledBridges.push(BridgeType.AVALANCHE_EVM); + } + + return disabledBridges; + }, [isCCTPDisabled, isABDisabled, isICTTDisabled]); const environment = useMemo(() => { if (typeof activeNetwork?.isTestnet !== 'boolean') { @@ -202,6 +215,7 @@ export function UnifiedBridgeProvider({ } setAssets(chainAssetsMap); + setIsReady(true); }); return () => { @@ -209,6 +223,8 @@ export function UnifiedBridgeProvider({ }; }, [core]); + const availableChainIds = useMemo(() => Object.keys(assets ?? {}), [assets]); + const buildChain = useCallback( (chainId: string): Chain => { const network = getNetwork(chainId); @@ -216,7 +232,7 @@ export function UnifiedBridgeProvider({ assert(network, CommonError.UnknownNetwork); return { - chainId, + chainId: network.caipId, chainName: network.chainName, rpcUrl: network.rpcUrl, networkToken: { @@ -236,9 +252,6 @@ export function UnifiedBridgeProvider({ return []; } - // UnifiedBridge SDK returns the chain IDs in CAIP2 format. - // This is good, but we need to translate it to numeric chain ids - // until we make the switch in extension: return assets[activeNetwork.caipId] ?? []; }, [activeNetwork, assets]); @@ -359,6 +372,8 @@ export function UnifiedBridgeProvider({ const identifier = asset.type === TokenType.NATIVE ? asset.symbol : asset.address; + assert(identifier, UnifiedBridgeError.InvalidFee); + return feeMap[identifier.toLowerCase()] ?? 0n; }, [activeNetwork, core, buildChain, getAsset] @@ -531,8 +546,11 @@ export function UnifiedBridgeProvider({ return ( { const displayTokenList: DisplayToken[] = useMemo(() => { - const initialList = [ - ...(tokensList - ? tokensList - .filter((token) => - searchQuery.length - ? token.name - .toLowerCase() - .includes(searchQuery.toLowerCase()) || - token.symbol.toLowerCase().includes(searchQuery.toLowerCase()) - : true - ) - .map((token): DisplayToken => { - return { - name: token.name, - symbol: token.symbol, - displayValue: token.balanceDisplayValue ?? '', - token, - decimals: isNFT(token) ? 0 : token.decimals, - }; - }) - : []), - ...(bridgeTokensList - ? bridgeTokensList - .filter((token) => - searchQuery.length - ? token.symbol - .toLowerCase() - .includes(searchQuery.toLowerCase()) || - token.symbolOnNetwork - ?.toLowerCase() - .includes(searchQuery.toLocaleLowerCase()) - : true - ) - .map((token): DisplayToken => { - return { - name: token.symbolOnNetwork || token.symbol, - symbol: token.asset.symbol, - displayValue: formatBalance(token.balance), - token, - decimals: isUnifiedBridgeAsset(token.asset) - ? token.asset.decimals - : token.asset.denomination, - }; - }) - : []), - ]; + const initialList = (tokensList ?? []) + .filter((token) => + searchQuery.length + ? token.name.toLowerCase().includes(searchQuery.toLowerCase()) || + token.symbol.toLowerCase().includes(searchQuery.toLowerCase()) + : true + ) + .map((token): DisplayToken => { + return { + name: token.name, + symbol: token.symbol, + displayValue: token.balanceDisplayValue ?? '', + token, + decimals: isNFT(token) ? 0 : token.decimals, + }; + }); const [tokensWithBalance, tokensWithoutBalance]: DisplayToken[][] = partition(initialList, (token) => { @@ -107,6 +68,6 @@ export const useDisplaytokenlist = ({ return tokenOne.name.localeCompare(tokenTwo.name); }), ]; - }, [tokensList, bridgeTokensList, searchQuery]); + }, [tokensList, searchQuery]); return displayTokenList; }; diff --git a/src/localization/locales/en/translation.json b/src/localization/locales/en/translation.json index e4624b680..c42a345dc 100644 --- a/src/localization/locales/en/translation.json +++ b/src/localization/locales/en/translation.json @@ -720,6 +720,7 @@ "Select chain": "Select chain", "Select derivation path": "Select derivation path", "Select one of the available verification methods below to proceed.": "Select one of the available verification methods below to proceed.", + "Select target chain": "Select target chain", "Select the Core wallet.": "Select the Core wallet.", "Select the first word": "Select the first word", "Select the word that comes after": "Select the word that comes after", @@ -792,7 +793,6 @@ "Swap transaction succeeded! 🎉": "Swap transaction succeeded! 🎉", "Swiss Franc": "Swiss Franc", "Switch": "Switch", - "Switch to {{chainName}}": "Switch to {{chainName}}", "Switch to {{chainName}} Network?": "Switch to {{chainName}} Network?", "Switch to {{name}}?": "Switch to {{name}}?", "Symbol": "Symbol", @@ -895,6 +895,7 @@ "Try typing the information again or go back to the account manager.": "Try typing the information again or go back to the account manager.", "Turkish": "Turkish", "URI": "URI", + "USDC is routed through Circle's Cross-Chain Transfer Protocol. Bridge FAQs": "USDC is routed through Circle's Cross-Chain Transfer Protocol. Bridge FAQs", "Unable to connect. View the troubleshoot guide here": "Unable to connect. View the troubleshoot guide here", "Unable to connect?": "Unable to connect?", "Unable to set TOTP configuration": "Unable to set TOTP configuration", @@ -1027,6 +1028,5 @@ "{{functionName}} is currently unavailable.": "{{functionName}} is currently unavailable.", "{{length}} Bytes": "{{length}} Bytes", "{{name}} successfully removed!": "{{name}} successfully removed!", - "{{symbol}} is routed through {{bridgeName}}. Bridge FAQs": "{{symbol}} is routed through {{bridgeName}}. Bridge FAQs", "{{walletName}} Added": "{{walletName}} Added" } diff --git a/src/pages/Bridge/Bridge.tsx b/src/pages/Bridge/Bridge.tsx index 0120b3d6c..a2e1395a8 100644 --- a/src/pages/Bridge/Bridge.tsx +++ b/src/pages/Bridge/Bridge.tsx @@ -1,27 +1,5 @@ -import { - BIG_ZERO, - Blockchain, - useBridgeSDK, - isAddressBlocklisted, -} from '@avalabs/core-bridge-sdk'; -import { PageTitle } from '@src/components/common/PageTitle'; -import { useAnalyticsContext } from '@src/contexts/AnalyticsProvider'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useHistory } from 'react-router-dom'; -import { BridgeProviders, useBridge } from './hooks/useBridge'; -import { FunctionIsOffline } from '@src/components/common/FunctionIsOffline'; -import { usePageHistory } from '@src/hooks/usePageHistory'; -import { useSyncBridgeConfig } from './hooks/useSyncBridgeConfig'; -import Big from 'big.js'; -import { useSetBridgeChainFromNetwork } from './hooks/useSetBridgeChainFromNetwork'; -import { useAccountsContext } from '@src/contexts/AccountsProvider'; -import { BridgeSanctions } from './components/BridgeSanctions'; -import { useNetworkContext } from '@src/contexts/NetworkProvider'; -import { - blockchainToNetwork, - networkToBlockchain, -} from './utils/blockchainConversion'; -import { useAvailableBlockchains } from './hooks/useAvailableBlockchains'; import { useTranslation } from 'react-i18next'; import { Button, @@ -30,6 +8,13 @@ import { Typography, toast, } from '@avalabs/core-k2-components'; + +import { PageTitle } from '@src/components/common/PageTitle'; +import { useAnalyticsContext } from '@src/contexts/AnalyticsProvider'; +import { FunctionIsOffline } from '@src/components/common/FunctionIsOffline'; +import { usePageHistory } from '@src/hooks/usePageHistory'; +import { useAccountsContext } from '@src/contexts/AccountsProvider'; +import { useNetworkContext } from '@src/contexts/NetworkProvider'; import { TokenType } from '@avalabs/vm-module-types'; import { FunctionNames, @@ -37,40 +22,42 @@ import { } from '@src/hooks/useIsFunctionAvailable'; import { useErrorMessage } from '@src/hooks/useErrorMessage'; import { isBitcoinNetwork } from '@src/background/services/network/utils/isBitcoinNetwork'; +import { useLiveBalance } from '@src/hooks/useLiveBalance'; +import { NetworkWithCaipId } from '@src/background/services/network/models'; -import { BridgeFormETH } from './components/BridgeFormETH'; -import { BridgeFormAVAX } from './components/BridgeFormAVAX'; -import { BridgeFormBTC } from './components/BridgeFormBTC'; -import { BridgeFormUnified } from './components/BridgeFormUnified'; -import { useUnifiedBridgeContext } from '@src/contexts/UnifiedBridgeProvider'; +import { useBridge } from './hooks/useBridge'; +import { BridgeForm } from './components/BridgeForm'; import { BridgeUnknownNetwork } from './components/BridgeUnknownNetwork'; -import { useLiveBalance } from '@src/hooks/useLiveBalance'; +import { useBridgeTxHandling } from './hooks/useBridgeTxHandling'; +import { BridgeFormSkeleton } from './components/BridgeFormSkeleton'; const POLLED_BALANCES = [TokenType.NATIVE, TokenType.ERC20]; export function Bridge() { - useLiveBalance(POLLED_BALANCES); // Make sure we always use fresh balances of bridgable tokens. - useSyncBridgeConfig(); // keep bridge config up-to-date - useSetBridgeChainFromNetwork(); - - const [currentAssetIdentifier, setCurrentAssetIdentifier] = - useState(); - const { amount, setAmount, bridgeFee, provider, minimum, targetChainId } = - useBridge(currentAssetIdentifier); + useLiveBalance(POLLED_BALANCES); // Make sure we always use the latest balances. const { - bridgeConfig, - currentAsset, - setCurrentAsset, - currentBlockchain, - setCurrentBlockchain, - targetBlockchain, - sourceAssets, - } = useBridgeSDK(); - const bridgeConfigError = bridgeConfig.error; + amount, + setAmount, + bridgableTokens, + availableChainIds, + bridgeFee, + estimateGas, + isReady, + minimum, + maximum, + receiveAmount, + setTargetChain, + possibleTargetChains, + asset, + setAsset, + targetChain, + transferableAssets, + sourceBalance, + transfer, + } = useBridge(); + const { t } = useTranslation(); - const availableBlockchains = useAvailableBlockchains(); - const { getAssetIdentifierOnTargetChain } = useUnifiedBridgeContext(); const { isFunctionAvailable } = useIsFunctionAvailable(FunctionNames.BRIDGE); @@ -84,7 +71,7 @@ export function Bridge() { const { accounts: { active: activeAccount }, } = useAccountsContext(); - const { network, setNetwork, networks } = useNetworkContext(); + const { network, setNetwork } = useNetworkContext(); const activeAddress = useMemo( () => @@ -96,98 +83,72 @@ export function Bridge() { [activeAccount?.addressBTC, activeAccount?.addressC, network] ); - const targetNetwork = useMemo(() => { - if (targetBlockchain) { - return blockchainToNetwork(targetBlockchain, networks, bridgeConfig); - } - }, [bridgeConfig, networks, targetBlockchain]); - const bridgePageHistoryData: { selectedToken?: string; - inputAmount?: Big; - selectedTokenAddress?: string; + inputAmount?: string; } = getPageHistoryData(); - // derive blockchain/network from network useEffect(() => { - const networkBlockchain = networkToBlockchain(network); - if (currentBlockchain !== networkBlockchain) { - setCurrentBlockchain(networkBlockchain); - } - }, [network, currentBlockchain, setCurrentBlockchain]); + if (!asset && bridgePageHistoryData.selectedToken) { + const matchingAsset = transferableAssets.find( + (a) => a.symbol === bridgePageHistoryData.selectedToken + ); - // Set source blockchain & amount from page storage - useEffect(() => { - if (!amount && bridgePageHistoryData.inputAmount) { - setAmount(new Big(bridgePageHistoryData.inputAmount)); + if (matchingAsset) { + setAsset(matchingAsset); + } + } + if (typeof amount !== 'bigint' && bridgePageHistoryData.inputAmount) { + setAmount(BigInt(bridgePageHistoryData.inputAmount)); } }, [ amount, + asset, + setAsset, + transferableAssets, bridgePageHistoryData.inputAmount, + bridgePageHistoryData.selectedToken, setAmount, - networks, - setNetwork, ]); - // Set token from page storage useEffect(() => { - const sourceSymbols = Object.keys(sourceAssets); - const symbol = bridgePageHistoryData.selectedToken; - - if ( - symbol && - !currentAsset && - sourceSymbols.length && - sourceSymbols.includes(symbol) // make sure we have the selected token available on the network to prevent an infinite loop - ) { - // Workaround for a race condition with useEffect in BridgeSDKProvider - // that also calls setCurrentAsset :( - const timer = setTimeout(() => { - setCurrentAsset(symbol); - setCurrentAssetIdentifier( - bridgePageHistoryData.selectedTokenAddress ?? '' - ); - }, 1); - - return () => { - clearTimeout(timer); - }; + const sourceSymbols = transferableAssets.map(({ symbol }) => symbol); + const prevSymbol = bridgePageHistoryData.selectedToken; + + if (prevSymbol && sourceSymbols.length) { + const prevAsset = transferableAssets.find( + ({ symbol }) => symbol === prevSymbol + ); + + if (prevAsset) { + setAsset(prevAsset); + } } - }, [ - bridgePageHistoryData.selectedToken, - bridgePageHistoryData.selectedTokenAddress, - currentAsset, - currentAssetIdentifier, - setCurrentAsset, - sourceAssets, - bridgeConfig, - networks, - targetBlockchain, - ]); + }, [bridgePageHistoryData.selectedToken, setAsset, transferableAssets]); const [isAmountTooLow, setIsAmountTooLow] = useState(false); const onInitiated = useCallback(() => { captureEncrypted('BridgeTransferStarted', { address: activeAddress, - sourceBlockchain: currentBlockchain, - targetBlockchain, + sourceBlockchain: network?.caipId, + targetBlockchain: targetChain?.caipId, }); - }, [captureEncrypted, activeAddress, currentBlockchain, targetBlockchain]); + }, [captureEncrypted, activeAddress, network?.caipId, targetChain?.caipId]); const onRejected = useCallback(() => { captureEncrypted('BridgeTransferRequestUserRejectedError', { address: activeAddress, - sourceBlockchain: currentBlockchain, - targetBlockchain, - fee: bridgeFee?.toNumber(), + sourceBlockchain: network?.caipId, + targetBlockchain: targetChain?.caipId, + fee: Number(bridgeFee ?? 0), }); }, [ activeAddress, - bridgeFee, captureEncrypted, - currentBlockchain, - targetBlockchain, + network?.caipId, + targetChain?.caipId, + bridgeFee, ]); const onFailure = useCallback( @@ -195,8 +156,8 @@ export function Bridge() { setBridgeError(t('There was a problem with the transfer')); captureEncrypted('BridgeTransferRequestError', { address: activeAddress, - sourceBlockchain: currentBlockchain, - targetBlockchain, + sourceBlockchain: network?.caipId, + targetBlockchain: targetChain?.caipId, }); const { title, hint } = getTranslatedError(transferError); @@ -214,10 +175,10 @@ export function Bridge() { [ activeAddress, captureEncrypted, - currentBlockchain, - targetBlockchain, getTranslatedError, + network?.caipId, t, + targetChain?.caipId, ] ); @@ -226,72 +187,90 @@ export function Bridge() { captureEncrypted('BridgeTransferRequestSucceeded', { address: activeAddress, txHash: hash, - sourceBlockchain: currentBlockchain, - targetBlockchain, + sourceBlockchain: network?.caipId, + targetBlockchain: targetChain?.caipId, }); const timestamp = Date.now(); // Navigate to transaction status page history.push( - `/bridge/transaction-status/${currentBlockchain}/${hash}/${timestamp}` + `/bridge/transaction-status/${network?.caipId}/${hash}/${timestamp}` ); }, [ activeAddress, captureEncrypted, - currentBlockchain, history, - targetBlockchain, + network?.caipId, + targetChain?.caipId, ] ); - const handleBlockchainChange = useCallback( - (blockchain: Blockchain) => { - const blockChainNetwork = blockchainToNetwork( - blockchain, - networks, - bridgeConfig - ); - - if (blockChainNetwork) { - setNetwork(blockChainNetwork); - const assetAddressOnOppositeChain = getAssetIdentifierOnTargetChain( - currentAsset, - blockChainNetwork.caipId - ); - - setCurrentAssetIdentifier(assetAddressOnOppositeChain); - setNavigationHistoryData({ - selectedTokenAddress: assetAddressOnOppositeChain, - selectedToken: currentAsset, - inputAmount: amount, - }); - } + const handleSourceChainChange = useCallback( + (chain: NetworkWithCaipId) => { + setNetwork(chain); + setNavigationHistoryData({ + selectedToken: asset ? asset.symbol : undefined, + inputAmount: amount, + }); // Reset because a denomination change will change its value - setAmount(BIG_ZERO); + setAmount(0n); setBridgeError(''); }, [ amount, - bridgeConfig, - getAssetIdentifierOnTargetChain, - currentAsset, - networks, + asset, setAmount, setNavigationHistoryData, setNetwork, setBridgeError, - setCurrentAssetIdentifier, ] ); - if ( - bridgeConfigError || - !isFunctionAvailable || - availableBlockchains.length < 2 // we need at least to blockchains to bridge between - ) { + const { onTransfer, isPending } = useBridgeTxHandling({ + transfer, + onInitiated, + onSuccess, + onFailure, + onRejected, + }); + + const formProps = { + onInitiated, + onSuccess, + onFailure, + onRejected, + handleSourceChainChange, + amount, + bridgeError, + isAmountTooLow, + isReady, + asset, + setAsset, + availableChainIds, + transferableAssets, + transfer, + onTransfer, + isPending, + setIsAmountTooLow, + setAmount, + setBridgeError, + setNavigationHistoryData, + targetChain, + estimateGas, + minimum, + maximum, + receiveAmount, + setTargetChain, + possibleTargetChains, + loading: false, // TODO: load balances + bridgableTokens, + sourceBalance, + }; + + if (!isFunctionAvailable) { return ( - - - - {/* To section */} - - - - - {t('To')} - - - {targetNetwork ? targetNetwork.chainName : ''} - - - } sx={{ rowGap: 2 }} /> - - 0} + unmountOnExit + mountOnEnter + > + + {t('Switch')}}> + + + + - {formattedReceiveAmount} - + 0} + mountOnEnter + unmountOnExit + > + + - {t('Estimated')} + {t('To')} - - + + } sx={{ rowGap: 2 }} /> + + - {formattedReceiveAmountCurrency} - + {t('Receive')} + + {formattedReceiveAmount} + + + + {t('Estimated')} + + + + {formattedReceiveAmountCurrency} + + - - + + @@ -658,54 +537,8 @@ export const BridgeForm = ({ gap: 2, }} > - {/* FIXME: Unified SDK can handle multiple bridges, but for now it's just the CCTP */} - {provider === BridgeProviders.Unified && ( - - {t('Powered by')} - - Circle - - ), - }} - /> - } - > - - - + {asset && targetChain && ( + )} + ); diff --git a/src/pages/Bridge/components/NetworkSelector.tsx b/src/pages/Bridge/components/NetworkSelector.tsx index 046a94409..d6a378129 100644 --- a/src/pages/Bridge/components/NetworkSelector.tsx +++ b/src/pages/Bridge/components/NetworkSelector.tsx @@ -1,119 +1,64 @@ -import { Blockchain } from '@avalabs/core-bridge-sdk'; -import { AvaxTokenIcon } from '@src/components/icons/AvaxTokenIcon'; -import { BitcoinLogo } from '@src/components/icons/BitcoinLogo'; import { useCallback, useRef, useState } from 'react'; -import { blockchainDisplayNameMap } from '../models'; import { Button, CheckIcon, ChevronDownIcon, ChevronUpIcon, - EthereumColorIcon, Menu, MenuItem, Stack, Typography, } from '@avalabs/core-k2-components'; +import { useTranslation } from 'react-i18next'; +import { useNetworkContext } from '@src/contexts/NetworkProvider'; +import { NetworkWithCaipId } from '@src/background/services/network/models'; interface NetworkSelectorProps { testId?: string; disabled?: boolean; - selected: Blockchain; - onSelect?: (blockchain: Blockchain) => void; - chains: Blockchain[]; + selected?: NetworkWithCaipId; + onSelect?: (blockchain: NetworkWithCaipId) => void; + chainIds: string[]; } -const getBlockChainLogo = (blockchain: Blockchain) => { - switch (blockchain) { - case Blockchain.AVALANCHE: - return ; - case Blockchain.ETHEREUM: - return ; - case Blockchain.BITCOIN: - return ; - default: - return <>; - } -}; - export function NetworkSelector({ testId, disabled, selected, onSelect, - chains, + chainIds, }: NetworkSelectorProps) { + const { t } = useTranslation(); const [isOpen, setIsOpen] = useState(false); const selectButtonRef = useRef(null); - - const selectedDisplayValue = blockchainDisplayNameMap.get(selected); + const { getNetwork } = useNetworkContext(); const handleClose = useCallback( - (blockchain: Blockchain) => { + (network: NetworkWithCaipId) => { setIsOpen(false); - onSelect?.(blockchain); + onSelect?.(network); }, [onSelect] ); - const getMenuItem = useCallback( - (dataId: string, blockchain: Blockchain) => { - if (!chains.includes(blockchain)) { - return null; - } - - return ( - { - handleClose(blockchain); - }} - disableRipple - sx={{ minHeight: 'auto', py: 1 }} - > - - - {getBlockChainLogo(blockchain)} - - {blockchainDisplayNameMap.get(blockchain)} - - - - {selected === blockchain && } - - - ); - }, - [chains, handleClose, selected] - ); - return ( - {getMenuItem('bridge-avax-chain-option', Blockchain.AVALANCHE)} - {getMenuItem('bridge-eth-chain-option', Blockchain.ETHEREUM)} - {getMenuItem('bridge-btc-chain-option', Blockchain.BITCOIN)} + {chainIds + .map((chainId) => getNetwork(chainId)) + .filter((n): n is NetworkWithCaipId => typeof n !== 'undefined') + .map((network) => { + return ( + { + handleClose(network); + }} + disableRipple + sx={{ minHeight: 'auto', py: 1 }} + > + + + + {network.chainName} + + + {selected === network && } + + + ); + })} ); diff --git a/src/pages/Bridge/hooks/useAssetBalancesEVM.ts b/src/pages/Bridge/hooks/useAssetBalancesEVM.ts deleted file mode 100644 index 1ba0bcd48..000000000 --- a/src/pages/Bridge/hooks/useAssetBalancesEVM.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { - Asset, - Blockchain, - EthereumAssets, - useBridgeSDK, - useGetTokenSymbolOnNetwork, -} from '@avalabs/core-bridge-sdk'; -import { uniqBy } from 'lodash'; - -import { getBalances } from '../utils/getBalances'; -import { AssetBalance } from '../models'; -import { useMemo } from 'react'; -import { useFeatureFlagContext } from '@src/contexts/FeatureFlagsProvider'; -import { useTokensWithBalances } from '@src/hooks/useTokensWithBalances'; -import { FeatureGates } from '@src/background/services/featureFlags/models'; -import { useAccountsContext } from '@src/contexts/AccountsProvider'; -import { AccountType } from '@src/background/services/accounts/models'; -import { useUnifiedBridgeContext } from '@src/contexts/UnifiedBridgeProvider'; - -import { isUnifiedBridgeAsset } from '../utils/isUnifiedBridgeAsset'; - -/** - * Get for the current chain. - * Get a list of bridge supported assets with the balances of the current blockchain. - * The list is sorted by balance. - */ -export function useAssetBalancesEVM( - chain: Blockchain.AVALANCHE | Blockchain.ETHEREUM, - selectedAsset?: Asset -): { - assetsWithBalances: AssetBalance[]; -} { - const { featureFlags } = useFeatureFlagContext(); - const { - accounts: { active: activeAccount }, - } = useAccountsContext(); - - const { avalancheAssets, ethereumAssets, currentBlockchain } = useBridgeSDK(); - const { transferableAssets: unifiedBridgeAssets } = useUnifiedBridgeContext(); - - const { getTokenSymbolOnNetwork } = useGetTokenSymbolOnNetwork(); - - const tokens = useTokensWithBalances({ - forceShowTokensWithoutBalances: true, - }); - - // For balances on the Avalanche side, for all bridge assets on avalanche - const balances = useMemo(() => { - const isAvalanche = - chain === Blockchain.AVALANCHE || - currentBlockchain === Blockchain.AVALANCHE; - const isEthereum = - chain === Blockchain.ETHEREUM || - currentBlockchain === Blockchain.ETHEREUM; - if (!isAvalanche && !isEthereum) { - return []; - } - - const filteredEthereumAssets: EthereumAssets = Object.keys(ethereumAssets) - .filter((key) => ethereumAssets[key]?.symbol !== 'BUSD') // do not allow BUSD.e onboardings - .filter((key) => ethereumAssets[key]?.symbol !== 'USDC') // do not use Legacy Bridge for USDC onboardings - .reduce((obj, key) => { - obj[key] = ethereumAssets[key]; - return obj; - }, {}); - - const abAssets = Object.values( - isAvalanche ? avalancheAssets : filteredEthereumAssets - ); - - if (!abAssets.length) { - return []; - } - - const allAssets = selectedAsset - ? [selectedAsset] - : // Deduplicate the assets since both Unified & legacy SDKs could allow bridging the same assets. - // unifiedBridgeAssets go first so that they're not the ones removed (we prefer Unified bridge over legacy) - uniqBy([...unifiedBridgeAssets, ...abAssets], (asset) => - isUnifiedBridgeAsset(asset) - ? asset.symbol - : getTokenSymbolOnNetwork(asset.symbol, chain) - ); - - const availableAssets = allAssets.filter((asset) => { - if (chain === Blockchain.AVALANCHE) { - if (isUnifiedBridgeAsset(asset)) { - return featureFlags[FeatureGates.BRIDGE_ETH]; - } - - const { nativeNetwork } = asset; - - if ( - nativeNetwork === Blockchain.ETHEREUM && - !featureFlags[FeatureGates.BRIDGE_ETH] - ) { - // ETH is not available filter ETH tokens out - return false; - } - if (nativeNetwork === Blockchain.BITCOIN) { - // Filter out BTC tokens if BTC bridge is not available, or - // the active account was imported via WalletConnect (the BTC address is unknown). - - const isBtcSupportedByActiveAccount = - activeAccount?.addressBTC && - activeAccount?.type !== AccountType.WALLET_CONNECT; - - return ( - featureFlags[FeatureGates.BRIDGE_BTC] && - isBtcSupportedByActiveAccount - ); - } - } - - // no further filtering is needed since it's not possible to bridge between eth and btc - return true; - }); - - return getBalances(availableAssets, tokens).map((token) => { - return { - ...token, - symbolOnNetwork: isUnifiedBridgeAsset(token.asset) - ? token.asset.symbol - : getTokenSymbolOnNetwork(token.symbol, chain), - }; - }); - }, [ - chain, - currentBlockchain, - selectedAsset, - avalancheAssets, - ethereumAssets, - tokens, - featureFlags, - getTokenSymbolOnNetwork, - activeAccount?.type, - activeAccount?.addressBTC, - unifiedBridgeAssets, - ]); - - const assetsWithBalances = balances.sort( - (asset1, asset2) => asset2.balance?.cmp(asset1.balance || 0) || 0 - ); - - return { assetsWithBalances }; -} diff --git a/src/pages/Bridge/hooks/useAvailableBlockchains.ts b/src/pages/Bridge/hooks/useAvailableBlockchains.ts deleted file mode 100644 index f6ffa10de..000000000 --- a/src/pages/Bridge/hooks/useAvailableBlockchains.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Blockchain } from '@avalabs/core-bridge-sdk'; -import { useAccountsContext } from '@src/contexts/AccountsProvider'; -import { useFeatureFlagContext } from '@src/contexts/FeatureFlagsProvider'; -import { useEffect, useState } from 'react'; -import { SUPPORTED_CHAINS } from '../models'; -import { FeatureGates } from '@src/background/services/featureFlags/models'; -import { - isFireblocksAccount, - isWalletConnectAccount, -} from '@src/background/services/accounts/utils/typeGuards'; -import isFireblocksApiSupported from '@src/background/services/fireblocks/utils/isFireblocksApiSupported'; - -export function useAvailableBlockchains() { - const { featureFlags } = useFeatureFlagContext(); - const { - accounts: { active: activeAccount }, - } = useAccountsContext(); - const [availableBlockchains, setAvailableBlockchains] = - useState(SUPPORTED_CHAINS); - - // Remove chains turned off by the feature flags and - // switch chain in case the selected one is not available - useEffect(() => { - const availableChains = SUPPORTED_CHAINS.filter((chain) => { - switch (chain) { - case Blockchain.BITCOIN: - if (!featureFlags[FeatureGates.BRIDGE_BTC]) { - return false; - } - - if (isWalletConnectAccount(activeAccount)) { - return false; - } - - if (isFireblocksAccount(activeAccount)) { - return isFireblocksApiSupported(activeAccount); - } - - return true; - case Blockchain.ETHEREUM: - return featureFlags[FeatureGates.BRIDGE_ETH]; - default: - return true; - } - }); - - setAvailableBlockchains(availableChains); - }, [featureFlags, activeAccount]); - - return availableBlockchains; -} diff --git a/src/pages/Bridge/hooks/useAvalancheBridge.test.ts b/src/pages/Bridge/hooks/useAvalancheBridge.test.ts deleted file mode 100644 index 07aebaf92..000000000 --- a/src/pages/Bridge/hooks/useAvalancheBridge.test.ts +++ /dev/null @@ -1,213 +0,0 @@ -import Big from 'big.js'; -import { - useBridgeSDK, - Blockchain, - AssetType, - isNativeAsset, -} from '@avalabs/core-bridge-sdk'; -import { act, renderHook } from '@testing-library/react-hooks'; - -import { useBridgeContext } from '@src/contexts/BridgeProvider'; -import { useAccountsContext } from '@src/contexts/AccountsProvider'; -import { useNetworkFeeContext } from '@src/contexts/NetworkFeeProvider'; -import { useConnectionContext } from '@src/contexts/ConnectionProvider'; - -import { useAvalancheBridge } from './useAvalancheBridge'; -import { useAssetBalancesEVM } from './useAssetBalancesEVM'; - -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (k) => k, - }), -})); -jest.mock('@avalabs/core-bridge-sdk'); -jest.mock('./useAssetBalancesEVM'); -jest.mock('@src/contexts/BridgeProvider'); -jest.mock('@src/contexts/AccountsProvider'); -jest.mock('@src/contexts/NetworkFeeProvider'); -jest.mock('@src/contexts/ConnectionProvider'); - -describe('src/pages/Bridge/hooks/useAvalancheBridge', () => { - let requestFn = jest.fn(); - let createBridgeTransactionFn = jest.fn(); - let transferEVMAssetFn = jest.fn(); - - const highFee = 20n; - const lowFee = 8n; - - const currentAssetData = { - assetType: AssetType.BTC, - denomination: 8, - wrappedAssetSymbol: 'BTC.b', - tokenName: 'Bitcoin', - symbol: 'BTC', - nativeNetwork: Blockchain.BITCOIN, - }; - - const btcWithBalance = { - symbol: 'BTC', - asset: currentAssetData, - balance: new Big('0.1'), - price: 60_000, - }; - - beforeEach(() => { - jest.resetAllMocks(); - - requestFn = jest.fn(); - transferEVMAssetFn = jest.fn(); - createBridgeTransactionFn = jest.fn(); - - jest.mocked(useConnectionContext).mockReturnValue({ - request: requestFn, - } as any); - - jest.mocked(useAccountsContext).mockReturnValue({ - accounts: { - active: { - addressC: 'user-c-address', - addressBTC: 'user-btc-address', - }, - }, - } as any); - - jest.mocked(useAssetBalancesEVM).mockReturnValue({ - assetsWithBalances: [btcWithBalance], - } as any); - - jest.mocked(useBridgeContext).mockReturnValue({ - createBridgeTransaction: createBridgeTransactionFn, - transferEVMAsset: transferEVMAssetFn, - } as any); - - jest.mocked(useBridgeSDK).mockReturnValue({ - bridgeConfig: { - config: {}, - }, - currentAsset: 'BTC', - currentAssetData, - setTransactionDetails: jest.fn(), - currentBlockchain: Blockchain.AVALANCHE, - targetBlockchain: Blockchain.BITCOIN, - } as any); - - jest.mocked(useNetworkFeeContext).mockReturnValue({ - networkFee: { - high: { - maxFee: highFee, - }, - low: { - maxFee: lowFee, - }, - displayDecimals: 8, - }, - } as any); - }); - - it('provides maximum transfer amount', async () => { - const amount = new Big('0.1'); - const fee = new Big('0.0003'); - const minimum = new Big('0.0006'); - - const { result: hook } = renderHook(() => - useAvalancheBridge(amount, fee, minimum) - ); - - // Wait for the state to be set - await new Promise(process.nextTick); - - expect(hook.current.maximum).toEqual(btcWithBalance.balance); - }); - - describe('transfer()', () => { - beforeEach(() => { - jest.mocked(isNativeAsset).mockReturnValue(true); - }); - - describe('when no asset is selected', () => { - beforeEach(() => { - jest.mocked(useBridgeSDK).mockReturnValue({ - bridgeConfig: { - config: {}, - }, - currentAsset: '', - currentAssetData: undefined, - setTransactionDetails: jest.fn(), - currentBlockchain: Blockchain.ETHEREUM, - } as any); - }); - - it('throws error', async () => { - const amount = new Big('0.1'); - const fee = new Big('0.0003'); - const minimum = new Big('0.0006'); - - const { result: hook } = renderHook(() => - useAvalancheBridge(amount, fee, minimum) - ); - - await act(async () => { - await expect(hook.current.transfer()).rejects.toThrow( - 'No asset selected' - ); - }); - }); - }); - - it("calls the provider's transfer function", async () => { - const amount = new Big('0.1'); - const fee = new Big('0.0003'); - const minimum = new Big('0.0006'); - - const { result: hook } = renderHook(() => - useAvalancheBridge(amount, fee, minimum) - ); - - const fakeHash = '0xHash'; - - transferEVMAssetFn.mockResolvedValue({ - hash: fakeHash, - }); - - await act(async () => { - const hash = await hook.current.transfer(); - - expect(transferEVMAssetFn).toHaveBeenCalledWith( - amount, - currentAssetData - ); - - expect(hash).toEqual(fakeHash); - }); - }); - - it('tracks the bridge transaction', async () => { - const amount = new Big('0.1'); - const fee = new Big('0.0003'); - const minimum = new Big('0.0006'); - - const { result: hook } = renderHook(() => - useAvalancheBridge(amount, fee, minimum) - ); - - const fakeHash = '0xHash'; - - transferEVMAssetFn.mockResolvedValue({ - hash: fakeHash, - }); - - await act(async () => { - await hook.current.transfer(); - - expect(createBridgeTransactionFn).toHaveBeenCalledWith({ - sourceChain: Blockchain.AVALANCHE, - sourceTxHash: fakeHash, - sourceStartedAt: expect.any(Number), - targetChain: Blockchain.BITCOIN, - amount, - symbol: 'BTC', - }); - }); - }); - }); -}); diff --git a/src/pages/Bridge/hooks/useAvalancheBridge.ts b/src/pages/Bridge/hooks/useAvalancheBridge.ts deleted file mode 100644 index 01cf8c360..000000000 --- a/src/pages/Bridge/hooks/useAvalancheBridge.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { BIG_ZERO, Blockchain, useBridgeSDK } from '@avalabs/core-bridge-sdk'; -import { BridgeAdapter } from './useBridge'; -import { useAssetBalancesEVM } from './useAssetBalancesEVM'; -import { useCallback } from 'react'; -import { useBridgeContext } from '@src/contexts/BridgeProvider'; -import Big from 'big.js'; - -/** - * Hook for when the source is Avalanche - */ -export function useAvalancheBridge( - amount: Big, - bridgeFee: Big, - minimum: Big -): BridgeAdapter { - const { - targetBlockchain, - currentBlockchain, - setTransactionDetails, - currentAssetData, - } = useBridgeSDK(); - - const { createBridgeTransaction, transferEVMAsset, estimateGas } = - useBridgeContext(); - - const isAvalancheBridge = currentBlockchain === Blockchain.AVALANCHE; - - const { assetsWithBalances: selectedAssetWithBalances } = useAssetBalancesEVM( - Blockchain.AVALANCHE, - isAvalancheBridge ? currentAssetData : undefined - ); - const sourceBalance = selectedAssetWithBalances[0]; - - const { assetsWithBalances } = useAssetBalancesEVM(Blockchain.AVALANCHE); - - const maximum = sourceBalance?.balance || BIG_ZERO; - const receiveAmount = amount.gt(minimum) ? amount.minus(bridgeFee) : BIG_ZERO; - - const transfer = useCallback(async () => { - if (!currentAssetData) { - throw new Error('No asset selected'); - } - - const timestamp = Date.now(); - const result = await transferEVMAsset(amount, currentAssetData); - - setTransactionDetails({ - tokenSymbol: currentAssetData.symbol, - amount, - }); - - createBridgeTransaction({ - sourceChain: Blockchain.AVALANCHE, - sourceTxHash: result.hash, - sourceStartedAt: timestamp, - targetChain: targetBlockchain, - amount, - symbol: currentAssetData.symbol, - }); - - return result.hash; - }, [ - amount, - createBridgeTransaction, - currentAssetData, - setTransactionDetails, - targetBlockchain, - transferEVMAsset, - ]); - - return { - sourceBalance, - assetsWithBalances, - receiveAmount, - maximum, - price: sourceBalance?.price, - estimateGas, - transfer, - }; -} diff --git a/src/pages/Bridge/hooks/useBridge.ts b/src/pages/Bridge/hooks/useBridge.ts index 6b3535ebd..0da83895d 100644 --- a/src/pages/Bridge/hooks/useBridge.ts +++ b/src/pages/Bridge/hooks/useBridge.ts @@ -1,108 +1,206 @@ -import Big from 'big.js'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { - BIG_ZERO, - Blockchain, - useBridgeSDK, - useBridgeFeeEstimate, - WrapStatus, - useMinimumTransferAmount, - Asset, -} from '@avalabs/core-bridge-sdk'; -import { useMemo, useState } from 'react'; - -import { AssetBalance } from '../models'; -import { useUnifiedBridgeContext } from '@src/contexts/UnifiedBridgeProvider'; + BridgeAsset, + TokenType as BridgeTokenType, +} from '@avalabs/bridge-unified'; +import { + NftTokenWithBalance, + TokenType, + TokenWithBalance, +} from '@avalabs/vm-module-types'; + +import { NetworkWithCaipId } from '@src/background/services/network/models'; import { useNetworkContext } from '@src/contexts/NetworkProvider'; -import { ChainId } from '@avalabs/core-chains-sdk'; -import { BridgeStepDetails } from '@avalabs/bridge-unified'; -import { chainIdToCaip } from '@src/utils/caipConversion'; - -export interface BridgeAdapter { - address?: string; - sourceBalance?: AssetBalance; - targetBalance?: AssetBalance; - assetsWithBalances?: AssetBalance[]; - loading?: boolean; - networkFee?: Big; - bridgeFee?: Big; - /** Amount minus network and bridge fees */ - receiveAmount?: Big; - /** Maximum transfer amount */ - maximum?: Big; - /** Minimum transfer amount */ - minimum?: Big; - /** Price for the current asset & currency code */ - price?: number; - wrapStatus?: WrapStatus; - txHash?: string; - /** - * Transfer funds to the target blockchain - * @returns the transaction hash - */ - transfer: () => Promise; - estimateGas(amount: Big, asset?: Asset): Promise; - bridgeStep?: BridgeStepDetails; -} +import { useUnifiedBridgeContext } from '@src/contexts/UnifiedBridgeProvider'; +import { useAnalyticsContext } from '@src/contexts/AnalyticsProvider'; +import { useTokensWithBalances } from '@src/hooks/useTokensWithBalances'; +import { isNFT } from '@src/background/services/balances/nft/utils/isNFT'; + +import { findMatchingBridgeAsset } from '../utils/findMatchingBridgeAsset'; interface Bridge { - amount: Big; - setAmount: (amount: Big) => void; - bridgeFee: Big; - provider: BridgeProviders; - minimum: Big; - targetChainId: string; + amount?: bigint; + setAmount: (amount: bigint) => void; + availableChainIds: string[]; + estimateGas: () => Promise; + isReady: boolean; + targetChain?: NetworkWithCaipId; + setTargetChain: (targetChain: NetworkWithCaipId) => void; + asset?: BridgeAsset; + setAsset: (asset: BridgeAsset) => void; + sourceBalance?: Exclude; + possibleTargetChains: string[]; + minimum?: bigint; + maximum?: bigint; + receiveAmount?: bigint; + bridgeFee?: bigint; + bridgableTokens: Exclude[]; + transferableAssets: BridgeAsset[]; + transfer: () => Promise; } -export enum BridgeProviders { - Avalanche, - Unified, -} +export function useBridge(): Bridge { + const { network, getNetwork } = useNetworkContext(); + const { capture } = useAnalyticsContext(); + const { + availableChainIds, + estimateTransferGas, + getFee, + isReady, + transferableAssets, + transferAsset, + } = useUnifiedBridgeContext(); + const [amount, setAmount] = useState(); + const [asset, setAsset] = useState(); + const firstTargetChainId = Object.keys(asset?.destinations ?? {})[0] ?? ''; + const [targetChain, setTargetChain] = useState( + firstTargetChainId ? getNetwork(firstTargetChainId) : undefined + ); + + const [receiveAmount, setReceiveAmount] = useState(); + const [maximum, setMaximum] = useState(); + const [minimum, setMinimum] = useState(); + const [bridgeFee, setBridgeFee] = useState(); + const balances = useTokensWithBalances({ + chainId: network?.chainId, + forceShowTokensWithoutBalances: true, + }); + + const bridgableTokens = useMemo(() => { + const nonNFTs = balances.filter( + (t): t is Exclude => !isNFT(t) + ); + + return nonNFTs.filter((t) => + findMatchingBridgeAsset(transferableAssets, t) + ); + }, [balances, transferableAssets]); + + const sourceBalance = useMemo(() => { + if (!asset) { + return; + } + + return bridgableTokens.find((token) => { + if ( + asset.type === BridgeTokenType.NATIVE && + token.type === TokenType.NATIVE + ) { + return asset.symbol.toLowerCase() === token.symbol.toLowerCase(); + } + + if ( + asset.type === BridgeTokenType.ERC20 && + token.type === TokenType.ERC20 + ) { + return asset.address?.toLowerCase() === token.address.toLowerCase(); + } + + return false; + }); + }, [asset, bridgableTokens]); + + const possibleTargetChains = useMemo(() => { + return Object.keys(asset?.destinations ?? {}); + }, [asset?.destinations]); + + useEffect(() => { + let isMounted = true; + + if (asset && amount && targetChain) { + getFee(asset.symbol, amount, targetChain.caipId).then((fee) => { + if (!isMounted) { + return; + } + + setBridgeFee(fee); + setMinimum(fee); + setReceiveAmount(amount - fee); + }); + + setMaximum(sourceBalance?.balance); + } + + return () => { + isMounted = false; + }; + }, [amount, asset, getFee, targetChain, sourceBalance?.balance]); + + const estimateGas = useCallback(async () => { + if (!asset?.symbol || !amount || !targetChain?.caipId) { + return 0n; + } + + return estimateTransferGas(asset.symbol, amount, targetChain?.caipId); + }, [estimateTransferGas, targetChain?.caipId, asset?.symbol, amount]); + + const transfer = useCallback(async () => { + capture('unifedBridgeTransferStarted', { + bridgeType: 'CCTP', // TODO: no longer CCTP only + sourceBlockchain: network?.caipId, + targetBlockchain: targetChain?.caipId, + }); + + if (!amount) { + throw new Error('No amount chosen'); + } + + if (!asset) { + throw new Error('No asset chosen'); + } + + if (!network?.caipId) { + throw new Error('No source chain chosen'); + } + + if (!targetChain?.caipId) { + throw new Error('No target chain chosen'); + } + + const hash = await transferAsset(asset.symbol, amount, targetChain?.caipId); + + return hash; + }, [ + amount, + asset, + targetChain?.caipId, + transferAsset, + capture, + network?.caipId, + ]); -export function useBridge(currentAssetIdentifier?: string): Bridge { - const { targetBlockchain } = useBridgeSDK(); - const { supportsAsset } = useUnifiedBridgeContext(); - - const [amount, setAmount] = useState(BIG_ZERO); - - const bridgeFee = useBridgeFeeEstimate(amount) || BIG_ZERO; - const minimum = useMinimumTransferAmount(amount); - const { isDeveloperMode } = useNetworkContext(); - - const targetChainId = useMemo(() => { - switch (targetBlockchain) { - case Blockchain.ETHEREUM: - return isDeveloperMode - ? ChainId.ETHEREUM_TEST_SEPOLIA - : ChainId.ETHEREUM_HOMESTEAD; - - case Blockchain.AVALANCHE: - return isDeveloperMode - ? ChainId.AVALANCHE_TESTNET_ID - : ChainId.AVALANCHE_MAINNET_ID; - - case Blockchain.BITCOIN: - return isDeveloperMode ? ChainId.BITCOIN_TESTNET : ChainId.BITCOIN; - - 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; + useEffect(() => { + if (targetChain && possibleTargetChains.includes(targetChain.caipId)) { + return; } - }, [isDeveloperMode, targetBlockchain]); + + if (possibleTargetChains[0]) { + const foundChain = getNetwork(possibleTargetChains[0]); + + if (foundChain) { + setTargetChain(foundChain); + } + } + }, [getNetwork, targetChain, possibleTargetChains]); return { amount, setAmount, - minimum, + bridgableTokens: bridgableTokens, + availableChainIds, bridgeFee, - targetChainId: chainIdToCaip(targetChainId), - provider: - currentAssetIdentifier && - supportsAsset(currentAssetIdentifier, chainIdToCaip(targetChainId)) - ? BridgeProviders.Unified - : BridgeProviders.Avalanche, + estimateGas, + isReady, + minimum, + maximum, + receiveAmount, + asset, + setAsset, + sourceBalance, + targetChain, + setTargetChain, + possibleTargetChains, + transferableAssets, + transfer, }; } diff --git a/src/pages/Bridge/hooks/useBtcBridge.test.ts b/src/pages/Bridge/hooks/useBtcBridge.test.ts deleted file mode 100644 index 7d4f5a641..000000000 --- a/src/pages/Bridge/hooks/useBtcBridge.test.ts +++ /dev/null @@ -1,250 +0,0 @@ -import Big from 'big.js'; -import { BN } from 'bn.js'; -import { act, renderHook } from '@testing-library/react-hooks'; -import { - useBridgeConfig, - useBridgeSDK, - getBtcAsset, - Blockchain, - btcToSatoshi, -} from '@avalabs/core-bridge-sdk'; - -import { useBridgeContext } from '@src/contexts/BridgeProvider'; -import { useAccountsContext } from '@src/contexts/AccountsProvider'; -import { useNetworkFeeContext } from '@src/contexts/NetworkFeeProvider'; -import { useConnectionContext } from '@src/contexts/ConnectionProvider'; -import { useTokensWithBalances } from '@src/hooks/useTokensWithBalances'; -import { DAppProviderRequest } from '@src/background/connections/dAppConnection/models'; - -import { useBtcBridge } from './useBtcBridge'; - -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (k) => k, - }), -})); -jest.mock('@avalabs/core-bridge-sdk'); -jest.mock('@src/contexts/BridgeProvider'); -jest.mock('@src/contexts/AccountsProvider'); -jest.mock('@src/contexts/NetworkFeeProvider'); -jest.mock('@src/contexts/ConnectionProvider'); -jest.mock('@src/hooks/useTokensWithBalances'); - -describe('src/pages/Bridge/hooks/useBtcBridge', () => { - let requestFn = jest.fn(); - let createBridgeTransactionFn = jest.fn(); - - const highFee = 20n; - const lowFee = 8n; - - beforeEach(() => { - jest.resetAllMocks(); - - requestFn = jest.fn(); - createBridgeTransactionFn = jest.fn(); - - jest.mocked(useConnectionContext).mockReturnValue({ - request: requestFn, - } as any); - - jest.mocked(useAccountsContext).mockReturnValue({ - accounts: { - active: { - addressBTC: 'user-btc-address', - }, - }, - } as any); - - jest.mocked(useTokensWithBalances).mockReturnValue([ - { - symbol: 'BTC', - decimals: 8, - balance: new BN('10000000', 8), - } as any, - ]); - - jest.mocked(useBridgeContext).mockReturnValue({ - createBridgeTransaction: createBridgeTransactionFn, - } as any); - - jest.mocked(getBtcAsset).mockReturnValue({ - symbol: 'BTC', - denomination: 8, - tokenName: 'Bitcoin', - } as any); - - jest.mocked(useBridgeSDK).mockReturnValue({ - setTransactionDetails: jest.fn(), - currentBlockchain: Blockchain.BITCOIN, - } as any); - - jest.mocked(useBridgeConfig).mockReturnValue({ - config: { - criticalBitcoin: { - walletAddresses: { - btc: 'bridge-btc-address', - avalanche: 'bridge-avax-address', - }, - }, - }, - } as any); - - jest.mocked(useNetworkFeeContext).mockReturnValue({ - networkFee: { - high: { - maxFee: highFee, - }, - low: { - maxFee: lowFee, - }, - displayDecimals: 8, - }, - } as any); - }); - - describe('transfer()', () => { - describe('when active account has no BTC address', () => { - beforeEach(() => { - jest.mocked(useAccountsContext).mockReturnValue({ - accounts: { - active: { - addressC: 'user-c-address', - addressBTC: '', - }, - }, - } as any); - }); - - it('throws error', async () => { - const amount = new Big('0.1'); - const { result: hook } = renderHook(() => useBtcBridge(amount)); - - await act(async () => { - await expect(hook.current.transfer()).rejects.toThrow( - 'Unsupported account' - ); - }); - }); - }); - - describe('when bridge config is not loaded yet', () => { - beforeEach(() => { - jest.mocked(useBridgeConfig).mockReturnValue({ config: undefined }); - }); - - it('throws error', async () => { - const amount = new Big('0.1'); - const { result: hook } = renderHook(() => useBtcBridge(amount)); - - await act(async () => { - await expect(hook.current.transfer()).rejects.toThrow( - 'Bridge not ready' - ); - }); - }); - }); - - describe('when fee rate is not loaded yet', () => { - beforeEach(() => { - jest.mocked(useNetworkFeeContext).mockReturnValue({ - currentFeeInfo: null, - } as any); - }); - - it('throws error', async () => { - const amount = new Big('0.1'); - const { result: hook } = renderHook(() => useBtcBridge(amount)); - - await act(async () => { - await expect(hook.current.transfer()).rejects.toThrow( - 'Bridge not ready' - ); - }); - }); - }); - - describe('when fee rate is not loaded yet', () => { - beforeEach(() => { - jest.mocked(useNetworkFeeContext).mockReturnValue({ - networkFee: null, - } as any); - }); - - it('throws error', async () => { - const amount = new Big('0.1'); - const { result: hook } = renderHook(() => useBtcBridge(amount)); - - await act(async () => { - await expect(hook.current.transfer()).rejects.toThrow( - 'Bridge not ready' - ); - }); - }); - }); - - describe('when amount is not provided', () => { - it('throws error', async () => { - const amount = new Big('0'); - const { result: hook } = renderHook(() => useBtcBridge(amount)); - - await act(async () => { - await expect(hook.current.transfer()).rejects.toThrow( - 'Amount not provided' - ); - }); - }); - }); - - it('sends a bitcoin_sendTransaction request with proper parameters', async () => { - const amount = new Big('0.1'); - const { result: hook } = renderHook(() => useBtcBridge(amount)); - - const fakeHash = '0xTxHash'; - - requestFn.mockResolvedValue(fakeHash); - - await act(async () => { - const hash = await hook.current.transfer(); - - expect(requestFn).toHaveBeenCalledWith( - { - method: DAppProviderRequest.BITCOIN_SEND_TRANSACTION, - params: { - from: 'user-btc-address', - to: 'bridge-btc-address', - amount: btcToSatoshi(amount), - feeRate: Number(highFee), - }, - }, - { - customApprovalScreenTitle: 'Confirm Bridge', - } - ); - - expect(hash).toEqual(fakeHash); - }); - }); - - it('tracks the bridge transaction', async () => { - const amount = new Big('0.1'); - const { result: hook } = renderHook(() => useBtcBridge(amount)); - - const fakeHash = '0xTxHash'; - - requestFn.mockResolvedValue(fakeHash); - - await act(async () => { - await hook.current.transfer(); - - expect(createBridgeTransactionFn).toHaveBeenCalledWith({ - sourceChain: Blockchain.BITCOIN, - sourceTxHash: fakeHash, - sourceStartedAt: expect.any(Number), - targetChain: Blockchain.AVALANCHE, - amount, - symbol: 'BTC', - }); - }); - }); - }); -}); diff --git a/src/pages/Bridge/hooks/useBtcBridge.ts b/src/pages/Bridge/hooks/useBtcBridge.ts deleted file mode 100644 index c95d42c8f..000000000 --- a/src/pages/Bridge/hooks/useBtcBridge.ts +++ /dev/null @@ -1,263 +0,0 @@ -import { - BIG_ZERO, - Blockchain, - btcToSatoshi, - getBtcAsset, - getBtcTransactionDetails, - satoshiToBtc, - useBridgeConfig, - useBridgeSDK, -} from '@avalabs/core-bridge-sdk'; -import { ChainId } from '@avalabs/core-chains-sdk'; -import { BitcoinSendTransactionParams } from '@avalabs/bitcoin-module'; -import { RpcMethod, TokenWithBalanceBTC } from '@avalabs/vm-module-types'; -import { - BitcoinInputUTXOWithOptionalScript, - getMaxTransferAmount, -} from '@avalabs/core-wallets-sdk'; -import { useAccountsContext } from '@src/contexts/AccountsProvider'; -import { useBridgeContext } from '@src/contexts/BridgeProvider'; -import { useConnectionContext } from '@src/contexts/ConnectionProvider'; -import { useNetworkContext } from '@src/contexts/NetworkProvider'; -import { useNetworkFeeContext } from '@src/contexts/NetworkFeeProvider'; -import Big from 'big.js'; -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { AssetBalance } from '../models'; -import { BridgeAdapter } from './useBridge'; -import { TransactionPriority } from '@src/background/services/networkFee/models'; -import { useTokensWithBalances } from '@src/hooks/useTokensWithBalances'; - -import { getBtcInputUtxos } from '@src/utils/send/btcSendUtils'; -import { useTranslation } from 'react-i18next'; -import { normalizeBalance } from '@src/utils/normalizeBalance'; - -/** - * Hook for Bitcoin to Avalanche transactions - */ -export function useBtcBridge(amountInBtc: Big): BridgeAdapter { - const { setTransactionDetails, currentBlockchain } = useBridgeSDK(); - const isBitcoinBridge = currentBlockchain === Blockchain.BITCOIN; - const { t } = useTranslation(); - const { request } = useConnectionContext(); - const { bitcoinProvider, isDeveloperMode } = useNetworkContext(); - const { networkFee: currentFeeInfo } = useNetworkFeeContext(); - const { config } = useBridgeConfig(); - const { createBridgeTransaction } = useBridgeContext(); - const btcTokens = useTokensWithBalances({ - forceShowTokensWithoutBalances: true, - chainId: isDeveloperMode ? ChainId.BITCOIN_TESTNET : ChainId.BITCOIN, - }); - const { - accounts: { active: activeAccount }, - } = useAccountsContext(); - - const [btcBalance, setBtcBalance] = useState(); - const [utxos, setUtxos] = useState([]); - - const btcToken = useMemo( - () => - btcTokens.find((tok): tok is TokenWithBalanceBTC => tok.symbol === 'BTC'), - [btcTokens] - ); - - // Update the fee rate so we're able to calculate the - // max. bridgable amount for the main bridge screen - const feeRate: number = useMemo(() => { - if (!currentFeeInfo) { - return 0; - } - - // Because BTC testnet fees are super high recently, - // defaulting to cheaper preset makes testing easier. - const preset: TransactionPriority = isDeveloperMode ? 'low' : 'high'; - - return Number(currentFeeInfo[preset].maxFee); - }, [currentFeeInfo, isDeveloperMode]); - - // Calculate the maximum bridgable BTC amount whwnever - const maximum = useMemo(() => { - if (!feeRate || !config || !activeAccount?.addressBTC) { - return Big(0); - } - - const maxAmt = getMaxTransferAmount( - utxos, - // As long as the address type is the same (P2WPKH) it should not matter. - config.criticalBitcoin.walletAddresses.btc, - activeAccount.addressBTC, - feeRate - ); - - return satoshiToBtc(maxAmt); - }, [utxos, config, feeRate, activeAccount?.addressBTC]); - - /** Amount minus network and bridge fees (in BTC) */ - const [receiveAmount, setReceiveAmount] = useState(); - const loading = !btcBalance || !currentFeeInfo || !feeRate; - const amountInSatoshis = btcToSatoshi(amountInBtc); - - const btcAsset = config && getBtcAsset(config); - const assetsWithBalances = btcBalance ? [btcBalance] : []; - - // Update balances for the UI - useEffect(() => { - if (!isBitcoinBridge || !btcAsset || !btcToken) { - return; - } - - setBtcBalance({ - symbol: btcAsset.symbol, - asset: btcAsset, - balance: normalizeBalance(btcToken.balance, btcToken.decimals), - logoUri: btcToken.logoUri, - price: btcToken.priceInCurrency, - unconfirmedBalance: btcToken.unconfirmedBalance - ? normalizeBalance(btcToken.unconfirmedBalance, btcToken.decimals) - : new Big(0), - }); - }, [btcToken, btcAsset, isBitcoinBridge]); - - // Filter UTXOs whenever balance or fee rate is updated - // so we can calculate the max. bridgable amount. - useEffect(() => { - let isMounted = true; - - if (!bitcoinProvider || !feeRate || !btcToken) { - return; - } - - getBtcInputUtxos(bitcoinProvider, btcToken, feeRate) - .then((_utxos) => { - if (isMounted) { - setUtxos(_utxos); - } - }) - .catch((err) => { - console.error(err); - if (isMounted) { - setUtxos([]); - } - }); - - return () => { - isMounted = false; - }; - }, [bitcoinProvider, btcToken, feeRate]); - - useEffect(() => { - if (!isBitcoinBridge || !config || !activeAccount?.addressBTC || !utxos) { - return; - } - - try { - const btcTx = getBtcTransactionDetails( - config, - activeAccount.addressBTC, - utxos, - amountInSatoshis, - feeRate - ); - - setReceiveAmount(satoshiToBtc(btcTx.receiveAmount)); - } catch (error) { - // getBtcTransaction throws an error when the amount is too low or too high - // so set these to 0 - setReceiveAmount(BIG_ZERO); - } - }, [ - amountInSatoshis, - activeAccount?.addressBTC, - config, - isBitcoinBridge, - utxos, - feeRate, - ]); - - const transferBTC = useCallback(async () => { - if (!config || !feeRate) { - throw new Error('Bridge not ready'); - } - - if (amountInBtc.lte(0)) { - throw new Error('Amount not provided'); - } - - if (!activeAccount?.addressBTC) { - throw new Error('Unsupported account'); - } - - const symbol = 'BTC'; - const hash = await request( - { - method: RpcMethod.BITCOIN_SEND_TRANSACTION, - params: { - from: activeAccount.addressBTC, - to: config.criticalBitcoin.walletAddresses.btc, - feeRate, - amount: btcToSatoshi(amountInBtc), - }, - }, - { customApprovalScreenTitle: t('Confirm Bridge') } - ); - - setTransactionDetails({ - tokenSymbol: symbol, - amount: amountInBtc, - }); - createBridgeTransaction({ - sourceChain: Blockchain.BITCOIN, - sourceTxHash: hash, - sourceStartedAt: Date.now(), - targetChain: Blockchain.AVALANCHE, - amount: amountInBtc, - symbol, - }); - - return hash; - }, [ - request, - activeAccount?.addressBTC, - t, - amountInBtc, - config, - createBridgeTransaction, - feeRate, - setTransactionDetails, - ]); - - const estimateGas = useCallback( - async (amount: Big) => { - if (!config || !activeAccount?.addressBTC) { - return; - } - - // 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 fakeFeeRate = 1; - const { fee: byteLength } = getBtcTransactionDetails( - config, - activeAccount.addressBTC, - utxos, - btcToSatoshi(amount), - fakeFeeRate - ); - - return BigInt(byteLength); - }, - [activeAccount?.addressBTC, config, utxos] - ); - - return { - address: activeAccount?.addressBTC, - sourceBalance: btcBalance, - assetsWithBalances, - loading, - receiveAmount, - maximum, - price: btcBalance?.price, - estimateGas, - transfer: transferBTC, - }; -} diff --git a/src/pages/Bridge/hooks/useEthBridge.test.ts b/src/pages/Bridge/hooks/useEthBridge.test.ts deleted file mode 100644 index 184ca508f..000000000 --- a/src/pages/Bridge/hooks/useEthBridge.test.ts +++ /dev/null @@ -1,228 +0,0 @@ -import Big from 'big.js'; -import { - useBridgeSDK, - Blockchain, - AssetType, - isNativeAsset, - getMaxTransferAmount, -} from '@avalabs/core-bridge-sdk'; -import { act, renderHook } from '@testing-library/react-hooks'; - -import { useBridgeContext } from '@src/contexts/BridgeProvider'; -import { useAccountsContext } from '@src/contexts/AccountsProvider'; -import { useNetworkFeeContext } from '@src/contexts/NetworkFeeProvider'; -import { useConnectionContext } from '@src/contexts/ConnectionProvider'; - -import { useEthBridge } from './useEthBridge'; -import { useAssetBalancesEVM } from './useAssetBalancesEVM'; -import { useNetworkContext } from '@src/contexts/NetworkProvider'; - -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (k) => k, - }), -})); -jest.mock('@avalabs/core-bridge-sdk'); -jest.mock('./useAssetBalancesEVM'); -jest.mock('@src/contexts/BridgeProvider'); -jest.mock('@src/contexts/NetworkProvider'); -jest.mock('@src/contexts/AccountsProvider'); -jest.mock('@src/contexts/NetworkFeeProvider'); -jest.mock('@src/contexts/ConnectionProvider'); - -describe('src/pages/Bridge/hooks/useEthBridge', () => { - let requestFn = jest.fn(); - let createBridgeTransactionFn = jest.fn(); - let transferEVMAssetFn = jest.fn(); - - const highFee = 20n; - const lowFee = 8n; - - const currentAssetData = { - assetType: AssetType.NATIVE, - denomination: 18, - wrappedAssetSymbol: 'WETH', - tokenName: 'Ethereum', - symbol: 'ETH', - nativeNetwork: Blockchain.ETHEREUM, - }; - - beforeEach(() => { - jest.resetAllMocks(); - - requestFn = jest.fn(); - transferEVMAssetFn = jest.fn(); - createBridgeTransactionFn = jest.fn(); - - jest.mocked(getMaxTransferAmount).mockResolvedValue(new Big('0.01')); - - jest.mocked(useConnectionContext).mockReturnValue({ - request: requestFn, - } as any); - - jest.mocked(useAccountsContext).mockReturnValue({ - accounts: { - active: { - addressC: 'user-c-address', - addressBTC: 'user-btc-address', - }, - }, - } as any); - - jest.mocked(useAssetBalancesEVM).mockReturnValue({ - assetsWithBalances: [ - { - symbol: 'ETH', - asset: currentAssetData, - balance: new Big('0.1'), - price: 2000, - }, - ], - } as any); - - jest.mocked(useBridgeContext).mockReturnValue({ - createBridgeTransaction: createBridgeTransactionFn, - transferEVMAsset: transferEVMAssetFn, - } as any); - - jest.mocked(useBridgeSDK).mockReturnValue({ - bridgeConfig: { - config: {}, - }, - currentAsset: 'ETH', - currentAssetData, - setTransactionDetails: jest.fn(), - currentBlockchain: Blockchain.ETHEREUM, - } as any); - - jest.mocked(useNetworkFeeContext).mockReturnValue({ - networkFee: { - high: { - maxFee: highFee, - }, - low: { - maxFee: lowFee, - }, - displayDecimals: 8, - }, - } as any); - - jest.mocked(useNetworkContext).mockReturnValue({ - ethereumProvider: {}, - } as any); - }); - - it('provides maximum transfer amount', async () => { - const amount = new Big('0.1'); - const fee = new Big('0.0003'); - const minimum = new Big('0.0006'); - - const fakeMax = new Big('0.9'); - jest.mocked(getMaxTransferAmount).mockResolvedValue(fakeMax); - - const { result: hook } = renderHook(() => - useEthBridge(amount, fee, minimum) - ); - - await act(async () => { - expect(getMaxTransferAmount).toHaveBeenCalled(); - }); - - // Wait for the state to be set - await new Promise(process.nextTick); - - expect(hook.current.maximum).toEqual(fakeMax); - }); - - describe('transfer()', () => { - beforeEach(() => { - jest.mocked(isNativeAsset).mockReturnValue(true); - }); - - describe('when no asset is selected', () => { - beforeEach(() => { - jest.mocked(useBridgeSDK).mockReturnValue({ - bridgeConfig: { - config: {}, - }, - currentAsset: '', - currentAssetData: undefined, - setTransactionDetails: jest.fn(), - currentBlockchain: Blockchain.ETHEREUM, - } as any); - }); - - it('throws error', async () => { - const amount = new Big('0.1'); - const fee = new Big('0.0003'); - const minimum = new Big('0.0006'); - - const { result: hook } = renderHook(() => - useEthBridge(amount, fee, minimum) - ); - - await act(async () => { - await expect(hook.current.transfer()).rejects.toThrow( - 'No asset selected' - ); - }); - }); - }); - - it("calls the provider's transfer function", async () => { - const amount = new Big('0.1'); - const fee = new Big('0.0003'); - const minimum = new Big('0.0006'); - - const { result: hook } = renderHook(() => - useEthBridge(amount, fee, minimum) - ); - - const fakeHash = '0xHash'; - - transferEVMAssetFn.mockResolvedValue({ - hash: fakeHash, - }); - - await act(async () => { - const hash = await hook.current.transfer(); - - expect(transferEVMAssetFn).toHaveBeenCalledWith( - amount, - currentAssetData - ); - - expect(hash).toEqual(fakeHash); - }); - }); - - it('tracks the bridge transaction', async () => { - const amount = new Big('0.1'); - const fee = new Big('0.0003'); - const minimum = new Big('0.0006'); - - const { result: hook } = renderHook(() => - useEthBridge(amount, fee, minimum) - ); - - const fakeHash = '0xHash'; - - transferEVMAssetFn.mockResolvedValue({ - hash: fakeHash, - }); - - await act(async () => { - await hook.current.transfer(); - - expect(createBridgeTransactionFn).toHaveBeenCalledWith({ - sourceChain: Blockchain.ETHEREUM, - sourceTxHash: fakeHash, - sourceStartedAt: expect.any(Number), - targetChain: Blockchain.AVALANCHE, - amount, - symbol: 'WETH', - }); - }); - }); - }); -}); diff --git a/src/pages/Bridge/hooks/useEthBridge.ts b/src/pages/Bridge/hooks/useEthBridge.ts deleted file mode 100644 index 605aaa14d..000000000 --- a/src/pages/Bridge/hooks/useEthBridge.ts +++ /dev/null @@ -1,135 +0,0 @@ -import Big from 'big.js'; -import { - BIG_ZERO, - Blockchain, - getAssets, - getMaxTransferAmount, - isNativeAsset, - useBridgeSDK, -} from '@avalabs/core-bridge-sdk'; -import { useBridgeContext } from '@src/contexts/BridgeProvider'; -import { useCallback, useEffect, useState } from 'react'; -import { useAssetBalancesEVM } from './useAssetBalancesEVM'; -import { BridgeAdapter } from './useBridge'; -import { useNetworkContext } from '@src/contexts/NetworkProvider'; - -/** - * Hook for when the bridge source chain is Ethereum - */ -export function useEthBridge( - amount: Big, - bridgeFee: Big, - minimum: Big -): BridgeAdapter { - const { - currentAsset, - currentAssetData, - bridgeConfig, - setTransactionDetails, - currentBlockchain, - } = useBridgeSDK(); - const [maximum, setMaximum] = useState(undefined); - const isEthereumBridge = currentBlockchain === Blockchain.ETHEREUM; - - const { createBridgeTransaction, transferEVMAsset, estimateGas } = - useBridgeContext(); - const { assetsWithBalances } = useAssetBalancesEVM(Blockchain.ETHEREUM); - const { ethereumProvider } = useNetworkContext(); - const sourceBalance = assetsWithBalances.find( - ({ asset }) => asset.symbol === currentAsset - ); - - const receiveAmount = amount.gt(minimum) ? amount.minus(bridgeFee) : BIG_ZERO; - const sourceBalanceAsString = sourceBalance?.balance?.toString(); - - useEffect(() => { - if ( - !currentAsset || - !isEthereumBridge || - !bridgeConfig.config || - !sourceBalanceAsString || - !ethereumProvider - ) { - return; - } - - const ethereumAssets = getAssets(currentBlockchain, bridgeConfig.config); - let isMounted = true; - - // Estimating gas can take a couple seconds - reset it before calculating - // so we don't use a stale value. - setMaximum(undefined); - - getMaxTransferAmount({ - currentBlockchain, - balance: new Big(sourceBalanceAsString), - currentAsset, - assets: ethereumAssets, - provider: ethereumProvider, - config: bridgeConfig.config, - }).then((max) => { - if (!isMounted) { - return; - } - - setMaximum(max ?? undefined); - }); - - return () => { - isMounted = false; - }; - }, [ - bridgeConfig?.config, - currentAsset, - currentBlockchain, - ethereumProvider, - isEthereumBridge, - sourceBalanceAsString, - ]); - - const transfer = useCallback(async () => { - if (!currentAssetData) { - throw new Error('No asset selected'); - } - - const timestamp = Date.now(); - - const symbol = isNativeAsset(currentAssetData) - ? currentAssetData.wrappedAssetSymbol - : currentAsset || ''; - - const result = await transferEVMAsset(amount, currentAssetData); - - setTransactionDetails({ - tokenSymbol: symbol, - amount, - }); - createBridgeTransaction({ - sourceChain: Blockchain.ETHEREUM, - sourceTxHash: result.hash, - sourceStartedAt: timestamp, - targetChain: Blockchain.AVALANCHE, - amount, - symbol, - }); - - return result.hash; - }, [ - amount, - currentAssetData, - createBridgeTransaction, - currentAsset, - setTransactionDetails, - transferEVMAsset, - ]); - - return { - sourceBalance, - assetsWithBalances, - receiveAmount, - maximum, - price: sourceBalance?.price, - estimateGas, - transfer, - }; -} diff --git a/src/pages/Bridge/hooks/useHasEnoughtForGas.ts b/src/pages/Bridge/hooks/useHasEnoughtForGas.ts index c9a61f846..5edb33e7e 100644 --- a/src/pages/Bridge/hooks/useHasEnoughtForGas.ts +++ b/src/pages/Bridge/hooks/useHasEnoughtForGas.ts @@ -22,7 +22,7 @@ export const useHasEnoughForGas = (gasLimit?: bigint): boolean => { // get gasPrice of network const balance = token && token.balance; - const estimatedGasCost = networkFee.low.maxFee * gasLimit; + const estimatedGasCost = (networkFee.low.maxFee / 2n) * gasLimit; // check if balance > gasPrice if (balance && estimatedGasCost) { setHasEnough( diff --git a/src/pages/Bridge/hooks/useSetBridgeChainFromNetwork.ts b/src/pages/Bridge/hooks/useSetBridgeChainFromNetwork.ts deleted file mode 100644 index 7fe4446b1..000000000 --- a/src/pages/Bridge/hooks/useSetBridgeChainFromNetwork.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Blockchain, useBridgeSDK } from '@avalabs/core-bridge-sdk'; -import { ChainId } from '@avalabs/core-chains-sdk'; -import { useNetworkContext } from '@src/contexts/NetworkProvider'; -import { useEffect } from 'react'; - -export const useSetBridgeChainFromNetwork = () => { - const { network } = useNetworkContext(); - const { setCurrentBlockchain } = useBridgeSDK(); - - useEffect(() => { - switch (network?.chainId) { - case ChainId.BITCOIN: - case ChainId.BITCOIN_TESTNET: - setCurrentBlockchain(Blockchain.BITCOIN); - } - // Run once - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); -}; diff --git a/src/pages/Bridge/hooks/useUnifiedBridge.ts b/src/pages/Bridge/hooks/useUnifiedBridge.ts deleted file mode 100644 index 730b9b2ae..000000000 --- a/src/pages/Bridge/hooks/useUnifiedBridge.ts +++ /dev/null @@ -1,187 +0,0 @@ -import Big from 'big.js'; -import { bigToBigInt } from '@avalabs/core-utils-sdk'; -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { - Asset, - BIG_ZERO, - Blockchain, - isNativeAsset, - useBridgeSDK, -} from '@avalabs/core-bridge-sdk'; - -import { useUnifiedBridgeContext } from '@src/contexts/UnifiedBridgeProvider'; -import { bigintToBig } from '@src/utils/bigintToBig'; -import { useNetworkContext } from '@src/contexts/NetworkProvider'; - -import { useAssetBalancesEVM } from './useAssetBalancesEVM'; -import { BridgeAdapter } from './useBridge'; -import { isUnifiedBridgeAsset } from '../utils/isUnifiedBridgeAsset'; -import { useAnalyticsContext } from '@src/contexts/AnalyticsProvider'; - -/** - * Hook for when the Unified Bridge SDK can handle the transfer - */ -export function useUnifiedBridge( - amount: Big, - targetChainId: string, - currentAssetIdentifier?: string -): BridgeAdapter { - const { - currentAsset, - currentAssetData, - setTransactionDetails, - currentBlockchain, - targetBlockchain, - } = useBridgeSDK(); - const { network } = useNetworkContext(); - const { capture } = useAnalyticsContext(); - const { estimateTransferGas, getFee, transferAsset, supportsAsset } = - useUnifiedBridgeContext(); - - const [receiveAmount, setReceiveAmount] = useState(); - const [maximum, setMaximum] = useState(); - const [minimum, setMinimum] = useState(); - const [bridgeFee, setBridgeFee] = useState(); - - const isEthereum = currentBlockchain === Blockchain.ETHEREUM; - const { assetsWithBalances } = useAssetBalancesEVM( - isEthereum ? Blockchain.ETHEREUM : Blockchain.AVALANCHE - ); - const sourceBalance = useMemo(() => { - if (!currentAsset || !currentAssetIdentifier || !network) { - return undefined; - } - - return assetsWithBalances.find(({ asset }) => { - return isUnifiedBridgeAsset(asset) && asset.symbol === currentAsset; - }); - }, [network, assetsWithBalances, currentAssetIdentifier, currentAsset]); - - useEffect(() => { - if (!maximum && sourceBalance?.balance) { - setMaximum(sourceBalance.balance); - } - }, [maximum, sourceBalance]); - - useEffect(() => { - let isMounted = true; - - if ( - currentAsset && - currentAssetData && - currentAssetIdentifier && - amount && - supportsAsset(currentAssetIdentifier, targetChainId) - ) { - const hasAmount = amount && !amount.eq(BIG_ZERO); - - if (hasAmount) { - getFee( - currentAsset, - bigToBigInt(amount, currentAssetData.denomination), - targetChainId - ).then((fee) => { - if (!isMounted) { - return; - } - - const feeBig = bigintToBig(fee, currentAssetData.denomination); - setBridgeFee(feeBig); - setMinimum(feeBig); - setReceiveAmount(amount.sub(feeBig)); - }); - } - - if (sourceBalance?.balance) { - setMaximum(sourceBalance.balance); - } - } - - return () => { - isMounted = false; - }; - }, [ - currentAsset, - currentAssetData, - currentAssetIdentifier, - amount, - targetChainId, - getFee, - sourceBalance?.balance, - supportsAsset, - ]); - - const estimateGas = useCallback( - (transferAmount: Big, asset: Asset) => { - if (!asset) { - throw new Error('No asset data'); - } - - const symbol = isNativeAsset(asset) - ? asset.wrappedAssetSymbol - : asset.symbol; - - return estimateTransferGas( - symbol, - bigToBigInt(transferAmount, asset.denomination), - targetChainId - ); - }, - [estimateTransferGas, targetChainId] - ); - - const transfer = useCallback(async () => { - capture('unifedBridgeTransferStarted', { - bridgeType: 'CCTP', - sourceBlockchain: currentBlockchain, - targetBlockchain, - }); - - if (!currentAsset) { - throw new Error('No asset chosen'); - } - - if (!currentAssetData) { - throw new Error('No asset data'); - } - - const symbol = isNativeAsset(currentAssetData) - ? currentAssetData.wrappedAssetSymbol - : currentAsset || ''; - - const hash = await transferAsset( - currentAsset, - bigToBigInt(amount, currentAssetData.denomination), - targetChainId - ); - - setTransactionDetails({ - tokenSymbol: symbol, - amount, - }); - - return hash; - }, [ - amount, - currentAssetData, - currentAsset, - setTransactionDetails, - transferAsset, - targetChainId, - capture, - currentBlockchain, - targetBlockchain, - ]); - - return { - sourceBalance, - estimateGas, - assetsWithBalances, - receiveAmount, - bridgeFee, - maximum, - minimum, - price: sourceBalance?.price, - transfer, - }; -} diff --git a/src/pages/Bridge/models.ts b/src/pages/Bridge/models.ts index f5905c954..a83c80e87 100644 --- a/src/pages/Bridge/models.ts +++ b/src/pages/Bridge/models.ts @@ -1,26 +1,12 @@ -import { Asset, Blockchain } from '@avalabs/core-bridge-sdk'; import { BridgeAsset } from '@avalabs/bridge-unified'; import Big from 'big.js'; export interface AssetBalance { symbol: string; - asset: Asset | BridgeAsset; + asset: BridgeAsset; balance: Big | undefined; symbolOnNetwork?: string; logoUri?: string; price?: number; unconfirmedBalance?: Big; } - -export const blockchainDisplayNameMap = new Map([ - [Blockchain.AVALANCHE, 'Avalanche C-Chain'], - [Blockchain.ETHEREUM, 'Ethereum'], - [Blockchain.BITCOIN, 'Bitcoin'], - [Blockchain.UNKNOWN, ''], -]); - -export const SUPPORTED_CHAINS = [ - Blockchain.AVALANCHE, - Blockchain.ETHEREUM, - Blockchain.BITCOIN, -]; diff --git a/src/pages/Bridge/utils/findMatchingBridgeAsset.ts b/src/pages/Bridge/utils/findMatchingBridgeAsset.ts new file mode 100644 index 000000000..39ad74817 --- /dev/null +++ b/src/pages/Bridge/utils/findMatchingBridgeAsset.ts @@ -0,0 +1,27 @@ +import { + BridgeAsset, + TokenType as BridgeTokenType, +} from '@avalabs/bridge-unified'; + +import { + NftTokenWithBalance, + TokenType, + TokenWithBalance, +} from '@avalabs/vm-module-types'; + +export const findMatchingBridgeAsset = ( + assets: BridgeAsset[], + token: Exclude +): BridgeAsset | undefined => { + return assets.find((a) => { + if (a.type === BridgeTokenType.NATIVE && token.type === TokenType.NATIVE) { + return a.symbol.toLowerCase() === a.symbol.toLowerCase(); + } + + if (a.type === BridgeTokenType.ERC20 && token.type === TokenType.ERC20) { + return a.address.toLowerCase() === token.address.toLowerCase(); + } + + return false; + }); +}; diff --git a/src/pages/Bridge/utils/getBalances.ts b/src/pages/Bridge/utils/getBalances.ts deleted file mode 100644 index 7cadd0dd4..000000000 --- a/src/pages/Bridge/utils/getBalances.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { Asset, isBtcAsset, isNativeAsset } from '@avalabs/core-bridge-sdk'; -import { AssetBalance } from '@src/pages/Bridge/models'; -import { - BridgeAsset, - TokenType as UnifiedTokenType, -} from '@avalabs/bridge-unified'; -import { isUnifiedBridgeAsset } from './isUnifiedBridgeAsset'; -import { normalizeBalance } from '@src/utils/normalizeBalance'; -import { - NetworkTokenWithBalance, - TokenType, - TokenWithBalance, - TokenWithBalanceERC20, -} from '@avalabs/vm-module-types'; - -/** - * Get balances of wrapped erc20 tokens on Avalanche - * @param assets - * @param tokens - */ -export function getBalances( - assets: Array, - tokens: TokenWithBalance[] -): AssetBalance[] { - const tokensByAddress = tokens.reduce<{ - [address: string]: - | TokenWithBalanceERC20 - | NetworkTokenWithBalance - | undefined; - }>((tokensMap, token) => { - if (token.type !== TokenType.ERC20 && token.type !== TokenType.NATIVE) { - return tokensMap; - } - - if (token.type !== TokenType.ERC20) { - tokensMap[token.symbol.toLowerCase()] = token; - return tokensMap; - } - // Need to convert the keys to lowercase because they are mixed case, and this messes up or comparison function - tokensMap[token.address.toLowerCase()] = token; - return tokensMap; - }, {}); - - return assets.map((asset) => { - const symbol = asset.symbol; - const token = isUnifiedBridgeAsset(asset) - ? tokensByAddress[ - asset.type === UnifiedTokenType.NATIVE - ? asset.symbol.toLowerCase() - : asset.address.toLowerCase() - ] - : isNativeAsset(asset) - ? tokensByAddress[asset.symbol.toLowerCase()] - : isBtcAsset(asset) - ? tokensByAddress[asset.wrappedContractAddress.toLowerCase()] - : tokensByAddress[asset.wrappedContractAddress?.toLowerCase()] || - tokensByAddress[asset.nativeContractAddress?.toLowerCase()]; - - const balance = token && normalizeBalance(token.balance, token.decimals); - const logoUri = token?.logoUri; - const price = token?.priceInCurrency; - - return { symbol, asset, balance, logoUri, price }; - }); -} diff --git a/src/pages/Bridge/utils/getTokenAddress.ts b/src/pages/Bridge/utils/getTokenAddress.ts deleted file mode 100644 index 77cf04031..000000000 --- a/src/pages/Bridge/utils/getTokenAddress.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { isEthAsset } from '@avalabs/core-bridge-sdk'; -import { AssetBalance } from '../models'; -import { isUnifiedBridgeAsset } from './isUnifiedBridgeAsset'; -import { TokenType } from '@avalabs/bridge-unified'; - -export const getTokenAddress = (token: AssetBalance): string => { - if (isUnifiedBridgeAsset(token.asset)) { - return token.asset.type === TokenType.ERC20 ? token.asset.address : ''; - } else if (isEthAsset(token.asset)) { - return token.asset.nativeContractAddress; - } - return ''; -}; diff --git a/src/pages/Bridge/utils/isUnifiedBridgeAsset.ts b/src/pages/Bridge/utils/isUnifiedBridgeAsset.ts deleted file mode 100644 index c4ae8328a..000000000 --- a/src/pages/Bridge/utils/isUnifiedBridgeAsset.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { BridgeAsset } from '@avalabs/bridge-unified'; - -export const isUnifiedBridgeAsset = (asset: unknown): asset is BridgeAsset => { - return asset !== null && typeof asset === 'object' && 'destinations' in asset; -}; diff --git a/yarn.lock b/yarn.lock index 20409fc72..649668c12 100644 --- a/yarn.lock +++ b/yarn.lock @@ -74,14 +74,16 @@ bn.js "5.2.1" zod "3.23.8" -"@avalabs/bridge-unified@0.0.0-feat-ictt-configs-20241009072139": - version "0.0.0-feat-ictt-configs-20241009072139" - resolved "https://registry.yarnpkg.com/@avalabs/bridge-unified/-/bridge-unified-0.0.0-feat-ictt-configs-20241009072139.tgz#f3e65be9247d381e11837e2d1103f99c62b6c137" - integrity sha512-ZHwrUMmCpIrBMgSAR3ogY3w8L9z4FDh7pQnO8eacRus18urFRimOiA9lk/ZC8E3TLVgYaeM3IFvtvMzKISR58w== +"@avalabs/bridge-unified@0.0.0-CP-8544-BTC-bridge-2-20241024221835": + version "0.0.0-CP-8544-BTC-bridge-2-20241024221835" + resolved "https://registry.yarnpkg.com/@avalabs/bridge-unified/-/bridge-unified-0.0.0-CP-8544-BTC-bridge-2-20241024221835.tgz#45422cc3ef3ccbe15032e3ebbd628299105bd0e6" + integrity sha512-vURh0FTQhBrfWX8nNJMnbAd0bdeo9ZZdlKHyI9jcHyWyer+8hFAeFUAXNuzp2FNorR4/JZWfoWej72AOc2nBJQ== dependencies: "@noble/hashes" "1.5.0" "@scure/base" "1.1.9" + "@scure/btc-signer" "1.3.2" abitype "0.9.3" + coinselect "3.1.13" lodash "4.17.21" viem "2.11.1" zod "3.23.8" @@ -4416,6 +4418,13 @@ dependencies: "@noble/hashes" "1.3.3" +"@noble/curves@~1.4.0": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.2.tgz#40309198c76ed71bc6dbf7ba24e81ceb4d0d1fe9" + integrity sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw== + dependencies: + "@noble/hashes" "1.4.0" + "@noble/hashes@1.2.0", "@noble/hashes@~1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.2.0.tgz#a3150eeb09cc7ab207ebf6d7b9ad311a9bdbed12" @@ -4431,16 +4440,16 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699" integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA== +"@noble/hashes@1.4.0", "@noble/hashes@^1.3.1", "@noble/hashes@~1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" + integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== + "@noble/hashes@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.5.0.tgz#abadc5ca20332db2b1b2aa3e496e9af1213570b0" integrity sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA== -"@noble/hashes@^1.3.1": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" - integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== - "@noble/secp256k1@1.7.1", "@noble/secp256k1@^1.7.1", "@noble/secp256k1@~1.7.0": version "1.7.1" resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c" @@ -4979,7 +4988,7 @@ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.5.tgz#1d85d17269fe97694b9c592552dd9e5e33552157" integrity sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ== -"@scure/base@1.1.9": +"@scure/base@1.1.9", "@scure/base@~1.1.6": version "1.1.9" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.9.tgz#e5e142fbbfe251091f9c5f1dd4c834ac04c3dbd1" integrity sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg== @@ -5050,6 +5059,16 @@ "@noble/hashes" "~1.3.2" "@scure/base" "~1.1.4" +"@scure/btc-signer@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@scure/btc-signer/-/btc-signer-1.3.2.tgz#56cf02a2e318097b1e4f531fac8ef114bdf4ddc8" + integrity sha512-BmcQHvxaaShKwgbFC0vDk0xzqbMhNtNmgXm6u7cz07FNtGsVItUuHow6NbgHmc+oJSBZJRym5dz8+Uu0JoEJhQ== + dependencies: + "@noble/curves" "~1.4.0" + "@noble/hashes" "~1.4.0" + "@scure/base" "~1.1.6" + micro-packed "~0.6.2" + "@semantic-release/commit-analyzer@9.0.2", "@semantic-release/commit-analyzer@^9.0.2": version "9.0.2" resolved "https://registry.yarnpkg.com/@semantic-release/commit-analyzer/-/commit-analyzer-9.0.2.tgz#a78e54f9834193b55f1073fa6258eecc9a545e03" @@ -15303,6 +15322,13 @@ micro-packed@~0.5.1: dependencies: "@scure/base" "~1.1.5" +micro-packed@~0.6.2: + version "0.6.3" + resolved "https://registry.yarnpkg.com/micro-packed/-/micro-packed-0.6.3.tgz#125e36bcfd4aa03dbdb31c81cda3bfea84367ae0" + integrity sha512-VmVkyc7lIzAq/XCPFuLc/CwQ7Ehs5XDK3IwqsZHiBIDttAI9Gs7go6Lv4lNRuAIKrGKcRTtthFKUNyHS0S4wJQ== + dependencies: + "@scure/base" "~1.1.5" + micro-signals@2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/micro-signals/-/micro-signals-2.4.0.tgz#007af19ea18051e360f9fa17023284779d839593"