bridge_ui: cleanup
Change-Id: Ibf9b6719fe31cd500a187c289357f493fbc177db
This commit is contained in:
parent
5a6cfe034f
commit
340899bbdc
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"));
|
Loading…
Reference in New Issue