bridge_ui: store, stepper, eth redeem

Change-Id: I0afddb5b066f1454d1c7b07bbdf81642b9216207
This commit is contained in:
Evan Gray 2021-08-07 02:38:02 -04:00 committed by Hendrik Hofstadt
parent b035ebc438
commit b4ca77497a
16 changed files with 932 additions and 531 deletions

View File

@ -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",

View File

@ -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": {

View File

@ -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() {
<Toolbar>
<img src={wormholeLogo} alt="Wormhole Logo" />
<div className={classes.spacer} />
<Link className={classes.link}>Placeholder</Link>
<Link className={classes.link}>Placeholder</Link>
<Link className={classes.link}>Placeholder</Link>
</Toolbar>
</AppBar>
<div className={classes.sideBar}></div>
<div className={classes.content}>
<Transfer />
</div>

View File

@ -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 ? (
<>
<Tooltip title={signerAddress}>
<Typography>
{signerAddress.substring(0, 6)}...
{signerAddress.substr(signerAddress.length - 4)}
</Typography>
</Tooltip>
<Button
color="secondary"
variant="contained"
size="small"
onClick={disconnect}
style={{ width: "100%", textTransform: "none" }}
>
Disconnect
</Button>
</>
) : (
<Button
color="primary"
variant="contained"
size="small"
onClick={connect}
style={{ width: "100%", textTransform: "none" }}
>
Connect
</Button>
)}
<ToggleConnectedButton
connect={connect}
disconnect={disconnect}
connected={!!signerAddress}
pk={signerAddress || ""}
/>
{providerError ? (
<Typography variant="body2" color="error">
{providerError}

View File

@ -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 ? (
<>
<Tooltip title={pk}>
<Typography>
{pk.substring(0, 3)}...{pk.substr(pk.length - 3)}
</Typography>
</Tooltip>
<Button
color="secondary"
variant="contained"
size="small"
onClick={disconnect}
style={{ width: "100%", textTransform: "none" }}
>
Disconnect
</Button>
</>
) : (
<Button
color="primary"
variant="contained"
size="small"
onClick={connect}
style={{ width: "100%", textTransform: "none" }}
>
Connect
</Button>
)}
</>
<ToggleConnectedButton
connect={connect}
disconnect={disconnect}
connected={connected}
pk={pk}
/>
);
};

View File

@ -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 ? (
<Tooltip title={pk}>
<Button
color="secondary"
variant="contained"
size="small"
onClick={disconnect}
className={classes.button}
>
Disconnect {pk.substring(0, is0x ? 6 : 3)}...
{pk.substr(pk.length - (is0x ? 4 : 3))}
</Button>
</Tooltip>
) : (
<Button
color="primary"
variant="contained"
size="small"
onClick={connect}
className={classes.button}
>
Connect
</Button>
);
};
export default ToggleConnectedButton;

View File

@ -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<ChainId>(CHAIN_ID_ETH);
const [toChain, setToChain] = useState<ChainId>(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 (
<div className={classes.transferBox}>
<Grid container>
<Grid item xs={4}>
<Typography>From</Typography>
<TextField
select
fullWidth
value={fromChain}
onChange={handleFromChange}
<Container maxWidth="md">
<Stepper activeStep={activeStep} orientation="vertical">
<Step>
<StepButton onClick={() => dispatch(setStep(0))}>
Select a source
</StepButton>
<StepContent>
<TextField
select
fullWidth
value={fromChain}
onChange={handleFromChange}
>
{CHAINS.map(({ id, name }) => (
<MenuItem key={id} value={id}>
{name}
</MenuItem>
))}
</TextField>
<KeyAndBalance chainId={fromChain} tokenAddress={assetAddress} />
<TextField
placeholder="Asset"
fullWidth
className={classes.transferField}
value={assetAddress}
onChange={handleAssetChange}
/>
<TextField
placeholder="Amount"
type="number"
fullWidth
className={classes.transferField}
value={amount}
onChange={handleAmountChange}
/>
<Button
onClick={handleNextClick}
variant="contained"
color="primary"
>
Next
</Button>
</StepContent>
</Step>
<Step>
<StepButton onClick={() => dispatch(setStep(1))}>
Select a target
</StepButton>
<StepContent>
<TextField
select
fullWidth
value={toChain}
onChange={handleToChange}
>
{CHAINS.map(({ id, name }) => (
<MenuItem key={id} value={id}>
{name}
</MenuItem>
))}
</TextField>
{/* TODO: determine "to" token address */}
<KeyAndBalance chainId={toChain} />
<Button
onClick={handleNextClick}
variant="contained"
color="primary"
>
Next
</Button>
</StepContent>
</Step>
<Step>
<StepButton onClick={() => dispatch(setStep(2))}>
Send tokens
</StepButton>
<StepContent>
{isWrapped ? (
<>
<Button
color="primary"
variant="contained"
className={classes.transferButton}
onClick={handleTransferClick}
disabled={!canAttemptTransfer}
>
Transfer
</Button>
{canAttemptTransfer ? null : (
<Typography variant="body2" color="error">
{!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"
: ""}
</Typography>
)}
</>
) : (
<>
<div style={{ position: "relative" }}>
<Button
color="primary"
variant="contained"
disabled={isCheckingWrapped || !canAttemptAttest}
onClick={handleAttestClick}
className={classes.transferButton}
>
Attest
</Button>
{isCheckingWrapped ? (
<CircularProgress
size={24}
color="inherit"
style={{
position: "absolute",
bottom: 0,
left: "50%",
marginLeft: -12,
marginBottom: 6,
}}
/>
) : null}
</div>
{isCheckingWrapped ? null : canAttemptAttest ? (
<Typography variant="body2">
<br />
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.
</Typography>
) : (
<Typography variant="body2" color="error">
{!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"
: ""}
</Typography>
)}
</>
)}
</StepContent>
</Step>
<Step>
<StepButton
onClick={() => dispatch(setStep(3))}
disabled={!signedVAA}
>
{CHAINS.map(({ id, name }) => (
<MenuItem key={id} value={id}>
{name}
</MenuItem>
))}
</TextField>
<KeyAndBalance chainId={fromChain} tokenAddress={assetAddress} />
</Grid>
<Grid item xs={4} className={classes.arrow}>
&rarr;
</Grid>
<Grid item xs={4}>
<Typography>To</Typography>
<TextField select fullWidth value={toChain} onChange={handleToChange}>
{CHAINS.map(({ id, name }) => (
<MenuItem key={id} value={id}>
{name}
</MenuItem>
))}
</TextField>
{/* TODO: determine "to" token address */}
<KeyAndBalance chainId={toChain} />
</Grid>
</Grid>
<TextField
placeholder="Asset"
fullWidth
className={classes.transferField}
value={assetAddress}
onChange={handleAssetChange}
/>
{isWrapped ? (
<>
<TextField
placeholder="Amount"
type="number"
fullWidth
className={classes.transferField}
value={amount}
onChange={handleAmountChange}
/>
<Button
color="primary"
variant="contained"
className={classes.transferButton}
onClick={handleTransferClick}
disabled={!canAttemptTransfer}
>
Transfer
</Button>
{canAttemptTransfer ? null : (
<Typography variant="body2" color="error">
{!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"
: ""}
</Typography>
)}
</>
) : (
<>
<div style={{ position: "relative" }}>
Redeem tokens
</StepButton>
<StepContent>
<Button
color="primary"
variant="contained"
disabled={isCheckingWrapped || !canAttemptAttest}
onClick={handleAttestClick}
className={classes.transferButton}
onClick={handleRedeemClick}
>
Attest
Redeem
</Button>
{isCheckingWrapped ? (
<CircularProgress
size={24}
color="inherit"
style={{
position: "absolute",
bottom: 0,
left: "50%",
marginLeft: -12,
marginBottom: 6,
}}
/>
) : null}
</div>
{isCheckingWrapped ? null : canAttemptAttest ? (
<Typography variant="body2">
<br />
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.
</Typography>
) : (
<Typography variant="body2" color="error">
{!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"
: ""}
</Typography>
)}
</>
)}
</div>
</StepContent>
</Step>
</Stepper>
</Container>
);
}

View File

@ -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 });

View File

@ -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(
<ThemeProvider theme={theme}>
<CssBaseline />
<SolanaWalletProvider>
<EthereumProviderProvider>
<App />
</EthereumProviderProvider>
</SolanaWalletProvider>
</ThemeProvider>,
<Provider store={store}>
<ThemeProvider theme={theme}>
<CssBaseline />
<SolanaWalletProvider>
<EthereumProviderProvider>
<App />
</EthereumProviderProvider>
</SolanaWalletProvider>
</ThemeProvider>
</Provider>,
document.getElementById("root")
);

View File

@ -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",
},
},
},

View File

@ -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<typeof store.getState>;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch;

View File

@ -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

View File

@ -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<Steps>) => {
state.activeStep = action.payload;
},
setSourceChain: (state, action: PayloadAction<ChainId>) => {
state.sourceChain = action.payload;
},
setTargetChain: (state, action: PayloadAction<ChainId>) => {
state.targetChain = action.payload;
},
setSignedVAA: (state, action: PayloadAction<Uint8Array>) => {
state.signedVAA = action.payload; //TODO: serialize
state.activeStep = 3;
},
},
});
export const {
incrementStep,
decrementStep,
setStep,
setSourceChain,
setTargetChain,
setSignedVAA,
} = transferSlice.actions;
export default transferSlice.reducer;

View File

@ -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 = {

View File

@ -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;

View File

@ -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 = {