bridge_ui: notistack, hooks, cleanup

Change-Id: Ia553e514afee655c6cd8e26320e539fc59041e49
This commit is contained in:
Evan Gray 2021-08-20 14:15:29 -04:00
parent 0fab2481ca
commit 5f5a2a56f5
24 changed files with 982 additions and 851 deletions

View File

@ -31,6 +31,7 @@
"@terra-money/wallet-provider": "^1.4.0-alpha.1",
"ethers": "^5.4.1",
"js-base64": "^3.6.1",
"notistack": "^1.0.10",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-redux": "^7.2.4",
@ -26881,6 +26882,24 @@
"node": ">=4"
}
},
"node_modules/notistack": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/notistack/-/notistack-1.0.10.tgz",
"integrity": "sha512-z0y4jJaVtOoH3kc3GtNUlhNTY+5LE04QDeLVujX3VPhhzg67zw055mZjrBF+nzpv3V9aiPNph1EgRU4+t8kQTQ==",
"dependencies": {
"clsx": "^1.1.0",
"hoist-non-react-statics": "^3.3.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/notistack"
},
"peerDependencies": {
"@material-ui/core": "^4.0.0",
"react": "^16.8.0 || ^17.0.0",
"react-dom": "^16.8.0 || ^17.0.0"
}
},
"node_modules/npm-bundled": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz",
@ -61189,6 +61208,15 @@
"sort-keys": "^1.0.0"
}
},
"notistack": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/notistack/-/notistack-1.0.10.tgz",
"integrity": "sha512-z0y4jJaVtOoH3kc3GtNUlhNTY+5LE04QDeLVujX3VPhhzg67zw055mZjrBF+nzpv3V9aiPNph1EgRU4+t8kQTQ==",
"requires": {
"clsx": "^1.1.0",
"hoist-non-react-statics": "^3.3.0"
}
},
"npm-bundled": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz",

View File

@ -25,6 +25,7 @@
"@terra-money/wallet-provider": "^1.4.0-alpha.1",
"ethers": "^5.4.1",
"js-base64": "^3.6.1",
"notistack": "^1.0.10",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-redux": "^7.2.4",

View File

@ -1,25 +1,5 @@
import {
CHAIN_ID_TERRA,
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
} from "@certusone/wormhole-sdk";
import { Button, CircularProgress, makeStyles } from "@material-ui/core";
import { useCallback } from "react";
import { useConnectedWallet } from "@terra-money/wallet-provider";
import { useDispatch, useSelector } from "react-redux";
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
import { useSolanaWallet } from "../../contexts/SolanaWalletContext";
import useAttestSignedVAA from "../../hooks/useAttestSignedVAA";
import { reset, setIsCreating } from "../../store/attestSlice";
import {
selectAttestIsCreating,
selectAttestTargetChain,
} from "../../store/selectors";
import {
createWrappedOnEth,
createWrappedOnSolana,
createWrappedOnTerra,
} from "../../utils/createWrappedOn";
import { useHandleCreateWrapped } from "../../hooks/useHandleCreateWrapped";
const useStyles = makeStyles((theme) => ({
transferButton: {
@ -30,49 +10,20 @@ const useStyles = makeStyles((theme) => ({
}));
function Create() {
const dispatch = useDispatch();
const classes = useStyles();
const targetChain = useSelector(selectAttestTargetChain);
const solanaWallet = useSolanaWallet();
const solPK = solanaWallet?.publicKey;
const signedVAA = useAttestSignedVAA();
const isCreating = useSelector(selectAttestIsCreating);
const { signer } = useEthereumProvider();
const terraWallet = useConnectedWallet();
const handleCreateClick = useCallback(() => {
if (targetChain === CHAIN_ID_SOLANA && signedVAA) {
(async () => {
dispatch(setIsCreating(true));
await createWrappedOnSolana(solanaWallet, solPK?.toString(), signedVAA);
dispatch(reset());
})();
}
if (targetChain === CHAIN_ID_ETH && signedVAA) {
(async () => {
dispatch(setIsCreating(true));
await createWrappedOnEth(signer, signedVAA);
dispatch(reset());
})();
}
if (targetChain === CHAIN_ID_TERRA && signedVAA) {
(async () => {
dispatch(setIsCreating(true));
createWrappedOnTerra(terraWallet, signedVAA);
})();
}
}, [dispatch, targetChain, solanaWallet, solPK, signedVAA, signer]);
const { handleClick, disabled, showLoader } = useHandleCreateWrapped();
return (
<div style={{ position: "relative" }}>
<Button
color="primary"
variant="contained"
className={classes.transferButton}
disabled={isCreating}
onClick={handleCreateClick}
disabled={disabled}
onClick={handleClick}
>
Create
</Button>
{isCreating ? (
{showLoader ? (
<CircularProgress
size={24}
color="inherit"

View File

@ -1,28 +1,5 @@
import {
CHAIN_ID_TERRA,
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
} from "@certusone/wormhole-sdk";
import { Button, CircularProgress, makeStyles } 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 { useConnectedWallet } from "@terra-money/wallet-provider";
import { setIsSending, setSignedVAAHex } from "../../store/attestSlice";
import {
selectAttestIsSendComplete,
selectAttestIsSending,
selectAttestIsTargetComplete,
selectAttestSourceAsset,
selectAttestSourceChain,
} from "../../store/selectors";
import { uint8ArrayToHex } from "../../utils/array";
import {
attestFromEth,
attestFromSolana,
attestFromTerra,
} from "../../utils/attestFrom";
import { useHandleAttest } from "../../hooks/useHandleAttest";
const useStyles = makeStyles((theme) => ({
transferButton: {
@ -32,67 +9,9 @@ const useStyles = makeStyles((theme) => ({
},
}));
// TODO: move attest to its own workflow
function Send() {
const classes = useStyles();
const dispatch = useDispatch();
const sourceChain = useSelector(selectAttestSourceChain);
const sourceAsset = useSelector(selectAttestSourceAsset);
const isTargetComplete = useSelector(selectAttestIsTargetComplete);
const isSending = useSelector(selectAttestIsSending);
const isSendComplete = useSelector(selectAttestIsSendComplete);
const { signer } = useEthereumProvider();
const solanaWallet = useSolanaWallet();
const terraWallet = useConnectedWallet();
const solPK = solanaWallet?.publicKey;
// TODO: dynamically get "to" wallet
const handleAttestClick = useCallback(() => {
if (sourceChain === CHAIN_ID_ETH) {
//TODO: just for testing, this should eventually use the store to communicate between steps
(async () => {
dispatch(setIsSending(true));
try {
const vaaBytes = await attestFromEth(signer, sourceAsset);
console.log("bytes in attest", vaaBytes);
vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
} catch (e) {
console.error(e);
dispatch(setIsSending(false));
}
})();
} else if (sourceChain === CHAIN_ID_SOLANA) {
//TODO: just for testing, this should eventually use the store to communicate between steps
(async () => {
dispatch(setIsSending(true));
try {
const vaaBytes = await attestFromSolana(
solanaWallet,
solPK?.toString(),
sourceAsset
);
console.log("bytes in attest", vaaBytes);
vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
} catch (e) {
console.error(e);
dispatch(setIsSending(false));
}
})();
} else if (sourceChain === CHAIN_ID_TERRA) {
//TODO: just for testing, this should eventually use the store to communicate between steps
(async () => {
dispatch(setIsSending(true));
try {
const vaaBytes = await attestFromTerra(terraWallet, sourceAsset);
console.log("bytes in attest", vaaBytes);
vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
} catch (e) {
console.error(e);
dispatch(setIsSending(false));
}
})();
}
}, [dispatch, sourceChain, signer, solanaWallet, solPK, sourceAsset]);
const { handleClick, disabled, showLoader } = useHandleAttest();
return (
<>
<div style={{ position: "relative" }}>
@ -100,12 +19,12 @@ function Send() {
color="primary"
variant="contained"
className={classes.transferButton}
onClick={handleAttestClick}
disabled={!isTargetComplete || isSending || isSendComplete}
onClick={handleClick}
disabled={disabled}
>
Attest
</Button>
{isSending ? (
{showLoader ? (
<CircularProgress
size={24}
color="inherit"

View File

@ -1,11 +1,9 @@
import { Toolbar, Typography } from "@material-ui/core";
import { Toolbar } from "@material-ui/core";
import DisconnectIcon from "@material-ui/icons/LinkOff";
import {
WalletDisconnectButton,
WalletMultiButton,
} from "@solana/wallet-adapter-material-ui";
import { useWallet } from "@solana/wallet-adapter-react";
import React, { FC } from "react";
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
const SolanaWalletKey = () => {

View File

@ -1,28 +1,5 @@
import {
CHAIN_ID_TERRA,
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
} from "@certusone/wormhole-sdk";
import { Button, CircularProgress, makeStyles } from "@material-ui/core";
import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useConnectedWallet } from "@terra-money/wallet-provider";
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
import { useSolanaWallet } from "../../contexts/SolanaWalletContext";
import useTransferSignedVAA from "../../hooks/useTransferSignedVAA";
import {
selectTransferIsRedeeming,
selectTransferIsSourceAssetWormholeWrapped,
selectTransferOriginChain,
selectTransferTargetAsset,
selectTransferTargetChain,
} from "../../store/selectors";
import { reset, setIsRedeeming } from "../../store/transferSlice";
import {
redeemOnEth,
redeemOnSolana,
redeemOnTerra,
} from "../../utils/redeemOn";
import { useHandleRedeem } from "../../hooks/useHandleRedeem";
const useStyles = makeStyles((theme) => ({
transferButton: {
@ -33,69 +10,20 @@ const useStyles = makeStyles((theme) => ({
}));
function Redeem() {
const dispatch = useDispatch();
const classes = useStyles();
const isSourceAssetWormholeWrapped = useSelector(
selectTransferIsSourceAssetWormholeWrapped
);
const originChain = useSelector(selectTransferOriginChain);
const targetChain = useSelector(selectTransferTargetChain);
const targetAsset = useSelector(selectTransferTargetAsset);
const solanaWallet = useSolanaWallet();
const solPK = solanaWallet?.publicKey;
const { signer } = useEthereumProvider();
const terraWallet = useConnectedWallet();
const signedVAA = useTransferSignedVAA();
const isRedeeming = useSelector(selectTransferIsRedeeming);
const handleRedeemClick = useCallback(() => {
if (targetChain === CHAIN_ID_ETH && signedVAA) {
(async () => {
dispatch(setIsRedeeming(true));
await redeemOnEth(signer, signedVAA);
dispatch(reset());
})();
}
if (targetChain === CHAIN_ID_SOLANA && signedVAA) {
(async () => {
dispatch(setIsRedeeming(true));
await redeemOnSolana(
solanaWallet,
solPK?.toString(),
signedVAA,
!!isSourceAssetWormholeWrapped && originChain === CHAIN_ID_SOLANA,
targetAsset || undefined
);
dispatch(reset());
})();
}
if (targetChain === CHAIN_ID_TERRA && signedVAA) {
dispatch(setIsRedeeming(true));
redeemOnTerra(terraWallet, signedVAA);
}
}, [
dispatch,
terraWallet,
targetChain,
signer,
signedVAA,
solanaWallet,
solPK,
isSourceAssetWormholeWrapped,
originChain,
targetAsset,
]);
const { handleClick, disabled, showLoader } = useHandleRedeem();
return (
<div style={{ position: "relative" }}>
<Button
color="primary"
variant="contained"
className={classes.transferButton}
disabled={isRedeeming}
onClick={handleRedeemClick}
disabled={disabled}
onClick={handleClick}
>
Redeem
</Button>
{isRedeeming ? (
{showLoader ? (
<CircularProgress
size={24}
color="inherit"

View File

@ -1,42 +1,5 @@
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";
import { useHandleTransfer } from "../../hooks/useHandleTransfer";
const useStyles = makeStyles((theme) => ({
transferButton: {
@ -46,150 +9,9 @@ const useStyles = makeStyles((theme) => ({
},
}));
// 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 terraWallet = useConnectedWallet();
const solanaWallet = useSolanaWallet();
const solPK = solanaWallet?.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(
solanaWallet,
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,
solanaWallet,
solPK,
sourceTokenPublicKey,
sourceAsset,
amount,
decimals,
targetChain,
originAsset,
originChain,
]);
const { handleClick, disabled, showLoader } = useHandleTransfer();
return (
<>
<div style={{ position: "relative" }}>
@ -197,12 +19,12 @@ function Send() {
color="primary"
variant="contained"
className={classes.transferButton}
onClick={handleTransferClick}
disabled={!isTargetComplete || isSending || isSendComplete}
onClick={handleClick}
disabled={disabled}
>
Transfer
</Button>
{isSending ? (
{showLoader ? (
<CircularProgress
size={24}
color="inherit"

View File

@ -31,7 +31,7 @@ function Source() {
const sourceAsset = useSelector(selectTransferSourceAsset);
const uiAmountString = useSelector(selectTransferSourceBalanceString);
const amount = useSelector(selectTransferAmount);
const isSourceComplete = true; // useSelector(selectTransferIsSourceComplete);
const isSourceComplete = useSelector(selectTransferIsSourceComplete);
const shouldLockFields = useSelector(selectTransferShouldLockFields);
const handleSourceChange = useCallback(
(event) => {

View File

@ -26,14 +26,9 @@ function useFetchTargetAsset() {
);
const originChain = useSelector(selectTransferOriginChain);
const originAsset = useSelector(selectTransferOriginAsset);
console.log(
"WH Wrapped?",
isSourceAssetWormholeWrapped,
originChain,
originAsset
);
const targetChain = useSelector(selectTransferTargetChain);
const { provider } = useEthereumProvider();
// TODO: this may not cover wrapped to wrapped, should always use origin?
useEffect(() => {
if (isSourceAssetWormholeWrapped && originChain === targetChain) {
dispatch(setTargetAsset(originAsset));
@ -57,7 +52,6 @@ function useFetchTargetAsset() {
try {
const asset = await getForeignAssetSol(sourceChain, sourceAsset);
if (!cancelled) {
console.log("solana target asset", asset);
dispatch(setTargetAsset(asset));
}
} catch (e) {

View File

@ -96,7 +96,6 @@ function useGetBalanceEffect(sourceOrTarget: "source" | "target") {
.getParsedTokenAccountsByOwner(solPK, { mint })
.then(({ value }) => {
if (!cancelled) {
console.log("parsed token accounts", value);
if (value.length) {
// TODO: allow selection between these target accounts
dispatch(

View File

@ -0,0 +1,198 @@
import {
attestFromEth,
attestFromSolana,
attestFromTerra,
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA,
getEmitterAddressEth,
getEmitterAddressSolana,
getEmitterAddressTerra,
parseSequenceFromLogEth,
parseSequenceFromLogSolana,
parseSequenceFromLogTerra,
} from "@certusone/wormhole-sdk";
import { WalletContextState } from "@solana/wallet-adapter-react";
import { Connection, PublicKey } from "@solana/web3.js";
import {
ConnectedWallet,
useConnectedWallet,
} from "@terra-money/wallet-provider";
import { useSnackbar } from "notistack";
import { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Signer } from "../../../sdk/js/node_modules/ethers/lib";
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
import { setIsSending, setSignedVAAHex } from "../store/attestSlice";
import {
selectAttestIsSendComplete,
selectAttestIsSending,
selectAttestIsTargetComplete,
selectAttestSourceAsset,
selectAttestSourceChain,
} from "../store/selectors";
import { 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";
async function eth(
dispatch: any,
enqueueSnackbar: any,
signer: Signer,
sourceAsset: string
) {
dispatch(setIsSending(true));
try {
const receipt = await attestFromEth(
ETH_TOKEN_BRIDGE_ADDRESS,
signer,
sourceAsset
);
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
);
dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
enqueueSnackbar("Fetched Signed VAA", { variant: "success" });
} catch (e) {
enqueueSnackbar(parseError(e), { variant: "error" });
dispatch(setIsSending(false));
}
}
async function solana(
dispatch: any,
enqueueSnackbar: any,
solPK: PublicKey,
sourceAsset: string,
wallet: WalletContextState
) {
dispatch(setIsSending(true));
try {
// TODO: share connection in context?
const connection = new Connection(SOLANA_HOST, "confirmed");
const transaction = await attestFromSolana(
connection,
SOL_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
solPK.toString(),
sourceAsset
);
const txid = await signSendAndConfirm(wallet, connection, transaction);
enqueueSnackbar("Transaction confirmed", { variant: "success" });
const info = await connection.getTransaction(txid);
if (!info) {
// TODO: error state
throw new Error("An error occurred while fetching the transaction info");
}
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) {
enqueueSnackbar(parseError(e), { variant: "error" });
dispatch(setIsSending(false));
}
}
async function terra(
dispatch: any,
enqueueSnackbar: any,
wallet: ConnectedWallet,
asset: string
) {
dispatch(setIsSending(true));
try {
const infoMaybe = await attestFromTerra(
TERRA_TOKEN_BRIDGE_ADDRESS,
wallet,
asset
);
const info = await waitForTerraExecution(wallet, infoMaybe);
enqueueSnackbar("Transaction confirmed", { variant: "success" });
const sequence = parseSequenceFromLogTerra(info);
const emitterAddress = await getEmitterAddressTerra(
TERRA_TOKEN_BRIDGE_ADDRESS
);
enqueueSnackbar("Fetching VAA", { variant: "info" });
const { vaaBytes } = await getSignedVAAWithRetry(
CHAIN_ID_TERRA,
emitterAddress,
sequence
);
dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
enqueueSnackbar("Fetched Signed VAA", { variant: "success" });
} catch (e) {
console.error(e);
dispatch(setIsSending(false));
}
}
export function useHandleAttest() {
const dispatch = useDispatch();
const { enqueueSnackbar } = useSnackbar();
const sourceChain = useSelector(selectAttestSourceChain);
const sourceAsset = useSelector(selectAttestSourceAsset);
const isTargetComplete = useSelector(selectAttestIsTargetComplete);
const isSending = useSelector(selectAttestIsSending);
const isSendComplete = useSelector(selectAttestIsSendComplete);
const { signer } = useEthereumProvider();
const solanaWallet = useSolanaWallet();
const solPK = solanaWallet?.publicKey;
const terraWallet = useConnectedWallet();
const disabled = !isTargetComplete || isSending || isSendComplete;
const handleAttestClick = useCallback(() => {
if (sourceChain === CHAIN_ID_ETH && !!signer) {
eth(dispatch, enqueueSnackbar, signer, sourceAsset);
} else if (sourceChain === CHAIN_ID_SOLANA && !!solanaWallet && !!solPK) {
solana(dispatch, enqueueSnackbar, solPK, sourceAsset, solanaWallet);
} else if (sourceChain === CHAIN_ID_TERRA && !!terraWallet) {
terra(dispatch, enqueueSnackbar, terraWallet, sourceAsset);
} else {
// enqueueSnackbar("Attesting from this chain is not yet supported", {
// variant: "error",
// });
}
}, [
dispatch,
enqueueSnackbar,
sourceChain,
signer,
solanaWallet,
solPK,
terraWallet,
sourceAsset,
]);
return useMemo(
() => ({
handleClick: handleAttestClick,
disabled,
showLoader: isSending,
}),
[handleAttestClick, disabled, isSending]
);
}

View File

@ -0,0 +1,160 @@
import {
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA,
createWrappedOnEth,
createWrappedOnSolana,
createWrappedOnTerra,
postVaaSolana,
} 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 { 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 useAttestSignedVAA from "../hooks/useAttestSignedVAA";
import { reset, setIsCreating } from "../store/attestSlice";
import {
selectAttestIsCreating,
selectAttestTargetChain,
} from "../store/selectors";
import {
ETH_TOKEN_BRIDGE_ADDRESS,
SOLANA_HOST,
SOL_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
TERRA_TOKEN_BRIDGE_ADDRESS,
} from "../utils/consts";
import parseError from "../utils/parseError";
import { signSendAndConfirm } from "../utils/solana";
async function eth(
dispatch: any,
enqueueSnackbar: any,
signer: Signer,
signedVAA: Uint8Array
) {
dispatch(setIsCreating(true));
try {
await createWrappedOnEth(ETH_TOKEN_BRIDGE_ADDRESS, signer, signedVAA);
dispatch(reset());
enqueueSnackbar("Transaction confirmed", { variant: "success" });
} catch (e) {
enqueueSnackbar(parseError(e), { variant: "error" });
dispatch(setIsCreating(false));
}
}
async function solana(
dispatch: any,
enqueueSnackbar: any,
wallet: WalletContextState,
payerAddress: string, // TODO: we may not need this since we have wallet
signedVAA: Uint8Array
) {
dispatch(setIsCreating(true));
try {
// TODO: share connection in context?
const connection = new Connection(SOLANA_HOST, "confirmed");
await postVaaSolana(
connection,
wallet.signTransaction,
SOL_BRIDGE_ADDRESS,
payerAddress,
Buffer.from(signedVAA)
);
const transaction = await createWrappedOnSolana(
connection,
SOL_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
payerAddress,
signedVAA
);
await signSendAndConfirm(wallet, connection, transaction);
dispatch(reset());
enqueueSnackbar("Transaction confirmed", { variant: "success" });
} catch (e) {
enqueueSnackbar(parseError(e), { variant: "error" });
dispatch(setIsCreating(false));
}
}
async function terra(
dispatch: any,
enqueueSnackbar: any,
wallet: ConnectedWallet,
signedVAA: Uint8Array
) {
dispatch(setIsCreating(true));
try {
await createWrappedOnTerra(TERRA_TOKEN_BRIDGE_ADDRESS, wallet, signedVAA);
dispatch(reset());
enqueueSnackbar("Transaction confirmed", { variant: "success" });
} catch (e) {
enqueueSnackbar(parseError(e), { variant: "error" });
dispatch(setIsCreating(false));
}
}
export function useHandleCreateWrapped() {
const dispatch = useDispatch();
const { enqueueSnackbar } = useSnackbar();
const targetChain = useSelector(selectAttestTargetChain);
const solanaWallet = useSolanaWallet();
const solPK = solanaWallet?.publicKey;
const signedVAA = useAttestSignedVAA();
const isCreating = useSelector(selectAttestIsCreating);
const { signer } = useEthereumProvider();
const terraWallet = useConnectedWallet();
const handleCreateClick = useCallback(() => {
if (targetChain === CHAIN_ID_ETH && !!signer && !!signedVAA) {
eth(dispatch, enqueueSnackbar, signer, signedVAA);
} else if (
targetChain === CHAIN_ID_SOLANA &&
!!solanaWallet &&
!!solPK &&
!!signedVAA
) {
solana(
dispatch,
enqueueSnackbar,
solanaWallet,
solPK.toString(),
signedVAA
);
} else if (targetChain === CHAIN_ID_TERRA && !!terraWallet && !!signedVAA) {
terra(dispatch, enqueueSnackbar, terraWallet, signedVAA);
} else {
// enqueueSnackbar(
// "Creating wrapped tokens on this chain is not yet supported",
// {
// variant: "error",
// }
// );
}
}, [
dispatch,
enqueueSnackbar,
targetChain,
solanaWallet,
solPK,
terraWallet,
signedVAA,
signer,
]);
return useMemo(
() => ({
handleClick: handleCreateClick,
disabled: !!isCreating,
showLoader: !!isCreating,
}),
[handleCreateClick, isCreating]
);
}

View File

@ -0,0 +1,190 @@
import {
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA,
postVaaSolana,
redeemOnEth,
redeemOnSolana,
} from "@certusone/wormhole-sdk";
import { WalletContextState } from "@solana/wallet-adapter-react";
import { Connection } from "@solana/web3.js";
import { MsgExecuteContract } from "@terra-money/terra.js";
import {
ConnectedWallet,
useConnectedWallet,
} from "@terra-money/wallet-provider";
import { Signer } from "ethers";
import { fromUint8Array } from "js-base64";
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 useTransferSignedVAA from "../hooks/useTransferSignedVAA";
import {
selectTransferIsRedeeming,
selectTransferIsSourceAssetWormholeWrapped,
selectTransferOriginChain,
selectTransferTargetAsset,
selectTransferTargetChain,
} from "../store/selectors";
import { reset, setIsRedeeming } from "../store/transferSlice";
import {
ETH_TOKEN_BRIDGE_ADDRESS,
SOLANA_HOST,
SOL_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
TERRA_TOKEN_BRIDGE_ADDRESS,
} from "../utils/consts";
import parseError from "../utils/parseError";
import { signSendAndConfirm } from "../utils/solana";
async function eth(
dispatch: any,
enqueueSnackbar: any,
signer: Signer,
signedVAA: Uint8Array
) {
dispatch(setIsRedeeming(true));
try {
await redeemOnEth(ETH_TOKEN_BRIDGE_ADDRESS, signer, signedVAA);
dispatch(reset());
enqueueSnackbar("Transaction confirmed", { variant: "success" });
} catch (e) {
enqueueSnackbar(parseError(e), { variant: "error" });
dispatch(setIsRedeeming(false));
}
}
async function solana(
dispatch: any,
enqueueSnackbar: any,
wallet: WalletContextState,
payerAddress: string, //TODO: we may not need this since we have wallet
signedVAA: Uint8Array,
isSolanaNative: boolean,
mintAddress?: string // TODO: read the signedVAA and create the account if it doesn't exist
) {
dispatch(setIsRedeeming(true));
try {
// TODO: share connection in context?
const connection = new Connection(SOLANA_HOST, "confirmed");
await postVaaSolana(
connection,
wallet.signTransaction,
SOL_BRIDGE_ADDRESS,
payerAddress,
Buffer.from(signedVAA)
);
// TODO: how do we retry in between these steps
const transaction = await redeemOnSolana(
connection,
SOL_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
payerAddress,
signedVAA,
isSolanaNative,
mintAddress
);
await signSendAndConfirm(wallet, connection, transaction);
dispatch(reset());
enqueueSnackbar("Transaction confirmed", { variant: "success" });
} catch (e) {
enqueueSnackbar(parseError(e), { variant: "error" });
dispatch(setIsRedeeming(false));
}
}
async function terra(
dispatch: any,
enqueueSnackbar: any,
wallet: ConnectedWallet,
signedVAA: Uint8Array
) {
dispatch(setIsRedeeming(true));
try {
await wallet.post({
msgs: [
new MsgExecuteContract(
wallet.terraAddress,
TERRA_TOKEN_BRIDGE_ADDRESS,
{
submit_vaa: {
data: fromUint8Array(signedVAA),
},
},
{ uluna: 1000 }
),
],
memo: "Complete Transfer",
});
dispatch(reset());
enqueueSnackbar("Transaction confirmed", { variant: "success" });
} catch (e) {
enqueueSnackbar(parseError(e), { variant: "error" });
dispatch(setIsRedeeming(false));
}
}
export function useHandleRedeem() {
const dispatch = useDispatch();
const { enqueueSnackbar } = useSnackbar();
const isSourceAssetWormholeWrapped = useSelector(
selectTransferIsSourceAssetWormholeWrapped
);
const originChain = useSelector(selectTransferOriginChain);
const targetChain = useSelector(selectTransferTargetChain);
const targetAsset = useSelector(selectTransferTargetAsset);
const solanaWallet = useSolanaWallet();
const solPK = solanaWallet?.publicKey;
const { signer } = useEthereumProvider();
const terraWallet = useConnectedWallet();
const signedVAA = useTransferSignedVAA();
const isRedeeming = useSelector(selectTransferIsRedeeming);
const handleRedeemClick = useCallback(() => {
if (targetChain === CHAIN_ID_ETH && !!signer && signedVAA) {
eth(dispatch, enqueueSnackbar, signer, signedVAA);
} else if (
targetChain === CHAIN_ID_SOLANA &&
!!solanaWallet &&
!!solPK &&
signedVAA
) {
solana(
dispatch,
enqueueSnackbar,
solanaWallet,
solPK.toString(),
signedVAA,
!!isSourceAssetWormholeWrapped && originChain === CHAIN_ID_SOLANA,
targetAsset || undefined
);
} else if (targetChain === CHAIN_ID_TERRA && !!terraWallet && signedVAA) {
terra(dispatch, enqueueSnackbar, terraWallet, signedVAA);
} else {
// enqueueSnackbar("Redeeming on this chain is not yet supported", {
// variant: "error",
// });
}
}, [
dispatch,
enqueueSnackbar,
targetChain,
signer,
signedVAA,
solanaWallet,
solPK,
terraWallet,
isSourceAssetWormholeWrapped,
originChain,
targetAsset,
]);
return useMemo(
() => ({
handleClick: handleRedeemClick,
disabled: !!isRedeeming,
showLoader: !!isRedeeming,
}),
[handleRedeemClick, isRedeeming]
);
}

View File

@ -0,0 +1,354 @@
import {
ChainId,
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA,
getEmitterAddressEth,
getEmitterAddressSolana,
parseSequenceFromLogEth,
parseSequenceFromLogSolana,
parseSequenceFromLogTerra,
transferFromEth,
transferFromSolana,
} from "@certusone/wormhole-sdk";
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
Token,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import { WalletContextState } from "@solana/wallet-adapter-react";
import { Connection, PublicKey } from "@solana/web3.js";
import { MsgExecuteContract } from "@terra-money/terra.js";
import {
ConnectedWallet,
useConnectedWallet,
} from "@terra-money/wallet-provider";
import { Signer } from "ethers";
import { arrayify, parseUnits, zeroPad } from "ethers/lib/utils";
import { useSnackbar } from "notistack";
import { useCallback, useEffect, useMemo, useRef } 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,
selectTransferTargetAsset,
selectTransferTargetChain,
selectTransferTargetParsedTokenAccount,
} from "../store/selectors";
import { setIsSending, setSignedVAAHex } 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 { signSendAndConfirm } from "../utils/solana";
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
);
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);
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,
targetAddressStr: string,
targetChain: ChainId,
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 targetAddress = zeroPad(arrayify(targetAddressStr), 32);
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");
}
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);
dispatch(setIsSending(false));
}
}
async function terra(
dispatch: any,
enqueueSnackbar: any,
wallet: ConnectedWallet,
asset: string,
amount: string,
targetAddressStr: string,
targetChain: ChainId
) {
dispatch(setIsSending(true));
try {
// TODO: SDK
const result = await wallet.post({
msgs: [
new MsgExecuteContract(
wallet.terraAddress,
TERRA_TOKEN_BRIDGE_ADDRESS,
{
initiate_transfer: {
asset: asset,
amount: amount,
recipient_chain: targetChain,
recipient: targetAddressStr,
fee: 1000,
nonce: 0,
},
},
{ uluna: 1000 }
),
],
memo: "Complete Transfer",
});
enqueueSnackbar("Transaction confirmed", { variant: "success" });
console.log(result);
const sequence = parseSequenceFromLogTerra(result);
console.log(sequence);
const emitterAddress = await getEmitterAddressSolana(
SOL_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);
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 targetAsset = useSelector(selectTransferTargetAsset);
const isTargetComplete = useSelector(selectTransferIsTargetComplete);
const isSending = useSelector(selectTransferIsSending);
const isSendComplete = useSelector(selectTransferIsSendComplete);
const { signer, signerAddress } = useEthereumProvider();
const solanaWallet = useSolanaWallet();
const solPK = solanaWallet?.publicKey;
const terraWallet = useConnectedWallet();
const sourceParsedTokenAccount = useSelector(
selectTransferSourceParsedTokenAccount
);
const sourceTokenPublicKey = sourceParsedTokenAccount?.publicKey;
const decimals = sourceParsedTokenAccount?.decimals;
const targetParsedTokenAccount = useSelector(
selectTransferTargetParsedTokenAccount
);
const disabled = !isTargetComplete || isSending || isSendComplete;
// 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;
// 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 &&
!!signer &&
decimals !== undefined &&
!!tpkRef.current
) {
eth(
dispatch,
enqueueSnackbar,
signer,
sourceAsset,
decimals,
amount,
targetChain,
tpkRef.current
);
} else if (
sourceChain === CHAIN_ID_SOLANA &&
!!solanaWallet &&
!!solPK &&
!!sourceTokenPublicKey &&
!!signerAddress &&
decimals !== undefined
) {
solana(
dispatch,
enqueueSnackbar,
solanaWallet,
solPK.toString(),
sourceTokenPublicKey,
sourceAsset,
amount, //TODO: avoid decimals, pass in parsed amount
decimals,
signerAddress,
targetChain,
originAsset,
originChain
);
} else if (
sourceChain === CHAIN_ID_TERRA &&
!!terraWallet &&
decimals !== undefined &&
!!signerAddress
) {
terra(
dispatch,
enqueueSnackbar,
terraWallet,
sourceAsset,
amount,
signerAddress, // TODO: only works for Eth
targetChain
);
} else {
// enqueueSnackbar("Transfers from this chain are not yet supported", {
// variant: "error",
// });
}
}, [
dispatch,
enqueueSnackbar,
sourceChain,
signer,
signerAddress,
solanaWallet,
solPK,
terraWallet,
sourceTokenPublicKey,
sourceAsset,
amount,
decimals,
targetChain,
originAsset,
originChain,
]);
return useMemo(
() => ({
handleClick: handleTransferClick,
disabled,
showLoader: isSending,
}),
[handleTransferClick, disabled, isSending]
);
}

View File

@ -1,5 +1,6 @@
import { CssBaseline } from "@material-ui/core";
import { ThemeProvider } from "@material-ui/core/styles";
import { SnackbarProvider } from "notistack";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { HashRouter } from "react-router-dom";
@ -16,15 +17,17 @@ ReactDOM.render(
<ThemeProvider theme={theme}>
<CssBaseline />
<RadialGradient />
<SolanaWalletProvider>
<EthereumProviderProvider>
<TerraWalletProvider>
<HashRouter>
<App />
</HashRouter>
</TerraWalletProvider>
</EthereumProviderProvider>
</SolanaWalletProvider>
<SnackbarProvider maxSnack={3}>
<SolanaWalletProvider>
<EthereumProviderProvider>
<TerraWalletProvider>
<HashRouter>
<App />
</HashRouter>
</TerraWalletProvider>
</EthereumProviderProvider>
</SolanaWalletProvider>
</SnackbarProvider>
</ThemeProvider>
</Provider>,
document.getElementById("root")

View File

@ -1,103 +0,0 @@
import {
attestFromEth as attestEthTx,
attestFromSolana as attestSolanaTx,
attestFromTerra as attestTerraTx,
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA,
getEmitterAddressEth,
getEmitterAddressSolana,
getEmitterAddressTerra,
parseSequenceFromLogEth,
parseSequenceFromLogSolana,
parseSequenceFromLogTerra,
} from "@certusone/wormhole-sdk";
import { WalletContextState } from "@solana/wallet-adapter-react";
import { Connection } from "@solana/web3.js";
import { ConnectedWallet as TerraConnectedWallet } from "@terra-money/wallet-provider";
import { ethers } from "ethers";
import {
ETH_BRIDGE_ADDRESS,
ETH_TOKEN_BRIDGE_ADDRESS,
TERRA_TOKEN_BRIDGE_ADDRESS,
SOLANA_HOST,
SOL_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
} from "./consts";
import { getSignedVAAWithRetry } from "./getSignedVAAWithRetry";
import { signSendConfirmAndGet } from "./solana";
import { waitForTerraExecution } from "./terra";
export async function attestFromEth(
signer: ethers.Signer | undefined,
tokenAddress: string
) {
if (!signer) return;
const receipt = await attestEthTx(
ETH_TOKEN_BRIDGE_ADDRESS,
signer,
tokenAddress
);
const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS);
const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
const { vaaBytes } = await getSignedVAAWithRetry(
CHAIN_ID_ETH,
emitterAddress,
sequence
);
return vaaBytes;
}
export async function attestFromSolana(
wallet: WalletContextState,
payerAddress: string | undefined, //TODO: we may not need this since we have wallet
mintAddress: string
) {
if (!wallet || !wallet.publicKey || !payerAddress) return;
// TODO: share connection in context?
const connection = new Connection(SOLANA_HOST, "confirmed");
const transaction = await attestSolanaTx(
connection,
SOL_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
payerAddress,
mintAddress
);
const info = await signSendConfirmAndGet(wallet, connection, transaction);
if (!info) {
throw new Error("An error occurred while fetching the transaction info");
}
const sequence = parseSequenceFromLogSolana(info);
const emitterAddress = await getEmitterAddressSolana(
SOL_TOKEN_BRIDGE_ADDRESS
);
const { vaaBytes } = await getSignedVAAWithRetry(
CHAIN_ID_SOLANA,
emitterAddress,
sequence
);
return vaaBytes;
}
export async function attestFromTerra(
wallet: TerraConnectedWallet | undefined,
asset: string | undefined
) {
if (!wallet || !asset) return;
const infoMaybe = await attestTerraTx(
TERRA_TOKEN_BRIDGE_ADDRESS,
wallet,
asset
);
const info = await waitForTerraExecution(wallet, infoMaybe);
const sequence = parseSequenceFromLogTerra(info);
const emitterAddress = await getEmitterAddressTerra(
TERRA_TOKEN_BRIDGE_ADDRESS
);
const result = await getSignedVAAWithRetry(
CHAIN_ID_TERRA,
emitterAddress,
sequence
);
return result && result.vaaBytes;
}

View File

@ -1,65 +0,0 @@
import {
postVaaSolana,
createWrappedOnEth as createWrappedOnEthTx,
createWrappedOnSolana as createWrappedOnSolanaTx,
createWrappedOnTerra as createWrappedOnTerraTx,
} from "@certusone/wormhole-sdk";
import { Connection } from "@solana/web3.js";
import { ethers } from "ethers";
import { ConnectedWallet as TerraConnectedWallet } from "@terra-money/wallet-provider";
import {
ETH_TOKEN_BRIDGE_ADDRESS,
SOLANA_HOST,
SOL_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
TERRA_TOKEN_BRIDGE_ADDRESS,
TERRA_BRIDGE_ADDRESS,
} from "./consts";
import { signSendAndConfirm } from "./solana";
import { WalletContextState } from "@solana/wallet-adapter-react";
export async function createWrappedOnEth(
signer: ethers.Signer | undefined,
signedVAA: Uint8Array
) {
if (!signer) return;
await createWrappedOnEthTx(ETH_TOKEN_BRIDGE_ADDRESS, signer, signedVAA);
}
export async function createWrappedOnTerra(
wallet: TerraConnectedWallet | undefined,
signedVAA: Uint8Array
) {
if (!wallet) return;
await createWrappedOnTerraTx(
TERRA_TOKEN_BRIDGE_ADDRESS,
wallet,
signedVAA
);
}
export async function createWrappedOnSolana(
wallet: WalletContextState | undefined,
payerAddress: string | undefined, //TODO: we may not need this since we have wallet
signedVAA: Uint8Array
) {
if (!wallet || !wallet.publicKey || !payerAddress) return;
// TODO: share connection in context?
const connection = new Connection(SOLANA_HOST, "confirmed");
await postVaaSolana(
connection,
wallet.signTransaction,
SOL_BRIDGE_ADDRESS,
payerAddress,
Buffer.from(signedVAA)
);
const transaction = await createWrappedOnSolanaTx(
connection,
SOL_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
payerAddress,
signedVAA
);
await signSendAndConfirm(wallet, connection, transaction);
}

View File

@ -0,0 +1,9 @@
const MM_ERR_WITH_INFO_START =
"VM Exception while processing transaction: revert ";
const parseError = (e: any) =>
e?.data?.message?.startsWith(MM_ERR_WITH_INFO_START)
? e.data.message.replace(MM_ERR_WITH_INFO_START, "")
: e?.message
? e.message
: "An unknown error occurred";
export default parseError;

View File

@ -1,80 +0,0 @@
import {
postVaaSolana,
redeemOnEth as redeemOnEthTx,
redeemOnSolana as redeemOnSolanaTx,
} from "@certusone/wormhole-sdk";
import { Connection } from "@solana/web3.js";
import { ethers } from "ethers";
import { fromUint8Array } from "js-base64";
import { ConnectedWallet as TerraConnectedWallet } from "@terra-money/wallet-provider";
import { MsgExecuteContract } from "@terra-money/terra.js";
import {
ETH_TOKEN_BRIDGE_ADDRESS,
SOLANA_HOST,
SOL_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
TERRA_TOKEN_BRIDGE_ADDRESS,
} from "./consts";
import { signSendAndConfirm } from "./solana";
import { WalletContextState } from "@solana/wallet-adapter-react";
export async function redeemOnEth(
signer: ethers.Signer | undefined,
signedVAA: Uint8Array
) {
if (!signer) return;
await redeemOnEthTx(ETH_TOKEN_BRIDGE_ADDRESS, signer, signedVAA);
}
export async function redeemOnSolana(
wallet: WalletContextState | undefined,
payerAddress: string | undefined, //TODO: we may not need this since we have wallet
signedVAA: Uint8Array,
isSolanaNative: boolean,
mintAddress?: string // TODO: read the signedVAA and create the account if it doesn't exist
) {
if (!wallet || !wallet.publicKey || !payerAddress) return;
// TODO: share connection in context?
const connection = new Connection(SOLANA_HOST, "confirmed");
await postVaaSolana(
connection,
wallet.signTransaction,
SOL_BRIDGE_ADDRESS,
payerAddress,
Buffer.from(signedVAA)
);
const transaction = await redeemOnSolanaTx(
connection,
SOL_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
payerAddress,
signedVAA,
isSolanaNative,
mintAddress
);
await signSendAndConfirm(wallet, connection, transaction);
}
export async function redeemOnTerra(
wallet: TerraConnectedWallet | undefined,
signedVAA: Uint8Array
) {
if (!wallet) return;
wallet &&
(await wallet.post({
msgs: [
new MsgExecuteContract(
wallet.terraAddress,
TERRA_TOKEN_BRIDGE_ADDRESS,
{
submit_vaa: {
data: fromUint8Array(signedVAA),
},
},
{ uluna: 1000 }
),
],
memo: "Complete Transfer",
}));
}

View File

@ -11,12 +11,3 @@ export async function signSendAndConfirm(
await connection.confirmTransaction(txid);
return txid;
}
export async function signSendConfirmAndGet(
wallet: WalletContextState,
connection: Connection,
transaction: Transaction
) {
const txid = await signSendAndConfirm(wallet, connection, transaction);
return await connection.getTransaction(txid);
}

View File

@ -1,5 +1,8 @@
import { TxResult, ConnectedWallet as TerraConnectedWallet } from "@terra-money/wallet-provider";
import { TxInfo, LCDClient } from "@terra-money/terra.js";
import {
TxResult,
ConnectedWallet as TerraConnectedWallet,
} from "@terra-money/wallet-provider";
import { LCDClient } from "@terra-money/terra.js";
// TODO: Loop txInfo for timed out transactions.
// lcd.tx.txInfo(transaction.result.txhash);
@ -7,9 +10,9 @@ export async function waitForTerraExecution(
wallet: TerraConnectedWallet,
transaction: TxResult
) {
const lcd = new LCDClient({
new LCDClient({
URL: wallet.network.lcd,
chainID: "columbus-4",
chainID: "columbus-4",
});
return transaction;
}

View File

@ -1,167 +0,0 @@
import {
ChainId,
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA,
getEmitterAddressEth,
getEmitterAddressSolana,
parseSequenceFromLogEth,
parseSequenceFromLogSolana,
parseSequenceFromLogTerra,
transferFromEth as transferFromEthTx,
transferFromSolana as transferFromSolanaTx,
} from "@certusone/wormhole-sdk";
import { fromUint8Array } from "js-base64";
import {
ConnectedWallet as TerraConnectedWallet,
TxResult,
} from "@terra-money/wallet-provider";
import { Connection } from "@solana/web3.js";
import { MsgExecuteContract } from "@terra-money/terra.js";
import { ethers } from "ethers";
import { arrayify, parseUnits, zeroPad } from "ethers/lib/utils";
import { hexToUint8Array } from "./array";
import {
ETH_BRIDGE_ADDRESS,
ETH_TOKEN_BRIDGE_ADDRESS,
SOLANA_HOST,
SOL_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
TERRA_TOKEN_BRIDGE_ADDRESS,
TERRA_TEST_TOKEN_ADDRESS,
} from "./consts";
import { getSignedVAAWithRetry } from "./getSignedVAAWithRetry";
import { signSendConfirmAndGet } from "./solana";
import { WalletContextState } from "@solana/wallet-adapter-react";
// TODO: overall better input checking and error handling
export async function transferFromEth(
signer: ethers.Signer | undefined,
tokenAddress: string,
decimals: number,
amount: string,
recipientChain: ChainId,
recipientAddress: Uint8Array | undefined
) {
if (!signer || !recipientAddress) return;
//TODO: check if token attestation exists on the target chain
const amountParsed = parseUnits(amount, decimals);
const receipt = await transferFromEthTx(
ETH_TOKEN_BRIDGE_ADDRESS,
signer,
tokenAddress,
amountParsed,
recipientChain,
recipientAddress
);
const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS);
const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
const { vaaBytes } = await getSignedVAAWithRetry(
CHAIN_ID_ETH,
emitterAddress,
sequence.toString()
);
return vaaBytes;
}
export async function transferFromSolana(
wallet: WalletContextState | undefined,
payerAddress: string | undefined, //TODO: we may not need this since we have wallet
fromAddress: string | undefined,
mintAddress: string,
amount: string,
decimals: number,
targetAddressStr: string | undefined,
targetChain: ChainId,
originAddressStr?: string,
originChain?: ChainId
) {
if (
!wallet ||
!wallet.publicKey ||
!payerAddress ||
!fromAddress ||
!targetAddressStr ||
(originChain && !originAddressStr)
)
return;
// TODO: share connection in context?
const connection = new Connection(SOLANA_HOST, "confirmed");
const targetAddress = zeroPad(arrayify(targetAddressStr), 32);
const amountParsed = parseUnits(amount, decimals).toBigInt();
const originAddress = originAddressStr
? zeroPad(hexToUint8Array(originAddressStr), 32)
: undefined;
const transaction = await transferFromSolanaTx(
connection,
SOL_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
payerAddress,
fromAddress,
mintAddress,
amountParsed,
targetAddress,
targetChain,
originAddress,
originChain
);
const info = await signSendConfirmAndGet(wallet, connection, transaction);
if (!info) {
throw new Error("An error occurred while fetching the transaction info");
}
const sequence = parseSequenceFromLogSolana(info);
const emitterAddress = await getEmitterAddressSolana(
SOL_TOKEN_BRIDGE_ADDRESS
);
const { vaaBytes } = await getSignedVAAWithRetry(
CHAIN_ID_SOLANA,
emitterAddress,
sequence
);
return vaaBytes;
}
export async function transferFromTerra(
wallet: TerraConnectedWallet | undefined,
asset: string,
amount: string,
targetAddressStr: string | undefined,
targetChain: ChainId
) {
if (!wallet) return;
const result: TxResult =
wallet &&
(await wallet.post({
msgs: [
new MsgExecuteContract(
wallet.terraAddress,
TERRA_TOKEN_BRIDGE_ADDRESS,
{
initiate_transfer: {
asset: TERRA_TEST_TOKEN_ADDRESS,
amount: amount,
recipient_chain: targetChain,
recipient: targetAddressStr,
fee: 1000,
nonce: 0,
},
},
{ uluna: 1000 }
),
],
memo: "Complete Transfer",
}));
console.log(result);
const sequence = parseSequenceFromLogTerra(result);
console.log(sequence);
const emitterAddress = await getEmitterAddressSolana(
SOL_TOKEN_BRIDGE_ADDRESS
);
console.log(emitterAddress);
const { vaaBytes } = await getSignedVAAWithRetry(
CHAIN_ID_TERRA,
emitterAddress,
sequence
);
return vaaBytes;
}

View File

@ -9,7 +9,6 @@ export function parseSequenceFromLogEth(
): string {
// TODO: dangerous!(?)
const bridgeLog = receipt.logs.filter((l) => {
console.log(l.address, bridgeAddress);
return l.address === bridgeAddress;
})[0];
const {

View File

@ -32,7 +32,6 @@ export async function redeemOnSolana(
await import("../solana/token/token_bridge");
const ixs = [];
if (isSolanaNative) {
console.log("COMPLETE TRANSFER NATIVE");
ixs.push(
ixFromRust(
complete_transfer_native_ix(