diff --git a/README.md b/README.md index 0f07e760..b96f2706 100755 --- a/README.md +++ b/README.md @@ -37,23 +37,28 @@ For running a local instance use the command: yarn start ``` +The codebase is configured to be run against the Geth <> Substrate node that can be set up by following the guide [here](https://chainbridge.chainsafe.io/local/) or executing: + +- `yarn start:substrate` to start, +- `yarn setup:example` to initialize + +Should the substrate chain you are targetting require different type definitions, the type definitions file should be added to `src/Contexts/Adaptors/SubstrateApis/` and the file name for the types set in the substrate bridge configs. + ### Build Update the configs for the bridge in `src/chainbridgeContext.ts`. There should be at least 2 chains configured for correct functioning of the bridge. Each chain accepts the following configuration parameters: ``` -type BridgeConfig = { - chainId: number // The bridge's chainId. - networkId: number // The networkId of this chain. - name: string // The human readable name of this chain. - bridgeAddress: string // The address on the brdige contract deployed on this chain. - erc20HandlerAddress: string // The ERC20 handler address. - rpcUrl: string // An RPC URL for this chain. - type: "Ethereum" | "Substrate" // The type of chain. - tokens: TokenConfig[] // An object to configure the tokens this bridge can transfer. See the TokenConfig object below. - nativeTokenSymbol: string // The native token symbol of this chain. - blockExplorer?: string //This should be the full path to display a tx hash, without the trailing slash, ie. https://etherscan.io/tx -} +export type BridgeConfig = { + networkId?: number; // The networkId of this chain. + chainId: number; // The bridge's chainId. + name: string; // The human readable name of this chain. + rpcUrl: string; // An RPC URL for this chain. + type: ChainType; // The type of chain. + tokens: TokenConfig[]; // An object to configure the tokens (see below) + nativeTokenSymbol: string; // The native token symbol of this chain. + decimals: number; +}; ``` ``` @@ -67,6 +72,33 @@ type TokenConfig = { }; ``` +EVM Chains should additionally be configured with the following params + +``` +export type EvmBridgeConfig = BridgeConfig & { + bridgeAddress: string; + erc20HandlerAddress: string; + type: "Ethereum"; + nativeTokenSymbol: string; + // This should be the full path to display a tx hash, without the trailing slash, ie. https://etherscan.io/tx + blockExplorer?: string; + defaultGasPrice?: number; + deployedBlockNumber?: number; +}; +``` + +Substrate chains should be configured with the following + +``` +export type SubstrateBridgeConfig = BridgeConfig & { + type: "Substrate"; + chainbridgePalletName: string; // The name of the chainbridge palette + transferPalletName: string; // The name of the pallet that should initiate transfers + transferFunctionName: string; // The name of the method to call to initiate a transfer + typesFileName: string; // The name of the Substrate types file. The file should be located in `src/Contexts/Adaptors/SubstrateApis` +}; +``` + Run `yarn build`. Deploy the contents of the `/build` folder to any static website host (eg. S3, Azure storage) or IPFS. diff --git a/craco.config.js b/craco.config.cjs similarity index 82% rename from craco.config.js rename to craco.config.cjs index 73cafe6f..6184a343 100644 --- a/craco.config.js +++ b/craco.config.cjs @@ -38,6 +38,16 @@ module.exports = { }), ], }, + module: { + rules: [ + ...webpackConfig.module.rules, + { + test: /\.mjs$/, + include: /node_modules/, + type: "javascript/auto", + }, + ], + }, devtool: "source-map", }), }, diff --git a/docker-compose-centrifuge.yml b/docker-compose-centrifuge.yml new file mode 100644 index 00000000..e3f36538 --- /dev/null +++ b/docker-compose-centrifuge.yml @@ -0,0 +1,17 @@ +# Copyright 2020 ChainSafe Systems +# SPDX-License-Identifier: LGPL-3.0-only + +version: "3" +services: + geth1: + image: "chainsafe/chainbridge-geth:20200505131100-5586a65" + container_name: geth1 + ports: + - "8545:8545" + + sub-chain: + image: "centrifugeio/centrifuge-chain:20201204140308-0809a17" + container_name: sub-chain + command: centrifuge-chain --dev --alice --ws-external --rpc-external + ports: + - "9944:9944" diff --git a/docker-compose-substrate.yml b/docker-compose-substrate.yml new file mode 100644 index 00000000..3e74fe76 --- /dev/null +++ b/docker-compose-substrate.yml @@ -0,0 +1,17 @@ +# Copyright 2020 ChainSafe Systems +# SPDX-License-Identifier: LGPL-3.0-only + +version: "3" +services: + geth1: + image: "chainsafe/chainbridge-geth:20200505131100-5586a65" + container_name: geth1 + ports: + - "8545:8545" + + sub-chain: + image: "chainsafe/chainbridge-substrate-chain:v1.3.0" + container_name: sub-chain + command: chainbridge-substrate-chain --dev --alice --ws-external --rpc-external + ports: + - "9944:9944" diff --git a/package.json b/package.json index f4c34f4e..a4053e34 100755 --- a/package.json +++ b/package.json @@ -6,13 +6,22 @@ "@babel/core": "^7.12.3", "@babel/runtime": "^7.12.1", "@chainsafe/chainbridge-contracts": "1.0.5", - "@chainsafe/web3-context": "1.2.0", "@chainsafe/common-components": "1.0.26", "@chainsafe/common-theme": "1.0.10", + "@chainsafe/web3-context": "1.2.0", "@material-ui/styles": "4.10.0", + "@polkadot/api": "3.11.1", + "@polkadot/extension-dapp": "0.37.1", + "@polkadot/keyring": "5.6.3", + "@polkadot/networks": "5.6.3", + "@polkadot/types": "3.9.3", + "@polkadot/ui-keyring": "0.69.1", + "@polkadot/ui-settings": "0.69.1", + "@polkadot/util": "5.6.3", + "@polkadot/util-crypto": "5.6.3", "@sentry/react": "^5.26.0", "@types/history": "^4.7.8", - "bnc-onboard": "1.19.2", + "bnc-onboard": "1.26.1", "clsx": "^1.1.1", "dayjs": "^1.9.1", "ethers": "5.0.32", @@ -25,6 +34,7 @@ "yup": "^0.29.3" }, "devDependencies": { + "@polkadot/typegen": "^4.11.2", "@sentry/cli": "1.58.0", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.0.4", @@ -52,8 +62,13 @@ "clean:dist": "rm -rf ./*/**/dist && rm -rf ./*/**/build && rm -rf ./*/**/storybook-static", "clean": "yarn clean:dependencies && yarn clean:dist", "prettier": "prettier --config .prettierrc 'packages/**/src/**/*.{ts,tsx,js,jsx,md}' --write", - "start:tunnel": "./ngrok http https://localhost:3000" + "start:tunnel": "./ngrok http https://localhost:3000", + "start:substrate": "docker-compose -f ./docker-compose-substrate.yml up -V", + "start:centrifuge": "docker-compose -f ./docker-compose-centrifuge.yml up -V", + "setup:example": "./scripts/setup-eth-example.sh && node --experimental-json-modules ./scripts/setup-sub-example.mjs", + "setup:centrifuge": "./scripts/setup-eth-centrifuge.sh" }, + "cracoConfig": "./craco.config.cjs", "eslintConfig": { "extends": "react-app" }, diff --git a/relayer-config.json b/relayer-config.json new file mode 100644 index 00000000..b94d0f8b --- /dev/null +++ b/relayer-config.json @@ -0,0 +1,29 @@ +{ + "chains": [ + { + "name": "eth", + "type": "ethereum", + "id": "0", + "endpoint": "ws://localhost:8545", + "from": "0xff93B45308FD417dF303D6515aB04D9e89a750Ca", + "opts": { + "bridge": "0x62877dDCd49aD22f5eDfc6ac108e9a4b5D2bD88B", + "erc20Handler": "0x3167776db165D8eA0f51790CA2bbf44Db5105ADF", + "erc721Handler": "0x3f709398808af36ADBA86ACC617FeB7F5B7B193E", + "genericHandler": "0x2B6Ab4b880A45a07d83Cf4d664Df4Ab85705Bc07", + "gasLimit": "1000000", + "maxGasPrice": "20000000" + } + }, + { + "name": "sub", + "type": "substrate", + "id": "1", + "endpoint": "ws://localhost:9944", + "from": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", + "opts": { + "useExtendedCall": "true" + } + } + ] +} diff --git a/scripts/bridgeTypes.json b/scripts/bridgeTypes.json new file mode 100644 index 00000000..83ab8c53 --- /dev/null +++ b/scripts/bridgeTypes.json @@ -0,0 +1,18 @@ +{ + "chainbridge::ChainId": "u8", + "ChainId": "u8", + "ResourceId": "[u8; 32]", + "DepositNonce": "u64", + "ProposalVotes": { + "votes_for": "Vec", + "votes_against": "Vec", + "status": "enum" + }, + "Erc721Token": { + "id": "TokenId", + "metadata": "Vec" + }, + "TokenId": "U256", + "Address": "AccountId", + "LookupSource": "AccountId" +} diff --git a/scripts/setup-eth-centrifuge.sh b/scripts/setup-eth-centrifuge.sh new file mode 100755 index 00000000..abefc39a --- /dev/null +++ b/scripts/setup-eth-centrifuge.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +cb-sol-cli deploy --all --relayerThreshold 1 +cb-sol-cli bridge register-resource --resourceId "0x00000000000000000000000000000009e974040e705c10fb4de576d6cc261900" --targetContract "0x21605f71845f372A9ed84253d2D024B7B10999f4" +cb-sol-cli bridge set-burn --tokenContract "0x21605f71845f372A9ed84253d2D024B7B10999f4" +cb-sol-cli erc20 add-minter --minter "0x3167776db165D8eA0f51790CA2bbf44Db5105ADF" diff --git a/scripts/setup-eth-example.sh b/scripts/setup-eth-example.sh new file mode 100755 index 00000000..9c0c73c1 --- /dev/null +++ b/scripts/setup-eth-example.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +cb-sol-cli deploy --all --relayerThreshold 1 +cb-sol-cli bridge register-resource --resourceId "0x000000000000000000000000000000c76ebe4a02bbc34786d860b355f5a5ce00" --targetContract "0x21605f71845f372A9ed84253d2D024B7B10999f4" +cb-sol-cli bridge set-burn --tokenContract "0x21605f71845f372A9ed84253d2D024B7B10999f4" +cb-sol-cli erc20 add-minter --minter "0x3167776db165D8eA0f51790CA2bbf44Db5105ADF" diff --git a/scripts/setup-sub-example.mjs b/scripts/setup-sub-example.mjs new file mode 100644 index 00000000..39e577d2 --- /dev/null +++ b/scripts/setup-sub-example.mjs @@ -0,0 +1,38 @@ +import { ApiPromise, WsProvider, Keyring } from "@polkadot/api"; +import types from "./bridgeTypes.json"; + +const keyring = new Keyring({ type: "sr25519" }); +const wsProvider = new WsProvider("ws://127.0.0.1:9944"); +const api = await ApiPromise.create({ provider: wsProvider, types: types }); +const ALICE = keyring.addFromUri("//Alice"); + +const addRelayer = (relayer) => { + return api.tx.sudo.sudo(api.tx.chainBridge.addRelayer(relayer)); +}; + +const whitelistChain = (chainId) => { + return api.tx.sudo.sudo(api.tx.chainBridge.whitelistChain(chainId)); +}; + +const registerResource = (rId, method) => { + return api.tx.sudo.sudo(api.tx.chainBridge.setResource(rId, method)); +}; + +(async function () { + let nonce = await api.rpc.system.accountNextIndex(ALICE.address); + let txHash = await addRelayer(ALICE.address).signAndSend(ALICE, { nonce }); + console.log(`Added relayer ALICE in tx ${txHash}`); + + nonce = await api.rpc.system.accountNextIndex(ALICE.address); + txHash = await whitelistChain(0).signAndSend(ALICE, { nonce }); + console.log(`Whitelisted chain 0 in tx ${txHash}`); + + nonce = await api.rpc.system.accountNextIndex(ALICE.address); + txHash = await registerResource( + "0x000000000000000000000000000000c76ebe4a02bbc34786d860b355f5a5ce00", + "0x4578616d706c652e7472616e73666572" + ).signAndSend(ALICE, { nonce }); + console.log(`Registered resource in tx ${txHash}`); + + await api.disconnect(); +})(); diff --git a/src/App.tsx b/src/App.tsx index e4095d77..12bec976 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,8 +11,9 @@ import Routes from "./Components/Routes"; import { lightTheme } from "./Themes/LightTheme"; import { ChainbridgeProvider } from "./Contexts/ChainbridgeContext"; import AppWrapper from "./Layouts/AppWrapper"; -import { Web3Provider } from "@chainsafe/web3-context"; +import { NetworkManagerProvider } from "./Contexts/NetworkManagerContext"; import { chainbridgeConfig } from "./chainbridgeConfig"; +import { Web3Provider } from "@chainsafe/web3-context"; import { utils } from "ethers"; import "@chainsafe/common-theme/dist/font-faces.css"; @@ -28,12 +29,19 @@ if ( } const App: React.FC<{}> = () => { - const tokens = chainbridgeConfig.chains.reduce((tca, bc) => { - return { - ...tca, - [bc.networkId]: bc.tokens, - }; - }, {}); + const tokens = chainbridgeConfig.chains + .filter((c) => c.type === "Ethereum") + .reduce((tca, bc: any) => { + if (bc.networkId) { + return { + ...tca, + [bc.networkId]: bc.tokens, + }; + } else { + return tca; + } + }, {}); + return ( ( @@ -59,28 +67,32 @@ const App: React.FC<{}> = () => { console.log("chainId: ", network), + network: (network) => + network && console.log("chainId: ", network), balance: (amount) => - console.log("balance: ", utils.formatEther(amount)), + amount && console.log("balance: ", utils.formatEther(amount)), }, }} checkNetwork={false} gasPricePollingInterval={120} gasPriceSetting="fast" > - - - - - - - + + + + + + + + + diff --git a/src/Components/Custom/AddressInput.tsx b/src/Components/Custom/AddressInput.tsx index c096f2ce..6bbdb6e0 100644 --- a/src/Components/Custom/AddressInput.tsx +++ b/src/Components/Custom/AddressInput.tsx @@ -30,6 +30,7 @@ interface IAddressInput extends FormikTextInputProps { classNames?: { input?: string; }; + sendToSameAccountHelper?: boolean; } const AddressInput: React.FC = ({ @@ -44,6 +45,7 @@ const AddressInput: React.FC = ({ label, labelClassName, captionMessage, + sendToSameAccountHelper = false, ...rest }: IAddressInput) => { const classes = useStyles(); @@ -83,13 +85,15 @@ const AddressInput: React.FC = ({ disabled={stored !== undefined} /> -
- toggleReceiver()} - /> -
+ {sendToSameAccountHelper && ( +
+ toggleReceiver()} + /> +
+ )} ); }; diff --git a/src/Components/Custom/TokenSelectInput.tsx b/src/Components/Custom/TokenSelectInput.tsx index 319cb0e5..337e553d 100644 --- a/src/Components/Custom/TokenSelectInput.tsx +++ b/src/Components/Custom/TokenSelectInput.tsx @@ -19,7 +19,7 @@ const TokenSelectInput: React.FC = ({ sync, ...rest }: ITokenSelectInput) => { - const [field] = useField(name); + const [field, , helpers] = useField(name); const labelParsed = tokens[field.value] ? `${label} ${tokens[field.value]?.balance} ${tokens[field.value]?.symbol}` : "Please select token"; @@ -35,6 +35,13 @@ const TokenSelectInput: React.FC = ({ // eslint-disable-next-line }, [field]); + useEffect(() => { + // If there is only one token, auto select + if (Object.keys(tokens).length === 1 && field.value === "") { + helpers.setValue(Object.keys(tokens)[0]); + } + }, [tokens, helpers, field.value]); + return ( createStyles({ @@ -185,24 +185,20 @@ type PreflightDetails = { const TransferPage = () => { const classes = useStyles(); + const { walletType, setWalletType } = useNetworkManager(); + const { - isReady, - checkIsReady, - wallet, - onboard, - tokens, - address, - network, - } = useWeb3(); - const { - homeChain, - destinationChains, - destinationChain, deposit, setDestinationChain, transactionStatus, resetDeposit, bridgeFee, + tokens, + isReady, + homeConfig, + destinationChainConfig, + destinationChains, + address, } = useChainbridge(); const [aboutOpen, setAboutOpen] = useState(false); @@ -217,12 +213,13 @@ const TransferPage = () => { tokenSymbol: "", }); - const handleConnect = async () => { - setWalletConnecting(true); - !wallet && (await onboard?.walletSelect()); - await checkIsReady(); - setWalletConnecting(false); - }; + useEffect(() => { + if (walletType !== "select" && walletConnecting === true) { + setWalletConnecting(false); + } else if (walletType === "select") { + setWalletConnecting(true); + } + }, [walletType, walletConnecting]); const DECIMALS = preflightDetails && tokens[preflightDetails.token] @@ -277,13 +274,14 @@ const TransferPage = () => { token: string().required("Please select a token"), receiver: string() .test("Valid address", "Please add a valid address", (value) => { + if (destinationChainConfig?.type === "Substrate") { + return isValidSubstrateAddress(value as string); + } return utils.isAddress(value as string); }) .required("Please add a receiving address"), }); - // TODO: line 467: How to pull correct HomeChain Symbol - return (
@@ -292,10 +290,10 @@ const TransferPage = () => { className={classes.connectButton} fullsize onClick={() => { - handleConnect(); + setWalletType("select"); }} > - Connect Metamask + Connect ) : walletConnecting ? (
@@ -321,7 +319,7 @@ const TransferPage = () => { variant="h2" className={classes.networkName} > - {homeChain?.name} + {homeConfig?.name}
)} @@ -344,20 +342,20 @@ const TransferPage = () => { >
({ label: dc.name, value: dc.chainId, }))} onChange={(value) => setDestinationChain(value)} - value={destinationChain?.chainId} + value={destinationChainConfig?.chainId} />
@@ -373,7 +371,7 @@ const TransferPage = () => { tokenSelectorKey="token" tokens={tokens} disabled={ - !destinationChain || + !destinationChainConfig || !preflightDetails.token || preflightDetails.token === "" } @@ -386,7 +384,7 @@ const TransferPage = () => { {
{ input: classes.addressInput, }} senderAddress={`${address}`} + sendToSameAccountHelper={ + destinationChainConfig?.type === homeConfig?.type + } />
{ open={changeNetworkOpen} close={() => setChangeNetworkOpen(false)} /> - bc.networkId)} - /> setPreflightModalOpen(false)} @@ -479,12 +475,14 @@ const TransferPage = () => { preflightDetails.token ); }} - sourceNetwork={homeChain?.name || ""} - targetNetwork={destinationChain?.name || ""} + sourceNetwork={homeConfig?.name || ""} + targetNetwork={destinationChainConfig?.name || ""} tokenSymbol={preflightDetails?.tokenSymbol || ""} value={preflightDetails?.tokenAmount || 0} /> + {/* This is here due to requiring router */} +
); }; diff --git a/src/Components/Pages/WrapperPage.tsx b/src/Components/Pages/WrapperPage.tsx index 2c7e70f4..8c1d6137 100644 --- a/src/Components/Pages/WrapperPage.tsx +++ b/src/Components/Pages/WrapperPage.tsx @@ -1,8 +1,7 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { makeStyles, createStyles, ITheme } from "@chainsafe/common-theme"; import AboutDrawer from "../../Modules/AboutDrawer"; import ChangeNetworkDrawer from "../../Modules/ChangeNetworkDrawer"; -import NetworkUnsupportedModal from "../../Modules/NetworkUnsupportedModal"; import { Button, Typography, @@ -11,18 +10,17 @@ import { } from "@chainsafe/common-components"; import { Form, Formik } from "formik"; import clsx from "clsx"; -import { useWeb3 } from "@chainsafe/web3-context"; import { useChainbridge } from "../../Contexts/ChainbridgeContext"; import { object, string } from "yup"; import { ReactComponent as ETHIcon } from "../../media/tokens/eth.svg"; -import { chainbridgeConfig, TokenConfig } from "../../chainbridgeConfig"; +import { TokenConfig } from "../../chainbridgeConfig"; import PreflightModalWrap from "../../Modules/PreflightModalWrap"; import WrapActiveModal from "../../Modules/WrapActiveModal"; -import { parseUnits } from "ethers/lib/utils"; import { forwardTo } from "../../Utils/History"; import { ROUTE_LINKS } from "../Routes"; -import { BigNumber, utils } from "ethers"; import SimpleTokenInput from "../Custom/SimpleTokenInput"; +import { useNetworkManager } from "../../Contexts/NetworkManagerContext"; +import NetworkUnsupportedModal from "../../Modules/NetworkUnsupportedModal"; const useStyles = makeStyles(({ constants, palette }: ITheme) => createStyles({ @@ -196,23 +194,18 @@ type PreflightDetails = { const MainPage = () => { const classes = useStyles(); + const { walletType, setWalletType, homeChainConfig } = useNetworkManager(); const { - isReady, - checkIsReady, - wallet, - onboard, - tokens, - ethBalance, - network, - address, - gasPrice, - } = useWeb3(); - const { - homeChain, wrapTokenConfig, wrapToken, unwrapToken, + homeConfig, + isReady, + tokens, + nativeTokenBalance, + address, } = useChainbridge(); + const [aboutOpen, setAboutOpen] = useState(false); const [walletConnecting, setWalletConnecting] = useState(false); const [changeNetworkOpen, setChangeNetworkOpen] = useState(false); @@ -233,15 +226,16 @@ const MainPage = () => { | undefined >(undefined); - const handleConnect = async () => { - setWalletConnecting(true); - !wallet && (await onboard?.walletSelect()); - await checkIsReady(); - setWalletConnecting(false); - }; + useEffect(() => { + if (walletType !== "select" && walletConnecting === true) { + setWalletConnecting(false); + } else if (walletType === "select") { + setWalletConnecting(true); + } + }, [walletType, walletConnecting]); const handleWrapToken = async () => { - if (!wrapTokenConfig || !wrapToken || !homeChain) return; + if (!wrapTokenConfig || !wrapToken || !homeConfig) return; try { setTxDetails({ @@ -250,21 +244,17 @@ const MainPage = () => { txState: "inProgress", action: action, }); - const tx = await wrapToken({ - value: parseUnits(`${preflightDetails.tokenAmount}`, DECIMALS), - gasPrice: BigNumber.from( - utils.parseUnits( - (homeChain.defaultGasPrice || gasPrice).toString(), - 9 - ) - ).toString(), - }); + const txHash = await wrapToken(preflightDetails.tokenAmount); + + if (txHash === "") { + setTxDetails(undefined); + throw Error("Wrap Transaction failed"); + } - await tx?.wait(); setTxDetails({ tokenInfo: wrapTokenConfig, value: preflightDetails.tokenAmount, - txHash: tx?.hash, + txHash: txHash, txState: "done", action: action, }); @@ -274,7 +264,7 @@ const MainPage = () => { }; const handleUnwrapToken = async () => { - if (!wrapTokenConfig || !unwrapToken || !homeChain) return; + if (!wrapTokenConfig || !unwrapToken || !homeConfig) return; try { setTxDetails({ @@ -283,20 +273,18 @@ const MainPage = () => { txState: "inProgress", action: action, }); - const tx = await unwrapToken( - parseUnits(`${preflightDetails.tokenAmount}`, DECIMALS), - { - gasPrice: utils - .parseUnits((homeChain.defaultGasPrice || gasPrice).toString(), 9) - .toString(), - } - ); - await tx?.wait(); + const txHash = await unwrapToken(preflightDetails.tokenAmount); + + if (txHash === "") { + setTxDetails(undefined); + throw Error("Unwrap Transaction failed"); + } + setTxDetails({ tokenInfo: wrapTokenConfig, value: preflightDetails.tokenAmount, - txHash: tx?.hash, + txHash: txHash, txState: "done", action: action, }); @@ -305,10 +293,9 @@ const MainPage = () => { } }; - const DECIMALS = 18; const REGEX = - DECIMALS > 0 - ? new RegExp(`^[0-9]{1,18}(.[0-9]{1,${DECIMALS}})?$`) + homeChainConfig?.decimals && homeChainConfig.decimals > 0 + ? new RegExp(`^[0-9]{1,18}(.[0-9]{1,${homeChainConfig.decimals}})?$`) : new RegExp(`^[0-9]{1,18}?$`); const wrapSchema = object().shape({ @@ -322,7 +309,9 @@ const MainPage = () => { }) .test("Max", "Insufficent funds", (value) => { return action === "wrap" - ? ethBalance && value && parseFloat(value) <= ethBalance + ? nativeTokenBalance && + value && + parseFloat(value) <= nativeTokenBalance ? true : false : tokens[wrapTokenConfig?.address || "0x"].balance && @@ -350,7 +339,7 @@ const MainPage = () => { className={classes.connectButton} fullsize onClick={() => { - handleConnect(); + setWalletType("select"); }} > Connect Metamask @@ -380,7 +369,7 @@ const MainPage = () => { variant="h2" className={classes.networkName} > - {homeChain?.name} + {homeConfig?.name} )} @@ -400,7 +389,7 @@ const MainPage = () => { >
@@ -417,7 +406,7 @@ const MainPage = () => { label="I want to convert" max={ action === "wrap" - ? ethBalance + ? nativeTokenBalance : tokens[wrapTokenConfig?.address || "0x"]?.balance } /> @@ -427,8 +416,8 @@ const MainPage = () => { Balance:{" "} {action === "wrap" - ? ethBalance - ? ethBalance.toFixed(2) + ? nativeTokenBalance + ? nativeTokenBalance.toFixed(2) : 0.0 : tokens[wrapTokenConfig?.address || "0x"].balance} @@ -479,13 +468,6 @@ const MainPage = () => { open={changeNetworkOpen} close={() => setChangeNetworkOpen(false)} /> - bc.tokens.find((t) => t.isNativeWrappedToken)) - .map((bc) => bc.networkId)} - /> setPreflightModalOpen(false)} @@ -499,17 +481,17 @@ const MainPage = () => { setPreflightModalOpen(false); } }} - sourceNetwork={homeChain?.name || ""} + sourceNetwork={homeConfig?.name || ""} tokenSymbol={ action === "wrap" - ? homeChain?.nativeTokenSymbol || "ETH" + ? homeConfig?.nativeTokenSymbol || "ETH" : wrapTokenConfig?.symbol || "wETH" } value={preflightDetails?.tokenAmount || 0} wrappedTitle={ action === "wrap" ? `${wrapTokenConfig?.name} (${wrapTokenConfig?.symbol})` - : homeChain?.nativeTokenSymbol || "ETH" + : homeConfig?.nativeTokenSymbol || "ETH" } action={action} /> @@ -522,6 +504,8 @@ const MainPage = () => { }} /> )} + {/* This is here due to requiring router */} + ); }; diff --git a/src/Contexts/Adaptors/EVMAdaptors.tsx b/src/Contexts/Adaptors/EVMAdaptors.tsx new file mode 100644 index 00000000..c4c1b0fe --- /dev/null +++ b/src/Contexts/Adaptors/EVMAdaptors.tsx @@ -0,0 +1,624 @@ +import React from "react"; +import { Bridge, BridgeFactory } from "@chainsafe/chainbridge-contracts"; +import { useWeb3 } from "@chainsafe/web3-context"; +import { BigNumber, ethers, utils } from "ethers"; +import { useCallback, useEffect, useState } from "react"; +import { + chainbridgeConfig, + EvmBridgeConfig, + TokenConfig, +} from "../../chainbridgeConfig"; +import { Erc20DetailedFactory } from "../../Contracts/Erc20DetailedFactory"; +import { Weth } from "../../Contracts/Weth"; +import { WethFactory } from "../../Contracts/WethFactory"; +import { useNetworkManager } from "../NetworkManagerContext"; +import { + IDestinationBridgeProviderProps, + IHomeBridgeProviderProps, +} from "./interfaces"; +import { HomeBridgeContext } from "../HomeBridgeContext"; +import { DestinationBridgeContext } from "../DestinationBridgeContext"; +import { parseUnits } from "ethers/lib/utils"; +import { decodeAddress } from "@polkadot/util-crypto"; + +const resetAllowanceLogicFor = [ + "0xdac17f958d2ee523a2206206994597c13d831ec7", //USDT + //Add other offending tokens here +]; + +export const EVMHomeAdaptorProvider = ({ + children, +}: IHomeBridgeProviderProps) => { + const { + isReady, + network, + provider, + gasPrice, + address, + tokens, + wallet, + checkIsReady, + ethBalance, + onboard, + resetOnboard, + } = useWeb3(); + + const getNetworkName = (id: any) => { + switch (Number(id)) { + case 5: + return "Localhost"; + case 1: + return "Mainnet"; + case 3: + return "Ropsten"; + case 4: + return "Rinkeby"; + // case 5: + // return "Goerli"; + case 6: + return "Kotti"; + case 42: + return "Kovan"; + case 61: + return "Ethereum Classic - Mainnet"; + default: + return "Other"; + } + }; + + const { + homeChainConfig, + setTransactionStatus, + setDepositNonce, + handleSetHomeChain, + homeChains, + setNetworkId, + } = useNetworkManager(); + + const [homeBridge, setHomeBridge] = useState(undefined); + const [relayerThreshold, setRelayerThreshold] = useState( + undefined + ); + const [bridgeFee, setBridgeFee] = useState(); + + const [depositAmount, setDepositAmount] = useState(); + const [selectedToken, setSelectedToken] = useState(""); + + // Contracts + const [wrapper, setWrapper] = useState(undefined); + const [wrapTokenConfig, setWrapperConfig] = useState( + undefined + ); + + useEffect(() => { + if (network) { + const chain = homeChains.find((chain) => chain.networkId === network); + setNetworkId(network); + if (chain) { + handleSetHomeChain(chain.chainId); + } + } + }, [handleSetHomeChain, homeChains, network, setNetworkId]); + + const [initialising, setInitialising] = useState(false); + const [walletSelected, setWalletSelected] = useState(false); + useEffect(() => { + if (initialising || homeBridge || !onboard) return; + console.log("starting init"); + setInitialising(true); + if (!walletSelected) { + onboard + .walletSelect("metamask") + .then((success) => { + setWalletSelected(success); + if (success) { + checkIsReady() + .then((success) => { + if (success) { + if (homeChainConfig && network && isReady && provider) { + const signer = provider.getSigner(); + if (!signer) { + console.log("No signer"); + setInitialising(false); + return; + } + + const bridge = BridgeFactory.connect( + (homeChainConfig as EvmBridgeConfig).bridgeAddress, + signer + ); + setHomeBridge(bridge); + + const wrapperToken = homeChainConfig.tokens.find( + (token) => token.isNativeWrappedToken + ); + + if (!wrapperToken) { + setWrapperConfig(undefined); + setWrapper(undefined); + } else { + setWrapperConfig(wrapperToken); + const connectedWeth = WethFactory.connect( + wrapperToken.address, + signer + ); + setWrapper(connectedWeth); + } + } + } + }) + .catch((error) => { + console.error(error); + }) + .finally(() => { + setInitialising(false); + }); + } + }) + .catch((error) => { + setInitialising(false); + console.error(error); + }); + } else { + checkIsReady() + .then((success) => { + if (success) { + if (homeChainConfig && network && isReady && provider) { + const signer = provider.getSigner(); + if (!signer) { + console.log("No signer"); + setInitialising(false); + return; + } + + const bridge = BridgeFactory.connect( + (homeChainConfig as EvmBridgeConfig).bridgeAddress, + signer + ); + setHomeBridge(bridge); + + const wrapperToken = homeChainConfig.tokens.find( + (token) => token.isNativeWrappedToken + ); + + if (!wrapperToken) { + setWrapperConfig(undefined); + setWrapper(undefined); + } else { + setWrapperConfig(wrapperToken); + const connectedWeth = WethFactory.connect( + wrapperToken.address, + signer + ); + setWrapper(connectedWeth); + } + } + } + }) + .catch((error) => { + console.error(error); + }) + .finally(() => { + setInitialising(false); + }); + } + }, [ + initialising, + homeChainConfig, + isReady, + provider, + checkIsReady, + network, + homeBridge, + onboard, + walletSelected, + ]); + + useEffect(() => { + const getRelayerThreshold = async () => { + if (homeBridge) { + const threshold = BigNumber.from( + await homeBridge._relayerThreshold() + ).toNumber(); + setRelayerThreshold(threshold); + } + }; + const getBridgeFee = async () => { + if (homeBridge) { + const bridgeFee = Number(utils.formatEther(await homeBridge._fee())); + setBridgeFee(bridgeFee); + } + }; + getRelayerThreshold(); + getBridgeFee(); + }, [homeBridge]); + + const handleConnect = useCallback(async () => { + if (wallet && wallet.connect && network) { + await onboard?.walletSelect("metamask"); + await wallet.connect(); + } + }, [wallet, network, onboard]); + + const deposit = useCallback( + async ( + amount: number, + recipient: string, + tokenAddress: string, + destinationChainId: number + ) => { + if (!homeChainConfig || !homeBridge) { + console.error("Home bridge contract is not instantiated"); + return; + } + const signer = provider?.getSigner(); + if (!address || !signer) { + console.log("No signer"); + return; + } + + const destinationChain = chainbridgeConfig.chains.find( + (c) => c.chainId === destinationChainId + ); + if (destinationChain?.type === "Substrate") { + recipient = `0x${Buffer.from(decodeAddress(recipient)).toString( + "hex" + )}`; + } + const token = homeChainConfig.tokens.find( + (token) => token.address === tokenAddress + ); + + if (!token) { + console.log("Invalid token selected"); + return; + } + setTransactionStatus("Initializing Transfer"); + setDepositAmount(amount); + setSelectedToken(tokenAddress); + const erc20 = Erc20DetailedFactory.connect(tokenAddress, signer); + const erc20Decimals = tokens[tokenAddress].decimals; + + const data = + "0x" + + utils + .hexZeroPad( + // TODO Wire up dynamic token decimals + BigNumber.from( + utils.parseUnits(amount.toString(), erc20Decimals) + ).toHexString(), + 32 + ) + .substr(2) + // Deposit Amount (32 bytes) + utils + .hexZeroPad(utils.hexlify((recipient.length - 2) / 2), 32) + .substr(2) + // len(recipientAddress) (32 bytes) + recipient.substr(2); // recipientAddress (?? bytes) + + try { + const currentAllowance = await erc20.allowance( + address, + (homeChainConfig as EvmBridgeConfig).erc20HandlerAddress + ); + + if ( + Number(utils.formatUnits(currentAllowance, erc20Decimals)) < amount + ) { + if ( + Number(utils.formatUnits(currentAllowance, erc20Decimals)) > 0 && + resetAllowanceLogicFor.includes(tokenAddress) + ) { + //We need to reset the user's allowance to 0 before we give them a new allowance + //TODO Should we alert the user this is happening here? + await ( + await erc20.approve( + (homeChainConfig as EvmBridgeConfig).erc20HandlerAddress, + BigNumber.from(utils.parseUnits("0", erc20Decimals)), + { + gasPrice: BigNumber.from( + utils.parseUnits( + ( + (homeChainConfig as EvmBridgeConfig).defaultGasPrice || + gasPrice + ).toString(), + 9 + ) + ).toString(), + } + ) + ).wait(1); + } + await ( + await erc20.approve( + (homeChainConfig as EvmBridgeConfig).erc20HandlerAddress, + BigNumber.from( + utils.parseUnits(amount.toString(), erc20Decimals) + ), + { + gasPrice: BigNumber.from( + utils.parseUnits( + ( + (homeChainConfig as EvmBridgeConfig).defaultGasPrice || + gasPrice + ).toString(), + 9 + ) + ).toString(), + } + ) + ).wait(1); + } + homeBridge.once( + homeBridge.filters.Deposit( + destinationChainId, + token.resourceId, + null + ), + (destChainId, resourceId, depositNonce) => { + setDepositNonce(`${depositNonce.toString()}`); + setTransactionStatus("In Transit"); + } + ); + + await ( + await homeBridge.deposit(destinationChainId, token.resourceId, data, { + gasPrice: utils.parseUnits( + ( + (homeChainConfig as EvmBridgeConfig).defaultGasPrice || gasPrice + ).toString(), + 9 + ), + value: utils.parseUnits((bridgeFee || 0).toString(), 18), + }) + ).wait(); + + return Promise.resolve(); + } catch (error) { + setTransactionStatus("Transfer Aborted"); + setSelectedToken(tokenAddress); + } + }, + [ + homeBridge, + address, + bridgeFee, + homeChainConfig, + gasPrice, + provider, + setDepositNonce, + setTransactionStatus, + tokens, + ] + ); + + const wrapToken = async (value: number): Promise => { + if (!wrapTokenConfig || !wrapper?.deposit || !homeChainConfig) + return "not ready"; + + try { + const tx = await wrapper.deposit({ + value: parseUnits(`${value}`, homeChainConfig.decimals), + gasPrice: BigNumber.from( + utils.parseUnits( + ( + (homeChainConfig as EvmBridgeConfig).defaultGasPrice || gasPrice + ).toString(), + 9 + ) + ).toString(), + }); + + await tx?.wait(); + if (tx?.hash) { + return tx?.hash; + } else { + return ""; + } + } catch (error) { + console.error(error); + return ""; + } + }; + + const unwrapToken = async (value: number): Promise => { + if (!wrapTokenConfig || !wrapper?.withdraw || !homeChainConfig) + return "not ready"; + + try { + const tx = await wrapper.deposit({ + value: parseUnits(`${value}`, homeChainConfig.decimals), + gasPrice: BigNumber.from( + utils.parseUnits( + ( + (homeChainConfig as EvmBridgeConfig).defaultGasPrice || gasPrice + ).toString(), + 9 + ) + ).toString(), + }); + + await tx?.wait(); + if (tx?.hash) { + return tx?.hash; + } else { + return ""; + } + } catch (error) { + console.error(error); + return ""; + } + }; + + return ( + { + await resetOnboard(); + }, + getNetworkName, + bridgeFee, + deposit, + depositAmount, + selectedToken, + setDepositAmount, + setSelectedToken, + tokens, + relayerThreshold, + wrapTokenConfig, + wrapper, + wrapToken, + unwrapToken, + isReady, + chainConfig: homeChainConfig, + address, + nativeTokenBalance: ethBalance, + }} + > + {children} + + ); +}; + +export const EVMDestinationAdaptorProvider = ({ + children, +}: IDestinationBridgeProviderProps) => { + console.log("EVM destination loaded"); + const { + depositNonce, + destinationChainConfig, + homeChainConfig, + tokensDispatch, + setTransactionStatus, + setTransferTxHash, + setDepositVotes, + depositVotes, + } = useNetworkManager(); + + const [destinationBridge, setDestinationBridge] = useState< + Bridge | undefined + >(undefined); + + useEffect(() => { + if (destinationBridge) return; + let provider; + if (destinationChainConfig?.rpcUrl.startsWith("wss")) { + if (destinationChainConfig.rpcUrl.includes("infura")) { + const parts = destinationChainConfig.rpcUrl.split("/"); + + provider = new ethers.providers.InfuraWebSocketProvider( + destinationChainConfig.networkId, + parts[parts.length - 1] + ); + } + if (destinationChainConfig.rpcUrl.includes("alchemyapi")) { + const parts = destinationChainConfig.rpcUrl.split("/"); + + provider = new ethers.providers.AlchemyWebSocketProvider( + destinationChainConfig.networkId, + parts[parts.length - 1] + ); + } + } else { + provider = new ethers.providers.JsonRpcProvider( + destinationChainConfig?.rpcUrl + ); + } + if (destinationChainConfig && provider) { + const bridge = BridgeFactory.connect( + (destinationChainConfig as EvmBridgeConfig).bridgeAddress, + provider + ); + setDestinationBridge(bridge); + } + }, [destinationChainConfig, destinationBridge]); + + useEffect(() => { + if ( + destinationChainConfig && + homeChainConfig?.chainId && + destinationBridge && + depositNonce + ) { + destinationBridge.on( + destinationBridge.filters.ProposalEvent( + homeChainConfig.chainId, + BigNumber.from(depositNonce), + null, + null, + null + ), + (originChainId, depositNonce, status, resourceId, dataHash, tx) => { + switch (BigNumber.from(status).toNumber()) { + case 1: + tokensDispatch({ + type: "addMessage", + payload: `Proposal created on ${destinationChainConfig.name}`, + }); + break; + case 2: + tokensDispatch({ + type: "addMessage", + payload: `Proposal has passed. Executing...`, + }); + break; + case 3: + setTransactionStatus("Transfer Completed"); + setTransferTxHash(tx.transactionHash); + break; + case 4: + setTransactionStatus("Transfer Aborted"); + setTransferTxHash(tx.transactionHash); + break; + } + } + ); + + destinationBridge.on( + destinationBridge.filters.ProposalVote( + homeChainConfig.chainId, + BigNumber.from(depositNonce), + null, + null + ), + async (originChainId, depositNonce, status, resourceId, tx) => { + const txReceipt = await tx.getTransactionReceipt(); + if (txReceipt.status === 1) { + setDepositVotes(depositVotes + 1); + } + tokensDispatch({ + type: "addMessage", + payload: { + address: String(txReceipt.from), + signed: txReceipt.status === 1 ? "Confirmed" : "Rejected", + }, + }); + } + ); + } + return () => { + //@ts-ignore + destinationBridge?.removeAllListeners(); + }; + }, [ + depositNonce, + homeChainConfig, + destinationBridge, + depositVotes, + destinationChainConfig, + setDepositVotes, + setTransactionStatus, + setTransferTxHash, + tokensDispatch, + ]); + + return ( + {}, + }} + > + {children} + + ); +}; diff --git a/src/Contexts/Adaptors/SubstrateAdaptors.tsx b/src/Contexts/Adaptors/SubstrateAdaptors.tsx new file mode 100644 index 00000000..1021d411 --- /dev/null +++ b/src/Contexts/Adaptors/SubstrateAdaptors.tsx @@ -0,0 +1,411 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { DestinationBridgeContext } from "../DestinationBridgeContext"; +import { HomeBridgeContext } from "../HomeBridgeContext"; +import { useNetworkManager } from "../NetworkManagerContext"; +import { createApi, submitDeposit } from "./SubstrateApis/ChainBridgeAPI"; +import { + IDestinationBridgeProviderProps, + IHomeBridgeProviderProps, +} from "./interfaces"; + +import { ApiPromise } from "@polkadot/api"; +import { + web3Accounts, + web3Enable, + web3FromSource, +} from "@polkadot/extension-dapp"; +import { TypeRegistry } from "@polkadot/types"; +import { Tokens } from "@chainsafe/web3-context/dist/context/tokensReducer"; +import { BigNumber as BN } from "bignumber.js"; +import { UnsubscribePromise, VoidFn } from "@polkadot/api/types"; +import { utils } from "ethers"; +import { SubstrateBridgeConfig } from "../../chainbridgeConfig"; + +type injectedAccountType = { + address: string; + meta: { + name: string; + source: string; + }; +}; + +export const SubstrateHomeAdaptorProvider = ({ + children, +}: IHomeBridgeProviderProps) => { + const registry = new TypeRegistry(); + const [api, setApi] = useState(); + const [isReady, setIsReady] = useState(false); + + const [address, setAddress] = useState(undefined); + + const { + homeChainConfig, + setTransactionStatus, + setDepositNonce, + handleSetHomeChain, + homeChains, + } = useNetworkManager(); + + const [relayerThreshold, setRelayerThreshold] = useState( + undefined + ); + const [bridgeFee] = useState(0); + + const [depositAmount, setDepositAmount] = useState(); + const [selectedToken, setSelectedToken] = useState("CSS"); + + const [tokens, setTokens] = useState({}); + + useEffect(() => { + // Attempt connect on load + handleConnect(); + }); + + const [initiaising, setInitialising] = useState(false); + useEffect(() => { + // Once the chain ID has been set in the network context, the homechain configuration will be automatically set thus triggering this + if (!homeChainConfig || initiaising || api) return; + setInitialising(true); + createApi(homeChainConfig.rpcUrl) + .then((api) => { + setApi(api); + setInitialising(false); + }) + .catch(console.error); + }, [homeChainConfig, registry, api, initiaising]); + + const getRelayerThreshold = useCallback(async () => { + if (api) { + const relayerThreshold = await api.query[ + (homeChainConfig as SubstrateBridgeConfig).chainbridgePalletName + ].relayerThreshold(); + setRelayerThreshold(Number(relayerThreshold.toHuman())); + } + }, [api, homeChainConfig]); + + const confirmChainID = useCallback(async () => { + if (api) { + const currentId = Number( + api.consts[ + (homeChainConfig as SubstrateBridgeConfig).chainbridgePalletName + ].chainIdentity.toHuman() + ); + if (homeChainConfig?.chainId !== currentId) { + const correctConfig = homeChains.find( + (item) => item.chainId === currentId + ); + if (correctConfig) { + handleSetHomeChain(currentId); + } + } + } + }, [api, handleSetHomeChain, homeChainConfig, homeChains]); + + useEffect(() => { + // For all constants & essential values like: + // Relayer Threshold, resources IDs & Bridge Fees + // It is recommended to collect state at this point + if (api) { + if (api.isConnected && homeChainConfig) { + getRelayerThreshold(); + confirmChainID(); + } + } + }, [api, getRelayerThreshold, confirmChainID, homeChainConfig]); + + useEffect(() => { + if (!homeChainConfig) return; + let unsubscribe: VoidFn | undefined; + if (api) { + api.query.system + .account(address, (result) => { + const { + data: { free: balance }, + } = result.toJSON() as any; + setTokens({ + [homeChainConfig.tokens[0].symbol || "TOKEN"]: { + decimals: homeChainConfig.decimals, + balance: parseInt( + utils.formatUnits(balance, homeChainConfig.decimals) + ), + balanceBN: new BN(balance).shiftedBy(-homeChainConfig.decimals), + name: homeChainConfig.tokens[0].name, + symbol: homeChainConfig.tokens[0].symbol, + }, + }); + }) + .then((unsub) => { + unsubscribe = unsub; + }) + .catch(console.error); + } + return () => { + unsubscribe && unsubscribe(); + }; + }, [api, address, homeChainConfig]); + + const handleConnect = useCallback(async () => { + // Requests permission to inject the wallet + if (!isReady && !address) { + web3Enable("chainbridge-ui") + .then(() => { + // web3Account resolves with the injected accounts + // or an empty array + web3Accounts() + .then((accounts) => { + return accounts.map(({ address, meta }) => ({ + address, + meta: { + ...meta, + name: `${meta.name} (${meta.source})`, + }, + })); + }) + .then((injectedAccounts) => { + // This is where the correct chain configuration is set to the network context + // Any operations before presenting the accounts to the UI or providing the config + // to the rest of the dapp should be done here + loadAccounts(injectedAccounts); + handleSetHomeChain( + homeChains.find((item) => item.type === "Substrate")?.chainId + ); + }) + .catch(console.error); + }) + .catch(console.error); + } + }, [isReady, address, handleSetHomeChain, homeChains]); + + useEffect(() => { + // This is a simple check + // The reason for having a isReady is that the UI can lazy load data from this point + api?.isReady.then(() => setIsReady(true)); + }, [api, setIsReady]); + + const loadAccounts = (injectedAccounts: injectedAccountType[] = []) => { + setAddress(injectedAccounts[0].address); + }; + + const deposit = useCallback( + async ( + amount: number, + recipient: string, + tokenAddress: string, + destinationChainId: number + ) => { + if (api && address) { + const allAccounts = await web3Accounts(); + const targetAccount = allAccounts.find( + (item) => item.address === address + ); + if (targetAccount) { + const transferExtrinsic = submitDeposit( + api, + amount, + recipient, + destinationChainId + ); + + const injector = await web3FromSource(targetAccount.meta.source); + setTransactionStatus("Initializing Transfer"); + setDepositAmount(amount); + transferExtrinsic + .signAndSend( + address, + { signer: injector.signer }, + ({ status, events, isFinalized }) => { + status.isInBlock && + console.log( + `Completed at block hash #${status.isInBlock.toString()}` + ); + + if (status.isFinalized) { + events.filter(({ event }) => { + return api.events[ + (homeChainConfig as SubstrateBridgeConfig) + .chainbridgePalletName + ].FungibleTransfer.is(event); + }); + api.query[ + (homeChainConfig as SubstrateBridgeConfig) + .chainbridgePalletName + ] + .chainNonces(destinationChainId) + .then((response) => { + setDepositNonce(`${response.toJSON()}`); + setTransactionStatus("In Transit"); + }) + .catch((error) => { + console.error(error); + }); + } else { + console.log(`Current status: ${status.type}`); + } + } + ) + .catch((error: any) => { + console.log(":( transaction failed", error); + setTransactionStatus("Transfer Aborted"); + }); + } + } + }, + [api, setDepositNonce, setTransactionStatus, address, homeChainConfig] + ); + + // Required for adaptor however not needed for substrate + const wrapToken = async (value: number): Promise => { + return "Not implemented"; + }; + + // Required for adaptor however not needed for substrate + const unwrapToken = async (value: number): Promise => { + return "Not implemented"; + }; + + return ( + { + await api?.disconnect(); + }, + getNetworkName: () => homeChainConfig?.name || "undefined", + bridgeFee, + deposit, + depositAmount, + selectedToken, + setDepositAmount, + setSelectedToken, + tokens: tokens, + relayerThreshold, + wrapTokenConfig: undefined, // Not implemented + wrapper: undefined, // Not implemented + wrapToken, // Not implemented + unwrapToken, // Not implemented + isReady: isReady, + chainConfig: homeChainConfig, + address: address, + nativeTokenBalance: 0, + }} + > + {children} + + ); +}; + +export const SubstrateDestinationAdaptorProvider = ({ + children, +}: IDestinationBridgeProviderProps) => { + const { + depositNonce, + destinationChainConfig, + setDepositVotes, + depositVotes, + tokensDispatch, + setTransactionStatus, + } = useNetworkManager(); + + const [api, setApi] = useState(); + + const [initiaising, setInitialising] = useState(false); + useEffect(() => { + // Once the chain ID has been set in the network context, the destination configuration will be automatically + // set thus triggering this + if (!destinationChainConfig || initiaising || api) return; + setInitialising(true); + createApi(destinationChainConfig.rpcUrl) + .then((api) => { + setApi(api); + setInitialising(false); + }) + .catch(console.error); + }, [destinationChainConfig, api, initiaising]); + + const [listenerActive, setListenerActive] = useState< + UnsubscribePromise | undefined + >(undefined); + + useEffect(() => { + if (api && !listenerActive && depositNonce) { + // Wire up event listeners + // Subscribe to system events via storage + const unsubscribe = api.query.system.events((events) => { + console.log("----- Received " + events.length + " event(s): -----"); + // loop through the Vec + events.forEach((record) => { + // extract the phase, event and the event types + const { event, phase } = record; + const types = event.typeDef; + // show what we are busy with + console.log( + event.section + + ":" + + event.method + + "::" + + "phase=" + + phase.toString() + ); + console.log(event.meta.documentation.toString()); + // loop through each of the parameters, displaying the type and data + event.data.forEach((data, index) => { + console.log(types[index].type + ";" + data.toString()); + }); + + if ( + event.section === + (destinationChainConfig as SubstrateBridgeConfig) + .chainbridgePalletName && + event.method === "VoteFor" + ) { + setDepositVotes(depositVotes + 1); + tokensDispatch({ + type: "addMessage", + payload: { + address: "Substrate Relayer", + signed: "Confirmed", + }, + }); + } + + if ( + event.section === + (destinationChainConfig as SubstrateBridgeConfig) + .chainbridgePalletName && + event.method === "ProposalApproved" + ) { + setDepositVotes(depositVotes + 1); + setTransactionStatus("Transfer Completed"); + } + }); + }); + setListenerActive(unsubscribe); + } else if (listenerActive && !depositNonce) { + const unsubscribeCall = async () => { + setListenerActive(undefined); + }; + unsubscribeCall(); + } + }, [ + api, + depositNonce, + depositVotes, + destinationChainConfig, + listenerActive, + setDepositVotes, + setTransactionStatus, + tokensDispatch, + ]); + + return ( + { + await api?.disconnect(); + }, + }} + > + {children} + + ); +}; diff --git a/src/Contexts/Adaptors/SubstrateApis/ChainBridgeAPI.tsx b/src/Contexts/Adaptors/SubstrateApis/ChainBridgeAPI.tsx new file mode 100644 index 00000000..60065a22 --- /dev/null +++ b/src/Contexts/Adaptors/SubstrateApis/ChainBridgeAPI.tsx @@ -0,0 +1,38 @@ +import { ApiPromise, WsProvider } from "@polkadot/api"; +import BigNumber from "bignumber.js"; +import { + chainbridgeConfig, + SubstrateBridgeConfig, +} from "../../../chainbridgeConfig"; + +export const createApi = async (rpcUrl: string) => { + const provider = new WsProvider(rpcUrl); + const subChainConfig = chainbridgeConfig.chains.find( + (c) => c.rpcUrl === rpcUrl + ) as SubstrateBridgeConfig; + const types = (await import(`./${subChainConfig.typesFileName}`)) as any; + return ApiPromise.create({ provider, types }); +}; + +export const submitDeposit = ( + api: ApiPromise, + amount: number, + recipient: string, + destinationChainId: number +) => { + const subChainConfig = chainbridgeConfig.chains.find( + (c) => c.chainId !== destinationChainId + ) as SubstrateBridgeConfig; + + return api.tx[subChainConfig.transferPalletName][ + subChainConfig.transferFunctionName + ]( + new BigNumber(amount) + .multipliedBy( + new BigNumber(10).pow(new BigNumber(subChainConfig.decimals)) + ) + .toString(10), + recipient, + destinationChainId + ); +}; diff --git a/src/Contexts/Adaptors/SubstrateApis/bridgeTypes.json b/src/Contexts/Adaptors/SubstrateApis/bridgeTypes.json new file mode 100644 index 00000000..83ab8c53 --- /dev/null +++ b/src/Contexts/Adaptors/SubstrateApis/bridgeTypes.json @@ -0,0 +1,18 @@ +{ + "chainbridge::ChainId": "u8", + "ChainId": "u8", + "ResourceId": "[u8; 32]", + "DepositNonce": "u64", + "ProposalVotes": { + "votes_for": "Vec", + "votes_against": "Vec", + "status": "enum" + }, + "Erc721Token": { + "id": "TokenId", + "metadata": "Vec" + }, + "TokenId": "U256", + "Address": "AccountId", + "LookupSource": "AccountId" +} diff --git a/src/Contexts/Adaptors/SubstrateApis/centrifuge-types.ts b/src/Contexts/Adaptors/SubstrateApis/centrifuge-types.ts new file mode 100644 index 00000000..261de267 --- /dev/null +++ b/src/Contexts/Adaptors/SubstrateApis/centrifuge-types.ts @@ -0,0 +1,92 @@ +// Copyright 2017-2021 @polkadot/types-known authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +/* eslint-disable */ + +import type { OverrideVersionedType } from "@polkadot/types/types"; + +const sharedTypes = { + // Anchor + AnchorData: { + anchoredBlock: "u64", + docRoot: "H256", + id: "H256", + }, + PreCommitData: { + expirationBlock: "u64", + identity: "H256", + signingRoot: "H256", + }, + + // Fees + Fee: { + key: "Hash", + price: "Balance", + }, + + // MultiAccount + MultiAccountData: { + deposit: "Balance", + depositor: "AccountId", + signatories: "Vec", + threshold: "u16", + }, + + // Bridge + ChainId: "u8", + DepositNonce: "u64", + ResourceId: "[u8; 32]", + "chainbridge::ChainId": "u8", + + // NFT + RegistryId: "H160", + TokenId: "U256", + AssetId: { + registryId: "RegistryId", + tokenId: "TokenId", + }, + AssetInfo: { + metadata: "Bytes", + }, + MintInfo: { + anchorId: "Hash", + proofs: "Vec", + staticHashes: "[Hash; 3]", + }, + Proof: { + leafHash: "H256", + sortedHashes: "H256", + }, + ProofMint: { + hashes: "Vec", + property: "Bytes", + salt: "[u8; 32]", + value: "Bytes", + }, + RegistryInfo: { + fields: "Vec", + ownerCanBurn: "bool", + }, +}; + +const versioned: OverrideVersionedType[] = [ + { + minmax: [240, 999], + types: { + ...sharedTypes, + AccountInfo: "AccountInfoWithRefCount", + Address: "LookupSource", + LookupSource: "IndicesLookupSource", + Multiplier: "Fixed64", + RefCount: "RefCountTo259", + }, + }, + { + minmax: [1000, undefined], + types: { + ...sharedTypes, + }, + }, +]; + +export default sharedTypes; diff --git a/src/Contexts/Adaptors/interfaces.tsx b/src/Contexts/Adaptors/interfaces.tsx new file mode 100644 index 00000000..39b6a8d0 --- /dev/null +++ b/src/Contexts/Adaptors/interfaces.tsx @@ -0,0 +1,58 @@ +import { Tokens } from "@chainsafe/web3-context/dist/context/tokensReducer"; +import { BridgeConfig, TokenConfig } from "../../chainbridgeConfig"; +import { Weth } from "../../Contracts/Weth"; + +export interface IHomeBridgeProviderProps { + children: React.ReactNode | React.ReactNode[]; +} + +export interface IDestinationBridgeProviderProps { + children: React.ReactNode | React.ReactNode[]; +} + +export interface IWeb3ProviderWrapper { + children: React.ReactNode | React.ReactNode[]; +} + +export interface HomeChainAdaptorContext { + chainConfig: BridgeConfig | undefined; + + getNetworkName: (id: any) => string; + + connect: () => Promise; + disconnect: () => Promise; + + deposit( + amount: number, + recipient: string, + tokenAddress: string, + destinationChainId: number + ): Promise; + + relayerThreshold: number | undefined; + + setDepositAmount: (input: number | undefined) => void; + depositAmount: number | undefined; + + setSelectedToken: (tokenAddress: string) => void; + selectedToken: string; + + bridgeFee: number | undefined; + + wrapTokenConfig: TokenConfig | undefined; + wrapper: Weth | undefined; + + wrapToken: (value: number) => Promise; + unwrapToken: (value: number) => Promise; + + isReady: boolean; + address: string | undefined; + + nativeTokenBalance: number | undefined; + + tokens: Tokens; +} + +export interface DestinationChainContext { + disconnect: () => Promise; +} diff --git a/src/Contexts/ChainbridgeContext.tsx b/src/Contexts/ChainbridgeContext.tsx index 6e48d189..f47eae68 100644 --- a/src/Contexts/ChainbridgeContext.tsx +++ b/src/Contexts/ChainbridgeContext.tsx @@ -1,119 +1,95 @@ -import { useWeb3 } from "@chainsafe/web3-context"; -import React, { useContext, useEffect, useReducer, useState } from "react"; -import { Bridge, BridgeFactory } from "@chainsafe/chainbridge-contracts"; -import { - BigNumber, - BigNumberish, - ContractTransaction, - ethers, - Overrides, - PayableOverrides, - utils, -} from "ethers"; -import { Erc20DetailedFactory } from "../Contracts/Erc20DetailedFactory"; +import React, { useCallback, useContext } from "react"; import { BridgeConfig, chainbridgeConfig, TokenConfig, } from "../chainbridgeConfig"; -import { transitMessageReducer } from "./Reducers/TransitMessageReducer"; -import { Weth } from "../Contracts/Weth"; -import { WethFactory } from "../Contracts/WethFactory"; +import { Tokens } from "@chainsafe/web3-context/dist/context/tokensReducer"; +import { + TransactionStatus, + useNetworkManager, + Vote, +} from "./NetworkManagerContext"; +import { useHomeBridge } from "./HomeBridgeContext"; +import NetworkSelectModal from "../Modules/NetworkSelectModal"; interface IChainbridgeContextProps { children: React.ReactNode | React.ReactNode[]; } -export type Vote = { - address: string; - signed: "Confirmed" | "Rejected"; -}; - -const resetAllowanceLogicFor = [ - "0xdac17f958d2ee523a2206206994597c13d831ec7", //USDT - //Add other offending tokens here -]; - type ChainbridgeContext = { - homeChain?: BridgeConfig; - destinationChain?: BridgeConfig; + homeConfig: BridgeConfig | undefined; + connect: () => Promise; + handleSetHomeChain: (chainId: number) => void; + setDestinationChain: (chainId: number | undefined) => void; destinationChains: Array<{ chainId: number; name: string }>; - setDestinationChain(chainId: number): void; + destinationChainConfig?: BridgeConfig; deposit( amount: number, recipient: string, tokenAddress: string ): Promise; resetDeposit(): void; - transactionStatus?: TransactionStatus; depositVotes: number; relayerThreshold?: number; depositNonce?: string; - inTransitMessages: Array; depositAmount?: number; bridgeFee?: number; + inTransitMessages: Array; transferTxHash?: string; selectedToken?: string; - wrapToken: - | (( - overrides?: PayableOverrides | undefined - ) => Promise) - | undefined; - unwrapToken: - | (( - wad: BigNumberish, - overrides?: Overrides | undefined - ) => Promise) - | undefined; + transactionStatus?: TransactionStatus; + wrapToken: (value: number) => Promise; + unwrapToken: (value: number) => Promise; wrapTokenConfig: TokenConfig | undefined; + tokens: Tokens; + nativeTokenBalance: number | undefined; + isReady: boolean | undefined; + address: string | undefined; + chainId?: number; }; -type TransactionStatus = - | "Initializing Transfer" - | "In Transit" - | "Transfer Completed" - | "Transfer Aborted"; - const ChainbridgeContext = React.createContext( undefined ); const ChainbridgeProvider = ({ children }: IChainbridgeContextProps) => { - const { isReady, network, provider, gasPrice, address, tokens } = useWeb3(); - const [homeChain, setHomeChain] = useState(); - const [relayerThreshold, setRelayerThreshold] = useState( - undefined - ); - const [destinationChain, setDestinationChain] = useState< - BridgeConfig | undefined - >(); - const [destinationChains, setDestinationChains] = useState( - [] - ); - // Contracts - const [homeBridge, setHomeBridge] = useState(undefined); - const [wrapper, setWrapper] = useState(undefined); - const [wrapTokenConfig, setWrapperConfig] = useState( - undefined - ); - const [destinationBridge, setDestinationBridge] = useState< - Bridge | undefined - >(undefined); - const [transactionStatus, setTransactionStatus] = useState< - TransactionStatus | undefined - >(undefined); - const [depositNonce, setDepositNonce] = useState( - undefined - ); - const [depositVotes, setDepositVotes] = useState(0); - const [inTransitMessages, tokensDispatch] = useReducer( - transitMessageReducer, - [] - ); - const [depositAmount, setDepositAmount] = useState(); - const [bridgeFee, setBridgeFee] = useState(); - const [transferTxHash, setTransferTxHash] = useState(""); - const [selectedToken, setSelectedToken] = useState(""); + const { + handleSetHomeChain, + destinationChainConfig, + setTransactionStatus, + setDestinationChain, + setDepositNonce, + setDepositVotes, + transferTxHash, + inTransitMessages, + tokensDispatch, + transactionStatus, + depositNonce, + depositVotes, + homeChainConfig, + destinationChains, + chainId, + } = useNetworkManager(); + + const { + connect, + setDepositAmount, + setSelectedToken, + chainConfig, + deposit, + relayerThreshold, + nativeTokenBalance, + address, + selectedToken, + bridgeFee, + depositAmount, + isReady, + wrapTokenConfig, + tokens, + wrapToken, + unwrapToken, + } = useHomeBridge(); const resetDeposit = () => { chainbridgeConfig.chains.length > 2 && setDestinationChain(undefined); @@ -127,347 +103,52 @@ const ChainbridgeProvider = ({ children }: IChainbridgeContextProps) => { setSelectedToken(""); }; - const handleSetDestination = (chainId: number) => { - const chain = destinationChains.find((c) => c.chainId === chainId); - if (!chain) { - throw new Error("Invalid destination chain selected"); - } - setDestinationChain(chain); - }; - - useEffect(() => { - if (destinationChain) { - let provider; - if (destinationChain?.rpcUrl.startsWith("wss")) { - if (destinationChain.rpcUrl.includes("infura")) { - const parts = destinationChain.rpcUrl.split("/"); - - provider = new ethers.providers.InfuraWebSocketProvider( - destinationChain.networkId, - parts[parts.length - 1] - ); - } - if (destinationChain.rpcUrl.includes("alchemyapi")) { - const parts = destinationChain.rpcUrl.split("/"); - - provider = new ethers.providers.AlchemyWebSocketProvider( - destinationChain.networkId, - parts[parts.length - 1] - ); - } - } else { - provider = new ethers.providers.JsonRpcProvider( - destinationChain?.rpcUrl - ); - } - if (provider) { - const bridge = BridgeFactory.connect( - destinationChain?.bridgeAddress, - provider - ); - setDestinationBridge(bridge); - } - } - }, [destinationChain]); - - useEffect(() => { - if (network && isReady) { - const home = chainbridgeConfig.chains.find( - (c) => c.networkId === network - ); - if (!home) { - setHomeChain(undefined); - setHomeBridge(undefined); - setWrapperConfig(undefined); - setWrapper(undefined); - return; - } - setHomeChain(home); - - const signer = provider?.getSigner(); - if (!signer) { - console.log("No signer"); - return; - } - - const bridge = BridgeFactory.connect(home.bridgeAddress, signer); - setHomeBridge(bridge); - setDestinationChains( - chainbridgeConfig.chains.filter((c) => c.networkId !== network) - ); - if (chainbridgeConfig.chains.length === 2) { - const destChain = chainbridgeConfig.chains.find( - (c) => c.networkId !== network + const handleDeposit = useCallback( + async (amount: number, recipient: string, tokenAddress: string) => { + if (chainConfig && destinationChainConfig) { + return await deposit( + amount, + recipient, + tokenAddress, + destinationChainConfig.chainId ); - - destChain && setDestinationChain(destChain); } - - const wrapperToken = home.tokens.find( - (token) => token.isNativeWrappedToken - ); - - if (!wrapperToken) { - setWrapperConfig(undefined); - setWrapper(undefined); - } else { - setWrapperConfig(wrapperToken); - const connectedWeth = WethFactory.connect(wrapperToken.address, signer); - setWrapper(connectedWeth); - } - } else { - setHomeChain(undefined); - setWrapperConfig(undefined); - setWrapper(undefined); - } - resetDeposit(); - }, [isReady, network, provider]); - - useEffect(() => { - const getRelayerThreshold = async () => { - if (homeBridge) { - const threshold = BigNumber.from( - await homeBridge._relayerThreshold() - ).toNumber(); - setRelayerThreshold(threshold); - } - }; - const getBridgeFee = async () => { - if (homeBridge) { - const bridgeFee = Number(utils.formatEther(await homeBridge._fee())); - setBridgeFee(bridgeFee); - } - }; - getRelayerThreshold(); - getBridgeFee(); - }, [homeBridge]); - - useEffect(() => { - if (homeChain && destinationBridge && depositNonce) { - destinationBridge.on( - destinationBridge.filters.ProposalEvent( - homeChain.chainId, - BigNumber.from(depositNonce), - null, - null, - null - ), - (originChainId, depositNonce, status, resourceId, dataHash, tx) => { - switch (BigNumber.from(status).toNumber()) { - case 1: - tokensDispatch({ - type: "addMessage", - payload: `Proposal created on ${destinationChain?.name}`, - }); - break; - case 2: - tokensDispatch({ - type: "addMessage", - payload: `Proposal has passed. Executing...`, - }); - break; - case 3: - setTransactionStatus("Transfer Completed"); - setTransferTxHash(tx.transactionHash); - break; - case 4: - setTransactionStatus("Transfer Aborted"); - setTransferTxHash(tx.transactionHash); - break; - } - } - ); - - destinationBridge.on( - destinationBridge.filters.ProposalVote( - homeChain.chainId, - BigNumber.from(depositNonce), - null, - null - ), - async (originChainId, depositNonce, status, resourceId, tx) => { - const txReceipt = await tx.getTransactionReceipt(); - if (txReceipt.status === 1) { - setDepositVotes(depositVotes + 1); - } - tokensDispatch({ - type: "addMessage", - payload: { - address: String(txReceipt.from), - signed: txReceipt.status === 1 ? "Confirmed" : "Rejected", - }, - }); - } - ); - } - return () => { - //@ts-ignore - destinationBridge?.removeAllListeners(); - }; - }, [ - depositNonce, - homeChain, - destinationBridge, - depositVotes, - destinationChain, - inTransitMessages, - ]); - - const deposit = async ( - amount: number, - recipient: string, - tokenAddress: string - ) => { - if (!homeBridge || !homeChain) { - console.log("Home bridge contract is not instantiated"); - return; - } - - if (!destinationChain || !destinationBridge) { - console.log("Destination bridge contract is not instantiated"); - return; - } - - const signer = provider?.getSigner(); - if (!address || !signer) { - console.log("No signer"); - return; - } - - const token = homeChain.tokens.find( - (token) => token.address === tokenAddress - ); - - if (!token) { - console.log("Invalid token selected"); - return; - } - setTransactionStatus("Initializing Transfer"); - setDepositAmount(amount); - setSelectedToken(tokenAddress); - const erc20 = Erc20DetailedFactory.connect(tokenAddress, signer); - const erc20Decimals = tokens[tokenAddress].decimals; - - const data = - "0x" + - utils - .hexZeroPad( - // TODO Wire up dynamic token decimals - BigNumber.from( - utils.parseUnits(amount.toString(), erc20Decimals) - ).toHexString(), - 32 - ) - .substr(2) + // Deposit Amount (32 bytes) - utils - .hexZeroPad(utils.hexlify((recipient.length - 2) / 2), 32) - .substr(2) + // len(recipientAddress) (32 bytes) - recipient.substr(2); // recipientAddress (?? bytes) - - try { - const currentAllowance = await erc20.allowance( - address, - homeChain.erc20HandlerAddress - ); - - if (Number(utils.formatUnits(currentAllowance, erc20Decimals)) < amount) { - if ( - Number(utils.formatUnits(currentAllowance, erc20Decimals)) > 0 && - resetAllowanceLogicFor.includes(tokenAddress) - ) { - //We need to reset the user's allowance to 0 before we give them a new allowance - //TODO Should we alert the user this is happening here? - await ( - await erc20.approve( - homeChain.erc20HandlerAddress, - BigNumber.from(utils.parseUnits("0", erc20Decimals)), - { - gasPrice: BigNumber.from( - utils.parseUnits( - (homeChain.defaultGasPrice || gasPrice).toString(), - 9 - ) - ).toString(), - } - ) - ).wait(1); - } - await ( - await erc20.approve( - homeChain.erc20HandlerAddress, - BigNumber.from(utils.parseUnits(amount.toString(), erc20Decimals)), - { - gasPrice: BigNumber.from( - utils.parseUnits( - (homeChain.defaultGasPrice || gasPrice).toString(), - 9 - ) - ).toString(), - } - ) - ).wait(1); - } - - homeBridge.once( - homeBridge.filters.Deposit( - destinationChain.chainId, - token.resourceId, - null - ), - (destChainId, resourceId, depositNonce) => { - setDepositNonce(`${depositNonce.toString()}`); - setTransactionStatus("In Transit"); - } - ); - - await ( - await homeBridge.deposit( - destinationChain.chainId, - token.resourceId, - data, - { - gasPrice: utils.parseUnits( - (homeChain.defaultGasPrice || gasPrice).toString(), - 9 - ), - value: utils.parseUnits((bridgeFee || 0).toString(), 18), - } - ) - ).wait(); - return Promise.resolve(); - } catch (error) { - console.log(error); - setTransactionStatus("Transfer Aborted"); - return Promise.reject(); - } - }; + }, + [deposit, destinationChainConfig, chainConfig] + ); return ( ({ - chainId: c.chainId, - name: c.name, - })), - setDestinationChain: handleSetDestination, - deposit, + homeConfig: homeChainConfig, + connect, + destinationChains, + handleSetHomeChain, + setDestinationChain, resetDeposit, + deposit: handleDeposit, + destinationChainConfig, depositVotes, - relayerThreshold: relayerThreshold, + relayerThreshold, depositNonce, bridgeFee, transactionStatus, inTransitMessages, - depositAmount, - transferTxHash, - selectedToken, - wrapToken: wrapper?.deposit, - wrapTokenConfig, - unwrapToken: wrapper?.withdraw, + depositAmount: depositAmount, + transferTxHash: transferTxHash, + selectedToken: selectedToken, + // TODO: Confirm if EVM specific + wrapToken, + wrapTokenConfig: wrapTokenConfig, + unwrapToken, + isReady: isReady, + nativeTokenBalance: nativeTokenBalance, + tokens, + address, + chainId, }} > + {children} ); @@ -476,7 +157,9 @@ const ChainbridgeProvider = ({ children }: IChainbridgeContextProps) => { const useChainbridge = () => { const context = useContext(ChainbridgeContext); if (context === undefined) { - throw new Error("useChainbridge must be called within a DriveProvider"); + throw new Error( + "useChainbridge must be called within a ChainbridgeProvider" + ); } return context; }; diff --git a/src/Contexts/DestinationBridgeContext.tsx b/src/Contexts/DestinationBridgeContext.tsx new file mode 100644 index 00000000..535473ac --- /dev/null +++ b/src/Contexts/DestinationBridgeContext.tsx @@ -0,0 +1,16 @@ +import React, { useContext } from "react"; +import { DestinationChainContext } from "./Adaptors/interfaces"; + +const DestinationBridgeContext = React.createContext< + DestinationChainContext | undefined +>(undefined); + +const useDestinationBridge = () => { + const context = useContext(DestinationBridgeContext); + if (context === undefined) { + throw new Error("useHomeBridge must be called within a HomeBridgeProvider"); + } + return context; +}; + +export { DestinationBridgeContext, useDestinationBridge }; diff --git a/src/Contexts/ExplorerContext.tsx b/src/Contexts/ExplorerContext.tsx index 6c86e434..7e7bb14e 100644 --- a/src/Contexts/ExplorerContext.tsx +++ b/src/Contexts/ExplorerContext.tsx @@ -5,7 +5,7 @@ import { Erc20HandlerFactory, } from "@chainsafe/chainbridge-contracts"; import { BigNumber, ethers, Event, providers } from "ethers"; -import { chainbridgeConfig } from "../chainbridgeConfig"; +import { chainbridgeConfig, EvmBridgeConfig } from "../chainbridgeConfig"; import { Transfers, transfersReducer } from "./Reducers/TransfersReducer"; interface IExplorerContextProps { @@ -25,252 +25,261 @@ const ExplorerProvider = ({ children }: IExplorerContextProps) => { const fetchTransfersAndListen = async () => { const bridges = await Promise.all( - chainbridgeConfig.chains.map(async (bridge) => { - console.log(`Checking events for ${bridge.name}`); + chainbridgeConfig.chains + .filter((c) => c.type === "Substrate") + .map(async (bridge) => { + console.log(`Checking events for ${bridge.name}`); - const provider = new providers.JsonRpcProvider( - bridge.rpcUrl, - bridge.networkId - ); - const bridgeContract = BridgeFactory.connect( - bridge.bridgeAddress, - provider - ); - const erc20HandlerContract = Erc20HandlerFactory.connect( - bridge.erc20HandlerAddress, - provider - ); - const depositFilter = bridgeContract.filters.Deposit(null, null, null); - const depositLogs = await provider.getLogs({ - ...depositFilter, - fromBlock: bridge.deployedBlockNumber, - }); - depositLogs.forEach(async (dl) => { - const parsedLog = bridgeContract.interface.parseLog(dl); - const depositRecord = await erc20HandlerContract.getDepositRecord( - parsedLog.args.depositNonce, - parsedLog.args.destinationChainID + const provider = new providers.JsonRpcProvider( + bridge.rpcUrl, + bridge.networkId ); - - transfersDispatch({ - type: "addTransfer", - payload: { - depositNonce: parsedLog.args.depositNonce.toNumber(), - transferDetails: { - fromAddress: depositRecord._depositer, - depositBlockNumber: dl.blockNumber, - depositTransactionHash: dl.transactionHash, - fromChainId: bridge.chainId, - fromNetworkName: bridge.name, - timestamp: (await provider.getBlock(dl.blockNumber)).timestamp, - toChainId: parsedLog.args.destinationChainID, - toNetworkName: - chainbridgeConfig.chains.find( - (c) => c.chainId === parsedLog.args.destinationChainID - )?.name || "", - toAddress: depositRecord._destinationRecipientAddress, - tokenAddress: depositRecord._tokenAddress, - amount: depositRecord._amount, - resourceId: parsedLog.args.resourceID, - }, - }, + const bridgeContract = BridgeFactory.connect( + (bridge as EvmBridgeConfig).bridgeAddress, + provider + ); + const erc20HandlerContract = Erc20HandlerFactory.connect( + (bridge as EvmBridgeConfig).erc20HandlerAddress, + provider + ); + const depositFilter = bridgeContract.filters.Deposit( + null, + null, + null + ); + const depositLogs = await provider.getLogs({ + ...depositFilter, + fromBlock: (bridge as EvmBridgeConfig).deployedBlockNumber, }); - }); - console.log(`Added ${bridge.name} ${depositLogs.length} deposits`); - bridgeContract.on( - depositFilter, - async ( - destChainId: number, - resourceId: string, - depositNonce: ethers.BigNumber, - tx: Event - ) => { + depositLogs.forEach(async (dl) => { + const parsedLog = bridgeContract.interface.parseLog(dl); const depositRecord = await erc20HandlerContract.getDepositRecord( - depositNonce, - destChainId + parsedLog.args.depositNonce, + parsedLog.args.destinationChainID ); transfersDispatch({ type: "addTransfer", payload: { - depositNonce: depositNonce.toNumber(), + depositNonce: parsedLog.args.depositNonce.toNumber(), transferDetails: { fromAddress: depositRecord._depositer, - depositBlockNumber: tx.blockNumber, - depositTransactionHash: tx.transactionHash, + depositBlockNumber: dl.blockNumber, + depositTransactionHash: dl.transactionHash, fromChainId: bridge.chainId, fromNetworkName: bridge.name, - timestamp: (await provider.getBlock(tx.blockNumber)) + timestamp: (await provider.getBlock(dl.blockNumber)) .timestamp, - toChainId: destChainId, + toChainId: parsedLog.args.destinationChainID, toNetworkName: chainbridgeConfig.chains.find( - (c) => c.chainId === destChainId + (c) => c.chainId === parsedLog.args.destinationChainID )?.name || "", toAddress: depositRecord._destinationRecipientAddress, tokenAddress: depositRecord._tokenAddress, amount: depositRecord._amount, - resourceId: resourceId, + resourceId: parsedLog.args.resourceID, }, }, }); - } - ); - const proposalEventFilter = bridgeContract.filters.ProposalEvent( - null, - null, - null, - null, - null - ); + }); + console.log(`Added ${bridge.name} ${depositLogs.length} deposits`); + bridgeContract.on( + depositFilter, + async ( + destChainId: number, + resourceId: string, + depositNonce: ethers.BigNumber, + tx: Event + ) => { + const depositRecord = await erc20HandlerContract.getDepositRecord( + depositNonce, + destChainId + ); - const proposalEventLogs = await provider.getLogs({ - ...proposalEventFilter, - fromBlock: bridge.deployedBlockNumber, - }); - proposalEventLogs.forEach(async (pel) => { - const parsedLog = bridgeContract.interface.parseLog(pel); - transfersDispatch({ - type: "addProposalEvent", - payload: { - depositNonce: parsedLog.args.depositNonce.toNumber(), - transferDetails: { - resourceId: parsedLog.args.resourceID, - fromChainId: parsedLog.args.originChainID, - fromNetworkName: - chainbridgeConfig.chains.find( - (c) => c.chainId === parsedLog.args.originChainID - )?.name || "", - toChainId: bridge.chainId, - toNetworkName: bridge.name, - }, - proposalEventDetails: { - proposalEventBlockNumber: pel.blockNumber, - proposalEventTransactionHash: pel.transactionHash, - dataHash: parsedLog.args.dataHash, - timestamp: (await provider.getBlock(pel.blockNumber)).timestamp, - proposalStatus: parsedLog.args.status, - }, - }, + transfersDispatch({ + type: "addTransfer", + payload: { + depositNonce: depositNonce.toNumber(), + transferDetails: { + fromAddress: depositRecord._depositer, + depositBlockNumber: tx.blockNumber, + depositTransactionHash: tx.transactionHash, + fromChainId: bridge.chainId, + fromNetworkName: bridge.name, + timestamp: (await provider.getBlock(tx.blockNumber)) + .timestamp, + toChainId: destChainId, + toNetworkName: + chainbridgeConfig.chains.find( + (c) => c.chainId === destChainId + )?.name || "", + toAddress: depositRecord._destinationRecipientAddress, + tokenAddress: depositRecord._tokenAddress, + amount: depositRecord._amount, + resourceId: resourceId, + }, + }, + }); + } + ); + const proposalEventFilter = bridgeContract.filters.ProposalEvent( + null, + null, + null, + null, + null + ); + + const proposalEventLogs = await provider.getLogs({ + ...proposalEventFilter, + fromBlock: (bridge as EvmBridgeConfig).deployedBlockNumber, }); - }); - console.log( - `Added ${bridge.name} ${proposalEventLogs.length} proposal events` - ); - bridgeContract.on( - proposalEventFilter, - async ( - originChainId: number, - depositNonce: BigNumber, - status: number, - resourceId: string, - dataHash: string, - tx: Event - ) => { + proposalEventLogs.forEach(async (pel) => { + const parsedLog = bridgeContract.interface.parseLog(pel); transfersDispatch({ type: "addProposalEvent", payload: { - depositNonce: depositNonce.toNumber(), + depositNonce: parsedLog.args.depositNonce.toNumber(), transferDetails: { - resourceId: resourceId, - fromChainId: originChainId, + resourceId: parsedLog.args.resourceID, + fromChainId: parsedLog.args.originChainID, fromNetworkName: chainbridgeConfig.chains.find( - (c) => c.chainId === originChainId + (c) => c.chainId === parsedLog.args.originChainID )?.name || "", toChainId: bridge.chainId, toNetworkName: bridge.name, }, proposalEventDetails: { - proposalEventBlockNumber: tx.blockNumber, - proposalEventTransactionHash: tx.transactionHash, - dataHash: dataHash, - timestamp: (await provider.getBlock(tx.blockNumber)) + proposalEventBlockNumber: pel.blockNumber, + proposalEventTransactionHash: pel.transactionHash, + dataHash: parsedLog.args.dataHash, + timestamp: (await provider.getBlock(pel.blockNumber)) .timestamp, - proposalStatus: status, + proposalStatus: parsedLog.args.status, }, }, }); - } - ); - const proposalVoteFilter = bridgeContract.filters.ProposalVote( - null, - null, - null, - null - ); - - const proposalVoteLogs = await provider.getLogs({ - ...proposalVoteFilter, - fromBlock: bridge.deployedBlockNumber, - }); - proposalVoteLogs.forEach(async (pvl) => { - const parsedLog = bridgeContract.interface.parseLog(pvl); + }); + console.log( + `Added ${bridge.name} ${proposalEventLogs.length} proposal events` + ); + bridgeContract.on( + proposalEventFilter, + async ( + originChainId: number, + depositNonce: BigNumber, + status: number, + resourceId: string, + dataHash: string, + tx: Event + ) => { + transfersDispatch({ + type: "addProposalEvent", + payload: { + depositNonce: depositNonce.toNumber(), + transferDetails: { + resourceId: resourceId, + fromChainId: originChainId, + fromNetworkName: + chainbridgeConfig.chains.find( + (c) => c.chainId === originChainId + )?.name || "", + toChainId: bridge.chainId, + toNetworkName: bridge.name, + }, + proposalEventDetails: { + proposalEventBlockNumber: tx.blockNumber, + proposalEventTransactionHash: tx.transactionHash, + dataHash: dataHash, + timestamp: (await provider.getBlock(tx.blockNumber)) + .timestamp, + proposalStatus: status, + }, + }, + }); + } + ); + const proposalVoteFilter = bridgeContract.filters.ProposalVote( + null, + null, + null, + null + ); - transfersDispatch({ - type: "addVote", - payload: { - depositNonce: parsedLog.args.depositNonce.toNumber(), - transferDetails: { - resourceId: parsedLog.args.resourceID, - fromChainId: parsedLog.args.originChainID, - fromNetworkName: - chainbridgeConfig.chains.find( - (c) => c.chainId === parsedLog.args.originChainID - )?.name || "", - toChainId: bridge.chainId, - toNetworkName: bridge.name, - }, - voteDetails: { - voteBlockNumber: pvl.blockNumber, - voteTransactionHash: pvl.transactionHash, - dataHash: parsedLog.args.dataHash, - timestamp: (await provider.getBlock(pvl.blockNumber)).timestamp, - voteStatus: parsedLog.args.status, - }, - }, + const proposalVoteLogs = await provider.getLogs({ + ...proposalVoteFilter, + fromBlock: (bridge as EvmBridgeConfig).deployedBlockNumber, }); - }); - console.log( - `Added ${bridge.name} ${proposalVoteLogs.length} proposal votes` - ); - bridgeContract.on( - proposalVoteFilter, - async ( - originChainId: number, - depositNonce: BigNumber, - status: number, // TODO: Confirm wether this is actually being used - resourceId: string, - tx: Event - ) => { + proposalVoteLogs.forEach(async (pvl) => { + const parsedLog = bridgeContract.interface.parseLog(pvl); + transfersDispatch({ type: "addVote", payload: { - depositNonce: depositNonce.toNumber(), + depositNonce: parsedLog.args.depositNonce.toNumber(), transferDetails: { - resourceId: resourceId, - fromChainId: originChainId, + resourceId: parsedLog.args.resourceID, + fromChainId: parsedLog.args.originChainID, fromNetworkName: chainbridgeConfig.chains.find( - (c) => c.chainId === originChainId + (c) => c.chainId === parsedLog.args.originChainID )?.name || "", toChainId: bridge.chainId, toNetworkName: bridge.name, }, voteDetails: { - voteBlockNumber: tx.blockNumber, - voteTransactionHash: tx.transactionHash, - dataHash: "", // TODO: Confirm whether this is available - timestamp: (await provider.getBlock(tx.blockNumber)) + voteBlockNumber: pvl.blockNumber, + voteTransactionHash: pvl.transactionHash, + dataHash: parsedLog.args.dataHash, + timestamp: (await provider.getBlock(pvl.blockNumber)) .timestamp, - voteStatus: status === 1 ? true : false, // TODO: Confirm whether this is the correct status + voteStatus: parsedLog.args.status, }, }, }); - } - ); - return bridgeContract; - }) + }); + console.log( + `Added ${bridge.name} ${proposalVoteLogs.length} proposal votes` + ); + bridgeContract.on( + proposalVoteFilter, + async ( + originChainId: number, + depositNonce: BigNumber, + status: number, // TODO: Confirm wether this is actually being used + resourceId: string, + tx: Event + ) => { + transfersDispatch({ + type: "addVote", + payload: { + depositNonce: depositNonce.toNumber(), + transferDetails: { + resourceId: resourceId, + fromChainId: originChainId, + fromNetworkName: + chainbridgeConfig.chains.find( + (c) => c.chainId === originChainId + )?.name || "", + toChainId: bridge.chainId, + toNetworkName: bridge.name, + }, + voteDetails: { + voteBlockNumber: tx.blockNumber, + voteTransactionHash: tx.transactionHash, + dataHash: "", // TODO: Confirm whether this is available + timestamp: (await provider.getBlock(tx.blockNumber)) + .timestamp, + voteStatus: status === 1 ? true : false, // TODO: Confirm whether this is the correct status + }, + }, + }); + } + ); + return bridgeContract; + }) ); return bridges; }; diff --git a/src/Contexts/HomeBridgeContext.tsx b/src/Contexts/HomeBridgeContext.tsx new file mode 100644 index 00000000..3d1d0796 --- /dev/null +++ b/src/Contexts/HomeBridgeContext.tsx @@ -0,0 +1,16 @@ +import React, { useContext } from "react"; +import { HomeChainAdaptorContext } from "./Adaptors/interfaces"; + +const HomeBridgeContext = React.createContext< + HomeChainAdaptorContext | undefined +>(undefined); + +const useHomeBridge = () => { + const context = useContext(HomeBridgeContext); + if (context === undefined) { + throw new Error("useHomeBridge must be called within a HomeBridgeProvider"); + } + return context; +}; + +export { HomeBridgeContext, useHomeBridge }; diff --git a/src/Contexts/NetworkManagerContext.tsx b/src/Contexts/NetworkManagerContext.tsx new file mode 100644 index 00000000..e6906941 --- /dev/null +++ b/src/Contexts/NetworkManagerContext.tsx @@ -0,0 +1,284 @@ +import React, { + Dispatch, + useCallback, + useContext, + useEffect, + useReducer, + useState, +} from "react"; +import { + BridgeConfig, + chainbridgeConfig, + ChainType, +} from "../chainbridgeConfig"; +import { + EVMDestinationAdaptorProvider, + EVMHomeAdaptorProvider, +} from "./Adaptors/EVMAdaptors"; +import { IDestinationBridgeProviderProps } from "./Adaptors/interfaces"; +import { + SubstrateDestinationAdaptorProvider, + SubstrateHomeAdaptorProvider, +} from "./Adaptors/SubstrateAdaptors"; +import { DestinationBridgeContext } from "./DestinationBridgeContext"; +import { HomeBridgeContext } from "./HomeBridgeContext"; +import { + AddMessageAction, + ResetAction, + transitMessageReducer, +} from "./Reducers/TransitMessageReducer"; + +interface INetworkManagerProviderProps { + children: React.ReactNode | React.ReactNode[]; +} + +export type WalletType = ChainType | "select" | "unset"; + +export type Vote = { + address: string; + signed: "Confirmed" | "Rejected"; +}; + +export type TransactionStatus = + | "Initializing Transfer" + | "In Transit" + | "Transfer Completed" + | "Transfer Aborted"; + +interface NetworkManagerContext { + walletType: WalletType; + setWalletType: (walletType: WalletType) => void; + + networkId: number; + setNetworkId: (id: number) => void; + + chainId?: number; + + homeChainConfig: BridgeConfig | undefined; + destinationChainConfig: BridgeConfig | undefined; + + destinationChains: Array<{ chainId: number; name: string }>; + homeChains: BridgeConfig[]; + handleSetHomeChain: (chainId: number | undefined) => void; + setDestinationChain: (chainId: number | undefined) => void; + + transactionStatus?: TransactionStatus; + setTransactionStatus: (message: TransactionStatus | undefined) => void; + inTransitMessages: Array; + + setDepositVotes: (input: number) => void; + depositVotes: number; + + setDepositNonce: (input: string | undefined) => void; + depositNonce: string | undefined; + + tokensDispatch: Dispatch; + + setTransferTxHash: (input: string) => void; + transferTxHash: string; +} + +const NetworkManagerContext = React.createContext< + NetworkManagerContext | undefined +>(undefined); + +const NetworkManagerProvider = ({ children }: INetworkManagerProviderProps) => { + const [walletType, setWalletType] = useState("unset"); + + const [networkId, setNetworkId] = useState(0); + + const [homeChainConfig, setHomeChainConfig] = useState< + BridgeConfig | undefined + >(); + const [homeChains, setHomeChains] = useState([]); + const [destinationChainConfig, setDestinationChain] = useState< + BridgeConfig | undefined + >(); + const [destinationChains, setDestinationChains] = useState( + [] + ); + + const [transferTxHash, setTransferTxHash] = useState(""); + const [transactionStatus, setTransactionStatus] = useState< + TransactionStatus | undefined + >(undefined); + const [depositNonce, setDepositNonce] = useState( + undefined + ); + const [depositVotes, setDepositVotes] = useState(0); + const [inTransitMessages, tokensDispatch] = useReducer( + transitMessageReducer, + [] + ); + + const handleSetHomeChain = useCallback( + (chainId: number | undefined) => { + if (!chainId && chainId !== 0) { + setHomeChainConfig(undefined); + return; + } + const chain = homeChains.find((c) => c.chainId === chainId); + + if (chain) { + setHomeChainConfig(chain); + setDestinationChains( + chainbridgeConfig.chains.filter( + (bridgeConfig: BridgeConfig) => + bridgeConfig.chainId !== chain.chainId + ) + ); + if (chainbridgeConfig.chains.length === 2) { + setDestinationChain( + chainbridgeConfig.chains.find( + (bridgeConfig: BridgeConfig) => + bridgeConfig.chainId !== chain.chainId + ) + ); + } + } + }, + [homeChains, setHomeChainConfig] + ); + + useEffect(() => { + if (walletType !== "unset") { + if (walletType === "select") { + setHomeChains(chainbridgeConfig.chains); + } else { + setHomeChains( + chainbridgeConfig.chains.filter( + (bridgeConfig: BridgeConfig) => bridgeConfig.type === walletType + ) + ); + } + } else { + setHomeChains([]); + } + }, [walletType]); + + const handleSetDestination = useCallback( + (chainId: number | undefined) => { + if (chainId === undefined) { + setDestinationChain(undefined); + } else if (homeChainConfig && !depositNonce) { + const chain = destinationChains.find((c) => c.chainId === chainId); + if (!chain) { + throw new Error("Invalid destination chain selected"); + } + setDestinationChain(chain); + } else { + throw new Error("Home chain not selected"); + } + }, + [depositNonce, destinationChains, homeChainConfig] + ); + + const DestinationProvider = ({ + children, + }: IDestinationBridgeProviderProps) => { + if (destinationChainConfig?.type === "Ethereum") { + return ( + + {children} + + ); + } else if (destinationChainConfig?.type === "Substrate") { + return ( + + {children} + + ); + } else { + return ( + {}, + }} + > + {children} + + ); + } + }; + + return ( + + {walletType === "Ethereum" ? ( + + {children} + + ) : walletType === "Substrate" ? ( + + {children} + + ) : ( + undefined, + disconnect: async () => {}, + getNetworkName: (id: any) => "", + isReady: false, + selectedToken: "", + deposit: async ( + amount: number, + recipient: string, + tokenAddress: string, + destinationChainId: number + ) => undefined, + setDepositAmount: () => undefined, + tokens: {}, + setSelectedToken: (input: string) => undefined, + address: undefined, + bridgeFee: undefined, + chainConfig: undefined, + depositAmount: undefined, + nativeTokenBalance: undefined, + relayerThreshold: undefined, + wrapTokenConfig: undefined, + wrapper: undefined, + wrapToken: async (value: number) => "", + unwrapToken: async (value: number) => "", + }} + > + {children} + + )} + + ); +}; + +const useNetworkManager = () => { + const context = useContext(NetworkManagerContext); + if (context === undefined) { + throw new Error( + "useNetworkManager must be called within a HomeNetworkProvider" + ); + } + return context; +}; + +export { NetworkManagerProvider, useNetworkManager }; diff --git a/src/Contexts/Reducers/TransitMessageReducer.tsx b/src/Contexts/Reducers/TransitMessageReducer.tsx index 0e830d57..e9a8bcf2 100644 --- a/src/Contexts/Reducers/TransitMessageReducer.tsx +++ b/src/Contexts/Reducers/TransitMessageReducer.tsx @@ -1,10 +1,11 @@ -import { Vote } from "../ChainbridgeContext"; +import { Vote } from "../NetworkManagerContext"; + +export type AddMessageAction = { type: "addMessage"; payload: string | Vote }; +export type ResetAction = { type: "resetMessages" }; export function transitMessageReducer( transitMessage: Array, - action: - | { type: "addMessage"; payload: string | Vote } - | { type: "resetMessages" } + action: AddMessageAction | ResetAction ) { switch (action.type) { case "addMessage": diff --git a/src/Layouts/AppHeader.tsx b/src/Layouts/AppHeader.tsx index 98a0c678..59f05e45 100644 --- a/src/Layouts/AppHeader.tsx +++ b/src/Layouts/AppHeader.tsx @@ -3,7 +3,6 @@ import React from "react"; import clsx from "clsx"; import { Typography } from "@chainsafe/common-components"; import { shortenAddress } from "../Utils/Helpers"; -import { useWeb3 } from "@chainsafe/web3-context"; import { useChainbridge } from "../Contexts/ChainbridgeContext"; const useStyles = makeStyles(({ constants, palette, zIndex }: ITheme) => { @@ -60,8 +59,7 @@ interface IAppHeader {} const AppHeader: React.FC = () => { const classes = useStyles(); - const { isReady, address } = useWeb3(); - const { homeChain } = useChainbridge(); + const { homeConfig, isReady, address } = useChainbridge(); return (
@@ -81,7 +79,7 @@ const AppHeader: React.FC = () => { {address && shortenAddress(address)} - connected to {homeChain?.name} + connected to {homeConfig?.name} )} diff --git a/src/Modules/ChangeNetworkDrawer.tsx b/src/Modules/ChangeNetworkDrawer.tsx index 7d1e3c5b..f9c84a27 100644 --- a/src/Modules/ChangeNetworkDrawer.tsx +++ b/src/Modules/ChangeNetworkDrawer.tsx @@ -3,7 +3,9 @@ import React from "react"; import { makeStyles, createStyles, ITheme } from "@chainsafe/common-theme"; import CustomDrawer from "../Components/Custom/CustomDrawer"; import { Button, Typography } from "@chainsafe/common-components"; -import { useWeb3 } from "@chainsafe/web3-context"; +import { useNetworkManager } from "../Contexts/NetworkManagerContext"; +import { useHomeBridge } from "../Contexts/HomeBridgeContext"; +import { useDestinationBridge } from "../Contexts/DestinationBridgeContext"; const useStyles = makeStyles(({ constants }: ITheme) => createStyles({ @@ -35,12 +37,13 @@ const ChangeNetworkDrawer: React.FC = ({ }) => { const classes = useStyles(); - const { checkIsReady, onboard } = useWeb3(); - - const handleConnect = async () => { - await onboard?.walletSelect(); - await checkIsReady(); - }; + const { + setWalletType, + handleSetHomeChain, + setDestinationChain, + } = useNetworkManager(); + const { disconnect } = useHomeBridge(); + const destinationBridge = useDestinationBridge(); return ( @@ -59,8 +62,12 @@ const ChangeNetworkDrawer: React.FC = ({ OK + +
+ + )} + {walletType === "Substrate" && ( + <> + + Connecting to node + + + + )} + + ); +}; + +export default NetworkSelectModal; diff --git a/src/Modules/NetworkUnsupportedModal.tsx b/src/Modules/NetworkUnsupportedModal.tsx index 144b33df..43285336 100644 --- a/src/Modules/NetworkUnsupportedModal.tsx +++ b/src/Modules/NetworkUnsupportedModal.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import { makeStyles, createStyles, ITheme } from "@chainsafe/common-theme"; import CustomModal from "../Components/Custom/CustomModal"; @@ -6,7 +6,12 @@ import { Button, ExclamationCircleInverseSvg, Typography, + useLocation, } from "@chainsafe/common-components"; +import { useNetworkManager } from "../Contexts/NetworkManagerContext"; +import { ROUTE_LINKS } from "../Components/Routes"; +import { useHomeBridge } from "../Contexts/HomeBridgeContext"; +import { chainbridgeConfig } from "../chainbridgeConfig"; const useStyles = makeStyles(({ constants, palette }: ITheme) => createStyles({ @@ -52,39 +57,36 @@ const useStyles = makeStyles(({ constants, palette }: ITheme) => }) ); -const networkName = (id: any) => { - switch (Number(id)) { - case 1: - return "Mainnet"; - case 3: - return "Ropsten"; - case 4: - return "Rinkeby"; - case 5: - return "Goerli"; - case 6: - return "Kotti"; - case 42: - return "Kovan"; - case 61: - return "Ethereum Classic - Mainnet"; - default: - return "Other"; - } -}; +const NetworkUnsupportedModal = () => { + const classes = useStyles(); + const { homeChainConfig, networkId } = useNetworkManager(); + const { getNetworkName, wrapTokenConfig, isReady } = useHomeBridge(); + const { pathname } = useLocation(); -interface INetworkUnsupportedModalProps { - open: boolean; - network: number | undefined; - supportedNetworks: number[]; -} + const [open, setOpen] = useState(false); + const [supportedNetworks, setSupportedNetworks] = useState([]); -const NetworkUnsupportedModal: React.FC = ({ - open, - network, - supportedNetworks, -}) => { - const classes = useStyles(); + useEffect(() => { + if (pathname === ROUTE_LINKS.Transfer) { + setOpen(!homeChainConfig && !!isReady); + setSupportedNetworks( + chainbridgeConfig.chains + .filter((bc) => bc.networkId !== undefined) + .map((bc) => Number(bc.networkId)) + ); + } else if (pathname === ROUTE_LINKS.Wrap) { + setOpen(!wrapTokenConfig && !!isReady); + setSupportedNetworks( + chainbridgeConfig.chains + .filter((bc) => bc.networkId !== undefined) + .filter((bc) => bc.tokens.find((t) => t.isNativeWrappedToken)) + .map((bc) => Number(bc.networkId)) + ); + } else { + setOpen(false); + setSupportedNetworks([]); + } + }, [pathname, setOpen, homeChainConfig, isReady, wrapTokenConfig]); return ( = ({ This app does not currently support transfers on{" "} - {networkName(network)}. Please change networks from within your + {getNetworkName(networkId)}. Please change networks from within your browser wallet.
@@ -111,7 +113,9 @@ const NetworkUnsupportedModal: React.FC = ({ This app is configured to work on{" "} {supportedNetworks.map( (n, i) => - `${networkName(n)}${i < supportedNetworks.length - 1 ? ", " : ""}` + `${getNetworkName(n)}${ + i < supportedNetworks.length - 1 ? ", " : "" + }` )}{" "} networks diff --git a/src/Modules/TransferActiveModal.tsx b/src/Modules/TransferActiveModal.tsx index 73964d94..304409a2 100644 --- a/src/Modules/TransferActiveModal.tsx +++ b/src/Modules/TransferActiveModal.tsx @@ -9,7 +9,7 @@ import { } from "@chainsafe/common-components"; import CustomModal from "../Components/Custom/CustomModal"; import { useChainbridge } from "../Contexts/ChainbridgeContext"; -import { useWeb3 } from "@chainsafe/web3-context"; +import { EvmBridgeConfig } from "../chainbridgeConfig"; const useStyles = makeStyles( ({ animation, constants, palette, typography }: ITheme) => @@ -148,14 +148,13 @@ const TransferActiveModal: React.FC = ({ depositVotes, relayerThreshold, inTransitMessages, - homeChain, - destinationChain, + homeConfig, + destinationChainConfig, depositAmount, transferTxHash, selectedToken, + tokens, } = useChainbridge(); - const { tokens } = useWeb3(); - const tokenSymbol = selectedToken && tokens[selectedToken]?.symbol; return ( = ({ Successfully transferred{" "} {depositAmount} {tokenSymbol} -
from {homeChain?.name} to {destinationChain?.name}. +
from {homeConfig?.name} to {destinationChainConfig?.name} + .
- )} + {homeConfig && + (homeConfig as EvmBridgeConfig).blockExplorer && + transferTxHash && ( + + )}