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() {
- 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)}
-
-
-
- Disconnect
-
- >
- ) : (
-
- Connect
-
- )}
+
{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)}
-
-
-
- Disconnect
-
- >
- ) : (
-
- Connect
-
- )}
- >
+
);
};
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 ? (
+
+
+ Disconnect {pk.substring(0, is0x ? 6 : 3)}...
+ {pk.substr(pk.length - (is0x ? 4 : 3))}
+
+
+ ) : (
+
+ Connect
+
+ );
+};
+
+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}
+
+ ))}
+
+
+
+
+
+ Next
+
+
+
+
+ dispatch(setStep(1))}>
+ Select a target
+
+
+
+ {CHAINS.map(({ id, name }) => (
+
+ {name}
+
+ ))}
+
+ {/* TODO: determine "to" token address */}
+
+
+ Next
+
+
+
+
+ dispatch(setStep(2))}>
+ Send tokens
+
+
+ {isWrapped ? (
+ <>
+
+ Transfer
+
+ {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"
+ : ""}
+
+ )}
+ >
+ ) : (
+ <>
+
+
+ Attest
+
+ {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 ? (
- <>
-
-
- Transfer
-
- {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
+
+
- Attest
+ Redeem
- {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 = {