bridge_ui: sol, eth bidirectional transfers
Change-Id: I0bbbbffddd3bec7771c79953556271000731cd36
This commit is contained in:
parent
b9359aab87
commit
6875559d4c
|
@ -3,7 +3,6 @@ import { useCallback } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
||||||
import { useSolanaWallet } from "../../contexts/SolanaWalletContext";
|
import { useSolanaWallet } from "../../contexts/SolanaWalletContext";
|
||||||
import useWrappedAsset from "../../hooks/useWrappedAsset";
|
|
||||||
import { setIsSending, setSignedVAAHex } from "../../store/attestSlice";
|
import { setIsSending, setSignedVAAHex } from "../../store/attestSlice";
|
||||||
import {
|
import {
|
||||||
selectAttestIsSendComplete,
|
selectAttestIsSendComplete,
|
||||||
|
@ -11,7 +10,6 @@ import {
|
||||||
selectAttestIsTargetComplete,
|
selectAttestIsTargetComplete,
|
||||||
selectAttestSourceAsset,
|
selectAttestSourceAsset,
|
||||||
selectAttestSourceChain,
|
selectAttestSourceChain,
|
||||||
selectAttestTargetChain,
|
|
||||||
} from "../../store/selectors";
|
} from "../../store/selectors";
|
||||||
import { uint8ArrayToHex } from "../../utils/array";
|
import { uint8ArrayToHex } from "../../utils/array";
|
||||||
import attestFrom, {
|
import attestFrom, {
|
||||||
|
@ -35,21 +33,12 @@ function Send() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const sourceChain = useSelector(selectAttestSourceChain);
|
const sourceChain = useSelector(selectAttestSourceChain);
|
||||||
const sourceAsset = useSelector(selectAttestSourceAsset);
|
const sourceAsset = useSelector(selectAttestSourceAsset);
|
||||||
const targetChain = useSelector(selectAttestTargetChain);
|
|
||||||
const isTargetComplete = useSelector(selectAttestIsTargetComplete);
|
const isTargetComplete = useSelector(selectAttestIsTargetComplete);
|
||||||
const isSending = useSelector(selectAttestIsSending);
|
const isSending = useSelector(selectAttestIsSending);
|
||||||
const isSendComplete = useSelector(selectAttestIsSendComplete);
|
const isSendComplete = useSelector(selectAttestIsSendComplete);
|
||||||
const { provider, signer } = useEthereumProvider();
|
const { provider, signer } = useEthereumProvider();
|
||||||
const { wallet } = useSolanaWallet();
|
const { wallet } = useSolanaWallet();
|
||||||
const solPK = wallet?.publicKey;
|
const solPK = wallet?.publicKey;
|
||||||
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);
|
|
||||||
// TODO: dynamically get "to" wallet
|
// TODO: dynamically get "to" wallet
|
||||||
const handleAttestClick = useCallback(() => {
|
const handleAttestClick = useCallback(() => {
|
||||||
// TODO: more generic way of calling these
|
// TODO: more generic way of calling these
|
||||||
|
|
|
@ -20,7 +20,7 @@ import Target from "./Target";
|
||||||
// TODO: ensure that both wallets are connected to the same known network
|
// TODO: ensure that both wallets are connected to the same known network
|
||||||
|
|
||||||
function Attest() {
|
function Attest() {
|
||||||
useGetBalanceEffect();
|
useGetBalanceEffect("source");
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const activeStep = useSelector(selectAttestActiveStep);
|
const activeStep = useSelector(selectAttestActiveStep);
|
||||||
const signedVAAHex = useSelector(selectAttestSignedVAAHex);
|
const signedVAAHex = useSelector(selectAttestSignedVAAHex);
|
||||||
|
|
|
@ -2,15 +2,18 @@ import { Button, CircularProgress, makeStyles } from "@material-ui/core";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
||||||
|
import { useSolanaWallet } from "../../contexts/SolanaWalletContext";
|
||||||
import useTransferSignedVAA from "../../hooks/useTransferSignedVAA";
|
import useTransferSignedVAA from "../../hooks/useTransferSignedVAA";
|
||||||
import {
|
import {
|
||||||
selectTransferIsRedeeming,
|
selectTransferIsRedeeming,
|
||||||
|
selectTransferIsSourceAssetWormholeWrapped,
|
||||||
|
selectTransferOriginChain,
|
||||||
|
selectTransferTargetAsset,
|
||||||
selectTransferTargetChain,
|
selectTransferTargetChain,
|
||||||
} from "../../store/selectors";
|
} from "../../store/selectors";
|
||||||
import { setIsRedeeming } from "../../store/transferSlice";
|
import { setIsRedeeming } from "../../store/transferSlice";
|
||||||
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "../../utils/consts";
|
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "../../utils/consts";
|
||||||
import redeemOn, { redeemOnEth, redeemOnSolana } from "../../utils/redeemOn";
|
import redeemOn, { redeemOnEth, redeemOnSolana } from "../../utils/redeemOn";
|
||||||
import { useSolanaWallet } from "../../contexts/SolanaWalletContext";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
transferButton: {
|
transferButton: {
|
||||||
|
@ -23,7 +26,12 @@ const useStyles = makeStyles((theme) => ({
|
||||||
function Redeem() {
|
function Redeem() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
const isSourceAssetWormholeWrapped = useSelector(
|
||||||
|
selectTransferIsSourceAssetWormholeWrapped
|
||||||
|
);
|
||||||
|
const originChain = useSelector(selectTransferOriginChain);
|
||||||
const targetChain = useSelector(selectTransferTargetChain);
|
const targetChain = useSelector(selectTransferTargetChain);
|
||||||
|
const targetAsset = useSelector(selectTransferTargetAsset);
|
||||||
const { wallet } = useSolanaWallet();
|
const { wallet } = useSolanaWallet();
|
||||||
const solPK = wallet?.publicKey;
|
const solPK = wallet?.publicKey;
|
||||||
const { provider, signer } = useEthereumProvider();
|
const { provider, signer } = useEthereumProvider();
|
||||||
|
@ -44,9 +52,26 @@ function Redeem() {
|
||||||
signedVAA
|
signedVAA
|
||||||
) {
|
) {
|
||||||
dispatch(setIsRedeeming(true));
|
dispatch(setIsRedeeming(true));
|
||||||
redeemOnSolana(wallet, solPK?.toString(), signedVAA);
|
redeemOnSolana(
|
||||||
|
wallet,
|
||||||
|
solPK?.toString(),
|
||||||
|
signedVAA,
|
||||||
|
!!isSourceAssetWormholeWrapped && originChain === CHAIN_ID_SOLANA,
|
||||||
|
targetAsset || undefined
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [dispatch, targetChain, provider, signer, signedVAA, wallet, solPK]);
|
}, [
|
||||||
|
dispatch,
|
||||||
|
targetChain,
|
||||||
|
provider,
|
||||||
|
signer,
|
||||||
|
signedVAA,
|
||||||
|
wallet,
|
||||||
|
solPK,
|
||||||
|
isSourceAssetWormholeWrapped,
|
||||||
|
originChain,
|
||||||
|
targetAsset,
|
||||||
|
]);
|
||||||
return (
|
return (
|
||||||
<div style={{ position: "relative" }}>
|
<div style={{ position: "relative" }}>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -1,18 +1,28 @@
|
||||||
import { Button, CircularProgress, makeStyles } from "@material-ui/core";
|
import { Button, CircularProgress, makeStyles } from "@material-ui/core";
|
||||||
import { useCallback } from "react";
|
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 { useDispatch, useSelector } from "react-redux";
|
||||||
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
||||||
import { useSolanaWallet } from "../../contexts/SolanaWalletContext";
|
import { useSolanaWallet } from "../../contexts/SolanaWalletContext";
|
||||||
import useWrappedAsset from "../../hooks/useWrappedAsset";
|
|
||||||
import {
|
import {
|
||||||
selectTransferAmount,
|
selectTransferAmount,
|
||||||
selectTransferIsSendComplete,
|
selectTransferIsSendComplete,
|
||||||
selectTransferIsSending,
|
selectTransferIsSending,
|
||||||
selectTransferIsTargetComplete,
|
selectTransferIsTargetComplete,
|
||||||
|
selectTransferOriginAsset,
|
||||||
|
selectTransferOriginChain,
|
||||||
selectTransferSourceAsset,
|
selectTransferSourceAsset,
|
||||||
selectTransferSourceChain,
|
selectTransferSourceChain,
|
||||||
selectTransferSourceParsedTokenAccount,
|
selectTransferSourceParsedTokenAccount,
|
||||||
|
selectTransferTargetAsset,
|
||||||
selectTransferTargetChain,
|
selectTransferTargetChain,
|
||||||
|
selectTransferTargetParsedTokenAccount,
|
||||||
} from "../../store/selectors";
|
} from "../../store/selectors";
|
||||||
import { setIsSending, setSignedVAAHex } from "../../store/transferSlice";
|
import { setIsSending, setSignedVAAHex } from "../../store/transferSlice";
|
||||||
import { uint8ArrayToHex } from "../../utils/array";
|
import { uint8ArrayToHex } from "../../utils/array";
|
||||||
|
@ -37,8 +47,11 @@ function Send() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const sourceChain = useSelector(selectTransferSourceChain);
|
const sourceChain = useSelector(selectTransferSourceChain);
|
||||||
const sourceAsset = useSelector(selectTransferSourceAsset);
|
const sourceAsset = useSelector(selectTransferSourceAsset);
|
||||||
|
const originChain = useSelector(selectTransferOriginChain);
|
||||||
|
const originAsset = useSelector(selectTransferOriginAsset);
|
||||||
const amount = useSelector(selectTransferAmount);
|
const amount = useSelector(selectTransferAmount);
|
||||||
const targetChain = useSelector(selectTransferTargetChain);
|
const targetChain = useSelector(selectTransferTargetChain);
|
||||||
|
const targetAsset = useSelector(selectTransferTargetAsset);
|
||||||
const isTargetComplete = useSelector(selectTransferIsTargetComplete);
|
const isTargetComplete = useSelector(selectTransferIsTargetComplete);
|
||||||
const isSending = useSelector(selectTransferIsSending);
|
const isSending = useSelector(selectTransferIsSending);
|
||||||
const isSendComplete = useSelector(selectTransferIsSendComplete);
|
const isSendComplete = useSelector(selectTransferIsSendComplete);
|
||||||
|
@ -48,16 +61,43 @@ function Send() {
|
||||||
const sourceParsedTokenAccount = useSelector(
|
const sourceParsedTokenAccount = useSelector(
|
||||||
selectTransferSourceParsedTokenAccount
|
selectTransferSourceParsedTokenAccount
|
||||||
);
|
);
|
||||||
const tokenPK = sourceParsedTokenAccount?.publicKey;
|
const sourceTokenPublicKey = sourceParsedTokenAccount?.publicKey;
|
||||||
const decimals = sourceParsedTokenAccount?.decimals;
|
const decimals = sourceParsedTokenAccount?.decimals;
|
||||||
const {
|
const targetParsedTokenAccount = useSelector(
|
||||||
isLoading: isCheckingWrapped,
|
selectTransferTargetParsedTokenAccount
|
||||||
// isWrapped,
|
);
|
||||||
wrappedAsset,
|
// TODO: we probably shouldn't get here if we don't have this public key
|
||||||
} = useWrappedAsset(targetChain, sourceChain, sourceAsset, provider);
|
// TODO: also this is just for solana... send help(ers)
|
||||||
// TODO: check this and send to separate flow
|
const targetTokenAccountPublicKey = targetParsedTokenAccount?.publicKey;
|
||||||
const isWrapped = true;
|
console.log(
|
||||||
console.log(isCheckingWrapped, isWrapped, wrappedAsset);
|
"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
|
// TODO: dynamically get "to" wallet
|
||||||
const handleTransferClick = useCallback(() => {
|
const handleTransferClick = useCallback(() => {
|
||||||
// TODO: we should separate state for transaction vs fetching vaa
|
// TODO: we should separate state for transaction vs fetching vaa
|
||||||
|
@ -72,6 +112,7 @@ function Send() {
|
||||||
(async () => {
|
(async () => {
|
||||||
dispatch(setIsSending(true));
|
dispatch(setIsSending(true));
|
||||||
try {
|
try {
|
||||||
|
console.log("actually sending", tpkRef.current);
|
||||||
const vaaBytes = await transferFromEth(
|
const vaaBytes = await transferFromEth(
|
||||||
provider,
|
provider,
|
||||||
signer,
|
signer,
|
||||||
|
@ -79,7 +120,7 @@ function Send() {
|
||||||
decimals,
|
decimals,
|
||||||
amount,
|
amount,
|
||||||
targetChain,
|
targetChain,
|
||||||
solPK?.toBytes()
|
tpkRef.current
|
||||||
);
|
);
|
||||||
console.log("bytes in transfer", vaaBytes);
|
console.log("bytes in transfer", vaaBytes);
|
||||||
vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
|
vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
|
||||||
|
@ -101,12 +142,14 @@ function Send() {
|
||||||
const vaaBytes = await transferFromSolana(
|
const vaaBytes = await transferFromSolana(
|
||||||
wallet,
|
wallet,
|
||||||
solPK?.toString(),
|
solPK?.toString(),
|
||||||
tokenPK,
|
sourceTokenPublicKey,
|
||||||
sourceAsset,
|
sourceAsset,
|
||||||
amount,
|
amount, //TODO: avoid decimals, pass in parsed amount
|
||||||
decimals,
|
decimals,
|
||||||
signerAddress,
|
signerAddress,
|
||||||
targetChain
|
targetChain,
|
||||||
|
originAsset,
|
||||||
|
originChain
|
||||||
);
|
);
|
||||||
console.log("bytes in transfer", vaaBytes);
|
console.log("bytes in transfer", vaaBytes);
|
||||||
vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
|
vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
|
||||||
|
@ -125,11 +168,13 @@ function Send() {
|
||||||
signerAddress,
|
signerAddress,
|
||||||
wallet,
|
wallet,
|
||||||
solPK,
|
solPK,
|
||||||
tokenPK,
|
sourceTokenPublicKey,
|
||||||
sourceAsset,
|
sourceAsset,
|
||||||
amount,
|
amount,
|
||||||
decimals,
|
decimals,
|
||||||
targetChain,
|
targetChain,
|
||||||
|
originAsset,
|
||||||
|
originChain,
|
||||||
]);
|
]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -73,6 +73,7 @@ function Source() {
|
||||||
))}
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
<KeyAndBalance chainId={sourceChain} balance={uiAmountString} />
|
<KeyAndBalance chainId={sourceChain} balance={uiAmountString} />
|
||||||
|
{/* TODO: token list for eth, check own */}
|
||||||
<TextField
|
<TextField
|
||||||
placeholder="Asset"
|
placeholder="Asset"
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|
|
@ -1,17 +1,29 @@
|
||||||
import { Button, MenuItem, TextField } from "@material-ui/core";
|
import { Button, makeStyles, MenuItem, TextField } from "@material-ui/core";
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import {
|
import {
|
||||||
|
selectTransferIsSourceAssetWormholeWrapped,
|
||||||
selectTransferIsTargetComplete,
|
selectTransferIsTargetComplete,
|
||||||
selectTransferShouldLockFields,
|
selectTransferShouldLockFields,
|
||||||
selectTransferSourceChain,
|
selectTransferSourceChain,
|
||||||
|
selectTransferTargetAsset,
|
||||||
|
selectTransferTargetBalanceString,
|
||||||
selectTransferTargetChain,
|
selectTransferTargetChain,
|
||||||
} from "../../store/selectors";
|
} from "../../store/selectors";
|
||||||
import { incrementStep, setTargetChain } from "../../store/transferSlice";
|
import { incrementStep, setTargetChain } from "../../store/transferSlice";
|
||||||
import { CHAINS } from "../../utils/consts";
|
import { hexToUint8Array } from "../../utils/array";
|
||||||
|
import { CHAINS, CHAIN_ID_SOLANA } from "../../utils/consts";
|
||||||
import KeyAndBalance from "../KeyAndBalance";
|
import KeyAndBalance from "../KeyAndBalance";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
transferField: {
|
||||||
|
marginTop: theme.spacing(5),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
function Target() {
|
function Target() {
|
||||||
|
const classes = useStyles();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const sourceChain = useSelector(selectTransferSourceChain);
|
const sourceChain = useSelector(selectTransferSourceChain);
|
||||||
const chains = useMemo(
|
const chains = useMemo(
|
||||||
|
@ -19,6 +31,19 @@ function Target() {
|
||||||
[sourceChain]
|
[sourceChain]
|
||||||
);
|
);
|
||||||
const targetChain = useSelector(selectTransferTargetChain);
|
const targetChain = useSelector(selectTransferTargetChain);
|
||||||
|
const targetAsset = useSelector(selectTransferTargetAsset);
|
||||||
|
const isSourceAssetWormholeWrapped = useSelector(
|
||||||
|
selectTransferIsSourceAssetWormholeWrapped
|
||||||
|
);
|
||||||
|
// TODO: wrapped stuff in hex, but native in not hex?
|
||||||
|
const readableTargetAsset =
|
||||||
|
isSourceAssetWormholeWrapped &&
|
||||||
|
targetChain === CHAIN_ID_SOLANA &&
|
||||||
|
targetAsset
|
||||||
|
? new PublicKey(hexToUint8Array(targetAsset)).toString()
|
||||||
|
: targetAsset || "";
|
||||||
|
// TODO: why doesn't this show up for solana wrapped?
|
||||||
|
const uiAmountString = useSelector(selectTransferTargetBalanceString);
|
||||||
const isTargetComplete = useSelector(selectTransferIsTargetComplete);
|
const isTargetComplete = useSelector(selectTransferIsTargetComplete);
|
||||||
const shouldLockFields = useSelector(selectTransferShouldLockFields);
|
const shouldLockFields = useSelector(selectTransferShouldLockFields);
|
||||||
const handleTargetChange = useCallback(
|
const handleTargetChange = useCallback(
|
||||||
|
@ -48,8 +73,14 @@ function Target() {
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
{/* TODO: determine "to" token address */}
|
<KeyAndBalance chainId={targetChain} balance={uiAmountString} />
|
||||||
<KeyAndBalance chainId={targetChain} />
|
<TextField
|
||||||
|
placeholder="Asset"
|
||||||
|
fullWidth
|
||||||
|
className={classes.transferField}
|
||||||
|
value={readableTargetAsset}
|
||||||
|
disabled={true}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
disabled={!isTargetComplete}
|
disabled={!isTargetComplete}
|
||||||
onClick={handleNextClick}
|
onClick={handleNextClick}
|
||||||
|
|
|
@ -6,6 +6,8 @@ import {
|
||||||
Stepper,
|
Stepper,
|
||||||
} from "@material-ui/core";
|
} from "@material-ui/core";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import useCheckIfWormholeWrapped from "../../hooks/useCheckIfWormholeWrapped";
|
||||||
|
import useFetchTargetAsset from "../../hooks/useFetchTargetAsset";
|
||||||
import useGetBalanceEffect from "../../hooks/useGetBalanceEffect";
|
import useGetBalanceEffect from "../../hooks/useGetBalanceEffect";
|
||||||
import {
|
import {
|
||||||
selectTransferActiveStep,
|
selectTransferActiveStep,
|
||||||
|
@ -23,7 +25,10 @@ import Target from "./Target";
|
||||||
// TODO: warn if amount exceeds balance
|
// TODO: warn if amount exceeds balance
|
||||||
|
|
||||||
function Transfer() {
|
function Transfer() {
|
||||||
useGetBalanceEffect();
|
useGetBalanceEffect("source");
|
||||||
|
useCheckIfWormholeWrapped();
|
||||||
|
useFetchTargetAsset();
|
||||||
|
useGetBalanceEffect("target");
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const activeStep = useSelector(selectTransferActiveStep);
|
const activeStep = useSelector(selectTransferActiveStep);
|
||||||
const signedVAAHex = useSelector(selectTransferSignedVAAHex);
|
const signedVAAHex = useSelector(selectTransferSignedVAAHex);
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||||
|
import {
|
||||||
|
selectTransferSourceAsset,
|
||||||
|
selectTransferSourceChain,
|
||||||
|
} from "../store/selectors";
|
||||||
|
import { setSourceWormholeWrappedInfo } from "../store/transferSlice";
|
||||||
|
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "../utils/consts";
|
||||||
|
import {
|
||||||
|
getOriginalAssetEth,
|
||||||
|
getOriginalAssetSol,
|
||||||
|
} from "../utils/getOriginalAsset";
|
||||||
|
|
||||||
|
function useCheckIfWormholeWrapped() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const sourceChain = useSelector(selectTransferSourceChain);
|
||||||
|
const sourceAsset = useSelector(selectTransferSourceAsset);
|
||||||
|
const { provider } = useEthereumProvider();
|
||||||
|
useEffect(() => {
|
||||||
|
// TODO: loading state, error state
|
||||||
|
dispatch(setSourceWormholeWrappedInfo(undefined));
|
||||||
|
let cancelled = false;
|
||||||
|
(async () => {
|
||||||
|
if (sourceChain === CHAIN_ID_ETH && provider) {
|
||||||
|
const wrappedInfo = await getOriginalAssetEth(provider, sourceAsset);
|
||||||
|
if (!cancelled) {
|
||||||
|
dispatch(setSourceWormholeWrappedInfo(wrappedInfo));
|
||||||
|
}
|
||||||
|
} else if (sourceChain === CHAIN_ID_SOLANA) {
|
||||||
|
try {
|
||||||
|
const wrappedInfo = await getOriginalAssetSol(sourceAsset);
|
||||||
|
if (!cancelled) {
|
||||||
|
dispatch(setSourceWormholeWrappedInfo(wrappedInfo));
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [dispatch, sourceChain, sourceAsset, provider]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useCheckIfWormholeWrapped;
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||||
|
import {
|
||||||
|
selectTransferIsSourceAssetWormholeWrapped,
|
||||||
|
selectTransferOriginAsset,
|
||||||
|
selectTransferOriginChain,
|
||||||
|
selectTransferSourceAsset,
|
||||||
|
selectTransferSourceChain,
|
||||||
|
selectTransferTargetChain,
|
||||||
|
} from "../store/selectors";
|
||||||
|
import { setTargetAsset } from "../store/transferSlice";
|
||||||
|
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "../utils/consts";
|
||||||
|
import {
|
||||||
|
getForeignAssetEth,
|
||||||
|
getForeignAssetSol,
|
||||||
|
} from "../utils/getForeignAsset";
|
||||||
|
|
||||||
|
function useFetchTargetAsset() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const sourceChain = useSelector(selectTransferSourceChain);
|
||||||
|
const sourceAsset = useSelector(selectTransferSourceAsset);
|
||||||
|
const isSourceAssetWormholeWrapped = useSelector(
|
||||||
|
selectTransferIsSourceAssetWormholeWrapped
|
||||||
|
);
|
||||||
|
const originChain = useSelector(selectTransferOriginChain);
|
||||||
|
const originAsset = useSelector(selectTransferOriginAsset);
|
||||||
|
console.log(
|
||||||
|
"WH Wrapped?",
|
||||||
|
isSourceAssetWormholeWrapped,
|
||||||
|
originChain,
|
||||||
|
originAsset
|
||||||
|
);
|
||||||
|
const targetChain = useSelector(selectTransferTargetChain);
|
||||||
|
const { provider } = useEthereumProvider();
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSourceAssetWormholeWrapped && originChain === targetChain) {
|
||||||
|
dispatch(setTargetAsset(originAsset));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// TODO: loading state, error state
|
||||||
|
dispatch(setTargetAsset(undefined));
|
||||||
|
let cancelled = false;
|
||||||
|
(async () => {
|
||||||
|
if (targetChain === CHAIN_ID_ETH && provider) {
|
||||||
|
const asset = await getForeignAssetEth(
|
||||||
|
provider,
|
||||||
|
sourceChain,
|
||||||
|
sourceAsset
|
||||||
|
);
|
||||||
|
if (!cancelled) {
|
||||||
|
dispatch(setTargetAsset(asset));
|
||||||
|
}
|
||||||
|
} else if (targetChain === CHAIN_ID_SOLANA) {
|
||||||
|
try {
|
||||||
|
const asset = await getForeignAssetSol(sourceChain, sourceAsset);
|
||||||
|
if (!cancelled) {
|
||||||
|
console.log("solana target asset", asset);
|
||||||
|
dispatch(setTargetAsset(asset));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (!cancelled) {
|
||||||
|
// TODO: warning for this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
dispatch,
|
||||||
|
isSourceAssetWormholeWrapped,
|
||||||
|
originChain,
|
||||||
|
originAsset,
|
||||||
|
targetChain,
|
||||||
|
sourceChain,
|
||||||
|
sourceAsset,
|
||||||
|
provider,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useFetchTargetAsset;
|
|
@ -8,8 +8,14 @@ import { TokenImplementation__factory } from "../ethers-contracts";
|
||||||
import {
|
import {
|
||||||
selectTransferSourceAsset,
|
selectTransferSourceAsset,
|
||||||
selectTransferSourceChain,
|
selectTransferSourceChain,
|
||||||
|
selectTransferTargetAsset,
|
||||||
|
selectTransferTargetChain,
|
||||||
} from "../store/selectors";
|
} from "../store/selectors";
|
||||||
import { setSourceParsedTokenAccount } from "../store/transferSlice";
|
import {
|
||||||
|
setSourceParsedTokenAccount,
|
||||||
|
setTargetParsedTokenAccount,
|
||||||
|
} from "../store/transferSlice";
|
||||||
|
import { hexToUint8Array } from "../utils/array";
|
||||||
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA, SOLANA_HOST } from "../utils/consts";
|
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA, SOLANA_HOST } from "../utils/consts";
|
||||||
|
|
||||||
function createParsedTokenAccount(
|
function createParsedTokenAccount(
|
||||||
|
@ -28,24 +34,44 @@ function createParsedTokenAccount(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function useGetBalanceEffect() {
|
/**
|
||||||
|
* Fetches the balance of an asset for the connected wallet
|
||||||
|
* @param sourceOrTarget determines whether this will fetch balance for the source or target account. Not intended to be switched on the same hook!
|
||||||
|
*/
|
||||||
|
function useGetBalanceEffect(sourceOrTarget: "source" | "target") {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const sourceChain = useSelector(selectTransferSourceChain);
|
const setAction =
|
||||||
const sourceAsset = useSelector(selectTransferSourceAsset);
|
sourceOrTarget === "source"
|
||||||
|
? setSourceParsedTokenAccount
|
||||||
|
: setTargetParsedTokenAccount;
|
||||||
|
const lookupChain = useSelector(
|
||||||
|
sourceOrTarget === "source"
|
||||||
|
? selectTransferSourceChain
|
||||||
|
: selectTransferTargetChain
|
||||||
|
);
|
||||||
|
const lookupAsset = useSelector(
|
||||||
|
sourceOrTarget === "source"
|
||||||
|
? selectTransferSourceAsset
|
||||||
|
: selectTransferTargetAsset
|
||||||
|
);
|
||||||
const { wallet } = useSolanaWallet();
|
const { wallet } = useSolanaWallet();
|
||||||
const solPK = wallet?.publicKey;
|
const solPK = wallet?.publicKey;
|
||||||
const { provider, signerAddress } = useEthereumProvider();
|
const { provider, signerAddress } = useEthereumProvider();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// TODO: loading state
|
// TODO: loading state
|
||||||
dispatch(setSourceParsedTokenAccount(undefined));
|
dispatch(setAction(undefined));
|
||||||
if (!sourceAsset) {
|
if (!lookupAsset) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
if (sourceChain === CHAIN_ID_SOLANA && solPK) {
|
if (lookupChain === CHAIN_ID_SOLANA && solPK) {
|
||||||
let mint;
|
let mint;
|
||||||
try {
|
try {
|
||||||
mint = new PublicKey(sourceAsset);
|
mint = new PublicKey(
|
||||||
|
sourceOrTarget === "source"
|
||||||
|
? lookupAsset
|
||||||
|
: hexToUint8Array(lookupAsset)
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -54,9 +80,11 @@ function useGetBalanceEffect() {
|
||||||
.getParsedTokenAccountsByOwner(solPK, { mint })
|
.getParsedTokenAccountsByOwner(solPK, { mint })
|
||||||
.then(({ value }) => {
|
.then(({ value }) => {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
|
console.log("parsed token accounts", value);
|
||||||
if (value.length) {
|
if (value.length) {
|
||||||
|
// TODO: allow selection between these target accounts
|
||||||
dispatch(
|
dispatch(
|
||||||
setSourceParsedTokenAccount(
|
setAction(
|
||||||
createParsedTokenAccount(
|
createParsedTokenAccount(
|
||||||
value[0].pubkey,
|
value[0].pubkey,
|
||||||
value[0].account.data.parsed?.info?.tokenAmount?.amount,
|
value[0].account.data.parsed?.info?.tokenAmount?.amount,
|
||||||
|
@ -78,15 +106,15 @@ function useGetBalanceEffect() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (sourceChain === CHAIN_ID_ETH && provider && signerAddress) {
|
if (lookupChain === CHAIN_ID_ETH && provider && signerAddress) {
|
||||||
const token = TokenImplementation__factory.connect(sourceAsset, provider);
|
const token = TokenImplementation__factory.connect(lookupAsset, provider);
|
||||||
token
|
token
|
||||||
.decimals()
|
.decimals()
|
||||||
.then((decimals) => {
|
.then((decimals) => {
|
||||||
token.balanceOf(signerAddress).then((n) => {
|
token.balanceOf(signerAddress).then((n) => {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
dispatch(
|
dispatch(
|
||||||
setSourceParsedTokenAccount(
|
setAction(
|
||||||
// TODO: verify accuracy
|
// TODO: verify accuracy
|
||||||
createParsedTokenAccount(
|
createParsedTokenAccount(
|
||||||
undefined,
|
undefined,
|
||||||
|
@ -109,7 +137,16 @@ function useGetBalanceEffect() {
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [dispatch, sourceChain, sourceAsset, solPK, provider, signerAddress]);
|
}, [
|
||||||
|
dispatch,
|
||||||
|
sourceOrTarget,
|
||||||
|
setAction,
|
||||||
|
lookupChain,
|
||||||
|
lookupAsset,
|
||||||
|
solPK,
|
||||||
|
provider,
|
||||||
|
signerAddress,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useGetBalanceEffect;
|
export default useGetBalanceEffect;
|
||||||
|
|
|
@ -1,74 +0,0 @@
|
||||||
import { ethers } from "ethers";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { ChainId, CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "../utils/consts";
|
|
||||||
import {
|
|
||||||
getAttestedAssetEth,
|
|
||||||
getAttestedAssetSol,
|
|
||||||
} from "../utils/getAttestedAsset";
|
|
||||||
export interface WrappedAssetState {
|
|
||||||
isLoading: boolean;
|
|
||||||
isWrapped: boolean;
|
|
||||||
wrappedAsset: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function useWrappedAsset(
|
|
||||||
checkChain: ChainId,
|
|
||||||
originChain: ChainId,
|
|
||||||
originAsset: string,
|
|
||||||
provider: ethers.providers.Web3Provider | undefined
|
|
||||||
) {
|
|
||||||
const [state, setState] = useState<WrappedAssetState>({
|
|
||||||
isLoading: false,
|
|
||||||
isWrapped: false,
|
|
||||||
wrappedAsset: null,
|
|
||||||
});
|
|
||||||
useEffect(() => {
|
|
||||||
let cancelled = false;
|
|
||||||
(async () => {
|
|
||||||
if (checkChain === CHAIN_ID_ETH && provider) {
|
|
||||||
setState({ isLoading: true, isWrapped: false, wrappedAsset: null });
|
|
||||||
const asset = await getAttestedAssetEth(
|
|
||||||
provider,
|
|
||||||
originChain,
|
|
||||||
originAsset
|
|
||||||
);
|
|
||||||
if (!cancelled) {
|
|
||||||
setState({
|
|
||||||
isLoading: false,
|
|
||||||
isWrapped: !!asset && asset !== ethers.constants.AddressZero,
|
|
||||||
wrappedAsset: asset,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (checkChain === CHAIN_ID_SOLANA) {
|
|
||||||
setState({ isLoading: true, isWrapped: false, wrappedAsset: null });
|
|
||||||
try {
|
|
||||||
const asset = await getAttestedAssetSol(originChain, originAsset);
|
|
||||||
if (!cancelled) {
|
|
||||||
setState({
|
|
||||||
isLoading: false,
|
|
||||||
isWrapped: !!asset,
|
|
||||||
wrappedAsset: asset,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (!cancelled) {
|
|
||||||
// TODO: warning for this
|
|
||||||
setState({
|
|
||||||
isLoading: false,
|
|
||||||
isWrapped: false,
|
|
||||||
wrappedAsset: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setState({ isLoading: false, isWrapped: false, wrappedAsset: null });
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, [checkChain, originChain, originAsset, provider]);
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useWrappedAsset;
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
import { ethers } from "ethers";
|
||||||
import { parseUnits } from "ethers/lib/utils";
|
import { parseUnits } from "ethers/lib/utils";
|
||||||
import { RootState } from ".";
|
import { RootState } from ".";
|
||||||
import { CHAIN_ID_SOLANA } from "../utils/consts";
|
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "../utils/consts";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Attest
|
* Attest
|
||||||
|
@ -43,6 +44,12 @@ export const selectTransferSourceChain = (state: RootState) =>
|
||||||
state.transfer.sourceChain;
|
state.transfer.sourceChain;
|
||||||
export const selectTransferSourceAsset = (state: RootState) =>
|
export const selectTransferSourceAsset = (state: RootState) =>
|
||||||
state.transfer.sourceAsset;
|
state.transfer.sourceAsset;
|
||||||
|
export const selectTransferIsSourceAssetWormholeWrapped = (state: RootState) =>
|
||||||
|
state.transfer.isSourceAssetWormholeWrapped;
|
||||||
|
export const selectTransferOriginChain = (state: RootState) =>
|
||||||
|
state.transfer.originChain;
|
||||||
|
export const selectTransferOriginAsset = (state: RootState) =>
|
||||||
|
state.transfer.originAsset;
|
||||||
export const selectTransferSourceParsedTokenAccount = (state: RootState) =>
|
export const selectTransferSourceParsedTokenAccount = (state: RootState) =>
|
||||||
state.transfer.sourceParsedTokenAccount;
|
state.transfer.sourceParsedTokenAccount;
|
||||||
export const selectTransferSourceBalanceString = (state: RootState) =>
|
export const selectTransferSourceBalanceString = (state: RootState) =>
|
||||||
|
@ -50,6 +57,12 @@ export const selectTransferSourceBalanceString = (state: RootState) =>
|
||||||
export const selectTransferAmount = (state: RootState) => state.transfer.amount;
|
export const selectTransferAmount = (state: RootState) => state.transfer.amount;
|
||||||
export const selectTransferTargetChain = (state: RootState) =>
|
export const selectTransferTargetChain = (state: RootState) =>
|
||||||
state.transfer.targetChain;
|
state.transfer.targetChain;
|
||||||
|
export const selectTransferTargetAsset = (state: RootState) =>
|
||||||
|
state.transfer.targetAsset;
|
||||||
|
export const selectTransferTargetParsedTokenAccount = (state: RootState) =>
|
||||||
|
state.transfer.targetParsedTokenAccount;
|
||||||
|
export const selectTransferTargetBalanceString = (state: RootState) =>
|
||||||
|
state.transfer.targetParsedTokenAccount?.uiAmountString || "";
|
||||||
export const selectTransferSignedVAAHex = (state: RootState) =>
|
export const selectTransferSignedVAAHex = (state: RootState) =>
|
||||||
state.transfer.signedVAAHex;
|
state.transfer.signedVAAHex;
|
||||||
export const selectTransferIsSending = (state: RootState) =>
|
export const selectTransferIsSending = (state: RootState) =>
|
||||||
|
@ -79,7 +92,15 @@ export const selectTransferIsSourceComplete = (state: RootState) =>
|
||||||
);
|
);
|
||||||
// TODO: check wrapped asset exists or is native transfer
|
// TODO: check wrapped asset exists or is native transfer
|
||||||
export const selectTransferIsTargetComplete = (state: RootState) =>
|
export const selectTransferIsTargetComplete = (state: RootState) =>
|
||||||
selectTransferIsSourceComplete(state) && !!state.transfer.targetChain;
|
selectTransferIsSourceComplete(state) &&
|
||||||
|
!!state.transfer.targetChain &&
|
||||||
|
!!state.transfer.targetAsset &&
|
||||||
|
(state.transfer.targetChain !== CHAIN_ID_ETH ||
|
||||||
|
state.transfer.targetAsset !== ethers.constants.AddressZero); //&&
|
||||||
|
// Associated Token Account exists
|
||||||
|
// (state.transfer.targetChain !== CHAIN_ID_SOLANA ||
|
||||||
|
// (!!state.transfer.targetParsedTokenAccount &&
|
||||||
|
// !!state.transfer.targetParsedTokenAccount.publicKey));
|
||||||
export const selectTransferIsSendComplete = (state: RootState) =>
|
export const selectTransferIsSendComplete = (state: RootState) =>
|
||||||
!!selectTransferSignedVAAHex(state);
|
!!selectTransferSignedVAAHex(state);
|
||||||
export const selectTransferShouldLockFields = (state: RootState) =>
|
export const selectTransferShouldLockFields = (state: RootState) =>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
ETH_TEST_TOKEN_ADDRESS,
|
ETH_TEST_TOKEN_ADDRESS,
|
||||||
SOL_TEST_TOKEN_ADDRESS,
|
SOL_TEST_TOKEN_ADDRESS,
|
||||||
} from "../utils/consts";
|
} from "../utils/consts";
|
||||||
|
import { WormholeWrappedInfo } from "../utils/getOriginalAsset";
|
||||||
|
|
||||||
const LAST_STEP = 3;
|
const LAST_STEP = 3;
|
||||||
|
|
||||||
|
@ -23,9 +24,14 @@ export interface TransferState {
|
||||||
activeStep: Steps;
|
activeStep: Steps;
|
||||||
sourceChain: ChainId;
|
sourceChain: ChainId;
|
||||||
sourceAsset: string;
|
sourceAsset: string;
|
||||||
|
isSourceAssetWormholeWrapped: boolean | undefined;
|
||||||
|
originChain: ChainId | undefined;
|
||||||
|
originAsset: string | undefined;
|
||||||
sourceParsedTokenAccount: ParsedTokenAccount | undefined;
|
sourceParsedTokenAccount: ParsedTokenAccount | undefined;
|
||||||
amount: string;
|
amount: string;
|
||||||
targetChain: ChainId;
|
targetChain: ChainId;
|
||||||
|
targetAsset: string | null | undefined;
|
||||||
|
targetParsedTokenAccount: ParsedTokenAccount | undefined;
|
||||||
signedVAAHex: string | undefined;
|
signedVAAHex: string | undefined;
|
||||||
isSending: boolean;
|
isSending: boolean;
|
||||||
isRedeeming: boolean;
|
isRedeeming: boolean;
|
||||||
|
@ -35,9 +41,14 @@ const initialState: TransferState = {
|
||||||
activeStep: 0,
|
activeStep: 0,
|
||||||
sourceChain: CHAIN_ID_SOLANA,
|
sourceChain: CHAIN_ID_SOLANA,
|
||||||
sourceAsset: SOL_TEST_TOKEN_ADDRESS,
|
sourceAsset: SOL_TEST_TOKEN_ADDRESS,
|
||||||
|
isSourceAssetWormholeWrapped: false,
|
||||||
sourceParsedTokenAccount: undefined,
|
sourceParsedTokenAccount: undefined,
|
||||||
|
originChain: undefined,
|
||||||
|
originAsset: undefined,
|
||||||
amount: "",
|
amount: "",
|
||||||
targetChain: CHAIN_ID_ETH,
|
targetChain: CHAIN_ID_ETH,
|
||||||
|
targetAsset: undefined,
|
||||||
|
targetParsedTokenAccount: undefined,
|
||||||
signedVAAHex: undefined,
|
signedVAAHex: undefined,
|
||||||
isSending: false,
|
isSending: false,
|
||||||
isRedeeming: false,
|
isRedeeming: false,
|
||||||
|
@ -73,6 +84,20 @@ export const transferSlice = createSlice({
|
||||||
setSourceAsset: (state, action: PayloadAction<string>) => {
|
setSourceAsset: (state, action: PayloadAction<string>) => {
|
||||||
state.sourceAsset = action.payload;
|
state.sourceAsset = action.payload;
|
||||||
},
|
},
|
||||||
|
setSourceWormholeWrappedInfo: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<WormholeWrappedInfo | undefined>
|
||||||
|
) => {
|
||||||
|
if (action.payload) {
|
||||||
|
state.isSourceAssetWormholeWrapped = action.payload.isWrapped;
|
||||||
|
state.originChain = action.payload.chainId;
|
||||||
|
state.originAsset = action.payload.assetAddress;
|
||||||
|
} else {
|
||||||
|
state.isSourceAssetWormholeWrapped = undefined;
|
||||||
|
state.originChain = undefined;
|
||||||
|
state.originAsset = undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
setSourceParsedTokenAccount: (
|
setSourceParsedTokenAccount: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<ParsedTokenAccount | undefined>
|
action: PayloadAction<ParsedTokenAccount | undefined>
|
||||||
|
@ -97,6 +122,18 @@ export const transferSlice = createSlice({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setTargetAsset: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<string | null | undefined>
|
||||||
|
) => {
|
||||||
|
state.targetAsset = action.payload;
|
||||||
|
},
|
||||||
|
setTargetParsedTokenAccount: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<ParsedTokenAccount | undefined>
|
||||||
|
) => {
|
||||||
|
state.targetParsedTokenAccount = action.payload;
|
||||||
|
},
|
||||||
setSignedVAAHex: (state, action: PayloadAction<string>) => {
|
setSignedVAAHex: (state, action: PayloadAction<string>) => {
|
||||||
state.signedVAAHex = action.payload;
|
state.signedVAAHex = action.payload;
|
||||||
state.isSending = false;
|
state.isSending = false;
|
||||||
|
@ -117,9 +154,12 @@ export const {
|
||||||
setStep,
|
setStep,
|
||||||
setSourceChain,
|
setSourceChain,
|
||||||
setSourceAsset,
|
setSourceAsset,
|
||||||
|
setSourceWormholeWrappedInfo,
|
||||||
setSourceParsedTokenAccount,
|
setSourceParsedTokenAccount,
|
||||||
setAmount,
|
setAmount,
|
||||||
setTargetChain,
|
setTargetChain,
|
||||||
|
setTargetAsset,
|
||||||
|
setTargetParsedTokenAccount,
|
||||||
setSignedVAAHex,
|
setSignedVAAHex,
|
||||||
setIsSending,
|
setIsSending,
|
||||||
setIsRedeeming,
|
setIsRedeeming,
|
||||||
|
|
|
@ -56,7 +56,7 @@ export async function attestFromEth(
|
||||||
const { vaaBytes } = await getSignedVAA(
|
const { vaaBytes } = await getSignedVAA(
|
||||||
CHAIN_ID_ETH,
|
CHAIN_ID_ETH,
|
||||||
emitterAddress,
|
emitterAddress,
|
||||||
sequence
|
sequence.toString()
|
||||||
);
|
);
|
||||||
console.log("SIGNED VAA:", vaaBytes);
|
console.log("SIGNED VAA:", vaaBytes);
|
||||||
return vaaBytes;
|
return vaaBytes;
|
||||||
|
|
|
@ -57,6 +57,7 @@ export async function createWrappedOnSolana(
|
||||||
signedVAA
|
signedVAA
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
console.log(ix.keys.map((x) => x.pubkey.toString()));
|
||||||
const transaction = new Transaction().add(ix);
|
const transaction = new Transaction().add(ix);
|
||||||
const { blockhash } = await connection.getRecentBlockhash();
|
const { blockhash } = await connection.getRecentBlockhash();
|
||||||
transaction.recentBlockhash = blockhash;
|
transaction.recentBlockhash = blockhash;
|
||||||
|
|
|
@ -10,7 +10,14 @@ import {
|
||||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||||
} from "./consts";
|
} from "./consts";
|
||||||
|
|
||||||
export async function getAttestedAssetEth(
|
/**
|
||||||
|
* Returns a foreign asset address on Ethereum for a provided native chain and asset address
|
||||||
|
* @param provider
|
||||||
|
* @param originChain
|
||||||
|
* @param originAsset
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function getForeignAssetEth(
|
||||||
provider: ethers.providers.Web3Provider,
|
provider: ethers.providers.Web3Provider,
|
||||||
originChain: ChainId,
|
originChain: ChainId,
|
||||||
originAsset: string
|
originAsset: string
|
||||||
|
@ -33,7 +40,13 @@ export async function getAttestedAssetEth(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAttestedAssetSol(
|
/**
|
||||||
|
* Returns a foreign asset address on Solana for a provided native chain and asset address
|
||||||
|
* @param originChain
|
||||||
|
* @param originAsset
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function getForeignAssetSol(
|
||||||
originChain: ChainId,
|
originChain: ChainId,
|
||||||
originAsset: string
|
originAsset: string
|
||||||
) {
|
) {
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { Connection, PublicKey } from "@solana/web3.js";
|
||||||
|
import { ethers } from "ethers";
|
||||||
|
import { Bridge__factory } from "../ethers-contracts";
|
||||||
|
import {
|
||||||
|
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||||
|
SOLANA_HOST,
|
||||||
|
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||||
|
} from "./consts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether or not an asset address on Ethereum is a wormhole wrapped asset
|
||||||
|
* @param provider
|
||||||
|
* @param assetAddress
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function getIsWrappedAssetEth(
|
||||||
|
provider: ethers.providers.Web3Provider,
|
||||||
|
assetAddress: string
|
||||||
|
) {
|
||||||
|
if (!assetAddress) return false;
|
||||||
|
const tokenBridge = Bridge__factory.connect(
|
||||||
|
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||||
|
provider
|
||||||
|
);
|
||||||
|
return await tokenBridge.isWrappedAsset(assetAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether or not an asset on Solana is a wormhole wrapped asset
|
||||||
|
* @param assetAddress
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function getIsWrappedAssetSol(mintAddress: string) {
|
||||||
|
if (!mintAddress) return false;
|
||||||
|
const { wrapped_meta_address } = await import("token-bridge");
|
||||||
|
const wrappedMetaAddress = wrapped_meta_address(
|
||||||
|
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||||
|
new PublicKey(mintAddress).toBytes()
|
||||||
|
);
|
||||||
|
const wrappedMetaAddressPK = new PublicKey(wrappedMetaAddress);
|
||||||
|
// TODO: share connection in context?
|
||||||
|
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||||
|
const wrappedMetaAccountInfo = await connection.getAccountInfo(
|
||||||
|
wrappedMetaAddressPK
|
||||||
|
);
|
||||||
|
return !!wrappedMetaAccountInfo;
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { Connection, PublicKey } from "@solana/web3.js";
|
||||||
|
import { ethers } from "ethers";
|
||||||
|
import { arrayify } from "ethers/lib/utils";
|
||||||
|
import { TokenImplementation__factory } from "../ethers-contracts";
|
||||||
|
import { uint8ArrayToHex } from "./array";
|
||||||
|
import {
|
||||||
|
ChainId,
|
||||||
|
CHAIN_ID_ETH,
|
||||||
|
CHAIN_ID_SOLANA,
|
||||||
|
SOLANA_HOST,
|
||||||
|
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||||
|
} from "./consts";
|
||||||
|
import { getIsWrappedAssetEth } from "./getIsWrappedAsset";
|
||||||
|
|
||||||
|
export interface WormholeWrappedInfo {
|
||||||
|
isWrapped: boolean;
|
||||||
|
chainId: ChainId;
|
||||||
|
assetAddress: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a origin chain and asset address on {originChain} for a provided Wormhole wrapped address
|
||||||
|
* @param provider
|
||||||
|
* @param wrappedAddress
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function getOriginalAssetEth(
|
||||||
|
provider: ethers.providers.Web3Provider,
|
||||||
|
wrappedAddress: string
|
||||||
|
): Promise<WormholeWrappedInfo> {
|
||||||
|
const isWrapped = await getIsWrappedAssetEth(provider, wrappedAddress);
|
||||||
|
if (isWrapped) {
|
||||||
|
const token = TokenImplementation__factory.connect(
|
||||||
|
wrappedAddress,
|
||||||
|
provider
|
||||||
|
);
|
||||||
|
const chainId = (await token.chainId()) as ChainId; // origin chain
|
||||||
|
const assetAddress = await token.nativeContract(); // origin address
|
||||||
|
// TODO: type this?
|
||||||
|
return {
|
||||||
|
isWrapped: true,
|
||||||
|
chainId,
|
||||||
|
assetAddress: uint8ArrayToHex(arrayify(assetAddress)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
isWrapped: false,
|
||||||
|
chainId: CHAIN_ID_ETH,
|
||||||
|
assetAddress: wrappedAddress,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOriginalAssetSol(
|
||||||
|
mintAddress: string
|
||||||
|
): Promise<WormholeWrappedInfo> {
|
||||||
|
if (mintAddress) {
|
||||||
|
// TODO: share some of this with getIsWrappedAssetSol, like a getWrappedMetaAccountAddress or something
|
||||||
|
const { parse_wrapped_meta, wrapped_meta_address } = await import(
|
||||||
|
"token-bridge"
|
||||||
|
);
|
||||||
|
const wrappedMetaAddress = wrapped_meta_address(
|
||||||
|
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||||
|
new PublicKey(mintAddress).toBytes()
|
||||||
|
);
|
||||||
|
const wrappedMetaAddressPK = new PublicKey(wrappedMetaAddress);
|
||||||
|
// TODO: share connection in context?
|
||||||
|
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||||
|
const wrappedMetaAccountInfo = await connection.getAccountInfo(
|
||||||
|
wrappedMetaAddressPK
|
||||||
|
);
|
||||||
|
if (wrappedMetaAccountInfo) {
|
||||||
|
const parsed = parse_wrapped_meta(wrappedMetaAccountInfo.data);
|
||||||
|
return {
|
||||||
|
isWrapped: true,
|
||||||
|
chainId: parsed.chain,
|
||||||
|
assetAddress: uint8ArrayToHex(parsed.token_address),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
isWrapped: false,
|
||||||
|
chainId: CHAIN_ID_SOLANA,
|
||||||
|
assetAddress: mintAddress,
|
||||||
|
};
|
||||||
|
}
|
|
@ -12,6 +12,11 @@ import Wallet from "@project-serum/sol-wallet-adapter";
|
||||||
import { Connection, PublicKey, Transaction } from "@solana/web3.js";
|
import { Connection, PublicKey, Transaction } from "@solana/web3.js";
|
||||||
import { postVaa } from "./postVaa";
|
import { postVaa } from "./postVaa";
|
||||||
import { ixFromRust } from "../sdk";
|
import { ixFromRust } from "../sdk";
|
||||||
|
import {
|
||||||
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||||
|
Token,
|
||||||
|
TOKEN_PROGRAM_ID,
|
||||||
|
} from "@solana/spl-token";
|
||||||
|
|
||||||
export async function redeemOnEth(
|
export async function redeemOnEth(
|
||||||
provider: ethers.providers.Web3Provider | undefined,
|
provider: ethers.providers.Web3Provider | undefined,
|
||||||
|
@ -30,7 +35,9 @@ export async function redeemOnEth(
|
||||||
export async function redeemOnSolana(
|
export async function redeemOnSolana(
|
||||||
wallet: Wallet | undefined,
|
wallet: Wallet | undefined,
|
||||||
payerAddress: string | undefined, //TODO: we may not need this since we have wallet
|
payerAddress: string | undefined, //TODO: we may not need this since we have wallet
|
||||||
signedVAA: Uint8Array
|
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;
|
if (!wallet || !wallet.publicKey || !payerAddress) return;
|
||||||
console.log("completing transfer");
|
console.log("completing transfer");
|
||||||
|
@ -40,7 +47,8 @@ export async function redeemOnSolana(
|
||||||
console.log("VAA:", signedVAA);
|
console.log("VAA:", signedVAA);
|
||||||
// TODO: share connection in context?
|
// TODO: share connection in context?
|
||||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||||
const { complete_transfer_wrapped_ix } = await import("token-bridge");
|
const { complete_transfer_wrapped_ix, complete_transfer_native_ix } =
|
||||||
|
await import("token-bridge");
|
||||||
|
|
||||||
await postVaa(
|
await postVaa(
|
||||||
connection,
|
connection,
|
||||||
|
@ -50,15 +58,63 @@ export async function redeemOnSolana(
|
||||||
Buffer.from(signedVAA)
|
Buffer.from(signedVAA)
|
||||||
);
|
);
|
||||||
console.log(Buffer.from(signedVAA).toString("hex"));
|
console.log(Buffer.from(signedVAA).toString("hex"));
|
||||||
const ix = ixFromRust(
|
const ixs = [];
|
||||||
complete_transfer_wrapped_ix(
|
if (isSolanaNative) {
|
||||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
console.log("COMPLETE TRANSFER NATIVE");
|
||||||
SOL_BRIDGE_ADDRESS,
|
ixs.push(
|
||||||
payerAddress,
|
ixFromRust(
|
||||||
signedVAA
|
complete_transfer_native_ix(
|
||||||
)
|
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||||
);
|
SOL_BRIDGE_ADDRESS,
|
||||||
const transaction = new Transaction().add(ix);
|
payerAddress,
|
||||||
|
signedVAA
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// TODO: we should always do this, they could buy wrapped somewhere else and transfer it back for the first time, but again, do it based on vaa
|
||||||
|
if (mintAddress) {
|
||||||
|
console.log("CHECK ASSOCIATED TOKEN ACCOUNT FOR", mintAddress);
|
||||||
|
const mintPublicKey = new PublicKey(mintAddress);
|
||||||
|
const associatedAddress = await Token.getAssociatedTokenAddress(
|
||||||
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||||
|
TOKEN_PROGRAM_ID,
|
||||||
|
mintPublicKey,
|
||||||
|
wallet.publicKey
|
||||||
|
);
|
||||||
|
const associatedAddressInfo = await connection.getAccountInfo(
|
||||||
|
associatedAddress
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"CREATE ASSOCIATED TOKEN ACCOUNT",
|
||||||
|
associatedAddress.toString()
|
||||||
|
);
|
||||||
|
if (!associatedAddressInfo) {
|
||||||
|
ixs.push(
|
||||||
|
await Token.createAssociatedTokenAccountInstruction(
|
||||||
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||||
|
TOKEN_PROGRAM_ID,
|
||||||
|
mintPublicKey,
|
||||||
|
associatedAddress,
|
||||||
|
wallet.publicKey,
|
||||||
|
wallet.publicKey
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("COMPLETE TRANSFER WRAPPED");
|
||||||
|
ixs.push(
|
||||||
|
ixFromRust(
|
||||||
|
complete_transfer_wrapped_ix(
|
||||||
|
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||||
|
SOL_BRIDGE_ADDRESS,
|
||||||
|
payerAddress,
|
||||||
|
signedVAA
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const transaction = new Transaction().add(...ixs);
|
||||||
const { blockhash } = await connection.getRecentBlockhash();
|
const { blockhash } = await connection.getRecentBlockhash();
|
||||||
transaction.recentBlockhash = blockhash;
|
transaction.recentBlockhash = blockhash;
|
||||||
transaction.feePayer = new PublicKey(payerAddress);
|
transaction.feePayer = new PublicKey(payerAddress);
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
TokenImplementation__factory,
|
TokenImplementation__factory,
|
||||||
} from "../ethers-contracts";
|
} from "../ethers-contracts";
|
||||||
import { getSignedVAA, ixFromRust } from "../sdk";
|
import { getSignedVAA, ixFromRust } from "../sdk";
|
||||||
|
import { hexToUint8Array } from "./array";
|
||||||
import {
|
import {
|
||||||
ChainId,
|
ChainId,
|
||||||
CHAIN_ID_ETH,
|
CHAIN_ID_ETH,
|
||||||
|
@ -108,14 +109,17 @@ export async function transferFromSolana(
|
||||||
amount: string,
|
amount: string,
|
||||||
decimals: number,
|
decimals: number,
|
||||||
targetAddressStr: string | undefined,
|
targetAddressStr: string | undefined,
|
||||||
targetChain: ChainId
|
targetChain: ChainId,
|
||||||
|
originAddress?: string,
|
||||||
|
originChain?: ChainId
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
!wallet ||
|
!wallet ||
|
||||||
!wallet.publicKey ||
|
!wallet.publicKey ||
|
||||||
!payerAddress ||
|
!payerAddress ||
|
||||||
!fromAddress ||
|
!fromAddress ||
|
||||||
!targetAddressStr
|
!targetAddressStr ||
|
||||||
|
(originChain && !originAddress)
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
const targetAddress = zeroPad(arrayify(targetAddressStr), 32);
|
const targetAddress = zeroPad(arrayify(targetAddressStr), 32);
|
||||||
|
@ -154,8 +158,12 @@ export async function transferFromSolana(
|
||||||
});
|
});
|
||||||
// TODO: pass in connection
|
// TODO: pass in connection
|
||||||
// Add transfer instruction to transaction
|
// Add transfer instruction to transaction
|
||||||
const { transfer_native_ix, approval_authority_address, emitter_address } =
|
const {
|
||||||
await import("token-bridge");
|
transfer_native_ix,
|
||||||
|
transfer_wrapped_ix,
|
||||||
|
approval_authority_address,
|
||||||
|
emitter_address,
|
||||||
|
} = await import("token-bridge");
|
||||||
const approvalIx = Token.createApproveInstruction(
|
const approvalIx = Token.createApproveInstruction(
|
||||||
TOKEN_PROGRAM_ID,
|
TOKEN_PROGRAM_ID,
|
||||||
new PublicKey(fromAddress),
|
new PublicKey(fromAddress),
|
||||||
|
@ -166,20 +174,39 @@ export async function transferFromSolana(
|
||||||
);
|
);
|
||||||
|
|
||||||
let messageKey = Keypair.generate();
|
let messageKey = Keypair.generate();
|
||||||
|
const isSolanaNative =
|
||||||
|
originChain === undefined || originChain === CHAIN_ID_SOLANA;
|
||||||
|
console.log(isSolanaNative ? "SENDING NATIVE" : "SENDING WRAPPED");
|
||||||
const ix = ixFromRust(
|
const ix = ixFromRust(
|
||||||
transfer_native_ix(
|
isSolanaNative
|
||||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
? transfer_native_ix(
|
||||||
SOL_BRIDGE_ADDRESS,
|
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||||
payerAddress,
|
SOL_BRIDGE_ADDRESS,
|
||||||
messageKey.publicKey.toString(),
|
payerAddress,
|
||||||
fromAddress,
|
messageKey.publicKey.toString(),
|
||||||
mintAddress,
|
fromAddress,
|
||||||
nonce,
|
mintAddress,
|
||||||
amountParsed,
|
nonce,
|
||||||
fee,
|
amountParsed,
|
||||||
targetAddress,
|
fee,
|
||||||
targetChain
|
targetAddress,
|
||||||
)
|
targetChain
|
||||||
|
)
|
||||||
|
: transfer_wrapped_ix(
|
||||||
|
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||||
|
SOL_BRIDGE_ADDRESS,
|
||||||
|
payerAddress,
|
||||||
|
messageKey.publicKey.toString(),
|
||||||
|
fromAddress,
|
||||||
|
payerAddress,
|
||||||
|
originChain as number, // checked by isSolanaNative
|
||||||
|
zeroPad(hexToUint8Array(originAddress as string), 32), // checked by initial check
|
||||||
|
nonce,
|
||||||
|
amountParsed,
|
||||||
|
fee,
|
||||||
|
targetAddress,
|
||||||
|
targetChain
|
||||||
|
)
|
||||||
);
|
);
|
||||||
const transaction = new Transaction().add(transferIx, approvalIx, ix);
|
const transaction = new Transaction().add(transferIx, approvalIx, ix);
|
||||||
const { blockhash } = await connection.getRecentBlockhash();
|
const { blockhash } = await connection.getRecentBlockhash();
|
||||||
|
|
Loading…
Reference in New Issue