diff --git a/bridge-frontend/.yarn/install-state.gz b/bridge-frontend/.yarn/install-state.gz index 0c45c92..948a3f1 100644 Binary files a/bridge-frontend/.yarn/install-state.gz and b/bridge-frontend/.yarn/install-state.gz differ diff --git a/bridge-frontend/package.json b/bridge-frontend/package.json index 18b113f..ca1d3bb 100644 --- a/bridge-frontend/package.json +++ b/bridge-frontend/package.json @@ -21,6 +21,7 @@ "@web3-onboard/core": "^2.2.6", "@web3-onboard/injected-wallets": "^2.0.5", "@web3-onboard/react": "^2.1.5", + "bs58": "^5.0.0", "buffer": "^6.0.3", "cacheable-request": "^10.2.7", "ethers": "5.7.2", diff --git a/bridge-frontend/src/App.tsx b/bridge-frontend/src/App.tsx index 31852b1..3285b0d 100644 --- a/bridge-frontend/src/App.tsx +++ b/bridge-frontend/src/App.tsx @@ -10,9 +10,9 @@ // License for the specific language governing permissions and limitations // under the License. -import { FC } from "react"; +import { FC, useEffect } from "react"; import injectedModule from "@web3-onboard/injected-wallets"; -import { init, useConnectWallet } from "@web3-onboard/react"; +import { init, useConnectWallet, useSetChain } from "@web3-onboard/react"; import { useState } from "react"; import { GraphQLProvider } from "./GraphQL"; @@ -20,13 +20,13 @@ import { Transfers } from "./Transfers"; import configFile from "./config.json"; import "./App.css"; import { - Box, - Stack, - SimpleGrid, - Button, - Heading, - Text, - Image, + Box, + Stack, + SimpleGrid, + Button, + Heading, + Text, + Image, } from "@chakra-ui/react"; import banner from "./banner.png"; import Header from "./Header"; @@ -35,57 +35,63 @@ const config: any = configFile; const injected: any = injectedModule(); init({ - wallets: [injected], - chains: Object.entries(config).map(([k, v]: [string, any], i) => ({ - id: k, - token: v.token, - label: v.label, - rpcUrl: v.rpcUrl, - })), - appMetadata: { - name: "Cartesi Rollups Test DApp", - icon: "", - description: "Demo app for Cartesi Rollups", - recommendedInjectedWallets: [ - { name: "MetaMask", url: "https://metamask.io" }, - ], - }, + wallets: [injected], + chains: Object.entries(config).map(([k, v]: [string, any], i) => ({ + id: k, + token: v.token, + label: v.label, + rpcUrl: v.rpcUrl, + })), + appMetadata: { + name: "Cartesi Rollups Test DApp", + icon: "", + description: "Demo app for Cartesi Rollups", + recommendedInjectedWallets: [ + { name: "MetaMask", url: "https://metamask.io" }, + ], + }, }); const App: FC = () => { - const [dappAddress, setDappAddress] = useState("0x47432A4070539BeF308B24a7AAE2940b801d0681"); + const [{ connectedChain }] = useSetChain(); + const [dappAddress, setDappAddress] = useState("unknown"); - const [{ wallet, connecting }, connect] = useConnectWallet(); + const [{ wallet, connecting }, connect] = useConnectWallet(); - return ( - <> -
- - {!wallet && ( - - - CarteZcash Bridge - - Connect a wallet to deposit or withdraw Eth from - the rollup - - - - - - )} - - - - - - ); + useEffect(() => { + if (connectedChain) { + setDappAddress(config[connectedChain.id].DAppAddress); + } + }, [connectedChain]); + + return ( + <> +
+ + {!wallet && ( + + + CarteZcash Bridge + + Connect a wallet to deposit or withdraw Eth from the rollup + + + + + + )} + + + + + + ); }; export default App; diff --git a/bridge-frontend/src/Transfers.tsx b/bridge-frontend/src/Transfers.tsx index 75991a0..871dd9b 100644 --- a/bridge-frontend/src/Transfers.tsx +++ b/bridge-frontend/src/Transfers.tsx @@ -15,257 +15,207 @@ import { ethers } from "ethers"; import { useRollups } from "./useRollups"; import { useWallets } from "@web3-onboard/react"; import { - Tabs, - TabList, - TabPanels, - TabPanel, - Tab, - Card, - useColorMode, + Tabs, + TabList, + TabPanels, + TabPanel, + Tab, + Card, + useColorMode, } from "@chakra-ui/react"; import { Button, Box } from "@chakra-ui/react"; -import { - NumberInput, - NumberInputField, - NumberInputStepper, - NumberIncrementStepper, - NumberDecrementStepper, -} from "@chakra-ui/react"; import { Input, Stack } from "@chakra-ui/react"; import { Accordion } from "@chakra-ui/react"; import { Text } from "@chakra-ui/react"; import { Vouchers } from "./Vouchers"; +import { EtherInput } from "./components/EtherInput"; +import { ZCashTaddressInput } from "./components/ZCashTaddressInput"; +import bs58 from "bs58"; interface IInputPropos { - dappAddress: string; + dappAddress: string; } export const Transfers: React.FC = (propos) => { - const rollups = useRollups(propos.dappAddress); - const [connectedWallet] = useWallets(); - const provider = new ethers.providers.Web3Provider( - connectedWallet.provider - ); - const { colorMode } = useColorMode(); - - const sendAddress = async () => { - if (rollups) { - try { - await rollups.relayContract.relayDAppAddress( - propos.dappAddress - ); - setDappRelayedAddress(true); - } catch (e) { - console.log(`${e}`); - } - } - }; - - const depositEtherToPortal = async ( - amount: number, - destAddress: string - ) => { - try { - if (rollups && provider) { - const data = ethers.utils.arrayify(destAddress); - const txOverrides = { - value: ethers.utils.parseEther(`${amount}`), - }; - console.log("Ether to deposit: ", txOverrides); - - // const tx = await ... - rollups.etherPortalContract.depositEther( - propos.dappAddress, - data, - txOverrides - ); - } - } catch (e) { - console.log(`${e}`); - } - }; - - const sendTransaction = async (transactionHex: string) => { - try { - if (rollups && provider) { - const input = ethers.utils.arrayify(transactionHex); - - rollups.inputContract.addInput(propos.dappAddress, input); - } - } catch (e) { - console.log(`${e}`); - } - }; - - const [dappRelayedAddress, setDappRelayedAddress] = - useState(false); - const [etherAmount, setEtherAmount] = useState(0); - const [destAddress, setDestAddress] = useState(""); - - const [transactionHex, setTransactionHex] = useState(""); - - return ( - { + if (rollups) { + try { + await rollups.relayContract.relayDAppAddress(propos.dappAddress); + // setDappRelayedAddress(true); + } catch (e) { + console.log(`${e}`); + } + } + }; + + const depositEtherToPortal = async (amount: string, destAddress: string) => { + try { + if (rollups && provider) { + // parse the t-address into bytes we can send to the contract + let address_bytes = bs58.decode(destAddress); + const data = ethers.utils.arrayify(address_bytes); + const txOverrides = { + value: ethers.utils.parseEther(amount), + }; + console.log("Ether to deposit: ", txOverrides); + + // const tx = await ... + rollups.etherPortalContract.depositEther( + propos.dappAddress, + data, + txOverrides + ); + } + } catch (e) { + console.log(`${e}`); + } + }; + + const sendTransaction = async (transactionHex: string) => { + try { + if (rollups && provider) { + const input = ethers.utils.arrayify(transactionHex); + + rollups.inputContract.addInput(propos.dappAddress, input); + } + } catch (e) { + console.log(`${e}`); + } + }; + + const [etherAmount, setEtherAmount] = useState(""); + const [destAddress, setDestAddress] = useState("t1"); + + const [transactionHex, setTransactionHex] = useState(""); + + return ( + + + - - + Deposit + + {/* + Transact + */} + + Withdraw + + + + + + + Deposit Eth to bridge it to CarteZcash + +

+ + + setEtherAmount(value)} + value={etherAmount} + /> + + setDestAddress(e)} + /> + - -
- - - - - Send ZCash transactions to have them executed on - the rollup - - - - - setTransactionHex(e.target.value) - } - > - - - - - - - - Withdraw by sending to the Mt Doom address - t1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs. After - the withdraw request, the user has to - execute a voucher to transfer assets from - CarteZcash to their account. - -
- {!dappRelayedAddress && ( -
- Let the dApp know its address!
- -
-
-
- )} - {dappRelayedAddress && ( - - )} -
-
- - - - - ); + Deposit + + +
+ + + {/* Skip this for now. It was part of the earlier demo but now we can sent transactions directly from the wallet + + Send ZCash transactions to have them executed on the rollup + + + + setTransactionHex(e.target.value)} + > + + + */} + + + + + To withdraw send funds to the exit address{" "} + {rollups?.rollupExitAddress} on the L2 then execute + the resulting voucher here + +
+ +
+
+ + + + + ); }; diff --git a/bridge-frontend/src/components/EtherInput.tsx b/bridge-frontend/src/components/EtherInput.tsx new file mode 100644 index 0000000..afc7574 --- /dev/null +++ b/bridge-frontend/src/components/EtherInput.tsx @@ -0,0 +1,94 @@ +import { useMemo, useState } from "react"; +import { Input } from "@chakra-ui/react"; + +const MAX_DECIMALS_USD = 2; +export const SIGNED_NUMBER_REGEX = /^-?\d+\.?\d*$/; + +function etherValueToDisplayValue(usdMode: boolean, etherValue: string, nativeCurrencyPrice: number) { + if (usdMode && nativeCurrencyPrice) { + const parsedEthValue = parseFloat(etherValue); + if (Number.isNaN(parsedEthValue)) { + return etherValue; + } else { + // We need to round the value rather than use toFixed, + // since otherwise a user would not be able to modify the decimal value + return ( + Math.round(parsedEthValue * nativeCurrencyPrice * 10 ** MAX_DECIMALS_USD) / + 10 ** MAX_DECIMALS_USD + ).toString(); + } + } else { + return etherValue; + } +} + +function displayValueToEtherValue(usdMode: boolean, displayValue: string, nativeCurrencyPrice: number) { + if (usdMode && nativeCurrencyPrice) { + const parsedDisplayValue = parseFloat(displayValue); + if (Number.isNaN(parsedDisplayValue)) { + // Invalid number. + return displayValue; + } else { + // Compute the ETH value if a valid number. + return (parsedDisplayValue / nativeCurrencyPrice).toString(); + } + } else { + return displayValue; + } +} + +/** + * Input for ETH amount with USD conversion. + * + * onChange will always be called with the value in ETH + */ +export const EtherInput = ({ + value, + name, + placeholder, + onChange, + disabled, +}: any) => { + const [transitoryDisplayValue, setTransitoryDisplayValue] = useState(); + + + // The displayValue is derived from the ether value that is controlled outside of the component + // In usdMode, it is converted to its usd value, in regular mode it is unaltered + const displayValue = useMemo(() => { + const newDisplayValue = etherValueToDisplayValue(false, value, 0); + if (transitoryDisplayValue && parseFloat(newDisplayValue) === parseFloat(transitoryDisplayValue)) { + return transitoryDisplayValue; + } + // Clear any transitory display values that might be set + setTransitoryDisplayValue(undefined); + return newDisplayValue; + }, [transitoryDisplayValue, value]); + + const handleChangeNumber = (e: React.ChangeEvent) => { + const newValue = e.target.value; + if (newValue && !SIGNED_NUMBER_REGEX.test(newValue)) { + return; + } + + // Since the display value is a derived state (calculated from the ether value), usdMode would not allow introducing a decimal point. + // This condition handles a transitory state for a display value with a trailing decimal sign + if (newValue.endsWith(".") || newValue.endsWith(".0")) { + setTransitoryDisplayValue(newValue); + } else { + setTransitoryDisplayValue(undefined); + } + + const newEthValue = displayValueToEtherValue(false, newValue, 0); + onChange(newEthValue); + }; + + return ( + + ); +}; diff --git a/bridge-frontend/src/components/ZCashTaddressInput.tsx b/bridge-frontend/src/components/ZCashTaddressInput.tsx new file mode 100644 index 0000000..ea934a8 --- /dev/null +++ b/bridge-frontend/src/components/ZCashTaddressInput.tsx @@ -0,0 +1,51 @@ +import { useState, useMemo } from "react"; +import { Input } from "@chakra-ui/react"; + +export const T_ADDRESS_REGEX = /^t1[a-zA-Z0-9]{1,33}$/; + +/** + * Input for ETH amount with USD conversion. + * + * onChange will always be called with the value in ETH + */ +export const ZCashTaddressInput = ({ + value, + name, + placeholder, + onChange, + disabled, +}: any) => { + const [transitoryDisplayValue, setTransitoryDisplayValue] = useState(); + + + // The displayValue is derived from the ether value that is controlled outside of the component + // In usdMode, it is converted to its usd value, in regular mode it is unaltered + const displayValue = useMemo(() => { + const newDisplayValue = value; + if (transitoryDisplayValue) { + return transitoryDisplayValue; + } + // Clear any transitory display values that might be set + setTransitoryDisplayValue(undefined); + return newDisplayValue; + }, [transitoryDisplayValue, value]); + + const handleChangeNumber = (e: React.ChangeEvent) => { + const newValue = e.target.value; + if (newValue && !T_ADDRESS_REGEX.test(newValue)) { + return; + } + + onChange(newValue); + }; + + return ( + + ); +}; diff --git a/bridge-frontend/src/config.json b/bridge-frontend/src/config.json index ddb8a3e..fcd189b 100644 --- a/bridge-frontend/src/config.json +++ b/bridge-frontend/src/config.json @@ -11,7 +11,9 @@ "Erc20PortalAddress":"0x9C21AEb2093C32DDbC53eEF24B873BDCd1aDa1DB", "Erc721PortalAddress":"0x237F8DD094C0e47f4236f12b4Fa01d6Dae89fb87", "Erc1155SinglePortalAddress":"0x7CFB0193Ca87eB6e48056885E026552c3A941FC4", - "Erc1155BatchPortalAddress":"0xedB53860A6B52bbb7561Ad596416ee9965B055Aa" + "Erc1155BatchPortalAddress":"0xedB53860A6B52bbb7561Ad596416ee9965B055Aa", + "RollupExitAddress": "t1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs", + "DAppAddress": "0x237F8DD094C0e47f4236f12b4Fa01d6Dae89fb87" }, "0xaa36a7":{ "token": "SepETH", @@ -25,6 +27,8 @@ "Erc20PortalAddress":"0x9C21AEb2093C32DDbC53eEF24B873BDCd1aDa1DB", "Erc721PortalAddress":"0x237F8DD094C0e47f4236f12b4Fa01d6Dae89fb87", "Erc1155SinglePortalAddress":"0x7CFB0193Ca87eB6e48056885E026552c3A941FC4", - "Erc1155BatchPortalAddress":"0xedB53860A6B52bbb7561Ad596416ee9965B055Aa" + "Erc1155BatchPortalAddress":"0xedB53860A6B52bbb7561Ad596416ee9965B055Aa", + "RollupExitAddress": "t1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs", + "DAppAddress": "0x237F8DD094C0e47f4236f12b4Fa01d6Dae89fb87" } } diff --git a/bridge-frontend/src/useRollups.tsx b/bridge-frontend/src/useRollups.tsx index a7cd951..0aeba18 100644 --- a/bridge-frontend/src/useRollups.tsx +++ b/bridge-frontend/src/useRollups.tsx @@ -50,6 +50,7 @@ export interface RollupsContracts { erc721PortalContract: ERC721Portal; erc1155SinglePortalContract: ERC1155SinglePortal; erc1155BatchPortalContract: ERC1155BatchPortal; + rollupExitAddress: string; } export const useRollups = (dAddress: string): RollupsContracts | undefined => { @@ -139,6 +140,8 @@ export const useRollups = (dAddress: string): RollupsContracts | undefined => { const erc1155BatchPortalContract = ERC1155BatchPortal__factory.connect(erc1155BatchPortalAddress, signer); + const rollupExitAddress = config[chain.id].RollupExitAddress; + return { dappContract, signer, @@ -149,6 +152,7 @@ export const useRollups = (dAddress: string): RollupsContracts | undefined => { erc721PortalContract, erc1155SinglePortalContract, erc1155BatchPortalContract, + rollupExitAddress }; }; if (connectedWallet?.provider && connectedChain) { diff --git a/bridge-frontend/yarn.lock b/bridge-frontend/yarn.lock index 4918f31..9221f95 100644 --- a/bridge-frontend/yarn.lock +++ b/bridge-frontend/yarn.lock @@ -8864,6 +8864,13 @@ __metadata: languageName: node linkType: hard +"base-x@npm:^4.0.0": + version: 4.0.0 + resolution: "base-x@npm:4.0.0" + checksum: 10c0/0cb47c94535144ab138f70bb5aa7e6e03049ead88615316b62457f110fc204f2c3baff5c64a1c1b33aeb068d79a68092c08a765c7ccfa133eee1e70e4c6eb903 + languageName: node + linkType: hard + "base64-js@npm:^1.3.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" @@ -9180,6 +9187,15 @@ __metadata: languageName: node linkType: hard +"bs58@npm:^5.0.0": + version: 5.0.0 + resolution: "bs58@npm:5.0.0" + dependencies: + base-x: "npm:^4.0.0" + checksum: 10c0/0d1b05630b11db48039421b5975cb2636ae0a42c62f770eec257b2e5c7d94cb5f015f440785f3ec50870a6e9b1132b35bd0a17c7223655b22229f24b2a3491d1 + languageName: node + linkType: hard + "bs58check@npm:^2.1.2": version: 2.1.2 resolution: "bs58check@npm:2.1.2" @@ -9459,6 +9475,7 @@ __metadata: "@web3-onboard/injected-wallets": "npm:^2.0.5" "@web3-onboard/react": "npm:^2.1.5" assert: "npm:^2.0.0" + bs58: "npm:^5.0.0" buffer: "npm:^6.0.3" cacheable-request: "npm:^10.2.7" crypto-browserify: "npm:^3.12.0"