wormhole/bridge_ui/src/components/Transfer/Send.tsx

220 lines
7.1 KiB
TypeScript

import { CHAIN_ID_TERRA, CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
import { Button, CircularProgress, makeStyles } from "@material-ui/core";
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
Token,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import { PublicKey } from "@solana/web3.js";
import { zeroPad } from "ethers/lib/utils";
import { useCallback, useEffect, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
import { useConnectedWallet } from "@terra-money/wallet-provider";
import { useSolanaWallet } from "../../contexts/SolanaWalletContext";
import {
selectTransferAmount,
selectTransferIsSendComplete,
selectTransferIsSending,
selectTransferIsTargetComplete,
selectTransferOriginAsset,
selectTransferOriginChain,
selectTransferSourceAsset,
selectTransferSourceChain,
selectTransferSourceParsedTokenAccount,
selectTransferTargetAsset,
selectTransferTargetChain,
selectTransferTargetParsedTokenAccount,
} from "../../store/selectors";
import { setIsSending, setSignedVAAHex } from "../../store/transferSlice";
import { uint8ArrayToHex } from "../../utils/array";
import {
transferFromEth,
transferFromSolana,
transferFromTerra,
} 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(selectTransferSourceChain);
const sourceAsset = useSelector(selectTransferSourceAsset);
const originChain = useSelector(selectTransferOriginChain);
const originAsset = useSelector(selectTransferOriginAsset);
const amount = useSelector(selectTransferAmount);
const targetChain = useSelector(selectTransferTargetChain);
const targetAsset = useSelector(selectTransferTargetAsset);
const isTargetComplete = useSelector(selectTransferIsTargetComplete);
const isSending = useSelector(selectTransferIsSending);
const isSendComplete = useSelector(selectTransferIsSendComplete);
const { signer, signerAddress } = useEthereumProvider();
const { wallet } = useSolanaWallet();
const terraWallet = useConnectedWallet();
const solPK = wallet?.publicKey;
const sourceParsedTokenAccount = useSelector(
selectTransferSourceParsedTokenAccount
);
const sourceTokenPublicKey = sourceParsedTokenAccount?.publicKey;
const decimals = sourceParsedTokenAccount?.decimals;
const targetParsedTokenAccount = useSelector(
selectTransferTargetParsedTokenAccount
);
// TODO: we probably shouldn't get here if we don't have this public key
// TODO: also this is just for solana... send help(ers)
const targetTokenAccountPublicKey = targetParsedTokenAccount?.publicKey;
console.log(
"Sending to:",
targetTokenAccountPublicKey,
targetTokenAccountPublicKey &&
new PublicKey(targetTokenAccountPublicKey).toBytes()
);
// TODO: AVOID THIS DANGEROUS CACOPHONY
const tpkRef = useRef<undefined | Uint8Array>(undefined);
useEffect(() => {
(async () => {
if (targetChain === CHAIN_ID_SOLANA) {
tpkRef.current = targetTokenAccountPublicKey
? zeroPad(new PublicKey(targetTokenAccountPublicKey).toBytes(), 32) // use the target's TokenAccount if it exists
: solPK && targetAsset // otherwise, use the associated token account (which we create in the case it doesn't exist)
? zeroPad(
(
await Token.getAssociatedTokenAddress(
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
new PublicKey(targetAsset),
solPK
)
).toBytes(),
32
)
: undefined;
} else tpkRef.current = undefined;
})();
}, [targetChain, solPK, targetAsset, targetTokenAccountPublicKey]);
// TODO: dynamically get "to" wallet
const handleTransferClick = useCallback(() => {
// TODO: we should separate state for transaction vs fetching vaa
// TODO: more generic way of calling these
if (sourceChain === CHAIN_ID_ETH && decimals) {
//TODO: just for testing, this should eventually use the store to communicate between steps
(async () => {
dispatch(setIsSending(true));
try {
console.log("actually sending", tpkRef.current);
const vaaBytes = await transferFromEth(
signer,
sourceAsset,
decimals,
amount,
targetChain,
tpkRef.current
);
console.log("bytes in transfer", vaaBytes);
vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
} catch (e) {
console.error(e);
dispatch(setIsSending(false));
}
})();
}
if (sourceChain === CHAIN_ID_SOLANA && decimals) {
//TODO: just for testing, this should eventually use the store to communicate between steps
(async () => {
dispatch(setIsSending(true));
try {
const vaaBytes = await transferFromSolana(
wallet,
solPK?.toString(),
sourceTokenPublicKey,
sourceAsset,
amount, //TODO: avoid decimals, pass in parsed amount
decimals,
signerAddress,
targetChain,
originAsset,
originChain
);
console.log("bytes in transfer", vaaBytes);
vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
} catch (e) {
console.error(e);
dispatch(setIsSending(false));
}
})();
}
if (sourceChain === CHAIN_ID_TERRA && decimals) {
(async () => {
dispatch(setIsSending(true));
try {
const vaaBytes = await transferFromTerra(
terraWallet,
sourceAsset,
amount,
"",
targetChain,
);
console.log("bytes in transfer", vaaBytes);
vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
} catch (e) {
console.error(e);
dispatch(setIsSending(false));
}
})();
}
}, [
dispatch,
sourceChain,
signer,
signerAddress,
wallet,
solPK,
sourceTokenPublicKey,
sourceAsset,
amount,
decimals,
targetChain,
originAsset,
originChain,
]);
return (
<>
<div style={{ position: "relative" }}>
<Button
color="primary"
variant="contained"
className={classes.transferButton}
onClick={handleTransferClick}
disabled={!isTargetComplete || isSending || isSendComplete}
>
Transfer
</Button>
{isSending ? (
<CircularProgress
size={24}
color="inherit"
style={{
position: "absolute",
bottom: 0,
left: "50%",
marginLeft: -12,
marginBottom: 6,
}}
/>
) : null}
</div>
</>
);
}
export default Send;