diff --git a/bridge_ui/package-lock.json b/bridge_ui/package-lock.json index c328bbf12..8ecc63f83 100644 --- a/bridge_ui/package-lock.json +++ b/bridge_ui/package-lock.json @@ -13,6 +13,7 @@ "@material-ui/core": "^4.12.2", "@metamask/detect-provider": "^1.2.0", "@project-serum/sol-wallet-adapter": "^0.2.5", + "@reduxjs/toolkit": "^1.6.1", "@solana/spl-token": "^0.1.6", "@solana/wallet-base": "^0.0.1", "@solana/web3.js": "^1.22.0", @@ -21,7 +22,9 @@ "ethers": "^5.4.1", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-redux": "^7.2.4", "react-scripts": "4.0.3", + "redux": "^3.7.2", "token-bridge": "file:rust_modules\\token" }, "devDependencies": { @@ -5250,6 +5253,46 @@ "integrity": "sha512-afmTuJrylUU/0OtqzaRkbyYFFNgCF73Bvel/sw90pvGrWIZ+vyoIJqA6eMSoA6+nb443kTmulmBtC9NerXboNg==", "dev": true }, + "node_modules/@reduxjs/toolkit": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.6.1.tgz", + "integrity": "sha512-pa3nqclCJaZPAyBhruQtiRwtTjottRrVJqziVZcWzI73i6L3miLTtUyWfauwv08HWtiXLx1xGyGt+yLFfW/d0A==", + "dependencies": { + "immer": "^9.0.1", + "redux": "^4.1.0", + "redux-thunk": "^2.3.0", + "reselect": "^4.0.0" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0", + "react-redux": "^7.2.1" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.5.tgz", + "integrity": "sha512-2WuIehr2y4lmYz9gaQzetPR2ECniCifk4ORaQbU3g5EalLt+0IVTosEPJ5BoYl/75ky2mivzdRzV8wWgQGOSYQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/@reduxjs/toolkit/node_modules/redux": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.1.tgz", + "integrity": "sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/@repeaterjs/repeater": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.4.tgz", @@ -7072,6 +7115,15 @@ "@types/node": "*" } }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/html-minifier-terser": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz", @@ -7234,6 +7286,25 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-redux": { + "version": "7.1.18", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.18.tgz", + "integrity": "sha512-9iwAsPyJ9DLTRH+OFeIrm9cAbIj1i2ANL3sKQFATqnPWRbg+jEFXyZOKHiQK/N86pNRXbb4HRxAxo0SIX1XwzQ==", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, + "node_modules/@types/react-redux/node_modules/redux": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.1.tgz", + "integrity": "sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/@types/react-transition-group": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.2.tgz", @@ -23346,8 +23417,7 @@ "node_modules/lodash-es": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "dev": true + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" }, "node_modules/lodash._reinterpolate": { "version": "3.0.0", @@ -30092,6 +30162,35 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/react-redux": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.4.tgz", + "integrity": "sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA==", + "dependencies": { + "@babel/runtime": "^7.12.1", + "@types/react-redux": "^7.1.16", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^16.13.1" + }, + "peerDependencies": { + "react": "^16.8.3 || ^17" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-redux/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/react-refresh": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz", @@ -30400,7 +30499,6 @@ "version": "3.7.2", "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz", "integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==", - "dev": true, "dependencies": { "lodash": "^4.2.1", "lodash-es": "^4.2.1", @@ -30469,11 +30567,15 @@ "@redux-saga/core": "^1.0.0" } }, + "node_modules/redux-thunk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", + "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==" + }, "node_modules/redux/node_modules/symbol-observable": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -30934,8 +31036,7 @@ "node_modules/reselect": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz", - "integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==", - "dev": true + "integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==" }, "node_modules/reselect-tree": { "version": "1.3.4", @@ -42420,6 +42521,32 @@ "integrity": "sha512-afmTuJrylUU/0OtqzaRkbyYFFNgCF73Bvel/sw90pvGrWIZ+vyoIJqA6eMSoA6+nb443kTmulmBtC9NerXboNg==", "dev": true }, + "@reduxjs/toolkit": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.6.1.tgz", + "integrity": "sha512-pa3nqclCJaZPAyBhruQtiRwtTjottRrVJqziVZcWzI73i6L3miLTtUyWfauwv08HWtiXLx1xGyGt+yLFfW/d0A==", + "requires": { + "immer": "^9.0.1", + "redux": "^4.1.0", + "redux-thunk": "^2.3.0", + "reselect": "^4.0.0" + }, + "dependencies": { + "immer": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.5.tgz", + "integrity": "sha512-2WuIehr2y4lmYz9gaQzetPR2ECniCifk4ORaQbU3g5EalLt+0IVTosEPJ5BoYl/75ky2mivzdRzV8wWgQGOSYQ==" + }, + "redux": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.1.tgz", + "integrity": "sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw==", + "requires": { + "@babel/runtime": "^7.9.2" + } + } + } + }, "@repeaterjs/repeater": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.4.tgz", @@ -44019,6 +44146,15 @@ "@types/node": "*" } }, + "@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "@types/html-minifier-terser": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz", @@ -44188,6 +44324,27 @@ } } }, + "@types/react-redux": { + "version": "7.1.18", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.18.tgz", + "integrity": "sha512-9iwAsPyJ9DLTRH+OFeIrm9cAbIj1i2ANL3sKQFATqnPWRbg+jEFXyZOKHiQK/N86pNRXbb4HRxAxo0SIX1XwzQ==", + "requires": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + }, + "dependencies": { + "redux": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.1.tgz", + "integrity": "sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw==", + "requires": { + "@babel/runtime": "^7.9.2" + } + } + } + }, "@types/react-transition-group": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.2.tgz", @@ -57155,8 +57312,7 @@ "lodash-es": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "dev": true + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" }, "lodash._reinterpolate": { "version": "3.0.0", @@ -62736,6 +62892,26 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "react-redux": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.4.tgz", + "integrity": "sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA==", + "requires": { + "@babel/runtime": "^7.12.1", + "@types/react-redux": "^7.1.16", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^16.13.1" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + } + } + }, "react-refresh": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz", @@ -62984,7 +63160,6 @@ "version": "3.7.2", "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz", "integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==", - "dev": true, "requires": { "lodash": "^4.2.1", "lodash-es": "^4.2.1", @@ -62995,8 +63170,7 @@ "symbol-observable": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", - "dev": true + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" } } }, @@ -63057,6 +63231,11 @@ "@redux-saga/core": "^1.0.0" } }, + "redux-thunk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", + "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==" + }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -63419,8 +63598,7 @@ "reselect": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz", - "integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==", - "dev": true + "integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==" }, "reselect-tree": { "version": "1.3.4", diff --git a/bridge_ui/package.json b/bridge_ui/package.json index 4888dc496..f7757224c 100644 --- a/bridge_ui/package.json +++ b/bridge_ui/package.json @@ -7,6 +7,7 @@ "@material-ui/core": "^4.12.2", "@metamask/detect-provider": "^1.2.0", "@project-serum/sol-wallet-adapter": "^0.2.5", + "@reduxjs/toolkit": "^1.6.1", "@solana/spl-token": "^0.1.6", "@solana/wallet-base": "^0.0.1", "@solana/web3.js": "^1.22.0", @@ -15,7 +16,9 @@ "ethers": "^5.4.1", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-redux": "^7.2.4", "react-scripts": "4.0.3", + "redux": "^3.7.2", "token-bridge": "file:rust_modules\\token" }, "scripts": { diff --git a/bridge_ui/src/App.js b/bridge_ui/src/App.js index 353af4bff..a77436207 100644 --- a/bridge_ui/src/App.js +++ b/bridge_ui/src/App.js @@ -1,4 +1,4 @@ -import { AppBar, Link, makeStyles, Toolbar } from "@material-ui/core"; +import { AppBar, makeStyles, Toolbar } from "@material-ui/core"; import Transfer from "./components/Transfer"; import wormholeLogo from "./icons/wormhole.svg"; @@ -6,8 +6,8 @@ const useStyles = makeStyles((theme) => ({ appBar: { borderBottom: `.5px solid ${theme.palette.divider}`, "& > .MuiToolbar-root": { - height: 82, margin: "auto", + width: "100%", maxWidth: 1100, }, }, @@ -19,18 +19,13 @@ const useStyles = makeStyles((theme) => ({ color: theme.palette.text.primary, marginLeft: theme.spacing(6), }, - sideBar: { - position: "fixed", - top: 0, - left: 0, - height: 733, - maxHeight: "80vh", - width: 50, - borderRight: `.5px solid ${theme.palette.divider}`, - borderBottom: `.5px solid ${theme.palette.divider}`, - }, content: { - margin: theme.spacing(10.5, 8), + [theme.breakpoints.up("sm")]: { + margin: theme.spacing(2, 0), + }, + [theme.breakpoints.up("md")]: { + margin: theme.spacing(4, 0), + }, }, })); @@ -42,12 +37,8 @@ function App() { Wormhole Logo
- Placeholder - Placeholder - Placeholder -
diff --git a/bridge_ui/src/components/EthereumSignerKey.tsx b/bridge_ui/src/components/EthereumSignerKey.tsx index 44cb38376..09d8a462c 100644 --- a/bridge_ui/src/components/EthereumSignerKey.tsx +++ b/bridge_ui/src/components/EthereumSignerKey.tsx @@ -1,40 +1,18 @@ -import { Button, Tooltip, Typography } from "@material-ui/core"; +import { Typography } from "@material-ui/core"; import { useEthereumProvider } from "../contexts/EthereumProviderContext"; +import ToggleConnectedButton from "./ToggleConnectedButton"; const EthereumSignerKey = () => { const { connect, disconnect, signerAddress, providerError } = useEthereumProvider(); return ( <> - {signerAddress ? ( - <> - - - {signerAddress.substring(0, 6)}... - {signerAddress.substr(signerAddress.length - 4)} - - - - - ) : ( - - )} + {providerError ? ( {providerError} diff --git a/bridge_ui/src/components/SolanaWalletKey.tsx b/bridge_ui/src/components/SolanaWalletKey.tsx index 5f80d9adc..10ec46fac 100644 --- a/bridge_ui/src/components/SolanaWalletKey.tsx +++ b/bridge_ui/src/components/SolanaWalletKey.tsx @@ -1,40 +1,16 @@ -import { Button, Tooltip, Typography } from "@material-ui/core"; import { useSolanaWallet } from "../contexts/SolanaWalletContext"; +import ToggleConnectedButton from "./ToggleConnectedButton"; const SolanaWalletKey = () => { const { connect, disconnect, connected, wallet } = useSolanaWallet(); const pk = wallet?.publicKey?.toString() || ""; return ( - <> - {connected ? ( - <> - - - {pk.substring(0, 3)}...{pk.substr(pk.length - 3)} - - - - - ) : ( - - )} - + ); }; diff --git a/bridge_ui/src/components/ToggleConnectedButton.tsx b/bridge_ui/src/components/ToggleConnectedButton.tsx new file mode 100644 index 000000000..3cfae25b1 --- /dev/null +++ b/bridge_ui/src/components/ToggleConnectedButton.tsx @@ -0,0 +1,51 @@ +import { Button, makeStyles, Tooltip } from "@material-ui/core"; + +const useStyles = makeStyles((theme) => ({ + button: { + display: "block", + margin: `${theme.spacing(1)}px auto`, + width: "100%", + maxWidth: 400, + }, +})); + +const ToggleConnectedButton = ({ + connect, + disconnect, + connected, + pk, +}: { + connect(): any; + disconnect(): any; + connected: boolean; + pk: string; +}) => { + const classes = useStyles(); + const is0x = pk.startsWith("0x"); + return connected ? ( + + + + ) : ( + + ); +}; + +export default ToggleConnectedButton; diff --git a/bridge_ui/src/components/Transfer.tsx b/bridge_ui/src/components/Transfer.tsx index 8a67e8a99..0b9ae49f8 100644 --- a/bridge_ui/src/components/Transfer.tsx +++ b/bridge_ui/src/components/Transfer.tsx @@ -1,24 +1,41 @@ import { Button, CircularProgress, - Grid, + Container, makeStyles, MenuItem, + Step, + StepButton, + StepContent, + Stepper, TextField, Typography, } from "@material-ui/core"; import { useCallback, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; import { useEthereumProvider } from "../contexts/EthereumProviderContext"; import { useSolanaWallet } from "../contexts/SolanaWalletContext"; import useEthereumBalance from "../hooks/useEthereumBalance"; import useSolanaBalance from "../hooks/useSolanaBalance"; import useWrappedAsset from "../hooks/useWrappedAsset"; +import { + selectActiveStep, + selectSignedVAA, + selectSourceChain, + selectTargetChain, +} from "../store/selectors"; +import { + incrementStep, + setSignedVAA, + setSourceChain, + setStep, + setTargetChain, +} from "../store/transferSlice"; import attestFrom, { attestFromEth, attestFromSolana, } from "../utils/attestFrom"; import { - ChainId, CHAINS, CHAINS_BY_ID, CHAIN_ID_ETH, @@ -26,6 +43,7 @@ import { ETH_TEST_TOKEN_ADDRESS, SOL_TEST_TOKEN_ADDRESS, } from "../utils/consts"; +import redeemOn, { redeemOnEth } from "../utils/redeemOn"; import transferFrom, { transferFromEth, transferFromSolana, @@ -48,7 +66,7 @@ const useStyles = makeStyles((theme) => ({ marginTop: theme.spacing(5), }, transferButton: { - marginTop: theme.spacing(7.5), + marginTop: theme.spacing(2), textTransform: "none", width: "100%", }, @@ -61,14 +79,15 @@ const useStyles = makeStyles((theme) => ({ function Transfer() { const classes = useStyles(); - //TODO: don't attempt to connect to any wallets until the user clicks a connect button - const [fromChain, setFromChain] = useState(CHAIN_ID_ETH); - const [toChain, setToChain] = useState(CHAIN_ID_SOLANA); - const [assetAddress, setAssetAddress] = useState(ETH_TEST_TOKEN_ADDRESS); + const dispatch = useDispatch(); + const activeStep = useSelector(selectActiveStep); + const fromChain = useSelector(selectSourceChain); + const toChain = useSelector(selectTargetChain); + const [assetAddress, setAssetAddress] = useState(SOL_TEST_TOKEN_ADDRESS); const [amount, setAmount] = useState(""); const handleFromChange = useCallback( (event) => { - setFromChain(event.target.value); + dispatch(setSourceChain(event.target.value)); // TODO: remove or check env - for testing purposes if (event.target.value === CHAIN_ID_ETH) { setAssetAddress(ETH_TEST_TOKEN_ADDRESS); @@ -77,16 +96,16 @@ function Transfer() { setAssetAddress(SOL_TEST_TOKEN_ADDRESS); } if (toChain === event.target.value) { - setToChain(fromChain); + dispatch(setTargetChain(fromChain)); } }, - [fromChain, toChain] + [dispatch, fromChain, toChain] ); const handleToChange = useCallback( (event) => { - setToChain(event.target.value); + dispatch(setTargetChain(event.target.value)); if (fromChain === event.target.value) { - setFromChain(toChain); + dispatch(setSourceChain(toChain)); // TODO: remove or check env - for testing purposes if (toChain === CHAIN_ID_ETH) { setAssetAddress(ETH_TEST_TOKEN_ADDRESS); @@ -96,7 +115,7 @@ function Transfer() { } } }, - [fromChain, toChain] + [dispatch, fromChain, toChain] ); const handleAssetChange = useCallback((event) => { setAssetAddress(event.target.value); @@ -121,9 +140,10 @@ function Transfer() { } = useSolanaBalance(assetAddress, solPK, fromChain === CHAIN_ID_SOLANA); const { isLoading: isCheckingWrapped, - isWrapped, + // isWrapped, wrappedAsset, } = useWrappedAsset(toChain, fromChain, assetAddress, provider); + const isWrapped = true; console.log(isCheckingWrapped, isWrapped, wrappedAsset); const handleAttestClick = useCallback(() => { // TODO: more generic way of calling these @@ -132,13 +152,26 @@ function Transfer() { fromChain === CHAIN_ID_ETH && attestFrom[fromChain] === attestFromEth ) { - attestFromEth(provider, signer, assetAddress); + //TODO: just for testing, this should eventually use the store to communicate between steps + (async () => { + const vaaBytes = await attestFromEth(provider, signer, assetAddress); + console.log("bytes in transfer", vaaBytes); + })(); } if ( fromChain === CHAIN_ID_SOLANA && attestFrom[fromChain] === attestFromSolana ) { - attestFromSolana(wallet, solPK?.toString(), assetAddress, solDecimals); + //TODO: just for testing, this should eventually use the store to communicate between steps + (async () => { + const vaaBytes = await attestFromSolana( + wallet, + solPK?.toString(), + assetAddress, + solDecimals + ); + console.log("bytes in transfer", vaaBytes); + })(); } } }, [fromChain, provider, signer, wallet, solPK, assetAddress, solDecimals]); @@ -150,33 +183,44 @@ function Transfer() { fromChain === CHAIN_ID_ETH && transferFrom[fromChain] === transferFromEth ) { - transferFromEth( - provider, - signer, - assetAddress, - ethDecimals, - amount, - toChain, - solPK?.toBytes() - ); + //TODO: just for testing, this should eventually use the store to communicate between steps + (async () => { + const vaaBytes = await transferFromEth( + provider, + signer, + assetAddress, + ethDecimals, + amount, + toChain, + solPK?.toBytes() + ); + console.log("bytes in transfer", vaaBytes); + vaaBytes && dispatch(setSignedVAA(vaaBytes)); + })(); } if ( fromChain === CHAIN_ID_SOLANA && transferFrom[fromChain] === transferFromSolana ) { - transferFromSolana( - wallet, - solPK?.toString(), - solTokenPK?.toString(), - assetAddress, - amount, - solDecimals, - signerAddress, - toChain - ); + //TODO: just for testing, this should eventually use the store to communicate between steps + (async () => { + const vaaBytes = await transferFromSolana( + wallet, + solPK?.toString(), + solTokenPK?.toString(), + assetAddress, + amount, + solDecimals, + signerAddress, + toChain + ); + console.log("bytes in transfer", vaaBytes); + vaaBytes && dispatch(setSignedVAA(vaaBytes)); + })(); } } }, [ + dispatch, fromChain, provider, signer, @@ -190,6 +234,16 @@ function Transfer() { solDecimals, toChain, ]); + const signedVAA = useSelector(selectSignedVAA); + const handleRedeemClick = useCallback(() => { + if ( + toChain === CHAIN_ID_ETH && + redeemOn[toChain] === redeemOnEth && + signedVAA + ) { + redeemOnEth(provider, signer, signedVAA); + } + }, [toChain, provider, signer, signedVAA]); // update this as we develop, just setting expectations with the button state const balance = Number(ethBalance) || solBalance; const isAttestImplemented = !!attestFrom[fromChain]; @@ -211,134 +265,186 @@ function Transfer() { isAddressDefined && isAmountPositive && isBalanceAtLeastAmount; + const handleNextClick = useCallback(() => { + dispatch(incrementStep()); + }, [dispatch]); return ( -
- - - From - + + + dispatch(setStep(0))}> + Select a source + + + + {CHAINS.map(({ id, name }) => ( + + {name} + + ))} + + + + + + + + + dispatch(setStep(1))}> + Select a target + + + + {CHAINS.map(({ id, name }) => ( + + {name} + + ))} + + {/* TODO: determine "to" token address */} + + + + + + dispatch(setStep(2))}> + Send tokens + + + {isWrapped ? ( + <> + + {canAttemptTransfer ? null : ( + + {!isTransferImplemented + ? `Transfer is not yet implemented for ${CHAINS_BY_ID[fromChain].name}` + : !isProviderConnected + ? "The source wallet is not connected" + : !isRecipientAvailable + ? "The receiving wallet is not connected" + : !isAddressDefined + ? "Please provide an asset address" + : !isAmountPositive + ? "The amount must be positive" + : !isBalanceAtLeastAmount + ? "The amount may not be greater than the balance" + : ""} + + )} + + ) : ( + <> +
+ + {isCheckingWrapped ? ( + + ) : null} +
+ {isCheckingWrapped ? null : canAttemptAttest ? ( + +
+ This token does not exist on {CHAINS_BY_ID[toChain].name}. + Someone must attest the the token to the target chain before + it can be transferred. +
+ ) : ( + + {!isAttestImplemented + ? `Transfer is not yet implemented for ${CHAINS_BY_ID[fromChain].name}` + : !isProviderConnected + ? "The source wallet is not connected" + : !isRecipientAvailable + ? "The receiving wallet is not connected" + : !isAddressDefined + ? "Please provide an asset address" + : ""} + + )} + + )} +
+
+ + dispatch(setStep(3))} + disabled={!signedVAA} > - {CHAINS.map(({ id, name }) => ( - - {name} - - ))} -
- -
- - → - - - To - - {CHAINS.map(({ id, name }) => ( - - {name} - - ))} - - {/* TODO: determine "to" token address */} - - -
- - {isWrapped ? ( - <> - - - {canAttemptTransfer ? null : ( - - {!isTransferImplemented - ? `Transfer is not yet implemented for ${CHAINS_BY_ID[fromChain].name}` - : !isProviderConnected - ? "The source wallet is not connected" - : !isRecipientAvailable - ? "The receiving wallet is not connected" - : !isAddressDefined - ? "Please provide an asset address" - : !isAmountPositive - ? "The amount must be positive" - : !isBalanceAtLeastAmount - ? "The amount may not be greater than the balance" - : ""} - - )} - - ) : ( - <> -
+ Redeem tokens + + - {isCheckingWrapped ? ( - - ) : null} -
- {isCheckingWrapped ? null : canAttemptAttest ? ( - -
- This token does not exist on {CHAINS_BY_ID[toChain].name}. Someone - must attest the the token to the target chain before it can be - transferred. -
- ) : ( - - {!isAttestImplemented - ? `Transfer is not yet implemented for ${CHAINS_BY_ID[fromChain].name}` - : !isProviderConnected - ? "The source wallet is not connected" - : !isRecipientAvailable - ? "The receiving wallet is not connected" - : !isAddressDefined - ? "Please provide an asset address" - : ""} - - )} - - )} -
+ + + + ); } diff --git a/bridge_ui/src/hooks/useWrappedAsset.ts b/bridge_ui/src/hooks/useWrappedAsset.ts index d30b7663c..8538a8252 100644 --- a/bridge_ui/src/hooks/useWrappedAsset.ts +++ b/bridge_ui/src/hooks/useWrappedAsset.ts @@ -41,13 +41,24 @@ function useWrappedAsset( } } else if (checkChain === CHAIN_ID_SOLANA) { setState({ isLoading: true, isWrapped: false, wrappedAsset: null }); - const asset = await getAttestedAssetSol(originChain, originAsset); - if (!cancelled) { - setState({ - isLoading: false, - isWrapped: !!asset, - wrappedAsset: asset, - }); + try { + const asset = await getAttestedAssetSol(originChain, originAsset); + if (!cancelled) { + setState({ + isLoading: false, + isWrapped: !!asset, + wrappedAsset: asset, + }); + } + } catch (e) { + if (!cancelled) { + // TODO: warning for this + setState({ + isLoading: false, + isWrapped: false, + wrappedAsset: null, + }); + } } } else { setState({ isLoading: false, isWrapped: false, wrappedAsset: null }); diff --git a/bridge_ui/src/index.js b/bridge_ui/src/index.js index 90dc024c2..3078f230e 100644 --- a/bridge_ui/src/index.js +++ b/bridge_ui/src/index.js @@ -1,19 +1,23 @@ import { CssBaseline } from "@material-ui/core"; import { ThemeProvider } from "@material-ui/core/styles"; import ReactDOM from "react-dom"; +import { Provider } from "react-redux"; import App from "./App"; +import { store } from "./store"; import { EthereumProviderProvider } from "./contexts/EthereumProviderContext"; import { SolanaWalletProvider } from "./contexts/SolanaWalletContext.tsx"; import { theme } from "./muiTheme"; ReactDOM.render( - - - - - - - - , + + + + + + + + + + , document.getElementById("root") ); diff --git a/bridge_ui/src/muiTheme.js b/bridge_ui/src/muiTheme.js index b750d800f..ad39cdaf1 100644 --- a/bridge_ui/src/muiTheme.js +++ b/bridge_ui/src/muiTheme.js @@ -10,13 +10,20 @@ export const theme = responsiveFontSizes( }, divider: "#4e4e54", primary: { - main: "#0074FF", + main: "rgba(0, 116, 255, 0.8)", // #0074FF + }, + secondary: { + main: "rgb(0,239,216,0.8)", // #00EFD8 + }, + error: { + main: "#FD3503", }, }, overrides: { MuiButton: { root: { borderRadius: 0, + textTransform: "none", }, }, }, diff --git a/bridge_ui/src/store/index.ts b/bridge_ui/src/store/index.ts new file mode 100644 index 000000000..917788921 --- /dev/null +++ b/bridge_ui/src/store/index.ts @@ -0,0 +1,13 @@ +import { configureStore } from "@reduxjs/toolkit"; +import transferReducer from "./transferSlice"; + +export const store = configureStore({ + reducer: { + transfer: transferReducer, + }, +}); + +// Infer the `RootState` and `AppDispatch` types from the store itself +export type RootState = ReturnType; +// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} +export type AppDispatch = typeof store.dispatch; diff --git a/bridge_ui/src/store/selectors.ts b/bridge_ui/src/store/selectors.ts new file mode 100644 index 000000000..0d44c95c6 --- /dev/null +++ b/bridge_ui/src/store/selectors.ts @@ -0,0 +1,8 @@ +import { RootState } from "."; + +export const selectActiveStep = (state: RootState) => state.transfer.activeStep; +export const selectSourceChain = (state: RootState) => + state.transfer.sourceChain; +export const selectTargetChain = (state: RootState) => + state.transfer.targetChain; +export const selectSignedVAA = (state: RootState) => state.transfer.signedVAA; //TODO: deserialize diff --git a/bridge_ui/src/store/transferSlice.ts b/bridge_ui/src/store/transferSlice.ts new file mode 100644 index 000000000..5376ca601 --- /dev/null +++ b/bridge_ui/src/store/transferSlice.ts @@ -0,0 +1,57 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { ChainId, CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "../utils/consts"; + +const LAST_STEP = 3; + +type Steps = 0 | 1 | 2 | 3; + +export interface TransferState { + activeStep: Steps; + sourceChain: ChainId; + targetChain: ChainId; + signedVAA: Uint8Array | undefined; +} + +const initialState: TransferState = { + activeStep: 0, + sourceChain: CHAIN_ID_SOLANA, + targetChain: CHAIN_ID_ETH, + signedVAA: undefined, +}; + +export const transferSlice = createSlice({ + name: "transfer", + initialState, + reducers: { + incrementStep: (state) => { + if (state.activeStep < LAST_STEP) state.activeStep++; + }, + decrementStep: (state) => { + if (state.activeStep > 0) state.activeStep--; + }, + setStep: (state, action: PayloadAction) => { + state.activeStep = action.payload; + }, + setSourceChain: (state, action: PayloadAction) => { + state.sourceChain = action.payload; + }, + setTargetChain: (state, action: PayloadAction) => { + state.targetChain = action.payload; + }, + setSignedVAA: (state, action: PayloadAction) => { + state.signedVAA = action.payload; //TODO: serialize + state.activeStep = 3; + }, + }, +}); + +export const { + incrementStep, + decrementStep, + setStep, + setSourceChain, + setTargetChain, + setSignedVAA, +} = transferSlice.actions; + +export default transferSlice.reducer; diff --git a/bridge_ui/src/utils/attestFrom.ts b/bridge_ui/src/utils/attestFrom.ts index 37ddecf97..b1f4ae893 100644 --- a/bridge_ui/src/utils/attestFrom.ts +++ b/bridge_ui/src/utils/attestFrom.ts @@ -21,135 +21,134 @@ import { // TODO: allow for / handle cancellation? // TODO: overall better input checking and error handling -export function attestFromEth( +export async function attestFromEth( provider: ethers.providers.Web3Provider | undefined, signer: ethers.Signer | undefined, tokenAddress: string ) { if (!provider || !signer) return; //TODO: more catches - (async () => { - const signerAddress = await signer.getAddress(); - console.log("Signer:", signerAddress); - console.log("Token:", tokenAddress); - const nonceConst = Math.random() * 100000; - const nonceBuffer = Buffer.alloc(4); - nonceBuffer.writeUInt32LE(nonceConst, 0); - console.log("Initiating attestation"); - console.log("Nonce:", nonceBuffer); - const bridge = Bridge__factory.connect(ETH_TOKEN_BRIDGE_ADDRESS, signer); - const v = await bridge.attestToken(tokenAddress, nonceBuffer); - const receipt = await v.wait(); - // TODO: dangerous!(?) - const bridgeLog = receipt.logs.filter((l) => { - console.log(l.address, ETH_BRIDGE_ADDRESS); - return l.address === ETH_BRIDGE_ADDRESS; - })[0]; - const { - args: { sequence }, - } = Implementation__factory.createInterface().parseLog(bridgeLog); - console.log("SEQ:", sequence); - const emitterAddress = Buffer.from( - zeroPad(arrayify(ETH_TOKEN_BRIDGE_ADDRESS), 32) - ).toString("hex"); - const { vaaBytes } = await getSignedVAA( - CHAIN_ID_ETH, - emitterAddress, - sequence - ); - console.log("SIGNED VAA:", vaaBytes); - })(); + const signerAddress = await signer.getAddress(); + console.log("Signer:", signerAddress); + console.log("Token:", tokenAddress); + const nonceConst = Math.random() * 100000; + const nonceBuffer = Buffer.alloc(4); + nonceBuffer.writeUInt32LE(nonceConst, 0); + console.log("Initiating attestation"); + console.log("Nonce:", nonceBuffer); + const bridge = Bridge__factory.connect(ETH_TOKEN_BRIDGE_ADDRESS, signer); + const v = await bridge.attestToken(tokenAddress, nonceBuffer); + const receipt = await v.wait(); + // TODO: log parsing should be part of a utility + // TODO: dangerous!(?) + const bridgeLog = receipt.logs.filter((l) => { + console.log(l.address, ETH_BRIDGE_ADDRESS); + return l.address === ETH_BRIDGE_ADDRESS; + })[0]; + const { + args: { sequence }, + } = Implementation__factory.createInterface().parseLog(bridgeLog); + console.log("SEQ:", sequence); + const emitterAddress = Buffer.from( + zeroPad(arrayify(ETH_TOKEN_BRIDGE_ADDRESS), 32) + ).toString("hex"); + const { vaaBytes } = await getSignedVAA( + CHAIN_ID_ETH, + emitterAddress, + sequence + ); + console.log("SIGNED VAA:", vaaBytes); + return vaaBytes; } // TODO: need to check transfer native vs transfer wrapped // TODO: switch out targetProvider for generic address (this likely involves getting these in their respective contexts) -export function attestFromSolana( +export async function attestFromSolana( wallet: Wallet | undefined, payerAddress: string | undefined, //TODO: we may not need this since we have wallet mintAddress: string, decimals: number ) { if (!wallet || !wallet.publicKey || !payerAddress) return; - (async () => { - const nonceConst = Math.random() * 100000; - const nonceBuffer = Buffer.alloc(4); - nonceBuffer.writeUInt32LE(nonceConst, 0); - const nonce = nonceBuffer.readUInt32LE(0); - console.log("program:", SOL_TOKEN_BRIDGE_ADDRESS); - console.log("bridge:", SOL_BRIDGE_ADDRESS); - console.log("payer:", payerAddress); - console.log("token:", mintAddress); - console.log("nonce:", nonce); - const bridge = await import("bridge"); - const feeAccount = await bridge.fee_collector_address(SOL_BRIDGE_ADDRESS); - const bridgeStatePK = new PublicKey( - bridge.state_address(SOL_BRIDGE_ADDRESS) - ); - // TODO: share connection in context? - const connection = new Connection(SOLANA_HOST, "confirmed"); - const bridgeStateAccountInfo = await connection.getAccountInfo( - bridgeStatePK - ); - if (bridgeStateAccountInfo?.data === undefined) { - throw new Error("bridge state not found"); - } - const bridgeState = bridge.parse_state( - new Uint8Array(bridgeStateAccountInfo?.data) - ); - const transferIx = SystemProgram.transfer({ - fromPubkey: new PublicKey(payerAddress), - toPubkey: new PublicKey(feeAccount), - lamports: bridgeState.config.fee, - }); - // TODO: pass in connection - // Add transfer instruction to transaction - const { attest_ix, emitter_address } = await import("token-bridge"); - const ix = ixFromRust( - attest_ix( - SOL_TOKEN_BRIDGE_ADDRESS, - SOL_BRIDGE_ADDRESS, - payerAddress, - mintAddress, - decimals, - mintAddress, // TODO: automate on wasm side - nonce - ) - ); - const transaction = new Transaction().add(transferIx, ix); - const { blockhash } = await connection.getRecentBlockhash(); - transaction.recentBlockhash = blockhash; - transaction.feePayer = new PublicKey(payerAddress); - // Sign transaction, broadcast, and confirm - const signed = await wallet.signTransaction(transaction); - console.log("SIGNED", signed); - const txid = await connection.sendRawTransaction(signed.serialize()); - console.log("SENT", txid); - const conf = await connection.confirmTransaction(txid); - console.log("CONFIRMED", conf); - const info = await connection.getTransaction(txid); - console.log("INFO", info); - // TODO: better parsing, safer - const SEQ_LOG = "Program log: Sequence: "; - const sequence = info?.meta?.logMessages - ?.filter((msg) => msg.startsWith(SEQ_LOG))[0] - .replace(SEQ_LOG, ""); - if (!sequence) { - throw new Error("sequence not found"); - } - console.log("SEQ", sequence); - const emitterAddress = Buffer.from( - zeroPad( - new PublicKey(emitter_address(SOL_TOKEN_BRIDGE_ADDRESS)).toBytes(), - 32 - ) - ).toString("hex"); - const { vaaBytes } = await getSignedVAA( - CHAIN_ID_SOLANA, - emitterAddress, - sequence - ); - console.log("SIGNED VAA:", vaaBytes); - })(); + const nonceConst = Math.random() * 100000; + const nonceBuffer = Buffer.alloc(4); + nonceBuffer.writeUInt32LE(nonceConst, 0); + const nonce = nonceBuffer.readUInt32LE(0); + console.log("program:", SOL_TOKEN_BRIDGE_ADDRESS); + console.log("bridge:", SOL_BRIDGE_ADDRESS); + console.log("payer:", payerAddress); + console.log("token:", mintAddress); + console.log("nonce:", nonce); + const bridge = await import("bridge"); + const feeAccount = await bridge.fee_collector_address(SOL_BRIDGE_ADDRESS); + const bridgeStatePK = new PublicKey(bridge.state_address(SOL_BRIDGE_ADDRESS)); + // TODO: share connection in context? + const connection = new Connection(SOLANA_HOST, "confirmed"); + const bridgeStateAccountInfo = await connection.getAccountInfo(bridgeStatePK); + if (bridgeStateAccountInfo?.data === undefined) { + throw new Error("bridge state not found"); + } + const bridgeState = bridge.parse_state( + new Uint8Array(bridgeStateAccountInfo?.data) + ); + const transferIx = SystemProgram.transfer({ + fromPubkey: new PublicKey(payerAddress), + toPubkey: new PublicKey(feeAccount), + lamports: bridgeState.config.fee, + }); + // TODO: pass in connection + // Add transfer instruction to transaction + const { attest_ix, emitter_address } = await import("token-bridge"); + const ix = ixFromRust( + attest_ix( + SOL_TOKEN_BRIDGE_ADDRESS, + SOL_BRIDGE_ADDRESS, + payerAddress, + mintAddress, + decimals, + mintAddress, // TODO: mint_metadata: what address is this supposed to be? + mintAddress, // TODO: spl_metadata: what address is this supposed to be? + "", // TODO: lookup symbol + "", //: TODO: lookup name + nonce + ) + ); + const transaction = new Transaction().add(transferIx, ix); + const { blockhash } = await connection.getRecentBlockhash(); + transaction.recentBlockhash = blockhash; + transaction.feePayer = new PublicKey(payerAddress); + // Sign transaction, broadcast, and confirm + const signed = await wallet.signTransaction(transaction); + console.log("SIGNED", signed); + const txid = await connection.sendRawTransaction(signed.serialize()); + console.log("SENT", txid); + const conf = await connection.confirmTransaction(txid); + console.log("CONFIRMED", conf); + const info = await connection.getTransaction(txid); + console.log("INFO", info); + // TODO: log parsing should be part of a utility + // TODO: better parsing, safer + const SEQ_LOG = "Program log: Sequence: "; + const sequence = info?.meta?.logMessages + ?.filter((msg) => msg.startsWith(SEQ_LOG))[0] + .replace(SEQ_LOG, ""); + if (!sequence) { + throw new Error("sequence not found"); + } + console.log("SEQ", sequence); + const emitterAddress = Buffer.from( + zeroPad( + new PublicKey(emitter_address(SOL_TOKEN_BRIDGE_ADDRESS)).toBytes(), + 32 + ) + ).toString("hex"); + const { vaaBytes } = await getSignedVAA( + CHAIN_ID_SOLANA, + emitterAddress, + sequence + ); + console.log("SIGNED VAA:", vaaBytes); + return vaaBytes; } const attestFrom = { diff --git a/bridge_ui/src/utils/redeemOn.ts b/bridge_ui/src/utils/redeemOn.ts new file mode 100644 index 000000000..91e4fd62c --- /dev/null +++ b/bridge_ui/src/utils/redeemOn.ts @@ -0,0 +1,23 @@ +import { ethers } from "ethers"; +import { Bridge__factory } from "../ethers-contracts"; +import { CHAIN_ID_ETH, ETH_TOKEN_BRIDGE_ADDRESS } from "./consts"; + +export async function redeemOnEth( + provider: ethers.providers.Web3Provider | undefined, + signer: ethers.Signer | undefined, + signedVAA: Uint8Array +) { + console.log(provider, signer, signedVAA); + if (!provider || !signer) return; + console.log("completing transfer"); + const bridge = Bridge__factory.connect(ETH_TOKEN_BRIDGE_ADDRESS, signer); + const v = await bridge.completeTransfer(signedVAA); + const receipt = await v.wait(); + console.log(receipt); +} + +const redeemOn = { + [CHAIN_ID_ETH]: redeemOnEth, +}; + +export default redeemOn; diff --git a/bridge_ui/src/utils/transferFrom.ts b/bridge_ui/src/utils/transferFrom.ts index a7ff5fd99..abcd37da3 100644 --- a/bridge_ui/src/utils/transferFrom.ts +++ b/bridge_ui/src/utils/transferFrom.ts @@ -27,7 +27,7 @@ import { // TODO: allow for / handle cancellation? // TODO: overall better input checking and error handling -export function transferFromEth( +export async function transferFromEth( provider: ethers.providers.Web3Provider | undefined, signer: ethers.Signer | undefined, tokenAddress: string, @@ -41,65 +41,65 @@ export function transferFromEth( //TODO: don't hardcode, fetch decimals / share them with balance, how do we determine recipient chain? //TODO: more catches const amountParsed = parseUnits(amount, decimals); - (async () => { - const signerAddress = await signer.getAddress(); - console.log("Signer:", signerAddress); - console.log("Token:", tokenAddress); - const token = TokenImplementation__factory.connect(tokenAddress, signer); - const allowance = await token.allowance( - signerAddress, - ETH_TOKEN_BRIDGE_ADDRESS - ); - console.log("Allowance", allowance.toString()); //TODO: should we check that this is zero and warn if it isn't? - const transaction = await token.approve( - ETH_TOKEN_BRIDGE_ADDRESS, - amountParsed - ); - console.log(transaction); - const fee = 0; // for now, this won't do anything, we may add later - const nonceConst = Math.random() * 100000; - const nonceBuffer = Buffer.alloc(4); - nonceBuffer.writeUInt32LE(nonceConst, 0); - console.log("Initiating transfer"); - console.log("Amount:", formatUnits(amountParsed, decimals)); - console.log("To chain:", recipientChain); - console.log("To address:", recipientAddress); - console.log("Fees:", fee); - console.log("Nonce:", nonceBuffer); - const bridge = Bridge__factory.connect(ETH_TOKEN_BRIDGE_ADDRESS, signer); - const v = await bridge.transferTokens( - tokenAddress, - amountParsed, - recipientChain, - recipientAddress, - fee, - nonceBuffer - ); - const receipt = await v.wait(); - // TODO: dangerous!(?) - const bridgeLog = receipt.logs.filter((l) => { - console.log(l.address, ETH_BRIDGE_ADDRESS); - return l.address === ETH_BRIDGE_ADDRESS; - })[0]; - const { - args: { sender, sequence }, - } = Implementation__factory.createInterface().parseLog(bridgeLog); - console.log(sender, sequence); - const emitterAddress = Buffer.from( - zeroPad(arrayify(ETH_TOKEN_BRIDGE_ADDRESS), 32) - ).toString("hex"); - const { vaaBytes } = await getSignedVAA( - CHAIN_ID_ETH, - emitterAddress, - sequence - ); - console.log("SIGNED VAA:", vaaBytes); - })(); + const signerAddress = await signer.getAddress(); + console.log("Signer:", signerAddress); + console.log("Token:", tokenAddress); + const token = TokenImplementation__factory.connect(tokenAddress, signer); + const allowance = await token.allowance( + signerAddress, + ETH_TOKEN_BRIDGE_ADDRESS + ); + console.log("Allowance", allowance.toString()); //TODO: should we check that this is zero and warn if it isn't? + const transaction = await token.approve( + ETH_TOKEN_BRIDGE_ADDRESS, + amountParsed + ); + console.log(transaction); + const fee = 0; // for now, this won't do anything, we may add later + const nonceConst = Math.random() * 100000; + const nonceBuffer = Buffer.alloc(4); + nonceBuffer.writeUInt32LE(nonceConst, 0); + console.log("Initiating transfer"); + console.log("Amount:", formatUnits(amountParsed, decimals)); + console.log("To chain:", recipientChain); + console.log("To address:", recipientAddress); + console.log("Fees:", fee); + console.log("Nonce:", nonceBuffer); + const bridge = Bridge__factory.connect(ETH_TOKEN_BRIDGE_ADDRESS, signer); + const v = await bridge.transferTokens( + tokenAddress, + amountParsed, + recipientChain, + recipientAddress, + fee, + nonceBuffer + ); + const receipt = await v.wait(); + // TODO: log parsing should be part of a utility + // TODO: dangerous!(?) + const bridgeLog = receipt.logs.filter((l) => { + console.log(l.address, ETH_BRIDGE_ADDRESS); + return l.address === ETH_BRIDGE_ADDRESS; + })[0]; + const { + args: { sender, sequence }, + } = Implementation__factory.createInterface().parseLog(bridgeLog); + console.log(sender, sequence); + const emitterAddress = Buffer.from( + zeroPad(arrayify(ETH_TOKEN_BRIDGE_ADDRESS), 32) + ).toString("hex"); + const { vaaBytes } = await getSignedVAA( + CHAIN_ID_ETH, + emitterAddress, + sequence + ); + console.log("SIGNED VAA:", vaaBytes); + return vaaBytes; } // TODO: need to check transfer native vs transfer wrapped // TODO: switch out targetProvider for generic address (this likely involves getting these in their respective contexts) -export function transferFromSolana( +export async function transferFromSolana( wallet: Wallet | undefined, payerAddress: string | undefined, //TODO: we may not need this since we have wallet fromAddress: string | undefined, @@ -117,106 +117,102 @@ export function transferFromSolana( !targetAddressStr ) return; - (async () => { - const targetAddress = zeroPad(arrayify(targetAddressStr), 32); - const nonceConst = Math.random() * 100000; - const nonceBuffer = Buffer.alloc(4); - nonceBuffer.writeUInt32LE(nonceConst, 0); - const nonce = nonceBuffer.readUInt32LE(0); - const amountParsed = parseUnits(amount, decimals).toBigInt(); - const fee = BigInt(0); // for now, this won't do anything, we may add later - console.log("program:", SOL_TOKEN_BRIDGE_ADDRESS); - console.log("bridge:", SOL_BRIDGE_ADDRESS); - console.log("payer:", payerAddress); - console.log("from:", fromAddress); - console.log("token:", mintAddress); - console.log("nonce:", nonce); - console.log("amount:", amountParsed); - console.log("fee:", fee); - console.log("target:", targetAddressStr, targetAddress); - console.log("chain:", targetChain); - const bridge = await import("bridge"); - const feeAccount = await bridge.fee_collector_address(SOL_BRIDGE_ADDRESS); - const bridgeStatePK = new PublicKey( - bridge.state_address(SOL_BRIDGE_ADDRESS) - ); - // TODO: share connection in context? - const connection = new Connection(SOLANA_HOST, "confirmed"); - const bridgeStateAccountInfo = await connection.getAccountInfo( - bridgeStatePK - ); - if (bridgeStateAccountInfo?.data === undefined) { - throw new Error("bridge state not found"); - } - const bridgeState = bridge.parse_state( - new Uint8Array(bridgeStateAccountInfo?.data) - ); - const transferIx = SystemProgram.transfer({ - fromPubkey: new PublicKey(payerAddress), - toPubkey: new PublicKey(feeAccount), - lamports: bridgeState.config.fee, - }); - // TODO: pass in connection - // Add transfer instruction to transaction - const { transfer_native_ix, approval_authority_address, emitter_address } = - await import("token-bridge"); - const approvalIx = Token.createApproveInstruction( - TOKEN_PROGRAM_ID, - new PublicKey(fromAddress), - new PublicKey(approval_authority_address(SOL_TOKEN_BRIDGE_ADDRESS)), - new PublicKey(payerAddress), - [], - Number(amountParsed) - ); - const ix = ixFromRust( - transfer_native_ix( - SOL_TOKEN_BRIDGE_ADDRESS, - SOL_BRIDGE_ADDRESS, - payerAddress, - fromAddress, - mintAddress, - nonce, - amountParsed, - fee, - targetAddress, - targetChain - ) - ); - const transaction = new Transaction().add(transferIx, approvalIx, ix); - const { blockhash } = await connection.getRecentBlockhash(); - transaction.recentBlockhash = blockhash; - transaction.feePayer = new PublicKey(payerAddress); - // Sign transaction, broadcast, and confirm - const signed = await wallet.signTransaction(transaction); - console.log("SIGNED", signed); - const txid = await connection.sendRawTransaction(signed.serialize()); - console.log("SENT", txid); - const conf = await connection.confirmTransaction(txid); - console.log("CONFIRMED", conf); - const info = await connection.getTransaction(txid); - console.log("INFO", info); - // TODO: better parsing, safer - const SEQ_LOG = "Program log: Sequence: "; - const sequence = info?.meta?.logMessages - ?.filter((msg) => msg.startsWith(SEQ_LOG))[0] - .replace(SEQ_LOG, ""); - if (!sequence) { - throw new Error("sequence not found"); - } - console.log("SEQ", sequence); - const emitterAddress = Buffer.from( - zeroPad( - new PublicKey(emitter_address(SOL_TOKEN_BRIDGE_ADDRESS)).toBytes(), - 32 - ) - ).toString("hex"); - const { vaaBytes } = await getSignedVAA( - CHAIN_ID_SOLANA, - emitterAddress, - sequence - ); - console.log("SIGNED VAA:", vaaBytes); - })(); + const targetAddress = zeroPad(arrayify(targetAddressStr), 32); + const nonceConst = Math.random() * 100000; + const nonceBuffer = Buffer.alloc(4); + nonceBuffer.writeUInt32LE(nonceConst, 0); + const nonce = nonceBuffer.readUInt32LE(0); + const amountParsed = parseUnits(amount, decimals).toBigInt(); + const fee = BigInt(0); // for now, this won't do anything, we may add later + console.log("program:", SOL_TOKEN_BRIDGE_ADDRESS); + console.log("bridge:", SOL_BRIDGE_ADDRESS); + console.log("payer:", payerAddress); + console.log("from:", fromAddress); + console.log("token:", mintAddress); + console.log("nonce:", nonce); + console.log("amount:", amountParsed); + console.log("fee:", fee); + console.log("target:", targetAddressStr, targetAddress); + console.log("chain:", targetChain); + const bridge = await import("bridge"); + const feeAccount = await bridge.fee_collector_address(SOL_BRIDGE_ADDRESS); + const bridgeStatePK = new PublicKey(bridge.state_address(SOL_BRIDGE_ADDRESS)); + // TODO: share connection in context? + const connection = new Connection(SOLANA_HOST, "confirmed"); + const bridgeStateAccountInfo = await connection.getAccountInfo(bridgeStatePK); + if (bridgeStateAccountInfo?.data === undefined) { + throw new Error("bridge state not found"); + } + const bridgeState = bridge.parse_state( + new Uint8Array(bridgeStateAccountInfo?.data) + ); + const transferIx = SystemProgram.transfer({ + fromPubkey: new PublicKey(payerAddress), + toPubkey: new PublicKey(feeAccount), + lamports: bridgeState.config.fee, + }); + // TODO: pass in connection + // Add transfer instruction to transaction + const { transfer_native_ix, approval_authority_address, emitter_address } = + await import("token-bridge"); + const approvalIx = Token.createApproveInstruction( + TOKEN_PROGRAM_ID, + new PublicKey(fromAddress), + new PublicKey(approval_authority_address(SOL_TOKEN_BRIDGE_ADDRESS)), + new PublicKey(payerAddress), + [], + Number(amountParsed) + ); + const ix = ixFromRust( + transfer_native_ix( + SOL_TOKEN_BRIDGE_ADDRESS, + SOL_BRIDGE_ADDRESS, + payerAddress, + fromAddress, + mintAddress, + nonce, + amountParsed, + fee, + targetAddress, + targetChain + ) + ); + const transaction = new Transaction().add(transferIx, approvalIx, ix); + const { blockhash } = await connection.getRecentBlockhash(); + transaction.recentBlockhash = blockhash; + transaction.feePayer = new PublicKey(payerAddress); + // Sign transaction, broadcast, and confirm + const signed = await wallet.signTransaction(transaction); + console.log("SIGNED", signed); + const txid = await connection.sendRawTransaction(signed.serialize()); + console.log("SENT", txid); + const conf = await connection.confirmTransaction(txid); + console.log("CONFIRMED", conf); + const info = await connection.getTransaction(txid); + console.log("INFO", info); + // TODO: log parsing should be part of a utility + // TODO: better parsing, safer + const SEQ_LOG = "Program log: Sequence: "; + const sequence = info?.meta?.logMessages + ?.filter((msg) => msg.startsWith(SEQ_LOG))[0] + .replace(SEQ_LOG, ""); + if (!sequence) { + throw new Error("sequence not found"); + } + console.log("SEQ", sequence); + const emitterAddress = Buffer.from( + zeroPad( + new PublicKey(emitter_address(SOL_TOKEN_BRIDGE_ADDRESS)).toBytes(), + 32 + ) + ).toString("hex"); + const { vaaBytes } = await getSignedVAA( + CHAIN_ID_SOLANA, + emitterAddress, + sequence + ); + console.log("SIGNED VAA:", vaaBytes); + return vaaBytes; } const transferFrom = {