bridge_ui: more skeleton, basic working eth xfer

Change-Id: I43e054fb1a39cb2434f272c18877aba107241cc5
This commit is contained in:
Evan Gray 2021-07-30 14:13:53 -04:00 committed by Hendrik Hofstadt
parent f5b6facec5
commit 591e68b5ee
5 changed files with 324 additions and 84 deletions

View File

@ -1,21 +1,6 @@
import {
AppBar,
Button,
Grid,
Link,
makeStyles,
MenuItem,
TextField,
Toolbar,
Typography,
} from "@material-ui/core";
import { useCallback } from "react";
import EthereumSignerKey from "./components/EthereumSignerKey";
import SolanaWalletKey from "./components/SolanaWalletKey";
import { useEthereumProvider } from "./contexts/EthereumProviderContext";
import { Bridge__factory } from "./ethers-contracts";
import { AppBar, Link, makeStyles, Toolbar } from "@material-ui/core";
import Transfer from "./components/Transfer";
import wormholeLogo from "./icons/wormhole.svg";
import { ETH_TOKEN_BRIDGE_ADDRESS } from "./utils/consts";
const useStyles = makeStyles((theme) => ({
appBar: {
@ -47,34 +32,10 @@ const useStyles = makeStyles((theme) => ({
content: {
margin: theme.spacing(10.5, 8),
},
transferBox: {
width: 540,
margin: "auto",
border: `.5px solid ${theme.palette.divider}`,
padding: theme.spacing(5.5, 12),
},
arrow: {
display: "flex",
alignItems: "center",
justifyContent: "center",
},
transferField: {
marginTop: theme.spacing(5),
},
transferButton: {
marginTop: theme.spacing(7.5),
textTransform: "none",
width: "100%",
},
}));
function App() {
const classes = useStyles();
const provider = useEthereumProvider();
const handleClick = useCallback(() => {
const bridge = Bridge__factory.connect(ETH_TOKEN_BRIDGE_ADDRESS, provider);
bridge.chainId().then((n) => console.log(n));
}, [provider]);
return (
<>
<AppBar position="static" color="inherit" className={classes.appBar}>
@ -88,48 +49,7 @@ function App() {
</AppBar>
<div className={classes.sideBar}></div>
<div className={classes.content}>
<div className={classes.transferBox}>
<Grid container>
<Grid item xs={4}>
<Typography>To</Typography>
<TextField select fullWidth value="ETH">
<MenuItem value="ETH">Ethereum</MenuItem>
<MenuItem value="SOL">Solana</MenuItem>
</TextField>
<EthereumSignerKey />
</Grid>
<Grid item xs={4} className={classes.arrow}>
&rarr;
</Grid>
<Grid item xs={4}>
<Typography>From</Typography>
<TextField select fullWidth value="SOL">
<MenuItem value="ETH">Ethereum</MenuItem>
<MenuItem value="SOL">Solana</MenuItem>
</TextField>
<SolanaWalletKey />
</Grid>
</Grid>
<TextField
placeholder="Asset"
fullWidth
className={classes.transferField}
/>
<TextField
placeholder="Amount"
type="number"
fullWidth
className={classes.transferField}
/>
<Button
color="primary"
variant="contained"
className={classes.transferButton}
onClick={handleClick}
>
Transfer
</Button>
</div>
<Transfer />
</div>
</>
);

View File

@ -0,0 +1,187 @@
import {
Button,
Grid,
makeStyles,
MenuItem,
TextField,
Typography,
} from "@material-ui/core";
import { useCallback, useState } from "react";
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
import useEthereumBalance from "../hooks/useEthereumBalance";
import {
ChainId,
CHAINS,
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
ETH_TEST_TOKEN_ADDRESS,
} from "../utils/consts";
import transferFrom from "../utils/transferFrom";
import EthereumSignerKey from "./EthereumSignerKey";
import SolanaWalletKey from "./SolanaWalletKey";
const useStyles = makeStyles((theme) => ({
transferBox: {
width: 540,
margin: "auto",
border: `.5px solid ${theme.palette.divider}`,
padding: theme.spacing(5.5, 12),
},
arrow: {
display: "flex",
alignItems: "center",
justifyContent: "center",
},
transferField: {
marginTop: theme.spacing(5),
},
transferButton: {
marginTop: theme.spacing(7.5),
textTransform: "none",
width: "100%",
},
}));
// TODO: loaders and such, navigation block?
// TODO: refresh displayed token amount after transfer somehow, could be resolved by having different components appear
// TODO: warn if amount exceeds balance
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 [amount, setAmount] = useState("");
const handleFromChange = useCallback(
(event) => {
setFromChain(event.target.value);
if (toChain === event.target.value) {
setToChain(fromChain);
}
},
[fromChain, toChain]
);
const handleToChange = useCallback(
(event) => {
setToChain(event.target.value);
if (fromChain === event.target.value) {
setFromChain(toChain);
}
},
[fromChain, toChain]
);
const handleAssetChange = useCallback((event) => {
setAssetAddress(event.target.value);
}, []);
const handleAmountChange = useCallback((event) => {
setAmount(event.target.value);
}, []);
const provider = useEthereumProvider();
const ethBalance = useEthereumBalance(assetAddress, provider);
const { wallet } = useSolanaWallet();
const solPK = wallet?.publicKey?.toBytes();
// TODO: dynamically get "to" wallet
const handleClick = useCallback(() => {
if (transferFrom[fromChain]) {
transferFrom[fromChain](provider, assetAddress, amount, toChain, solPK);
}
}, [fromChain, provider, solPK, assetAddress, amount, toChain]);
// update this as we develop, just setting expectations with the button state
const isTransferImplemented = !!transferFrom[fromChain];
const isProviderConnected = !!provider;
const isRecipientAvailable = !!solPK;
const isAddressDefined = !!assetAddress;
const isAmountPositive = Number(amount) > 0; // TODO: this needs per-chain, bn parsing
const isBalanceAtLeastAmount = Number(ethBalance) >= Number(amount); // TODO: ditto
const canAttemptTransfer =
isTransferImplemented &&
isProviderConnected &&
isRecipientAvailable &&
isAddressDefined &&
isAmountPositive &&
isBalanceAtLeastAmount;
return (
<div className={classes.transferBox}>
<Grid container>
<Grid item xs={4}>
<Typography>To</Typography>
<TextField
select
fullWidth
value={fromChain}
onChange={handleFromChange}
>
{CHAINS.map(({ id, name }) => (
<MenuItem key={id} value={id}>
{name}
</MenuItem>
))}
</TextField>
<EthereumSignerKey />
<Typography>{ethBalance}</Typography>
</Grid>
<Grid item xs={4} className={classes.arrow}>
&rarr;
</Grid>
<Grid item xs={4}>
<Typography>From</Typography>
<TextField select fullWidth value={toChain} onChange={handleToChange}>
{CHAINS.map(({ id, name }) => (
<MenuItem key={id} value={id}>
{name}
</MenuItem>
))}
</TextField>
<SolanaWalletKey />
</Grid>
</Grid>
<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
color="primary"
variant="contained"
className={classes.transferButton}
onClick={handleClick}
disabled={!canAttemptTransfer}
>
Transfer
</Button>
{canAttemptTransfer ? null : (
<Typography variant="body2" color="error">
{!isTransferImplemented
? `Transfer is not yet implemented for ${CHAINS[fromChain]}`
: !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"
: !isBalanceAtLeastAmount
? "The amount may not be greater than the balance"
: ""}
</Typography>
)}
</div>
);
}
export default Transfer;

View File

@ -0,0 +1,37 @@
import { ethers } from "ethers";
import { formatUnits } from "ethers/lib/utils";
import { useEffect, useState } from "react";
import { TokenImplementation__factory } from "../ethers-contracts";
function useEthereumBalance(address: string, provider?: ethers.providers.Web3Provider) {
//TODO: should this check allowance too or subtract allowance?
const [balance, setBalance] = useState<string>('')
useEffect(()=>{
if (!address || !provider) {
setBalance('')
return
}
let cancelled = false
const token = TokenImplementation__factory.connect(address, provider);
token.decimals().then((decimals) => {
console.log(decimals);
provider
?.getSigner()
.getAddress()
.then((pk) => {
console.log(pk)
token.balanceOf(pk).then((n) => {
if (!cancelled) {
setBalance(formatUnits(n,decimals))
}
});
});
});
return () => {
cancelled = true
}
},[address, provider])
return balance
}
export default useEthereumBalance

View File

@ -1,2 +1,32 @@
export type ChainId = 1 | 2 | 3 | 4
export const CHAIN_ID_SOLANA: ChainId = 1
export const CHAIN_ID_ETH: ChainId = 2
export const CHAIN_ID_TERRA: ChainId = 3
export const CHAIN_ID_BSC: ChainId = 4
export interface ChainInfo {
id: ChainId
name: string
}
export const CHAINS = [
{
id: CHAIN_ID_BSC,
name: 'Binance Smart Chain'
},
{
id: CHAIN_ID_ETH,
name: 'Ethereum'
},
{
id: CHAIN_ID_SOLANA,
name: 'Solana'
},
{
id: CHAIN_ID_TERRA,
name: 'Terra'
},
]
export const SOLANA_HOST = 'http://localhost:8899'
export const ETH_TOKEN_BRIDGE_ADDRESS = "0x254dffcd3277c0b1660f6d42efbb754edababc2b"
export const ETH_TEST_TOKEN_ADDRESS = "0x0290FB167208Af455bB137780163b7B7a9a10C16"
export const ETH_TOKEN_BRIDGE_ADDRESS = "0xe982e462b094850f12af94d21d470e21be9d0e9c"
export const SOL_TEST_TOKEN_ADDRESS = "2WDq7wSs9zYrpx2kbHDA4RUTRch2CCTP6ZWaH4GNfnQQ"
export const SOL_TOKEN_BRIDGE_ADDRESS = "B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE"

View File

@ -0,0 +1,66 @@
import { ethers } from "ethers";
import { formatUnits, parseUnits } from "ethers/lib/utils";
import { Bridge__factory, TokenImplementation__factory } from "../ethers-contracts";
import { ChainId, CHAIN_ID_ETH, ETH_TOKEN_BRIDGE_ADDRESS } from "./consts";
// TODO: this should probably be extended from the context somehow so that the signatures match
// TODO: allow for / handle cancellation?
// TODO: overall better input checking and error handling
function transferFromEth(provider: ethers.providers.Web3Provider | undefined, tokenAddress: string, amount: string, recipientChain: ChainId, recipientAddress: Uint8Array | undefined) {
if (!provider || !recipientAddress) return;
const signer = provider.getSigner();
if (!signer) return;
//TODO: check if token attestation exists on the target chain
//TODO: don't hardcode, fetch decimals / share them with balance, how do we determine recipient chain?
//TODO: more catches
const amountParsed = parseUnits(amount, 18);
signer.getAddress().then((signerAddress) => {
console.log("Signer:", signerAddress);
console.log("Token:", tokenAddress)
const token = TokenImplementation__factory.connect(
tokenAddress,
signer
);
token
.allowance(signerAddress, ETH_TOKEN_BRIDGE_ADDRESS)
.then((allowance) => {
console.log("Allowance", allowance.toString()); //TODO: should we check that this is zero and warn if it isn't?
token
.approve(ETH_TOKEN_BRIDGE_ADDRESS, amountParsed)
.then((transaction) => {
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, 18));
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
);
bridge
.transferTokens(
tokenAddress,
amountParsed,
recipientChain,
recipientAddress,
fee,
nonceBuffer
)
.then((v) => console.log("Success:", v))
.catch((r) => console.error(r)); //TODO: integrate toast messages
});
});
});
}
const transferFrom = {
[CHAIN_ID_ETH]: transferFromEth
}
export default transferFrom