diff --git a/.changeset/heavy-balloons-behave.md b/.changeset/heavy-balloons-behave.md new file mode 100644 index 00000000..8f20a981 --- /dev/null +++ b/.changeset/heavy-balloons-behave.md @@ -0,0 +1,6 @@ +--- +'@rosen-ui/wallet-api': minor +--- + +- Implement standardized error messages for wallets to centralize and streamline error reporting +- Implement the isConnected method to verify whether the connection to the wallet extension is established diff --git a/.changeset/proud-suits-sneeze.md b/.changeset/proud-suits-sneeze.md new file mode 100644 index 00000000..fc9c6438 --- /dev/null +++ b/.changeset/proud-suits-sneeze.md @@ -0,0 +1,5 @@ +--- +'@rosen-ui/metamask-wallet': minor +--- + +Improve MetamaskWallet class to support all EVM chains diff --git a/apps/rosen/.env.example b/apps/rosen/.env.example index 7c321708..6372dfc6 100644 --- a/apps/rosen/.env.example +++ b/apps/rosen/.env.example @@ -1,7 +1,9 @@ CARDANO_KOIOS_API='' ERGO_EXPLORER_API='' BITCOIN_ESPLORA_API='' -ETHEREUM_BLAST_API= +ETHEREUM_RPC_API='' +BINANCE_RPC_API='' +NEXT_PUBLIC_BINANCE_LOCK_ADDRESS='' NEXT_PUBLIC_ERGO_LOCK_ADDRESS='' NEXT_PUBLIC_CARDANO_LOCK_ADDRESS='' NEXT_PUBLIC_BITCOIN_LOCK_ADDRESS='' diff --git a/apps/rosen/app/(bridge)/page.tsx b/apps/rosen/app/(bridge)/page.tsx index f9e0ca3f..053ca39b 100644 --- a/apps/rosen/app/(bridge)/page.tsx +++ b/apps/rosen/app/(bridge)/page.tsx @@ -7,6 +7,8 @@ import { Alert, styled } from '@rosen-bridge/ui-kit'; import { NETWORKS } from '@rosen-ui/constants'; import { RosenAmountValue } from '@rosen-ui/types'; +import { WalletProvider } from '@/_hooks'; + import { BridgeForm } from './BridgeForm'; import { BridgeTransaction } from './BridgeTransaction'; import { ConnectOrSubmitButton } from './ConnectOrSubmitButton'; @@ -79,30 +81,37 @@ const RosenBridge = () => { <> - - - - {methods.getValues().source == NETWORKS.ETHEREUM && ( - - If you are using Ledger, you may need to enable 'Blind - signing' and 'Debug data' in the Ledger (Ethereum - > Settings) due to{' '} - - a known issue in Ledger and MetaMask interaction - - . - - )} - - + + + + + {/* + TODO: Add a condition that activates this alert specifically when MetaMask is selected + local:ergo/rosen-bridge/ui#486 + */} + {(methods.getValues().source == NETWORKS.BINANCE || + methods.getValues().source == NETWORKS.ETHEREUM) && ( + + If you are using Ledger, you may need to enable 'Blind + signing' and 'Debug data' in the Ledger (Ethereum + > Settings) due to{' '} + + a known issue in Ledger and MetaMask interaction + + . + + )} + + + ); diff --git a/apps/rosen/app/App.tsx b/apps/rosen/app/App.tsx index 6625e7bf..302f8452 100644 --- a/apps/rosen/app/App.tsx +++ b/apps/rosen/app/App.tsx @@ -11,7 +11,6 @@ import { App as AppBase } from '@rosen-bridge/ui-kit'; import { theme } from '@/_theme/theme'; -import { WalletContextProvider } from './_contexts/walletContext'; import { TokenMapProvider } from './_hooks'; import { SideBar } from './SideBar'; import { Toolbar } from './Toolbar'; @@ -20,9 +19,7 @@ export const App = ({ children }: { children?: React.ReactNode }) => { return ( } theme={theme} toolbar={}> - - {children} - + {children} ); diff --git a/apps/rosen/app/_actions/calculateFee.ts b/apps/rosen/app/_actions/calculateFee.ts index efaa3562..28954eae 100644 --- a/apps/rosen/app/_actions/calculateFee.ts +++ b/apps/rosen/app/_actions/calculateFee.ts @@ -3,7 +3,7 @@ import { ErgoNetworkType, MinimumFeeBox } from '@rosen-bridge/minimum-fee'; import cardanoKoiosClientFactory from '@rosen-clients/cardano-koios'; import ergoExplorerClientFactory from '@rosen-clients/ergo-explorer'; -import { getHeight as ethereumGetHeight } from '@rosen-network/ethereum'; +import { EvmChains, getHeight } from '@rosen-network/evm'; import { NETWORKS, NETWORK_VALUES } from '@rosen-ui/constants'; import { Network } from '@rosen-ui/types'; import Joi from 'joi'; @@ -18,7 +18,8 @@ const ergoExplorerClient = ergoExplorerClientFactory( ); const GetHeight = { - [NETWORKS.ETHEREUM]: ethereumGetHeight, + [NETWORKS.BINANCE]: () => getHeight(EvmChains.BINANCE), + [NETWORKS.ETHEREUM]: () => getHeight(EvmChains.ETHEREUM), [NETWORKS.CARDANO]: async () => (await cardanoKoiosClient.getTip())[0].block_no, [NETWORKS.ERGO]: async () => diff --git a/apps/rosen/app/_contexts/walletContext.tsx b/apps/rosen/app/_contexts/walletContext.tsx deleted file mode 100644 index cffc5c26..00000000 --- a/apps/rosen/app/_contexts/walletContext.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useReducer, createContext } from 'react'; - -import { Wallet } from '@rosen-ui/wallet-api'; - -type Action = - | { - type: 'set'; - wallet: Wallet; - } - | { type: 'remove' }; -type Dispatch = (action: Action) => void; - -type State = { selectedWallet: Wallet | null }; - -/** - * a context to make wallet state available to all the - * program - */ -export const WalletContext = createContext< - { state: State; dispatch: Dispatch } | undefined ->(undefined); - -function walletReducer(state: State, action: Action) { - switch (action.type) { - case 'set': { - return { - selectedWallet: action.wallet, - }; - } - case 'remove': { - return { - selectedWallet: null, - }; - } - default: { - throw new Error(`Unhandled action type`); - } - } -} - -type WalletContextProviderProps = { children: React.ReactNode }; - -/** - * the context provider for the wallet state - */ - -function WalletContextProvider({ children }: WalletContextProviderProps) { - const [state, dispatch] = useReducer(walletReducer, { - selectedWallet: null, - }); - const value = { state, dispatch }; - - return ( - {children} - ); -} - -export { WalletContextProvider }; diff --git a/apps/rosen/app/_hooks/useBridgeForm.ts b/apps/rosen/app/_hooks/useBridgeForm.ts index cf400b46..7bd0396a 100644 --- a/apps/rosen/app/_hooks/useBridgeForm.ts +++ b/apps/rosen/app/_hooks/useBridgeForm.ts @@ -5,13 +5,13 @@ import { Network, RosenAmountValue } from '@rosen-ui/types'; import { getNonDecimalString } from '@rosen-ui/utils'; import { validateAddress } from '@/_actions'; -import { WalletContext } from '@/_contexts/walletContext'; import { availableNetworks } from '@/_networks'; import { unwrap } from '@/_safeServerAction'; import { getMaxTransfer, getMinTransfer } from '@/_utils'; import { useTokenMap } from './useTokenMap'; import { useTransactionFormData } from './useTransactionFormData'; +import { WalletContext } from './useWallet'; /** * handles the form field registrations and form state changes @@ -67,7 +67,7 @@ export const useBridgeForm = () => { getNonDecimalString(value, decimals), ) as RosenAmountValue; - if (walletGlobalContext?.state.selectedWallet) { + if (walletGlobalContext?.selectedWallet) { // prevent user from entering more than token amount const selectedNetwork = @@ -76,15 +76,14 @@ export const useBridgeForm = () => { const maxTransfer = await getMaxTransfer( selectedNetwork, { - balance: - await walletGlobalContext!.state.selectedWallet.getBalance( - tokenField.value, - ), + balance: await walletGlobalContext!.selectedWallet.getBalance( + tokenField.value, + ), isNative: tokenField.value.metaData.type === 'native', }, async () => ({ fromAddress: - await walletGlobalContext!.state.selectedWallet!.getAddress(), + await walletGlobalContext!.selectedWallet!.getAddress(), toAddress: addressField.value, toChain: targetField.value as Network, }), diff --git a/apps/rosen/app/_hooks/useWallet.ts b/apps/rosen/app/_hooks/useWallet.ts deleted file mode 100644 index 3f8619b6..00000000 --- a/apps/rosen/app/_hooks/useWallet.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { useEffect, useContext, useCallback, useRef } from 'react'; - -import { useLocalStorageManager } from '@rosen-ui/common-hooks'; -import { Wallet } from '@rosen-ui/wallet-api'; - -import { WalletContext } from '@/_contexts/walletContext'; - -import { useNetwork } from './useNetwork'; - -interface WalletDescriptor { - readonly name: string; - readonly expDate: string; -} - -/** - * generates and return the wallet object to save in the local storage - */ -const toWalletDescriptor = (wallet: Wallet): WalletDescriptor => { - let expDate = new Date(); - return { - name: wallet.name, - expDate: expDate.setDate(expDate.getDate() + 2).toString(), - }; -}; - -/** - * handles the wallet connections for all the networks - * and reconnect to the wallet on app startup - */ -export const useWallet = () => { - const walletGlobalContext = useContext(WalletContext); - const isConnecting = useRef(false); - const { get, set } = useLocalStorageManager(); - - const { selectedSource } = useNetwork(); - - /** - * searches in the available wallets in the selected network - * and return the wallet object if it finds a match - */ - const getWallet = useCallback( - (name: string) => { - return selectedSource?.wallets - .filter((wallet) => wallet.isAvailable()) - .find((wallet) => wallet.name === name); - }, - [selectedSource], - ); - - /** - * searches in local storage for already selected wallets and - * returns the wallet object if it finds match - */ - const getCurrentWallet = useCallback((): Wallet | undefined => { - const currentWalletDescriptor = - selectedSource && get(selectedSource?.name); - - if (!currentWalletDescriptor) { - return undefined; - } - - return currentWalletDescriptor - ? getWallet(currentWalletDescriptor.name) - : undefined; - }, [selectedSource, getWallet, get]); - - /** - * disconnects the previously selected wallet and - * calls the connection callbacks - */ - const setSelectedWallet = useCallback( - async (wallet: Wallet) => { - const prevWallet = getCurrentWallet(); - const status = await wallet.connect(); - - if (typeof status === 'boolean' && status) { - set(selectedSource!.name, toWalletDescriptor(wallet)); - walletGlobalContext?.dispatch({ type: 'set', wallet }); - } - }, - [selectedSource, getCurrentWallet, walletGlobalContext, set], - ); - - const handleConnection = useCallback(async () => { - const selectedWallet = getCurrentWallet(); - if ( - selectedWallet?.name !== walletGlobalContext?.state.selectedWallet?.name - ) { - if (selectedWallet) { - const status = await selectedWallet.connect(); - if (typeof status === 'boolean' && status) { - walletGlobalContext?.dispatch({ - type: 'set', - wallet: selectedWallet, - }); - } - isConnecting.current = false; - } else { - walletGlobalContext?.dispatch({ type: 'remove' }); - } - } - }, [walletGlobalContext, getCurrentWallet]); - - useEffect(() => { - if (typeof window === 'object') { - handleConnection(); - } - }, [handleConnection]); - - return selectedSource - ? { - setSelectedWallet, - selectedWallet: walletGlobalContext?.state.selectedWallet, - wallets: selectedSource.wallets, - getBalance: walletGlobalContext?.state.selectedWallet?.getBalance, - } - : {}; -}; diff --git a/apps/rosen/app/_hooks/useWallet.tsx b/apps/rosen/app/_hooks/useWallet.tsx new file mode 100644 index 00000000..82604568 --- /dev/null +++ b/apps/rosen/app/_hooks/useWallet.tsx @@ -0,0 +1,103 @@ +import { + ReactNode, + createContext, + useCallback, + useContext, + useEffect, + useState, +} from 'react'; + +import { useSnackbar } from '@rosen-bridge/ui-kit'; +import { Wallet } from '@rosen-ui/wallet-api'; + +import { useNetwork } from './useNetwork'; + +/** + * handles the wallet connections for all the networks + * and reconnect to the wallet on app startup + */ +export const useWallet = () => { + const context = useContext(WalletContext); + + if (!context) { + throw new Error('useWallet must be used within WalletProvider'); + } + + return context; +}; + +export type WalletContextType = { + setSelectedWallet: (wallet: Wallet) => Promise; + selectedWallet?: Wallet; + wallets: Wallet[]; +}; + +export const WalletContext = createContext(null); + +export const WalletProvider = ({ children }: { children: ReactNode }) => { + const { selectedSource } = useNetwork(); + + const { openSnackbar } = useSnackbar(); + + const [selected, setSelected] = useState(); + + const select = useCallback( + async (wallet: Wallet) => { + try { + const isConnected = await wallet.connect(); + + if (isConnected === false) return; + + await wallet.switchChain?.(selectedSource.name); + } catch (error: any) { + return openSnackbar(error.message, 'error'); + } + + setSelected(wallet); + + if (!selectedSource) return; + + localStorage.setItem('rosen:wallet:' + selectedSource.name, wallet.name); + }, + [selectedSource, openSnackbar, setSelected], + ); + + useEffect(() => { + (async () => { + setSelected(undefined); + + if (!selectedSource) return; + + const name = localStorage.getItem('rosen:wallet:' + selectedSource.name); + + const wallet = selectedSource.wallets.find( + (wallet) => wallet.name === name && wallet.isAvailable(), + ); + + if (!wallet) return; + + if ((await wallet.isConnected?.()) === false) return; + + try { + await wallet.connect(); + + await wallet.switchChain?.(selectedSource.name, true); + + setSelected(wallet); + } catch (error) { + setSelected(undefined); + throw error; + } + })(); + }, [selectedSource, setSelected]); + + const state = { + setSelectedWallet: select, + selectedWallet: selected, + wallets: selectedSource?.wallets || [], + }; + + return ( + {children} + ); +}; diff --git a/apps/rosen/app/_networks/binance/client.ts b/apps/rosen/app/_networks/binance/client.ts new file mode 100644 index 00000000..4b762285 --- /dev/null +++ b/apps/rosen/app/_networks/binance/client.ts @@ -0,0 +1,32 @@ +import { BinanceIcon } from '@rosen-bridge/icons'; +import { NETWORK_LABELS, NETWORKS } from '@rosen-ui/constants'; +import { MetaMaskWallet } from '@rosen-ui/metamask-wallet'; + +import { unwrap } from '@/_safeServerAction'; +import { getTokenMap } from '@/_tokenMap/getClientTokenMap'; +import { BinanceNetwork as BinanceNetworkType } from '@/_types'; + +import { getMaxTransfer } from './getMaxTransfer'; +import { generateLockData, generateTxParameters } from './server'; + +/** + * the main object for Binance network + * providing access to network info and wallets and network specific + * functionality + */ +export const BinanceNetwork: BinanceNetworkType = { + name: NETWORKS.BINANCE, + label: NETWORK_LABELS.BINANCE, + wallets: [ + new MetaMaskWallet({ + getTokenMap, + generateLockData: unwrap(generateLockData), + generateTxParameters: unwrap(generateTxParameters), + }), + ], + nextHeightInterval: 200, + logo: BinanceIcon, + lockAddress: process.env.NEXT_PUBLIC_BINANCE_LOCK_ADDRESS!, + getMaxTransfer: unwrap(getMaxTransfer), + toSafeAddress: (address) => address.toLowerCase(), +}; diff --git a/apps/rosen/app/_networks/binance/getMaxTransfer.ts b/apps/rosen/app/_networks/binance/getMaxTransfer.ts new file mode 100644 index 00000000..6c03a307 --- /dev/null +++ b/apps/rosen/app/_networks/binance/getMaxTransfer.ts @@ -0,0 +1,38 @@ +'use server'; + +import { + EvmChains, + NATIVE_TOKEN_TRANSFER_GAS, + getFeeData, +} from '@rosen-network/evm'; +import { NATIVE_TOKENS, NETWORKS } from '@rosen-ui/constants'; + +import { wrap } from '@/_safeServerAction'; +import { getTokenMap } from '@/_tokenMap/getServerTokenMap'; +import { BinanceNetwork } from '@/_types'; + +/** + * get max transfer for binance + */ +const getMaxTransferCore: BinanceNetwork['getMaxTransfer'] = async ({ + balance, + isNative, +}) => { + const feeData = await getFeeData(EvmChains.BINANCE); + if (!feeData.gasPrice) throw Error(`gas price is null`); + const estimatedFee = feeData.gasPrice * NATIVE_TOKEN_TRANSFER_GAS; + const tokenMap = getTokenMap(); + + const wrappedFee = tokenMap.wrapAmount( + NATIVE_TOKENS.BINANCE, + estimatedFee, + NETWORKS.BINANCE, + ).amount; + const offset = isNative ? wrappedFee : 0n; + const amount = balance - offset; + return amount < 0n ? 0n : amount; +}; + +export const getMaxTransfer = wrap(getMaxTransferCore, { + traceKey: 'getMaxTransferBinance', +}); diff --git a/apps/rosen/app/_networks/binance/index.ts b/apps/rosen/app/_networks/binance/index.ts new file mode 100644 index 00000000..4f1cce44 --- /dev/null +++ b/apps/rosen/app/_networks/binance/index.ts @@ -0,0 +1 @@ +export * from './client'; diff --git a/apps/rosen/app/_networks/binance/server.ts b/apps/rosen/app/_networks/binance/server.ts new file mode 100644 index 00000000..fb0ec10e --- /dev/null +++ b/apps/rosen/app/_networks/binance/server.ts @@ -0,0 +1,20 @@ +'use server'; + +import { + generateLockData as generateLockDataCore, + generateTxParameters as generateTxParametersCore, +} from '@rosen-network/evm'; + +import { wrap } from '@/_safeServerAction'; +import { getTokenMap } from '@/_tokenMap/getServerTokenMap'; + +export const generateLockData = wrap(generateLockDataCore, { + traceKey: 'generateLockData', +}); + +export const generateTxParameters = wrap( + generateTxParametersCore(getTokenMap()), + { + traceKey: 'generateTxParameters', + }, +); diff --git a/apps/rosen/app/_networks/cardano/client.ts b/apps/rosen/app/_networks/cardano/client.ts index 905d3e01..31ec67ab 100644 --- a/apps/rosen/app/_networks/cardano/client.ts +++ b/apps/rosen/app/_networks/cardano/client.ts @@ -37,7 +37,7 @@ export const CardanoNetwork: CardanoNetworkType = { new LaceWallet(config), new NamiWallet(config), ], - nextHeightInterval: 25, + nextHeightInterval: 30, logo: CardanoIcon, lockAddress: process.env.NEXT_PUBLIC_CARDANO_LOCK_ADDRESS!, getMaxTransfer: unwrap(getMaxTransfer), diff --git a/apps/rosen/app/_networks/ethereum/client.ts b/apps/rosen/app/_networks/ethereum/client.ts index 06c1a223..b586f265 100644 --- a/apps/rosen/app/_networks/ethereum/client.ts +++ b/apps/rosen/app/_networks/ethereum/client.ts @@ -25,7 +25,7 @@ export const EthereumNetwork: EthereumNetworkType = { }), ], logo: EthereumIcon, - nextHeightInterval: 0, + nextHeightInterval: 50, lockAddress: process.env.NEXT_PUBLIC_ETHEREUM_LOCK_ADDRESS!, getMaxTransfer: unwrap(getMaxTransfer), toSafeAddress: (address) => address.toLowerCase(), diff --git a/apps/rosen/app/_networks/ethereum/getMaxTransfer.ts b/apps/rosen/app/_networks/ethereum/getMaxTransfer.ts index c23d2f4e..70284c7a 100644 --- a/apps/rosen/app/_networks/ethereum/getMaxTransfer.ts +++ b/apps/rosen/app/_networks/ethereum/getMaxTransfer.ts @@ -1,6 +1,10 @@ 'use server'; -import { ETH_TRANSFER_GAS, getFeeData } from '@rosen-network/ethereum'; +import { + EvmChains, + NATIVE_TOKEN_TRANSFER_GAS, + getFeeData, +} from '@rosen-network/evm'; import { NATIVE_TOKENS, NETWORKS } from '@rosen-ui/constants'; import { wrap } from '@/_safeServerAction'; @@ -14,9 +18,9 @@ const getMaxTransferCore: EthereumNetwork['getMaxTransfer'] = async ({ balance, isNative, }) => { - const feeData = await getFeeData(); + const feeData = await getFeeData(EvmChains.ETHEREUM); if (!feeData.gasPrice) throw Error(`gas price is null`); - const estimatedFee = feeData.gasPrice * ETH_TRANSFER_GAS; + const estimatedFee = feeData.gasPrice * NATIVE_TOKEN_TRANSFER_GAS; const tokenMap = getTokenMap(); const wrappedFee = tokenMap.wrapAmount( diff --git a/apps/rosen/app/_networks/ethereum/server.ts b/apps/rosen/app/_networks/ethereum/server.ts index d9c9a1b4..fb0ec10e 100644 --- a/apps/rosen/app/_networks/ethereum/server.ts +++ b/apps/rosen/app/_networks/ethereum/server.ts @@ -3,7 +3,7 @@ import { generateLockData as generateLockDataCore, generateTxParameters as generateTxParametersCore, -} from '@rosen-network/ethereum'; +} from '@rosen-network/evm'; import { wrap } from '@/_safeServerAction'; import { getTokenMap } from '@/_tokenMap/getServerTokenMap'; diff --git a/apps/rosen/app/_networks/index.ts b/apps/rosen/app/_networks/index.ts index 51b573ab..b1b50dee 100644 --- a/apps/rosen/app/_networks/index.ts +++ b/apps/rosen/app/_networks/index.ts @@ -1,11 +1,13 @@ import { NETWORKS } from '@rosen-ui/constants'; +import { BinanceNetwork } from './binance'; import { BitcoinNetwork } from './bitcoin'; import { CardanoNetwork } from './cardano'; import { ErgoNetwork } from './ergo'; import { EthereumNetwork } from './ethereum'; export const availableNetworks = { + [NETWORKS.BINANCE]: BinanceNetwork, [NETWORKS.ERGO]: ErgoNetwork, [NETWORKS.ETHEREUM]: EthereumNetwork, [NETWORKS.CARDANO]: CardanoNetwork, diff --git a/apps/rosen/app/_types/network.ts b/apps/rosen/app/_types/network.ts index 82bedb09..25bb4de5 100644 --- a/apps/rosen/app/_types/network.ts +++ b/apps/rosen/app/_types/network.ts @@ -34,6 +34,7 @@ interface BitcoinMaxTransferExtra { }; } +export interface BinanceNetwork extends BaseNetwork<'binance'> {} export interface EthereumNetwork extends BaseNetwork<'ethereum'> {} export interface ErgoNetwork extends BaseNetwork<'ergo'> {} export interface CardanoNetwork extends BaseNetwork<'cardano'> {} diff --git a/apps/rosen/app/_utils/getMaxTransfer.ts b/apps/rosen/app/_utils/getMaxTransfer.ts index 003201aa..3448ca6b 100644 --- a/apps/rosen/app/_utils/getMaxTransfer.ts +++ b/apps/rosen/app/_utils/getMaxTransfer.ts @@ -2,6 +2,7 @@ import { NETWORKS } from '@rosen-ui/constants'; import { Network, RosenAmountValue } from '@rosen-ui/types'; import { + BinanceNetwork, BitcoinNetwork, CardanoNetwork, ErgoNetwork, @@ -16,7 +17,12 @@ import { * @returns THIS IS A WRAPPED-VALUE */ export const getMaxTransfer = async ( - network: ErgoNetwork | CardanoNetwork | BitcoinNetwork | EthereumNetwork, + network: + | BinanceNetwork + | ErgoNetwork + | CardanoNetwork + | BitcoinNetwork + | EthereumNetwork, tokenInfo: { balance: RosenAmountValue; isNative: boolean; diff --git a/apps/rosen/package.json b/apps/rosen/package.json index c0a58cc7..3544d0c3 100644 --- a/apps/rosen/package.json +++ b/apps/rosen/package.json @@ -16,7 +16,7 @@ "dependencies": { "@emurgo/cardano-serialization-lib-browser": "^11.5.0", "@emurgo/cardano-serialization-lib-nodejs": "^11.5.0", - "@rosen-bridge/address-codec": "^0.3.0", + "@rosen-bridge/address-codec": "^0.4.0", "@rosen-bridge/bitcoin-utxo-selection": "^0.2.0", "@rosen-bridge/cardano-utxo-selection": "^1.0.0", "@rosen-bridge/cli": "^0.2.0", @@ -34,7 +34,7 @@ "@rosen-network/bitcoin": "^2.0.0", "@rosen-network/cardano": "^2.0.0", "@rosen-network/ergo": "^2.0.0", - "@rosen-network/ethereum": "^1.0.0", + "@rosen-network/evm": "^0.1.1", "@rosen-ui/asset-calculator": "^2.0.1", "@rosen-ui/common-hooks": "^0.1.1", "@rosen-ui/constants": "^0.0.5", diff --git a/build.sh b/build.sh index 818c4402..27bd1e37 100755 --- a/build.sh +++ b/build.sh @@ -15,7 +15,7 @@ npm run build --workspace wallets/wallet-api npm run build --workspace networks/bitcoin npm run build --workspace networks/cardano npm run build --workspace networks/ergo -npm run build --workspace networks/ethereum +npm run build --workspace networks/evm npm run build --workspace wallets/eternl npm run build --workspace wallets/lace npm run build --workspace wallets/metamask diff --git a/networks/bitcoin/package.json b/networks/bitcoin/package.json index 8cc3e599..442436cf 100644 --- a/networks/bitcoin/package.json +++ b/networks/bitcoin/package.json @@ -14,7 +14,7 @@ "test": "vitest" }, "dependencies": { - "@rosen-bridge/address-codec": "^0.3.0", + "@rosen-bridge/address-codec": "^0.4.0", "@rosen-bridge/bitcoin-utxo-selection": "^0.2.0", "@rosen-ui/constants": "^0.0.5", "axios": "^1.7.2", diff --git a/networks/ethereum/CHANGELOG.md b/networks/evm/CHANGELOG.md similarity index 82% rename from networks/ethereum/CHANGELOG.md rename to networks/evm/CHANGELOG.md index d2cada61..7d95ed2e 100644 --- a/networks/ethereum/CHANGELOG.md +++ b/networks/evm/CHANGELOG.md @@ -1,15 +1,11 @@ -# @rosen-network/ethereum - -## 1.0.0 - -### Major Changes - -- Eliminate the reliance on the @rosen-ui/wallet-api package and remove any unrelated type definitions +# @rosen-network/evm ## 0.1.1 ### Patch Changes +- Change the name of @rosen-network/ethereum to @rosen-network/evm +- Eliminate the reliance on the @rosen-ui/wallet-api package and remove any unrelated type definitions - Enhance the generateUnsignedTx utility functions within the networks package - Initialize the Ethereum network package. - Strengthen type safety and enforce robust typing for Chain and Network types diff --git a/networks/ethereum/package.json b/networks/evm/package.json similarity index 83% rename from networks/ethereum/package.json rename to networks/evm/package.json index f049a7a1..bbac7086 100644 --- a/networks/ethereum/package.json +++ b/networks/evm/package.json @@ -1,6 +1,6 @@ { - "name": "@rosen-network/ethereum", - "version": "1.0.0", + "name": "@rosen-network/evm", + "version": "0.1.1", "private": true, "description": "This is a private package utilized within Rosen Bridge UI app", "main": "dist/src/index.js", @@ -13,7 +13,7 @@ "type-check": "tsc --noEmit" }, "dependencies": { - "@rosen-bridge/address-codec": "^0.3.0", + "@rosen-bridge/address-codec": "^0.4.0", "@rosen-bridge/tokens": "^1.2.1", "ethers": "^6.13.2" }, diff --git a/networks/ethereum/src/constants.ts b/networks/evm/src/constants.ts similarity index 94% rename from networks/ethereum/src/constants.ts rename to networks/evm/src/constants.ts index 25889678..e4f29417 100644 --- a/networks/ethereum/src/constants.ts +++ b/networks/evm/src/constants.ts @@ -1,7 +1,6 @@ import { InterfaceAbi } from 'ethers'; -export const ETH = 'eth'; -export const ETH_TRANSFER_GAS = 21000n; +export const NATIVE_TOKEN_TRANSFER_GAS = 21000n; export const transferABI: InterfaceAbi = [ { diff --git a/networks/ethereum/src/generateTxParameters.ts b/networks/evm/src/generateTxParameters.ts similarity index 93% rename from networks/ethereum/src/generateTxParameters.ts rename to networks/evm/src/generateTxParameters.ts index e3d14a65..967c39b7 100644 --- a/networks/ethereum/src/generateTxParameters.ts +++ b/networks/evm/src/generateTxParameters.ts @@ -3,7 +3,7 @@ import { NETWORKS } from '@rosen-ui/constants'; import { RosenAmountValue } from '@rosen-ui/types'; import { Contract } from 'ethers'; -import { ETH, transferABI } from './constants'; +import { transferABI } from './constants'; /** * generates ethereum lock tx @@ -27,7 +27,7 @@ export const generateTxParameters = ).amount; let transactionParameters; - if (tokenId === ETH) { + if (token.metaData.type === 'native') { transactionParameters = { to: lockAddress, from: fromAddress, diff --git a/networks/ethereum/src/index.ts b/networks/evm/src/index.ts similarity index 78% rename from networks/ethereum/src/index.ts rename to networks/evm/src/index.ts index d1e10975..1c484809 100644 --- a/networks/ethereum/src/index.ts +++ b/networks/evm/src/index.ts @@ -1,3 +1,4 @@ export * from './constants'; export * from './generateTxParameters'; export * from './utils'; +export * from './types'; diff --git a/networks/evm/src/types.ts b/networks/evm/src/types.ts new file mode 100644 index 00000000..6bf40c7b --- /dev/null +++ b/networks/evm/src/types.ts @@ -0,0 +1,4 @@ +export enum EvmChains { + ETHEREUM = 'ethereum', + BINANCE = 'binance', +} diff --git a/networks/ethereum/src/utils.ts b/networks/evm/src/utils.ts similarity index 65% rename from networks/ethereum/src/utils.ts rename to networks/evm/src/utils.ts index 7d8bcf6a..6c3ecb59 100644 --- a/networks/ethereum/src/utils.ts +++ b/networks/evm/src/utils.ts @@ -3,6 +3,8 @@ import { NETWORK_VALUES } from '@rosen-ui/constants'; import { Network } from '@rosen-ui/types'; import { FeeData, isAddress, JsonRpcProvider } from 'ethers'; +import { EvmChains } from './types'; + /** * generates metadata for lock transaction * @param toChain @@ -43,21 +45,19 @@ export const generateLockData = async ( }; /** - * gets Ethereum current block height + * gets EVM chain current block height * @returns */ -export const getHeight = async (): Promise => { - return await new JsonRpcProvider( - process.env.ETHEREUM_BLAST_API, - ).getBlockNumber(); +export const getHeight = async (chain: EvmChains): Promise => { + return await new JsonRpcProvider(getChainRpcUrl(chain)).getBlockNumber(); }; /** - * gets Ethereum fee data + * gets EVM chain fee data * @returns */ -export const getFeeData = async (): Promise => { - return await new JsonRpcProvider(process.env.ETHEREUM_BLAST_API).getFeeData(); +export const getFeeData = async (chain: EvmChains): Promise => { + return await new JsonRpcProvider(getChainRpcUrl(chain)).getFeeData(); }; /** @@ -68,3 +68,18 @@ export const getFeeData = async (): Promise => { export const isValidAddress = (addr: string) => { return isAddress(addr); }; + +/** + * returns the corresponding chain RPC url + * @param chain + */ +const getChainRpcUrl = (chain: EvmChains) => { + switch (chain) { + case EvmChains.ETHEREUM: + return process.env.ETHEREUM_RPC_API; + case EvmChains.BINANCE: + return process.env.BINANCE_RPC_API; + default: + throw Error(`chain [${chain}] is not registered as an EVM chain`); + } +}; diff --git a/networks/ethereum/tsconfig.json b/networks/evm/tsconfig.json similarity index 100% rename from networks/ethereum/tsconfig.json rename to networks/evm/tsconfig.json diff --git a/package-lock.json b/package-lock.json index 910cabba..17e13272 100644 --- a/package-lock.json +++ b/package-lock.json @@ -227,7 +227,7 @@ "dependencies": { "@emurgo/cardano-serialization-lib-browser": "^11.5.0", "@emurgo/cardano-serialization-lib-nodejs": "^11.5.0", - "@rosen-bridge/address-codec": "^0.3.0", + "@rosen-bridge/address-codec": "^0.4.0", "@rosen-bridge/bitcoin-utxo-selection": "^0.2.0", "@rosen-bridge/cardano-utxo-selection": "^1.0.0", "@rosen-bridge/cli": "^0.2.0", @@ -245,7 +245,7 @@ "@rosen-network/bitcoin": "^2.0.0", "@rosen-network/cardano": "^2.0.0", "@rosen-network/ergo": "^2.0.0", - "@rosen-network/ethereum": "^1.0.0", + "@rosen-network/evm": "^0.1.1", "@rosen-ui/asset-calculator": "^2.0.1", "@rosen-ui/common-hooks": "^0.1.1", "@rosen-ui/constants": "^0.0.5", @@ -309,6 +309,46 @@ "typescript": "^5.0.0" } }, + "apps/rosen-service/node_modules/@types/node": { + "version": "18.19.1", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "apps/rosen-service/node_modules/tsconfig-paths": { + "version": "4.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "apps/rosen/node_modules/@rosen-bridge/address-codec": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@rosen-bridge/address-codec/-/address-codec-0.4.0.tgz", + "integrity": "sha512-YjHEXDytiTw7UAfDhyKbFIvrubKQr295qf5X5PF3CQjqhRC/Po1wE+j/FLc6eRK1IU++5G8AkICH8WDuek35KQ==", + "dependencies": { + "@emurgo/cardano-serialization-lib-nodejs": "^11.5.0", + "bitcoinjs-lib": "^6.1.5", + "ergo-lib-wasm-nodejs": "^0.24.1", + "ethers": "^6.13.2" + }, + "engines": { + "node": ">=20.11.0" + } + }, + "apps/rosen/node_modules/@types/node": { + "version": "20.5.7", + "dev": true, + "license": "MIT" + }, "apps/rosen/node_modules/@typescript-eslint/parser": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", @@ -749,7 +789,7 @@ "name": "@rosen-network/bitcoin", "version": "2.0.0", "dependencies": { - "@rosen-bridge/address-codec": "^0.3.0", + "@rosen-bridge/address-codec": "^0.4.0", "@rosen-bridge/bitcoin-utxo-selection": "^0.2.0", "@rosen-ui/constants": "^0.0.5", "axios": "^1.7.2", @@ -759,6 +799,20 @@ "typescript": "^5.0.0" } }, + "networks/bitcoin/node_modules/@rosen-bridge/address-codec": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@rosen-bridge/address-codec/-/address-codec-0.4.0.tgz", + "integrity": "sha512-YjHEXDytiTw7UAfDhyKbFIvrubKQr295qf5X5PF3CQjqhRC/Po1wE+j/FLc6eRK1IU++5G8AkICH8WDuek35KQ==", + "dependencies": { + "@emurgo/cardano-serialization-lib-nodejs": "^11.5.0", + "bitcoinjs-lib": "^6.1.5", + "ergo-lib-wasm-nodejs": "^0.24.1", + "ethers": "^6.13.2" + }, + "engines": { + "node": ">=20.11.0" + } + }, "networks/cardano": { "name": "@rosen-network/cardano", "version": "2.0.0", @@ -782,11 +836,11 @@ "typescript": "^5.0.0" } }, - "networks/ethereum": { - "name": "@rosen-network/ethereum", - "version": "1.0.0", + "networks/evm": { + "name": "@rosen-network/evm", + "version": "0.1.1", "dependencies": { - "@rosen-bridge/address-codec": "^0.3.0", + "@rosen-bridge/address-codec": "^0.4.0", "@rosen-bridge/tokens": "^1.2.1", "ethers": "^6.13.2" }, @@ -794,6 +848,20 @@ "typescript": "^5.0.0" } }, + "networks/evm/node_modules/@rosen-bridge/address-codec": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@rosen-bridge/address-codec/-/address-codec-0.4.0.tgz", + "integrity": "sha512-YjHEXDytiTw7UAfDhyKbFIvrubKQr295qf5X5PF3CQjqhRC/Po1wE+j/FLc6eRK1IU++5G8AkICH8WDuek35KQ==", + "dependencies": { + "@emurgo/cardano-serialization-lib-nodejs": "^11.5.0", + "bitcoinjs-lib": "^6.1.5", + "ergo-lib-wasm-nodejs": "^0.24.1", + "ethers": "^6.13.2" + }, + "engines": { + "node": ">=20.11.0" + } + }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "dev": true, @@ -9589,8 +9657,8 @@ "resolved": "networks/ergo", "link": true }, - "node_modules/@rosen-network/ethereum": { - "resolved": "networks/ethereum", + "node_modules/@rosen-network/evm": { + "resolved": "networks/evm", "link": true }, "node_modules/@rosen-ui/asset-calculator": { @@ -26925,7 +26993,7 @@ "@metamask/sdk": "^0.28.2", "@rosen-bridge/icons": "^1.0.0", "@rosen-bridge/tokens": "^1.2.1", - "@rosen-network/ethereum": "^1.0.0", + "@rosen-network/evm": "^0.1.1", "@rosen-ui/constants": "^0.0.5", "@rosen-ui/types": "^0.3.1", "@rosen-ui/wallet-api": "^1.0.3" diff --git a/package.json b/package.json index c830c83e..88c51cc6 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,8 @@ "coverage": "npm run test -- --coverage", "version": "npx changeset version && npm i && npx changeset --empty", "version:rosen": "npx changeset version --ignore @rosen-bridge/watcher-app --ignore @rosen-bridge/guard-app && npm i && npx changeset --empty", - "version:watcher": "npx changeset version --ignore=@rosen-bridge/rosen-service --ignore @rosen-bridge/rosen-app --ignore @rosen-bridge/guard-app --ignore @rosen-network/ergo --ignore @rosen-network/cardano --ignore @rosen-network/bitcoin --ignore @rosen-network/ethereum --ignore @rosen-ui/eternl-wallet --ignore @rosen-ui/lace-wallet --ignore @rosen-ui/metamask-wallet --ignore @rosen-ui/nami-wallet --ignore @rosen-ui/nautilus-wallet --ignore @rosen-ui/okx-wallet --ignore @rosen-ui/asset-calculator --ignore @rosen-ui/wallet-api && npm i && npx changeset --empty", - "version:guard": "npx changeset version --ignore=@rosen-bridge/rosen-service --ignore @rosen-bridge/watcher-app --ignore @rosen-bridge/rosen-app --ignore @rosen-network/ergo --ignore @rosen-network/cardano --ignore @rosen-network/bitcoin --ignore @rosen-network/ethereum --ignore @rosen-ui/eternl-wallet --ignore @rosen-ui/lace-wallet --ignore @rosen-ui/metamask-wallet --ignore @rosen-ui/nami-wallet --ignore @rosen-ui/nautilus-wallet --ignore @rosen-ui/okx-wallet --ignore @rosen-ui/asset-calculator --ignore @rosen-ui/wallet-api && npm i && npx changeset --empty" + "version:watcher": "npx changeset version --ignore=@rosen-bridge/rosen-service --ignore @rosen-bridge/rosen-app --ignore @rosen-bridge/guard-app --ignore @rosen-network/ergo --ignore @rosen-network/cardano --ignore @rosen-network/bitcoin --ignore @rosen-network/evm --ignore @rosen-ui/eternl-wallet --ignore @rosen-ui/lace-wallet --ignore @rosen-ui/metamask-wallet --ignore @rosen-ui/nami-wallet --ignore @rosen-ui/nautilus-wallet --ignore @rosen-ui/okx-wallet --ignore @rosen-ui/asset-calculator --ignore @rosen-ui/wallet-api && npm i && npx changeset --empty", + "version:guard": "npx changeset version --ignore=@rosen-bridge/rosen-service --ignore @rosen-bridge/watcher-app --ignore @rosen-bridge/rosen-app --ignore @rosen-network/ergo --ignore @rosen-network/cardano --ignore @rosen-network/bitcoin --ignore @rosen-network/evm --ignore @rosen-ui/eternl-wallet --ignore @rosen-ui/lace-wallet --ignore @rosen-ui/metamask-wallet --ignore @rosen-ui/nami-wallet --ignore @rosen-ui/nautilus-wallet --ignore @rosen-ui/okx-wallet --ignore @rosen-ui/asset-calculator --ignore @rosen-ui/wallet-api && npm i && npx changeset --empty" }, "dependencies": { "@changesets/cli": "^2.27.1" diff --git a/packages/constants/src/index.ts b/packages/constants/src/index.ts index 4607fe10..b714263b 100644 --- a/packages/constants/src/index.ts +++ b/packages/constants/src/index.ts @@ -17,6 +17,7 @@ export const NETWORKS = { CARDANO: 'cardano', BITCOIN: 'bitcoin', ETHEREUM: 'ethereum', + BINANCE: 'binance', } as const; export const NATIVE_TOKENS = { @@ -24,6 +25,7 @@ export const NATIVE_TOKENS = { CARDANO: 'ada', BITCOIN: 'btc', ETHEREUM: 'eth', + BINANCE: 'bnb', } as const; /** @@ -37,4 +39,5 @@ export const NETWORK_LABELS: { [key in keyof typeof NETWORKS]: string } = { CARDANO: 'Cardano', BITCOIN: 'Bitcoin', ETHEREUM: 'Ethereum', + BINANCE: 'Binance', }; diff --git a/packages/icons/src/networks/binance.svg b/packages/icons/src/networks/binance.svg new file mode 100644 index 00000000..21f0556a --- /dev/null +++ b/packages/icons/src/networks/binance.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/icons/src/networks/index.ts b/packages/icons/src/networks/index.ts index 14359298..3c72a812 100644 --- a/packages/icons/src/networks/index.ts +++ b/packages/icons/src/networks/index.ts @@ -1,3 +1,4 @@ +export { ReactComponent as BinanceIcon } from './binance.svg'; export { ReactComponent as BitcoinIcon } from './bitcoin.svg'; export { ReactComponent as CardanoIcon } from './cardano.svg'; export { ReactComponent as ErgoIcon } from './ergo.svg'; diff --git a/packages/utils/src/getAddressUrl.ts b/packages/utils/src/getAddressUrl.ts index d18b67ed..63696b7a 100644 --- a/packages/utils/src/getAddressUrl.ts +++ b/packages/utils/src/getAddressUrl.ts @@ -2,6 +2,7 @@ import { NETWORKS } from '@rosen-ui/constants'; import { Network } from '@rosen-ui/types'; const baseAddressURLs: { [key in Network]: string } = { + [NETWORKS.BINANCE]: 'https://bscscan.com/address', [NETWORKS.ERGO]: 'https://explorer.ergoplatform.com/en/addresses', [NETWORKS.CARDANO]: 'https://cardanoscan.io/address', [NETWORKS.BITCOIN]: 'https://mempool.space/address', diff --git a/packages/utils/src/getTokenUrl.ts b/packages/utils/src/getTokenUrl.ts index 22e30743..fbe3bc3b 100644 --- a/packages/utils/src/getTokenUrl.ts +++ b/packages/utils/src/getTokenUrl.ts @@ -2,6 +2,7 @@ import { NETWORKS } from '@rosen-ui/constants'; import { Network } from '@rosen-ui/types'; const baseTokenURLs: { [key in Network]: string } = { + [NETWORKS.BINANCE]: 'https://bscscan.com/token', [NETWORKS.ERGO]: 'https://explorer.ergoplatform.com/en/token', [NETWORKS.CARDANO]: 'https://cardanoscan.io/token', [NETWORKS.BITCOIN]: '', diff --git a/packages/utils/src/getTxUrl.ts b/packages/utils/src/getTxUrl.ts index 2e1dd427..370b9e87 100644 --- a/packages/utils/src/getTxUrl.ts +++ b/packages/utils/src/getTxUrl.ts @@ -9,6 +9,7 @@ import { Network } from '@rosen-ui/types'; */ const baseTxURLs: { [key in Network]: string } = { + [NETWORKS.BINANCE]: 'https://bscscan.com/tx', [NETWORKS.ERGO]: 'https://explorer.ergoplatform.com/transactions', [NETWORKS.CARDANO]: 'https://cardanoscan.io/transaction', [NETWORKS.BITCOIN]: 'https://mempool.space/tx', diff --git a/wallets/metamask/package.json b/wallets/metamask/package.json index 2a1ebad3..5a601f83 100644 --- a/wallets/metamask/package.json +++ b/wallets/metamask/package.json @@ -19,7 +19,7 @@ "@metamask/sdk": "^0.28.2", "@rosen-bridge/icons": "^1.0.0", "@rosen-bridge/tokens": "^1.2.1", - "@rosen-network/ethereum": "^1.0.0", + "@rosen-network/evm": "^0.1.1", "@rosen-ui/constants": "^0.0.5", "@rosen-ui/types": "^0.3.1", "@rosen-ui/wallet-api": "^1.0.3" diff --git a/wallets/metamask/src/types.ts b/wallets/metamask/src/types.ts index 3bb8c912..b0aae09d 100644 --- a/wallets/metamask/src/types.ts +++ b/wallets/metamask/src/types.ts @@ -2,7 +2,7 @@ import { TokenMap } from '@rosen-bridge/tokens'; import type { generateTxParameters, generateLockData, -} from '@rosen-network/ethereum'; +} from '@rosen-network/evm'; export type WalletConfig = { getTokenMap(): Promise; diff --git a/wallets/metamask/src/wallet.ts b/wallets/metamask/src/wallet.ts index 108db4ca..f101d776 100644 --- a/wallets/metamask/src/wallet.ts +++ b/wallets/metamask/src/wallet.ts @@ -1,9 +1,20 @@ import { MetaMaskSDK } from '@metamask/sdk'; import { MetaMaskIcon } from '@rosen-bridge/icons'; import { RosenChainToken } from '@rosen-bridge/tokens'; -import { tokenABI } from '@rosen-network/ethereum/dist/src/constants'; +import { tokenABI } from '@rosen-network/evm/dist/src/constants'; import { NETWORKS } from '@rosen-ui/constants'; -import { Wallet, WalletTransferParams } from '@rosen-ui/wallet-api'; +import { Network, RosenAmountValue } from '@rosen-ui/types'; +import { + ChainNotAddedError, + ChainSwitchingRejectedError, + UnsupportedChainError, + Wallet, + InteractionError, + WalletTransferParams, + AddressRetrievalError, + ConnectionRejectedError, + UserDeniedTransactionSignatureError, +} from '@rosen-ui/wallet-api'; import { BrowserProvider, Contract } from 'ethers'; import { WalletConfig } from './types'; @@ -17,8 +28,6 @@ export class MetaMaskWallet implements Wallet { link = 'https://metamask.io/'; - connecWaiting?: Promise; - private api = new MetaMaskSDK({ dappMetadata: { name: 'Rosen Bridge', @@ -26,34 +35,58 @@ export class MetaMaskWallet implements Wallet { enableAnalytics: false, }); + private get provider() { + const provider = this.api.getProvider(); + + if (!provider) throw new InteractionError(this.name); + + return provider; + } + constructor(private config: WalletConfig) {} - async connect(): Promise { - this.connecWaiting ||= this.api.connect(); - try { - await this.connecWaiting; - this.connecWaiting = undefined; - return true; - } catch { - this.connecWaiting = undefined; - return false; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private dispatchError(error: any, cases: { [key: number]: () => Error }) { + if (error?.code in cases) { + throw cases[error.code](); + } + if (error.message) { + throw new Error(error.message, { cause: error }); } + throw error; } - getAddress(): Promise { - throw new Error('Not implemented'); + private async permissions() { + return (await this.provider.request({ + method: 'wallet_getPermissions', + params: [], + })) as { caveats: { type: string; value: string[] }[] }[]; } - async getBalance(token: RosenChainToken): Promise { - const provider = this.api.getProvider(); - - if (!provider) return 0n; + async connect(): Promise { + try { + await this.api.connect(); + } catch (error) { + this.dispatchError(error, { + 4001: () => new ConnectionRejectedError(this.name, error), + }); + } + } - const accounts = await provider.request({ + async getAddress(): Promise { + const accounts = await this.provider.request({ method: 'eth_accounts', }); - if (!accounts?.length) return 0n; + const account = accounts?.at(0); + + if (!account) throw new AddressRetrievalError(this.name); + + return account; + } + + async getBalance(token: RosenChainToken): Promise { + const address = await this.getAddress(); const tokenMap = await this.config.getTokenMap(); @@ -61,10 +94,10 @@ export class MetaMaskWallet implements Wallet { let amount; - if (tokenId == 'eth') { - amount = await provider.request({ + if (token.metaData.type === 'native') { + amount = await this.provider.request({ method: 'eth_getBalance', - params: [accounts[0], 'latest'], + params: [address, 'latest'], }); } else { const browserProvider = new BrowserProvider(window.ethereum!); @@ -75,7 +108,7 @@ export class MetaMaskWallet implements Wallet { await browserProvider.getSigner(), ); - amount = await contract.balanceOf(accounts[0]); + amount = await contract.balanceOf(address); } if (!amount) return 0n; @@ -90,27 +123,51 @@ export class MetaMaskWallet implements Wallet { } isAvailable(): boolean { - return ( - typeof window.ethereum !== 'undefined' && - window.ethereum.isMetaMask && - !!window.ethereum._metamask - ); + return this.api.isExtensionActive(); } - async transfer(params: WalletTransferParams): Promise { - const provider = this.api.getProvider(); + async isConnected(): Promise { + return !!(await this.permissions()).length; + } - if (!provider) throw Error(`Failed to interact with metamask`); + async switchChain(chain: Network, silent?: boolean): Promise { + const chains = { + [NETWORKS.BINANCE]: '0x38', + [NETWORKS.ETHEREUM]: '0x1', + } as { [key in Network]?: string }; - const accounts = await provider.request({ - method: 'eth_accounts', - }); + const chainId = chains[chain]; - if (!accounts?.length) - throw Error(`Failed to fetch accounts from metamask`); + if (!chainId) throw new UnsupportedChainError(this.name, chain); - if (!accounts[0]) - throw Error(`Failed to get address of first account from metamask`); + if (silent) { + const has = (await this.permissions()) + .map((permission) => permission.caveats) + .flat() + .some( + (caveat) => + caveat.type === 'restrictNetworkSwitching' && + caveat.value.includes(chainId), + ); + + if (!has) throw new Error(); + } + + try { + await this.provider.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId }], + }); + } catch (error) { + this.dispatchError(error, { + 4001: () => new ChainSwitchingRejectedError(this.name, chain, error), + 4902: () => new ChainNotAddedError(this.name, chain, error), + }); + } + } + + async transfer(params: WalletTransferParams): Promise { + const address = await this.getAddress(); const rosenData = await this.config.generateLockData( params.toChain, @@ -126,17 +183,23 @@ export class MetaMaskWallet implements Wallet { const transactionParameters = await this.config.generateTxParameters( tokenId, params.lockAddress, - accounts[0], + address, params.amount, rosenData, params.token, ); - const result = await provider.request({ - method: 'eth_sendTransaction', - params: [transactionParameters], - }); + try { + return (await this.provider.request({ + method: 'eth_sendTransaction', + params: [transactionParameters], + }))!; + } catch (error) { + this.dispatchError(error, { + 4001: () => new UserDeniedTransactionSignatureError(this.name, error), + }); + } - return result ?? ''; + return ''; } } diff --git a/wallets/wallet-api/src/types/errors.ts b/wallets/wallet-api/src/types/errors.ts new file mode 100644 index 00000000..d6b3270d --- /dev/null +++ b/wallets/wallet-api/src/types/errors.ts @@ -0,0 +1,75 @@ +import { Network } from '@rosen-ui/types'; + +export class AddressRetrievalError extends Error { + constructor(public wallet: string) { + super(`Failed to retrieve the address from the [${wallet}] wallet`); + } +} + +export class InteractionError extends Error { + constructor(public wallet: string) { + super(`Failed to interact with the [${wallet}] wallet`); + } +} + +export class UnsupportedChainError extends Error { + constructor( + public wallet: string, + public chain: Network, + ) { + super( + `The chain [${chain}] is not supported for switching in the [${wallet}] wallet`, + ); + } +} + +export class ChainSwitchingRejectedError extends Error { + constructor( + public wallet: string, + public chain: Network, + public cause: unknown, + ) { + super( + `The request to switch to [${chain}] in the [${wallet}] wallet was rejected by the user`, + { cause }, + ); + } +} + +export class ChainNotAddedError extends Error { + constructor( + public wallet: string, + public chain: Network, + public cause: unknown, + ) { + super( + `The chain [${chain}] has not been added to your [${wallet}] wallet. To proceed, please add it using the ${wallet} extension and try again`, + { cause }, + ); + } +} + +export class ConnectionRejectedError extends Error { + constructor( + public wallet: string, + public cause: unknown, + ) { + super(`User rejected the connection request for the [${wallet}] wallet`, { + cause, + }); + } +} + +export class UserDeniedTransactionSignatureError extends Error { + constructor( + public wallet: string, + public cause: unknown, + ) { + super( + `Transaction signature denied by the user for the [${wallet}] wallet`, + { + cause, + }, + ); + } +} diff --git a/wallets/wallet-api/src/types/index.ts b/wallets/wallet-api/src/types/index.ts index 692a439b..681744ce 100644 --- a/wallets/wallet-api/src/types/index.ts +++ b/wallets/wallet-api/src/types/index.ts @@ -1,2 +1,3 @@ export * from './common'; +export * from './errors'; export * from './wallet'; diff --git a/wallets/wallet-api/src/types/wallet.ts b/wallets/wallet-api/src/types/wallet.ts index 7a7445dc..e12fcd6d 100644 --- a/wallets/wallet-api/src/types/wallet.ts +++ b/wallets/wallet-api/src/types/wallet.ts @@ -12,10 +12,12 @@ export interface Wallet { name: string; label: string; link: string; - connect(): Promise; + connect(): Promise; getAddress(): Promise; getBalance(token: RosenChainToken): Promise; isAvailable(): boolean; + isConnected?(): Promise; + switchChain?(chain: Network, silent?: boolean): Promise; transfer(params: WalletTransferParams): Promise; }