bridge_ui: cleanup

Change-Id: Ibf9b6719fe31cd500a187c289357f493fbc177db
This commit is contained in:
Evan Gray 2021-08-08 17:40:07 -04:00 committed by Hendrik Hofstadt
parent 5a6cfe034f
commit 340899bbdc
10 changed files with 589 additions and 458 deletions

View File

@ -1,451 +0,0 @@
import {
Button,
CircularProgress,
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 {
CHAINS,
CHAINS_BY_ID,
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
ETH_TEST_TOKEN_ADDRESS,
SOL_TEST_TOKEN_ADDRESS,
} from "../utils/consts";
import redeemOn, { redeemOnEth } from "../utils/redeemOn";
import transferFrom, {
transferFromEth,
transferFromSolana,
} from "../utils/transferFrom";
import KeyAndBalance from "./KeyAndBalance";
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(2),
textTransform: "none",
width: "100%",
},
}));
// TODO: ensure that both wallets are connected to the same known network
// 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();
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) => {
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);
}
if (event.target.value === CHAIN_ID_SOLANA) {
setAssetAddress(SOL_TEST_TOKEN_ADDRESS);
}
if (toChain === event.target.value) {
dispatch(setTargetChain(fromChain));
}
},
[dispatch, fromChain, toChain]
);
const handleToChange = useCallback(
(event) => {
dispatch(setTargetChain(event.target.value));
if (fromChain === event.target.value) {
dispatch(setSourceChain(toChain));
// TODO: remove or check env - for testing purposes
if (toChain === CHAIN_ID_ETH) {
setAssetAddress(ETH_TEST_TOKEN_ADDRESS);
}
if (toChain === CHAIN_ID_SOLANA) {
setAssetAddress(SOL_TEST_TOKEN_ADDRESS);
}
}
},
[dispatch, fromChain, toChain]
);
const handleAssetChange = useCallback((event) => {
setAssetAddress(event.target.value);
}, []);
const handleAmountChange = useCallback((event) => {
setAmount(event.target.value);
}, []);
const { provider, signer, signerAddress } = useEthereumProvider();
const { decimals: ethDecimals, uiAmountString: ethBalance } =
useEthereumBalance(
assetAddress,
signerAddress,
provider,
fromChain === CHAIN_ID_ETH
);
const { wallet } = useSolanaWallet();
const solPK = wallet?.publicKey;
const {
tokenAccount: solTokenPK,
decimals: solDecimals,
uiAmount: solBalance,
} = useSolanaBalance(assetAddress, solPK, fromChain === CHAIN_ID_SOLANA);
const {
isLoading: isCheckingWrapped,
// 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
if (attestFrom[fromChain]) {
if (
fromChain === CHAIN_ID_ETH &&
attestFrom[fromChain] === attestFromEth
) {
//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
) {
//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]);
// TODO: dynamically get "to" wallet
const handleTransferClick = useCallback(() => {
// TODO: more generic way of calling these
if (transferFrom[fromChain]) {
if (
fromChain === CHAIN_ID_ETH &&
transferFrom[fromChain] === transferFromEth
) {
//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
) {
//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,
signerAddress,
wallet,
solPK,
solTokenPK,
assetAddress,
amount,
ethDecimals,
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];
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 = balance >= Number(amount); // TODO: ditto
const canAttemptAttest =
isAttestImplemented &&
isProviderConnected &&
isRecipientAvailable &&
isAddressDefined;
const canAttemptTransfer =
isTransferImplemented &&
isProviderConnected &&
isRecipientAvailable &&
isAddressDefined &&
isAmountPositive &&
isBalanceAtLeastAmount;
const handleNextClick = useCallback(() => {
dispatch(incrementStep());
}, [dispatch]);
return (
<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}
>
Redeem tokens
</StepButton>
<StepContent>
<Button
color="primary"
variant="contained"
className={classes.transferButton}
onClick={handleRedeemClick}
>
Redeem
</Button>
</StepContent>
</Step>
</Stepper>
</Container>
);
}
export default Transfer;

View File

@ -0,0 +1,44 @@
import { Button, makeStyles } from "@material-ui/core";
import { useCallback } from "react";
import { useSelector } from "react-redux";
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
import useTransferSignedVAA from "../../hooks/useTransferSignedVAA";
import { selectTargetChain } from "../../store/selectors";
import { CHAIN_ID_ETH } from "../../utils/consts";
import redeemOn, { redeemOnEth } from "../../utils/redeemOn";
const useStyles = makeStyles((theme) => ({
transferButton: {
marginTop: theme.spacing(2),
textTransform: "none",
width: "100%",
},
}));
function Redeem() {
const classes = useStyles();
const targetChain = useSelector(selectTargetChain);
const { provider, signer } = useEthereumProvider();
const signedVAA = useTransferSignedVAA();
const handleRedeemClick = useCallback(() => {
if (
targetChain === CHAIN_ID_ETH &&
redeemOn[targetChain] === redeemOnEth &&
signedVAA
) {
redeemOnEth(provider, signer, signedVAA);
}
}, [targetChain, provider, signer, signedVAA]);
return (
<Button
color="primary"
variant="contained"
className={classes.transferButton}
onClick={handleRedeemClick}
>
Redeem
</Button>
);
}
export default Redeem;

View File

@ -0,0 +1,265 @@
import {
Button,
CircularProgress,
makeStyles,
Typography,
} from "@material-ui/core";
import { useCallback } 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 {
selectAmount,
selectSourceAsset,
selectSourceChain,
selectTargetChain,
} from "../../store/selectors";
import { setSignedVAAHex } from "../../store/transferSlice";
import { uint8ArrayToHex } from "../../utils/array";
import attestFrom, {
attestFromEth,
attestFromSolana,
} from "../../utils/attestFrom";
import {
CHAINS_BY_ID,
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
} from "../../utils/consts";
import transferFrom, {
transferFromEth,
transferFromSolana,
} from "../../utils/transferFrom";
const useStyles = makeStyles((theme) => ({
transferButton: {
marginTop: theme.spacing(2),
textTransform: "none",
width: "100%",
},
}));
// TODO: move attest to its own workflow
function Send() {
const classes = useStyles();
const dispatch = useDispatch();
const sourceChain = useSelector(selectSourceChain);
const sourceAsset = useSelector(selectSourceAsset);
const amount = useSelector(selectAmount);
const targetChain = useSelector(selectTargetChain);
const { provider, signer, signerAddress } = useEthereumProvider();
const { decimals: ethDecimals, uiAmountString: ethBalance } =
useEthereumBalance(
sourceAsset,
signerAddress,
provider,
sourceChain === CHAIN_ID_ETH
);
const { wallet } = useSolanaWallet();
const solPK = wallet?.publicKey;
const {
tokenAccount: solTokenPK,
decimals: solDecimals,
uiAmount: solBalance,
} = useSolanaBalance(sourceAsset, solPK, sourceChain === CHAIN_ID_SOLANA);
const {
isLoading: isCheckingWrapped,
// isWrapped,
wrappedAsset,
} = useWrappedAsset(targetChain, sourceChain, sourceAsset, provider);
// TODO: check this and send to separate flow
const isWrapped = true;
console.log(isCheckingWrapped, isWrapped, wrappedAsset);
const handleAttestClick = useCallback(() => {
// TODO: more generic way of calling these
if (attestFrom[sourceChain]) {
if (
sourceChain === CHAIN_ID_ETH &&
attestFrom[sourceChain] === attestFromEth
) {
//TODO: just for testing, this should eventually use the store to communicate between steps
(async () => {
const vaaBytes = await attestFromEth(provider, signer, sourceAsset);
console.log("bytes in transfer", vaaBytes);
})();
}
if (
sourceChain === CHAIN_ID_SOLANA &&
attestFrom[sourceChain] === attestFromSolana
) {
//TODO: just for testing, this should eventually use the store to communicate between steps
(async () => {
const vaaBytes = await attestFromSolana(
wallet,
solPK?.toString(),
sourceAsset,
solDecimals
);
console.log("bytes in transfer", vaaBytes);
})();
}
}
}, [sourceChain, provider, signer, wallet, solPK, sourceAsset, solDecimals]);
// TODO: dynamically get "to" wallet
const handleTransferClick = useCallback(() => {
// TODO: more generic way of calling these
if (transferFrom[sourceChain]) {
if (
sourceChain === CHAIN_ID_ETH &&
transferFrom[sourceChain] === transferFromEth
) {
//TODO: just for testing, this should eventually use the store to communicate between steps
(async () => {
const vaaBytes = await transferFromEth(
provider,
signer,
sourceAsset,
ethDecimals,
amount,
targetChain,
solPK?.toBytes()
);
console.log("bytes in transfer", vaaBytes);
vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
})();
}
if (
sourceChain === CHAIN_ID_SOLANA &&
transferFrom[sourceChain] === transferFromSolana
) {
//TODO: just for testing, this should eventually use the store to communicate between steps
(async () => {
const vaaBytes = await transferFromSolana(
wallet,
solPK?.toString(),
solTokenPK?.toString(),
sourceAsset,
amount,
solDecimals,
signerAddress,
targetChain
);
console.log("bytes in transfer", vaaBytes);
vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
})();
}
}
}, [
dispatch,
sourceChain,
provider,
signer,
signerAddress,
wallet,
solPK,
solTokenPK,
sourceAsset,
amount,
ethDecimals,
solDecimals,
targetChain,
]);
// update this as we develop, just setting expectations with the button state
const balance = Number(ethBalance) || solBalance;
const isAttestImplemented = !!attestFrom[sourceChain];
const isTransferImplemented = !!transferFrom[sourceChain];
const isProviderConnected = !!provider;
const isRecipientAvailable = !!solPK;
const isAddressDefined = !!sourceAsset;
const isAmountPositive = Number(amount) > 0; // TODO: this needs per-chain, bn parsing
const isBalanceAtLeastAmount = balance >= Number(amount); // TODO: ditto
const canAttemptAttest =
isAttestImplemented &&
isProviderConnected &&
isRecipientAvailable &&
isAddressDefined;
const canAttemptTransfer =
isTransferImplemented &&
isProviderConnected &&
isRecipientAvailable &&
isAddressDefined &&
isAmountPositive &&
isBalanceAtLeastAmount;
return 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[sourceChain].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[targetChain].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[sourceChain].name}`
: !isProviderConnected
? "The source wallet is not connected"
: !isRecipientAvailable
? "The receiving wallet is not connected"
: !isAddressDefined
? "Please provide an asset address"
: ""}
</Typography>
)}
</>
);
}
export default Send;

View File

@ -0,0 +1,91 @@
import { Button, makeStyles, MenuItem, TextField } from "@material-ui/core";
import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
selectAmount,
selectSourceAsset,
selectSourceChain,
} from "../../store/selectors";
import {
incrementStep,
setAmount,
setSourceAsset,
setSourceChain,
} from "../../store/transferSlice";
import { CHAINS } from "../../utils/consts";
import KeyAndBalance from "../KeyAndBalance";
const useStyles = makeStyles((theme) => ({
transferField: {
marginTop: theme.spacing(5),
},
}));
function Source() {
const classes = useStyles();
const dispatch = useDispatch();
const sourceChain = useSelector(selectSourceChain);
const sourceAsset = useSelector(selectSourceAsset);
const amount = useSelector(selectAmount);
const handleSourceChange = useCallback(
(event) => {
dispatch(setSourceChain(event.target.value));
},
[dispatch]
);
const handleAssetChange = useCallback(
(event) => {
dispatch(setSourceAsset(event.target.value));
},
[dispatch]
);
const handleAmountChange = useCallback(
(event) => {
dispatch(setAmount(event.target.value));
},
[dispatch]
);
const handleNextClick = useCallback(
(event) => {
dispatch(incrementStep());
},
[dispatch]
);
return (
<>
<TextField
select
fullWidth
value={sourceChain}
onChange={handleSourceChange}
>
{CHAINS.map(({ id, name }) => (
<MenuItem key={id} value={id}>
{name}
</MenuItem>
))}
</TextField>
<KeyAndBalance chainId={sourceChain} tokenAddress={sourceAsset} />
<TextField
placeholder="Asset"
fullWidth
className={classes.transferField}
value={sourceAsset}
onChange={handleAssetChange}
/>
<TextField
placeholder="Amount"
type="number"
fullWidth
className={classes.transferField}
value={amount}
onChange={handleAmountChange}
/>
<Button onClick={handleNextClick} variant="contained" color="primary">
Next
</Button>
</>
);
}
export default Source;

View File

@ -0,0 +1,52 @@
import { Button, MenuItem, TextField } from "@material-ui/core";
import { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { selectSourceChain, selectTargetChain } from "../../store/selectors";
import { incrementStep, setTargetChain } from "../../store/transferSlice";
import { CHAINS } from "../../utils/consts";
import KeyAndBalance from "../KeyAndBalance";
function Target() {
const dispatch = useDispatch();
const sourceChain = useSelector(selectSourceChain);
const chains = useMemo(
() => CHAINS.filter((c) => c.id !== sourceChain),
[sourceChain]
);
const targetChain = useSelector(selectTargetChain);
const handleTargetChange = useCallback(
(event) => {
dispatch(setTargetChain(event.target.value));
},
[dispatch]
);
const handleNextClick = useCallback(
(event) => {
dispatch(incrementStep());
},
[dispatch]
);
return (
<>
<TextField
select
fullWidth
value={targetChain}
onChange={handleTargetChange}
>
{chains.map(({ id, name }) => (
<MenuItem key={id} value={id}>
{name}
</MenuItem>
))}
</TextField>
{/* TODO: determine "to" token address */}
<KeyAndBalance chainId={targetChain} />
<Button onClick={handleNextClick} variant="contained" color="primary">
Next
</Button>
</>
);
}
export default Target;

View File

@ -0,0 +1,68 @@
import {
Container,
Step,
StepButton,
StepContent,
Stepper,
} from "@material-ui/core";
import { useDispatch, useSelector } from "react-redux";
import { selectActiveStep, selectSignedVAAHex } from "../../store/selectors";
import { setStep } from "../../store/transferSlice";
import Redeem from "./Redeem";
import Send from "./Send";
import Source from "./Source";
import Target from "./Target";
// TODO: ensure that both wallets are connected to the same known network
// 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 dispatch = useDispatch();
const activeStep = useSelector(selectActiveStep);
const signedVAAHex = useSelector(selectSignedVAAHex);
return (
<Container maxWidth="md">
<Stepper activeStep={activeStep} orientation="vertical">
<Step>
<StepButton onClick={() => dispatch(setStep(0))}>
Select a source
</StepButton>
<StepContent>
<Source />
</StepContent>
</Step>
<Step>
<StepButton onClick={() => dispatch(setStep(1))}>
Select a target
</StepButton>
<StepContent>
<Target />
</StepContent>
</Step>
<Step>
<StepButton onClick={() => dispatch(setStep(2))}>
Send tokens
</StepButton>
<StepContent>
<Send />
</StepContent>
</Step>
<Step>
<StepButton
onClick={() => dispatch(setStep(3))}
disabled={!signedVAAHex}
>
Redeem tokens
</StepButton>
<StepContent>
<Redeem />
</StepContent>
</Step>
</Stepper>
</Container>
);
}
export default Transfer;

View File

@ -0,0 +1,13 @@
import { useMemo } from "react";
import { useSelector } from "react-redux";
import { selectSignedVAAHex } from "../store/selectors";
import { hexToUint8Array } from "../utils/array";
export default function useTransferSignedVAA() {
const signedVAAHex = useSelector(selectSignedVAAHex);
const signedVAA = useMemo(
() => (signedVAAHex ? hexToUint8Array(signedVAAHex) : undefined),
[signedVAAHex]
);
return signedVAA;
}

View File

@ -3,6 +3,10 @@ import { RootState } from ".";
export const selectActiveStep = (state: RootState) => state.transfer.activeStep;
export const selectSourceChain = (state: RootState) =>
state.transfer.sourceChain;
export const selectSourceAsset = (state: RootState) =>
state.transfer.sourceAsset;
export const selectAmount = (state: RootState) => state.transfer.amount;
export const selectTargetChain = (state: RootState) =>
state.transfer.targetChain;
export const selectSignedVAA = (state: RootState) => state.transfer.signedVAA; //TODO: deserialize
export const selectSignedVAAHex = (state: RootState) =>
state.transfer.signedVAAHex;

View File

@ -1,5 +1,11 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { ChainId, CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "../utils/consts";
import {
ChainId,
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
ETH_TEST_TOKEN_ADDRESS,
SOL_TEST_TOKEN_ADDRESS,
} from "../utils/consts";
const LAST_STEP = 3;
@ -8,15 +14,19 @@ type Steps = 0 | 1 | 2 | 3;
export interface TransferState {
activeStep: Steps;
sourceChain: ChainId;
sourceAsset: string;
amount: string;
targetChain: ChainId;
signedVAA: Uint8Array | undefined;
signedVAAHex: string | undefined;
}
const initialState: TransferState = {
activeStep: 0,
sourceChain: CHAIN_ID_SOLANA,
sourceAsset: SOL_TEST_TOKEN_ADDRESS,
amount: "",
targetChain: CHAIN_ID_ETH,
signedVAA: undefined,
signedVAAHex: undefined,
};
export const transferSlice = createSlice({
@ -33,13 +43,42 @@ export const transferSlice = createSlice({
state.activeStep = action.payload;
},
setSourceChain: (state, action: PayloadAction<ChainId>) => {
const prevSourceChain = state.sourceChain;
state.sourceChain = action.payload;
// TODO: remove or check env - for testing purposes
if (action.payload === CHAIN_ID_ETH) {
state.sourceAsset = ETH_TEST_TOKEN_ADDRESS;
}
if (action.payload === CHAIN_ID_SOLANA) {
state.sourceAsset = SOL_TEST_TOKEN_ADDRESS;
}
if (state.targetChain === action.payload) {
state.targetChain = prevSourceChain;
}
},
setSourceAsset: (state, action: PayloadAction<string>) => {
state.sourceAsset = action.payload;
},
setAmount: (state, action: PayloadAction<string>) => {
state.amount = action.payload;
},
setTargetChain: (state, action: PayloadAction<ChainId>) => {
const prevTargetChain = state.targetChain;
state.targetChain = action.payload;
if (state.sourceChain === action.payload) {
state.sourceChain = prevTargetChain;
state.activeStep = 0;
// TODO: remove or check env - for testing purposes
if (state.targetChain === CHAIN_ID_ETH) {
state.sourceAsset = ETH_TEST_TOKEN_ADDRESS;
}
if (state.targetChain === CHAIN_ID_SOLANA) {
state.sourceAsset = SOL_TEST_TOKEN_ADDRESS;
}
}
},
setSignedVAA: (state, action: PayloadAction<Uint8Array>) => {
state.signedVAA = action.payload; //TODO: serialize
setSignedVAAHex: (state, action: PayloadAction<string>) => {
state.signedVAAHex = action.payload;
state.activeStep = 3;
},
},
@ -50,8 +89,10 @@ export const {
decrementStep,
setStep,
setSourceChain,
setSourceAsset,
setAmount,
setTargetChain,
setSignedVAA,
setSignedVAAHex,
} = transferSlice.actions;
export default transferSlice.reducer;

View File

@ -0,0 +1,4 @@
export const uint8ArrayToHex = (a: Uint8Array) =>
Buffer.from(a).toString("hex");
export const hexToUint8Array = (h: string) =>
new Uint8Array(Buffer.from(h, "hex"));