bridge_ui: more skeleton, basic working eth xfer
Change-Id: I43e054fb1a39cb2434f272c18877aba107241cc5
This commit is contained in:
parent
f5b6facec5
commit
591e68b5ee
|
@ -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}>
|
||||
→
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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}>
|
||||
→
|
||||
</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;
|
|
@ -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
|
|
@ -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"
|
|
@ -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
|
Loading…
Reference in New Issue