pyth-crosschain/bridge_ui/src/hooks/useHandleTransfer.ts

335 lines
9.7 KiB
TypeScript

import {
ChainId,
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA,
getEmitterAddressEth,
getEmitterAddressSolana,
getEmitterAddressTerra,
parseSequenceFromLogEth,
parseSequenceFromLogSolana,
parseSequenceFromLogTerra,
transferFromEth,
transferFromSolana,
transferFromTerra,
} from "@certusone/wormhole-sdk";
import { WalletContextState } from "@solana/wallet-adapter-react";
import { Connection } from "@solana/web3.js";
import {
ConnectedWallet,
useConnectedWallet,
} from "@terra-money/wallet-provider";
import { Signer } from "ethers";
import { parseUnits, zeroPad } from "ethers/lib/utils";
import { useSnackbar } from "notistack";
import { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
import {
selectTransferAmount,
selectTransferIsSendComplete,
selectTransferIsSending,
selectTransferIsTargetComplete,
selectTransferOriginAsset,
selectTransferOriginChain,
selectTransferSourceAsset,
selectTransferSourceChain,
selectTransferSourceParsedTokenAccount,
selectTransferTargetChain,
} from "../store/selectors";
import {
setIsSending,
setSignedVAAHex,
setTransferTx,
} from "../store/transferSlice";
import { hexToUint8Array, uint8ArrayToHex } from "../utils/array";
import {
ETH_BRIDGE_ADDRESS,
ETH_TOKEN_BRIDGE_ADDRESS,
SOLANA_HOST,
SOL_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
TERRA_TOKEN_BRIDGE_ADDRESS,
} from "../utils/consts";
import { getSignedVAAWithRetry } from "../utils/getSignedVAAWithRetry";
import parseError from "../utils/parseError";
import { signSendAndConfirm } from "../utils/solana";
import { waitForTerraExecution } from "../utils/terra";
import useTransferTargetAddressHex from "./useTransferTargetAddress";
async function eth(
dispatch: any,
enqueueSnackbar: any,
signer: Signer,
tokenAddress: string,
decimals: number,
amount: string,
recipientChain: ChainId,
recipientAddress: Uint8Array
) {
dispatch(setIsSending(true));
try {
const amountParsed = parseUnits(amount, decimals);
const receipt = await transferFromEth(
ETH_TOKEN_BRIDGE_ADDRESS,
signer,
tokenAddress,
amountParsed,
recipientChain,
recipientAddress
);
dispatch(
setTransferTx({ id: receipt.transactionHash, block: receipt.blockNumber })
);
enqueueSnackbar("Transaction confirmed", { variant: "success" });
const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS);
const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
enqueueSnackbar("Fetching VAA", { variant: "info" });
const { vaaBytes } = await getSignedVAAWithRetry(
CHAIN_ID_ETH,
emitterAddress,
sequence.toString()
);
dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
enqueueSnackbar("Fetched Signed VAA", { variant: "success" });
} catch (e) {
console.error(e);
enqueueSnackbar(parseError(e), { variant: "error" });
dispatch(setIsSending(false));
}
}
async function solana(
dispatch: any,
enqueueSnackbar: any,
wallet: WalletContextState,
payerAddress: string, //TODO: we may not need this since we have wallet
fromAddress: string,
mintAddress: string,
amount: string,
decimals: number,
targetChain: ChainId,
targetAddress: Uint8Array,
originAddressStr?: string,
originChain?: ChainId
) {
dispatch(setIsSending(true));
try {
//TODO: check if token attestation exists on the target chain
// TODO: share connection in context?
const connection = new Connection(SOLANA_HOST, "confirmed");
const amountParsed = parseUnits(amount, decimals).toBigInt();
const originAddress = originAddressStr
? zeroPad(hexToUint8Array(originAddressStr), 32)
: undefined;
const transaction = await transferFromSolana(
connection,
SOL_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
payerAddress,
fromAddress,
mintAddress,
amountParsed,
targetAddress,
targetChain,
originAddress,
originChain
);
const txid = await signSendAndConfirm(wallet, connection, transaction);
enqueueSnackbar("Transaction confirmed", { variant: "success" });
const info = await connection.getTransaction(txid);
if (!info) {
throw new Error("An error occurred while fetching the transaction info");
}
dispatch(setTransferTx({ id: txid, block: info.slot }));
enqueueSnackbar("Transaction confirmed", { variant: "success" });
const sequence = parseSequenceFromLogSolana(info);
const emitterAddress = await getEmitterAddressSolana(
SOL_TOKEN_BRIDGE_ADDRESS
);
enqueueSnackbar("Fetching VAA", { variant: "info" });
const { vaaBytes } = await getSignedVAAWithRetry(
CHAIN_ID_SOLANA,
emitterAddress,
sequence
);
dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
enqueueSnackbar("Fetched Signed VAA", { variant: "success" });
} catch (e) {
console.error(e);
enqueueSnackbar(parseError(e), { variant: "error" });
dispatch(setIsSending(false));
}
}
async function terra(
dispatch: any,
enqueueSnackbar: any,
wallet: ConnectedWallet,
asset: string,
amount: string,
decimals: number,
targetChain: ChainId,
targetAddress: Uint8Array
) {
dispatch(setIsSending(true));
try {
const amountParsed = parseUnits(amount, decimals).toString();
const msgs = await transferFromTerra(
wallet.terraAddress,
TERRA_TOKEN_BRIDGE_ADDRESS,
asset,
amountParsed,
targetChain,
targetAddress
);
const result = await wallet.post({
msgs: [...msgs],
memo: "Wormhole - Initiate Transfer",
});
console.log(result);
const info = await waitForTerraExecution(result);
console.log(info);
dispatch(setTransferTx({ id: info.txhash, block: info.height }));
enqueueSnackbar("Transaction confirmed", { variant: "success" });
const sequence = parseSequenceFromLogTerra(info);
console.log(sequence);
if (!sequence) {
throw new Error("Sequence not found");
}
const emitterAddress = await getEmitterAddressTerra(
TERRA_TOKEN_BRIDGE_ADDRESS
);
console.log(emitterAddress);
enqueueSnackbar("Fetching VAA", { variant: "info" });
const { vaaBytes } = await getSignedVAAWithRetry(
CHAIN_ID_TERRA,
emitterAddress,
sequence
);
enqueueSnackbar("Fetched Signed VAA", { variant: "success" });
dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
} catch (e) {
console.error(e);
enqueueSnackbar(parseError(e), { variant: "error" });
dispatch(setIsSending(false));
}
}
export function useHandleTransfer() {
const dispatch = useDispatch();
const { enqueueSnackbar } = useSnackbar();
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 targetAddress = useTransferTargetAddressHex();
const isTargetComplete = useSelector(selectTransferIsTargetComplete);
const isSending = useSelector(selectTransferIsSending);
const isSendComplete = useSelector(selectTransferIsSendComplete);
const { signer } = useEthereumProvider();
const solanaWallet = useSolanaWallet();
const solPK = solanaWallet?.publicKey;
const terraWallet = useConnectedWallet();
const sourceParsedTokenAccount = useSelector(
selectTransferSourceParsedTokenAccount
);
const sourceTokenPublicKey = sourceParsedTokenAccount?.publicKey;
const decimals = sourceParsedTokenAccount?.decimals;
const disabled = !isTargetComplete || isSending || isSendComplete;
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 &&
!!signer &&
!!sourceAsset &&
decimals !== undefined &&
!!targetAddress
) {
eth(
dispatch,
enqueueSnackbar,
signer,
sourceAsset,
decimals,
amount,
targetChain,
targetAddress
);
} else if (
sourceChain === CHAIN_ID_SOLANA &&
!!solanaWallet &&
!!solPK &&
!!sourceAsset &&
!!sourceTokenPublicKey &&
!!targetAddress &&
decimals !== undefined
) {
solana(
dispatch,
enqueueSnackbar,
solanaWallet,
solPK.toString(),
sourceTokenPublicKey,
sourceAsset,
amount,
decimals,
targetChain,
targetAddress,
originAsset,
originChain
);
} else if (
sourceChain === CHAIN_ID_TERRA &&
!!terraWallet &&
!!sourceAsset &&
decimals !== undefined &&
!!targetAddress
) {
terra(
dispatch,
enqueueSnackbar,
terraWallet,
sourceAsset,
amount,
decimals,
targetChain,
targetAddress
);
} else {
// enqueueSnackbar("Transfers from this chain are not yet supported", {
// variant: "error",
// });
}
}, [
dispatch,
enqueueSnackbar,
sourceChain,
signer,
solanaWallet,
solPK,
terraWallet,
sourceTokenPublicKey,
sourceAsset,
amount,
decimals,
targetChain,
targetAddress,
originAsset,
originChain,
]);
return useMemo(
() => ({
handleClick: handleTransferClick,
disabled,
showLoader: isSending,
}),
[handleTransferClick, disabled, isSending]
);
}