bridge_ui: sol, eth bidirectional transfers

Change-Id: I0bbbbffddd3bec7771c79953556271000731cd36
This commit is contained in:
Evan Gray 2021-08-12 16:45:14 -04:00 committed by Evan Gray
parent b9359aab87
commit 6875559d4c
20 changed files with 633 additions and 156 deletions

View File

@ -3,7 +3,6 @@ import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
import { useSolanaWallet } from "../../contexts/SolanaWalletContext";
import useWrappedAsset from "../../hooks/useWrappedAsset";
import { setIsSending, setSignedVAAHex } from "../../store/attestSlice";
import {
selectAttestIsSendComplete,
@ -11,7 +10,6 @@ import {
selectAttestIsTargetComplete,
selectAttestSourceAsset,
selectAttestSourceChain,
selectAttestTargetChain,
} from "../../store/selectors";
import { uint8ArrayToHex } from "../../utils/array";
import attestFrom, {
@ -35,21 +33,12 @@ function Send() {
const dispatch = useDispatch();
const sourceChain = useSelector(selectAttestSourceChain);
const sourceAsset = useSelector(selectAttestSourceAsset);
const targetChain = useSelector(selectAttestTargetChain);
const isTargetComplete = useSelector(selectAttestIsTargetComplete);
const isSending = useSelector(selectAttestIsSending);
const isSendComplete = useSelector(selectAttestIsSendComplete);
const { provider, signer } = useEthereumProvider();
const { wallet } = useSolanaWallet();
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
const handleAttestClick = useCallback(() => {
// TODO: more generic way of calling these

View File

@ -20,7 +20,7 @@ import Target from "./Target";
// TODO: ensure that both wallets are connected to the same known network
function Attest() {
useGetBalanceEffect();
useGetBalanceEffect("source");
const dispatch = useDispatch();
const activeStep = useSelector(selectAttestActiveStep);
const signedVAAHex = useSelector(selectAttestSignedVAAHex);

View File

@ -2,15 +2,18 @@ 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 useTransferSignedVAA from "../../hooks/useTransferSignedVAA";
import {
selectTransferIsRedeeming,
selectTransferIsSourceAssetWormholeWrapped,
selectTransferOriginChain,
selectTransferTargetAsset,
selectTransferTargetChain,
} from "../../store/selectors";
import { setIsRedeeming } from "../../store/transferSlice";
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "../../utils/consts";
import redeemOn, { redeemOnEth, redeemOnSolana } from "../../utils/redeemOn";
import { useSolanaWallet } from "../../contexts/SolanaWalletContext";
const useStyles = makeStyles((theme) => ({
transferButton: {
@ -23,7 +26,12 @@ 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 { wallet } = useSolanaWallet();
const solPK = wallet?.publicKey;
const { provider, signer } = useEthereumProvider();
@ -44,9 +52,26 @@ function Redeem() {
signedVAA
) {
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 (
<div style={{ position: "relative" }}>
<Button

View File

@ -1,18 +1,28 @@
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 { useEthereumProvider } from "../../contexts/EthereumProviderContext";
import { useSolanaWallet } from "../../contexts/SolanaWalletContext";
import useWrappedAsset from "../../hooks/useWrappedAsset";
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";
@ -37,8 +47,11 @@ function Send() {
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);
@ -48,16 +61,43 @@ function Send() {
const sourceParsedTokenAccount = useSelector(
selectTransferSourceParsedTokenAccount
);
const tokenPK = sourceParsedTokenAccount?.publicKey;
const sourceTokenPublicKey = sourceParsedTokenAccount?.publicKey;
const decimals = sourceParsedTokenAccount?.decimals;
const {
isLoading: isCheckingWrapped,
// isWrapped,
wrappedAsset,
} = useWrappedAsset(targetChain, sourceChain, sourceAsset, provider);
// TODO: check this and send to separate flow
const isWrapped = true;
console.log(isCheckingWrapped, isWrapped, wrappedAsset);
const 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
@ -72,6 +112,7 @@ function Send() {
(async () => {
dispatch(setIsSending(true));
try {
console.log("actually sending", tpkRef.current);
const vaaBytes = await transferFromEth(
provider,
signer,
@ -79,7 +120,7 @@ function Send() {
decimals,
amount,
targetChain,
solPK?.toBytes()
tpkRef.current
);
console.log("bytes in transfer", vaaBytes);
vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
@ -101,12 +142,14 @@ function Send() {
const vaaBytes = await transferFromSolana(
wallet,
solPK?.toString(),
tokenPK,
sourceTokenPublicKey,
sourceAsset,
amount,
amount, //TODO: avoid decimals, pass in parsed amount
decimals,
signerAddress,
targetChain
targetChain,
originAsset,
originChain
);
console.log("bytes in transfer", vaaBytes);
vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
@ -125,11 +168,13 @@ function Send() {
signerAddress,
wallet,
solPK,
tokenPK,
sourceTokenPublicKey,
sourceAsset,
amount,
decimals,
targetChain,
originAsset,
originChain,
]);
return (
<>

View File

@ -73,6 +73,7 @@ function Source() {
))}
</TextField>
<KeyAndBalance chainId={sourceChain} balance={uiAmountString} />
{/* TODO: token list for eth, check own */}
<TextField
placeholder="Asset"
fullWidth

View File

@ -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 { useDispatch, useSelector } from "react-redux";
import {
selectTransferIsSourceAssetWormholeWrapped,
selectTransferIsTargetComplete,
selectTransferShouldLockFields,
selectTransferSourceChain,
selectTransferTargetAsset,
selectTransferTargetBalanceString,
selectTransferTargetChain,
} from "../../store/selectors";
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";
const useStyles = makeStyles((theme) => ({
transferField: {
marginTop: theme.spacing(5),
},
}));
function Target() {
const classes = useStyles();
const dispatch = useDispatch();
const sourceChain = useSelector(selectTransferSourceChain);
const chains = useMemo(
@ -19,6 +31,19 @@ function Target() {
[sourceChain]
);
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 shouldLockFields = useSelector(selectTransferShouldLockFields);
const handleTargetChange = useCallback(
@ -48,8 +73,14 @@ function Target() {
</MenuItem>
))}
</TextField>
{/* TODO: determine "to" token address */}
<KeyAndBalance chainId={targetChain} />
<KeyAndBalance chainId={targetChain} balance={uiAmountString} />
<TextField
placeholder="Asset"
fullWidth
className={classes.transferField}
value={readableTargetAsset}
disabled={true}
/>
<Button
disabled={!isTargetComplete}
onClick={handleNextClick}

View File

@ -6,6 +6,8 @@ import {
Stepper,
} from "@material-ui/core";
import { useDispatch, useSelector } from "react-redux";
import useCheckIfWormholeWrapped from "../../hooks/useCheckIfWormholeWrapped";
import useFetchTargetAsset from "../../hooks/useFetchTargetAsset";
import useGetBalanceEffect from "../../hooks/useGetBalanceEffect";
import {
selectTransferActiveStep,
@ -23,7 +25,10 @@ import Target from "./Target";
// TODO: warn if amount exceeds balance
function Transfer() {
useGetBalanceEffect();
useGetBalanceEffect("source");
useCheckIfWormholeWrapped();
useFetchTargetAsset();
useGetBalanceEffect("target");
const dispatch = useDispatch();
const activeStep = useSelector(selectTransferActiveStep);
const signedVAAHex = useSelector(selectTransferSignedVAAHex);

View File

@ -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;

View File

@ -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;

View File

@ -8,8 +8,14 @@ import { TokenImplementation__factory } from "../ethers-contracts";
import {
selectTransferSourceAsset,
selectTransferSourceChain,
selectTransferTargetAsset,
selectTransferTargetChain,
} 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";
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 sourceChain = useSelector(selectTransferSourceChain);
const sourceAsset = useSelector(selectTransferSourceAsset);
const setAction =
sourceOrTarget === "source"
? setSourceParsedTokenAccount
: setTargetParsedTokenAccount;
const lookupChain = useSelector(
sourceOrTarget === "source"
? selectTransferSourceChain
: selectTransferTargetChain
);
const lookupAsset = useSelector(
sourceOrTarget === "source"
? selectTransferSourceAsset
: selectTransferTargetAsset
);
const { wallet } = useSolanaWallet();
const solPK = wallet?.publicKey;
const { provider, signerAddress } = useEthereumProvider();
useEffect(() => {
// TODO: loading state
dispatch(setSourceParsedTokenAccount(undefined));
if (!sourceAsset) {
dispatch(setAction(undefined));
if (!lookupAsset) {
return;
}
let cancelled = false;
if (sourceChain === CHAIN_ID_SOLANA && solPK) {
if (lookupChain === CHAIN_ID_SOLANA && solPK) {
let mint;
try {
mint = new PublicKey(sourceAsset);
mint = new PublicKey(
sourceOrTarget === "source"
? lookupAsset
: hexToUint8Array(lookupAsset)
);
} catch (e) {
return;
}
@ -54,9 +80,11 @@ function useGetBalanceEffect() {
.getParsedTokenAccountsByOwner(solPK, { mint })
.then(({ value }) => {
if (!cancelled) {
console.log("parsed token accounts", value);
if (value.length) {
// TODO: allow selection between these target accounts
dispatch(
setSourceParsedTokenAccount(
setAction(
createParsedTokenAccount(
value[0].pubkey,
value[0].account.data.parsed?.info?.tokenAmount?.amount,
@ -78,15 +106,15 @@ function useGetBalanceEffect() {
}
});
}
if (sourceChain === CHAIN_ID_ETH && provider && signerAddress) {
const token = TokenImplementation__factory.connect(sourceAsset, provider);
if (lookupChain === CHAIN_ID_ETH && provider && signerAddress) {
const token = TokenImplementation__factory.connect(lookupAsset, provider);
token
.decimals()
.then((decimals) => {
token.balanceOf(signerAddress).then((n) => {
if (!cancelled) {
dispatch(
setSourceParsedTokenAccount(
setAction(
// TODO: verify accuracy
createParsedTokenAccount(
undefined,
@ -109,7 +137,16 @@ function useGetBalanceEffect() {
return () => {
cancelled = true;
};
}, [dispatch, sourceChain, sourceAsset, solPK, provider, signerAddress]);
}, [
dispatch,
sourceOrTarget,
setAction,
lookupChain,
lookupAsset,
solPK,
provider,
signerAddress,
]);
}
export default useGetBalanceEffect;

View File

@ -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;

View File

@ -1,6 +1,7 @@
import { ethers } from "ethers";
import { parseUnits } from "ethers/lib/utils";
import { RootState } from ".";
import { CHAIN_ID_SOLANA } from "../utils/consts";
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "../utils/consts";
/*
* Attest
@ -43,6 +44,12 @@ export const selectTransferSourceChain = (state: RootState) =>
state.transfer.sourceChain;
export const selectTransferSourceAsset = (state: RootState) =>
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) =>
state.transfer.sourceParsedTokenAccount;
export const selectTransferSourceBalanceString = (state: RootState) =>
@ -50,6 +57,12 @@ export const selectTransferSourceBalanceString = (state: RootState) =>
export const selectTransferAmount = (state: RootState) => state.transfer.amount;
export const selectTransferTargetChain = (state: RootState) =>
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) =>
state.transfer.signedVAAHex;
export const selectTransferIsSending = (state: RootState) =>
@ -79,7 +92,15 @@ export const selectTransferIsSourceComplete = (state: RootState) =>
);
// TODO: check wrapped asset exists or is native transfer
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) =>
!!selectTransferSignedVAAHex(state);
export const selectTransferShouldLockFields = (state: RootState) =>

View File

@ -6,6 +6,7 @@ import {
ETH_TEST_TOKEN_ADDRESS,
SOL_TEST_TOKEN_ADDRESS,
} from "../utils/consts";
import { WormholeWrappedInfo } from "../utils/getOriginalAsset";
const LAST_STEP = 3;
@ -23,9 +24,14 @@ export interface TransferState {
activeStep: Steps;
sourceChain: ChainId;
sourceAsset: string;
isSourceAssetWormholeWrapped: boolean | undefined;
originChain: ChainId | undefined;
originAsset: string | undefined;
sourceParsedTokenAccount: ParsedTokenAccount | undefined;
amount: string;
targetChain: ChainId;
targetAsset: string | null | undefined;
targetParsedTokenAccount: ParsedTokenAccount | undefined;
signedVAAHex: string | undefined;
isSending: boolean;
isRedeeming: boolean;
@ -35,9 +41,14 @@ const initialState: TransferState = {
activeStep: 0,
sourceChain: CHAIN_ID_SOLANA,
sourceAsset: SOL_TEST_TOKEN_ADDRESS,
isSourceAssetWormholeWrapped: false,
sourceParsedTokenAccount: undefined,
originChain: undefined,
originAsset: undefined,
amount: "",
targetChain: CHAIN_ID_ETH,
targetAsset: undefined,
targetParsedTokenAccount: undefined,
signedVAAHex: undefined,
isSending: false,
isRedeeming: false,
@ -73,6 +84,20 @@ export const transferSlice = createSlice({
setSourceAsset: (state, action: PayloadAction<string>) => {
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: (
state,
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>) => {
state.signedVAAHex = action.payload;
state.isSending = false;
@ -117,9 +154,12 @@ export const {
setStep,
setSourceChain,
setSourceAsset,
setSourceWormholeWrappedInfo,
setSourceParsedTokenAccount,
setAmount,
setTargetChain,
setTargetAsset,
setTargetParsedTokenAccount,
setSignedVAAHex,
setIsSending,
setIsRedeeming,

View File

@ -56,7 +56,7 @@ export async function attestFromEth(
const { vaaBytes } = await getSignedVAA(
CHAIN_ID_ETH,
emitterAddress,
sequence
sequence.toString()
);
console.log("SIGNED VAA:", vaaBytes);
return vaaBytes;

View File

@ -57,6 +57,7 @@ export async function createWrappedOnSolana(
signedVAA
)
);
console.log(ix.keys.map((x) => x.pubkey.toString()));
const transaction = new Transaction().add(ix);
const { blockhash } = await connection.getRecentBlockhash();
transaction.recentBlockhash = blockhash;

View File

@ -10,7 +10,14 @@ import {
SOL_TOKEN_BRIDGE_ADDRESS,
} 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,
originChain: ChainId,
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,
originAsset: string
) {

View File

@ -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;
}

View File

@ -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,
};
}

View File

@ -12,6 +12,11 @@ import Wallet from "@project-serum/sol-wallet-adapter";
import { Connection, PublicKey, Transaction } from "@solana/web3.js";
import { postVaa } from "./postVaa";
import { ixFromRust } from "../sdk";
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
Token,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
export async function redeemOnEth(
provider: ethers.providers.Web3Provider | undefined,
@ -30,7 +35,9 @@ export async function redeemOnEth(
export async function redeemOnSolana(
wallet: Wallet | undefined,
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;
console.log("completing transfer");
@ -40,7 +47,8 @@ export async function redeemOnSolana(
console.log("VAA:", signedVAA);
// TODO: share connection in context?
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(
connection,
@ -50,15 +58,63 @@ export async function redeemOnSolana(
Buffer.from(signedVAA)
);
console.log(Buffer.from(signedVAA).toString("hex"));
const ix = ixFromRust(
complete_transfer_wrapped_ix(
SOL_TOKEN_BRIDGE_ADDRESS,
SOL_BRIDGE_ADDRESS,
payerAddress,
signedVAA
)
);
const transaction = new Transaction().add(ix);
const ixs = [];
if (isSolanaNative) {
console.log("COMPLETE TRANSFER NATIVE");
ixs.push(
ixFromRust(
complete_transfer_native_ix(
SOL_TOKEN_BRIDGE_ADDRESS,
SOL_BRIDGE_ADDRESS,
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();
transaction.recentBlockhash = blockhash;
transaction.feePayer = new PublicKey(payerAddress);

View File

@ -15,6 +15,7 @@ import {
TokenImplementation__factory,
} from "../ethers-contracts";
import { getSignedVAA, ixFromRust } from "../sdk";
import { hexToUint8Array } from "./array";
import {
ChainId,
CHAIN_ID_ETH,
@ -108,14 +109,17 @@ export async function transferFromSolana(
amount: string,
decimals: number,
targetAddressStr: string | undefined,
targetChain: ChainId
targetChain: ChainId,
originAddress?: string,
originChain?: ChainId
) {
if (
!wallet ||
!wallet.publicKey ||
!payerAddress ||
!fromAddress ||
!targetAddressStr
!targetAddressStr ||
(originChain && !originAddress)
)
return;
const targetAddress = zeroPad(arrayify(targetAddressStr), 32);
@ -154,8 +158,12 @@ export async function transferFromSolana(
});
// TODO: pass in connection
// Add transfer instruction to transaction
const { transfer_native_ix, approval_authority_address, emitter_address } =
await import("token-bridge");
const {
transfer_native_ix,
transfer_wrapped_ix,
approval_authority_address,
emitter_address,
} = await import("token-bridge");
const approvalIx = Token.createApproveInstruction(
TOKEN_PROGRAM_ID,
new PublicKey(fromAddress),
@ -166,20 +174,39 @@ export async function transferFromSolana(
);
let messageKey = Keypair.generate();
const isSolanaNative =
originChain === undefined || originChain === CHAIN_ID_SOLANA;
console.log(isSolanaNative ? "SENDING NATIVE" : "SENDING WRAPPED");
const ix = ixFromRust(
transfer_native_ix(
SOL_TOKEN_BRIDGE_ADDRESS,
SOL_BRIDGE_ADDRESS,
payerAddress,
messageKey.publicKey.toString(),
fromAddress,
mintAddress,
nonce,
amountParsed,
fee,
targetAddress,
targetChain
)
isSolanaNative
? transfer_native_ix(
SOL_TOKEN_BRIDGE_ADDRESS,
SOL_BRIDGE_ADDRESS,
payerAddress,
messageKey.publicKey.toString(),
fromAddress,
mintAddress,
nonce,
amountParsed,
fee,
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 { blockhash } = await connection.getRecentBlockhash();