bridge_ui: support 2 step solana redeem

Change-Id: Ic2e3fdd97a8fdfb6aae7e22678f2d82a08fed174
This commit is contained in:
Evan Gray 2021-09-21 17:02:42 -04:00
parent 1f917c56d8
commit c67410cd15
14 changed files with 203 additions and 51 deletions

View File

@ -103,7 +103,7 @@ export default function Workflow({
const connection = useMemo(
() => new Connection(SOLANA_HOST, "confirmed"),
[]
); //TODO confirmed or finalized?
);
const wallet = useSolanaWallet();
const { isReady } = useIsWalletReady(CHAIN_ID_SOLANA);
const solanaTokenMap = useSolanaTokenMap();

View File

@ -11,6 +11,7 @@ import {
import {
Box,
Button,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
@ -27,6 +28,7 @@ import { Restore } from "@material-ui/icons";
import { Alert } from "@material-ui/lab";
import { Connection } from "@solana/web3.js";
import { BigNumber, ethers } from "ethers";
import { useSnackbar } from "notistack";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
@ -50,6 +52,7 @@ import {
} from "../../utils/consts";
import { getSignedVAAWithRetry } from "../../utils/getSignedVAAWithRetry";
import { METADATA_REPLACE } from "../../utils/metaplex";
import parseError from "../../utils/parseError";
import KeyAndBalance from "../KeyAndBalance";
const useStyles = makeStyles((theme) => ({
@ -60,7 +63,11 @@ const useStyles = makeStyles((theme) => ({
},
}));
async function eth(provider: ethers.providers.Web3Provider, tx: string) {
async function eth(
provider: ethers.providers.Web3Provider,
tx: string,
enqueueSnackbar: any
) {
try {
const receipt = await provider.getTransactionReceipt(tx);
const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS);
@ -71,14 +78,15 @@ async function eth(provider: ethers.providers.Web3Provider, tx: string) {
sequence.toString(),
WORMHOLE_RPC_HOSTS.length
);
return uint8ArrayToHex(vaaBytes);
return { vaa: uint8ArrayToHex(vaaBytes), error: null };
} catch (e) {
console.error(e);
enqueueSnackbar(parseError(e), { variant: "error" });
return { vaa: null, error: parseError(e) };
}
return "";
}
async function solana(tx: string) {
async function solana(tx: string, enqueueSnackbar: any) {
try {
const connection = new Connection(SOLANA_HOST, "confirmed");
const info = await connection.getTransaction(tx);
@ -95,11 +103,12 @@ async function solana(tx: string) {
sequence.toString(),
WORMHOLE_RPC_HOSTS.length
);
return uint8ArrayToHex(vaaBytes);
return { vaa: uint8ArrayToHex(vaaBytes), error: null };
} catch (e) {
console.error(e);
enqueueSnackbar(parseError(e), { variant: "error" });
return { vaa: null, error: parseError(e) };
}
return "";
}
// note: actual first byte is message type
@ -114,7 +123,7 @@ async function solana(tx: string) {
// ? u16 recipient_chain
// TODO: move to wasm / sdk, share with solana
const parsePayload = (arr: Buffer) => {
export const parsePayload = (arr: Buffer) => {
const originAddress = arr.slice(1, 1 + 32).toString("hex");
const originChain = arr.readUInt16BE(33) as ChainId;
const symbol = Buffer.from(arr.slice(35, 35 + 32))
@ -152,12 +161,16 @@ function RecoveryDialogContent({
onClose: () => void;
disabled: boolean;
}) {
const { enqueueSnackbar } = useSnackbar();
const dispatch = useDispatch();
const { provider } = useEthereumProvider();
const currentSourceChain = useSelector(selectNFTSourceChain);
const [recoverySourceChain, setRecoverySourceChain] =
useState(currentSourceChain);
const [recoverySourceTx, setRecoverySourceTx] = useState("");
const [recoverySourceTxIsLoading, setRecoverySourceTxIsLoading] =
useState(false);
const [recoverySourceTxError, setRecoverySourceTxError] = useState("");
const currentSignedVAA = useSelector(selectNFTSignedVAAHex);
const [recoverySignedVAA, setRecoverySignedVAA] = useState(currentSignedVAA);
const [recoveryParsedVAA, setRecoveryParsedVAA] = useState<any>(null);
@ -171,25 +184,48 @@ function RecoveryDialogContent({
if (recoverySourceTx) {
let cancelled = false;
if (recoverySourceChain === CHAIN_ID_ETH && provider) {
setRecoverySourceTxError("");
setRecoverySourceTxIsLoading(true);
(async () => {
const vaa = await eth(provider, recoverySourceTx);
const { vaa, error } = await eth(
provider,
recoverySourceTx,
enqueueSnackbar
);
if (!cancelled) {
setRecoverySourceTxIsLoading(false);
if (vaa) {
setRecoverySignedVAA(vaa);
}
if (error) {
setRecoverySourceTxError(error);
}
}
})();
} else if (recoverySourceChain === CHAIN_ID_SOLANA) {
setRecoverySourceTxError("");
setRecoverySourceTxIsLoading(true);
(async () => {
const vaa = await solana(recoverySourceTx);
const { vaa, error } = await solana(
recoverySourceTx,
enqueueSnackbar
);
if (!cancelled) {
setRecoverySourceTxIsLoading(false);
if (vaa) {
setRecoverySignedVAA(vaa);
}
if (error) {
setRecoverySourceTxError(error);
}
}
})();
}
return () => {
cancelled = true;
};
}
}, [recoverySourceChain, recoverySourceTx, provider]);
}, [recoverySourceChain, recoverySourceTx, provider, enqueueSnackbar]);
useEffect(() => {
setRecoverySignedVAA(currentSignedVAA);
}, [currentSignedVAA]);
@ -280,23 +316,45 @@ function RecoveryDialogContent({
<KeyAndBalance chainId={recoverySourceChain} />
) : null}
<TextField
label="Source Tx"
disabled={!!recoverySignedVAA}
label="Source Tx (paste here)"
disabled={!!recoverySignedVAA || recoverySourceTxIsLoading}
value={recoverySourceTx}
onChange={handleSourceTxChange}
error={!!recoverySourceTxError}
helperText={recoverySourceTxError}
fullWidth
margin="normal"
/>
<Box position="relative">
<Box mt={4}>
<Typography>or</Typography>
</Box>
<TextField
label="Signed VAA (Hex)"
disabled={recoverySourceTxIsLoading}
value={recoverySignedVAA || ""}
onChange={handleSignedVAAChange}
fullWidth
margin="normal"
/>
{recoverySourceTxIsLoading ? (
<Box
position="absolute"
style={{
top: 0,
right: 0,
left: 0,
bottom: 0,
backgroundColor: "rgba(0,0,0,0.5)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<CircularProgress />
</Box>
) : null}
</Box>
<Box my={4}>
<Divider />
</Box>

View File

@ -12,6 +12,7 @@ import {
import { CHAINS_BY_ID } from "../../utils/consts";
import ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance";
import ShowTx from "../ShowTx";
import StepDescription from "../StepDescription";
import TransactionProgress from "../TransactionProgress";
import WaitingForWalletMessage from "./WaitingForWalletMessage";
@ -55,6 +56,7 @@ function Send() {
Transfer
</ButtonWithLoader>
<WaitingForWalletMessage />
{transferTx ? <ShowTx chainId={sourceChain} tx={transferTx} /> : null}
<TransactionProgress
chainId={sourceChain}
tx={transferTx}

View File

@ -173,7 +173,7 @@ export default function NFTViewer({
cancelled = true;
};
}
}, [uri, metadata.image]);
}, [uri]);
const classes = useStyles();
const animLower = metadata.animation_url?.toLowerCase();
// const has3DModel = animLower?.endsWith('gltf') || animLower?.endsWith('glb')

View File

@ -97,7 +97,7 @@ function useGetBalanceEffect(sourceOrTarget: "source" | "target") {
} catch (e) {
return;
}
const connection = new Connection(SOLANA_HOST, "finalized");
const connection = new Connection(SOLANA_HOST, "confirmed");
connection
.getParsedTokenAccountsByOwner(solPK, { mint })
.then(({ value }) => {

View File

@ -275,7 +275,7 @@ const getSolanaParsedTokenAccounts = (
dispatch: Dispatch,
nft: boolean
) => {
const connection = new Connection(SOLANA_HOST, "finalized");
const connection = new Connection(SOLANA_HOST, "confirmed");
dispatch(
nft ? fetchSourceParsedTokenAccountsNFT() : fetchSourceParsedTokenAccounts()
);
@ -426,7 +426,7 @@ function useGetAvailableTokens(nft: boolean = false) {
//SOLT devnet token
// mintAddresses.push("2WDq7wSs9zYrpx2kbHDA4RUTRch2CCTP6ZWaH4GNfnQQ");
const connection = new Connection(SOLANA_HOST, "finalized");
const connection = new Connection(SOLANA_HOST, "confirmed");
getMultipleAccountsRPC(
connection,
mintAddresses.map((x) => new PublicKey(x))

View File

@ -1,28 +1,36 @@
import {
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
getClaimAddressSolana,
postVaaSolana,
} from "@certusone/wormhole-sdk";
import {
createMetaOnSolana,
getForeignAssetSol,
isNFTVAASolanaNative,
redeemOnEth,
redeemOnSolana,
} from "@certusone/wormhole-sdk/lib/nft_bridge";
import { arrayify } from "@ethersproject/bytes";
import { WalletContextState } from "@solana/wallet-adapter-react";
import { Connection } from "@solana/web3.js";
import { Signer } from "ethers";
import { useSnackbar } from "notistack";
import { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { parsePayload } from "../components/NFT/Recovery";
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
import { setIsRedeeming, setRedeemTx } from "../store/nftSlice";
import { selectNFTIsRedeeming, selectNFTTargetChain } from "../store/selectors";
import { hexToUint8Array } from "../utils/array";
import {
ETH_NFT_BRIDGE_ADDRESS,
SOLANA_HOST,
SOL_BRIDGE_ADDRESS,
SOL_NFT_BRIDGE_ADDRESS,
} from "../utils/consts";
import { getMetadataAddress } from "../utils/metaplex";
import parseError from "../utils/parseError";
import { signSendAndConfirm } from "../utils/solana";
import useNFTSignedVAA from "./useNFTSignedVAA";
@ -63,6 +71,13 @@ async function solana(
throw new Error("wallet.signTransaction is undefined");
}
const connection = new Connection(SOLANA_HOST, "confirmed");
const claimAddress = await getClaimAddressSolana(
SOL_NFT_BRIDGE_ADDRESS,
signedVAA
);
const claimInfo = await connection.getAccountInfo(claimAddress);
let txid;
if (!claimInfo) {
await postVaaSolana(
connection,
wallet.signTransaction,
@ -78,9 +93,38 @@ async function solana(
payerAddress,
signedVAA
);
const txid = await signSendAndConfirm(wallet, connection, transaction);
txid = await signSendAndConfirm(wallet, connection, transaction);
// TODO: didn't want to make an info call we didn't need, can we get the block without it by modifying the above call?
dispatch(setRedeemTx({ id: txid, block: 1 }));
}
const isNative = await isNFTVAASolanaNative(signedVAA);
if (!isNative) {
const { parse_vaa } = await import(
"@certusone/wormhole-sdk/lib/solana/core/bridge"
);
const parsedVAA = parse_vaa(signedVAA);
const { originChain, originAddress, tokenId } = parsePayload(
Buffer.from(new Uint8Array(parsedVAA.payload))
);
const mintAddress = await getForeignAssetSol(
SOL_NFT_BRIDGE_ADDRESS,
originChain,
hexToUint8Array(originAddress),
arrayify(tokenId)
);
const [metadataAddress] = await getMetadataAddress(mintAddress);
const metadata = await connection.getAccountInfo(metadataAddress);
if (!metadata) {
const transaction = await createMetaOnSolana(
connection,
SOL_BRIDGE_ADDRESS,
SOL_NFT_BRIDGE_ADDRESS,
payerAddress,
signedVAA
);
txid = await signSendAndConfirm(wallet, connection, transaction);
}
}
dispatch(setRedeemTx({ id: txid || "", block: 1 }));
enqueueSnackbar("Transaction confirmed", { variant: "success" });
} catch (e) {
enqueueSnackbar(parseError(e), { variant: "error" });

View File

@ -15,7 +15,7 @@ const getMetaplexData = async (mintAddresses: string[]) => {
promises.push(getMetadataAddress(address));
}
const metaAddresses = await Promise.all(promises);
const connection = new Connection(SOLANA_HOST, "finalized");
const connection = new Connection(SOLANA_HOST, "confirmed");
const results = await getMultipleAccountsRPC(
connection,
metaAddresses.map((pair) => pair && pair[0])

View File

@ -38,7 +38,7 @@ export async function getMultipleAccountsRPC(
connection: Connection,
pubkeys: PublicKey[]
): Promise<(AccountInfo<Buffer> | null)[]> {
return getMultipleAccounts(connection, pubkeys, "finalized");
return getMultipleAccounts(connection, pubkeys, "confirmed");
}
export const getMultipleAccounts = async (

View File

@ -2,6 +2,14 @@
## Unreleased
## 0.0.5
### Added
NFT Bridge Support
getClaimAddressSolana
createMetaOnSolana
## 0.0.4
### Added

View File

@ -1,6 +1,6 @@
{
"name": "@certusone/wormhole-sdk",
"version": "0.0.4",
"version": "0.0.5",
"description": "SDK for interacting with Wormhole",
"homepage": "https://wormholenetwork.com",
"main": "lib/index.js",

View File

@ -0,0 +1,9 @@
import { PublicKey } from "@solana/web3.js";
export async function getClaimAddressSolana(
programAddress: string,
signedVAA: Uint8Array
) {
const { claim_address } = await import("../solana/core/bridge");
return new PublicKey(claim_address(programAddress, signedVAA));
}

View File

@ -1,2 +1,3 @@
export * from "./getClaimAddress";
export * from "./getEmitterAddress";
export * from "./parseSequenceFromLog";

View File

@ -15,6 +15,15 @@ export async function redeemOnEth(
return receipt;
}
export async function isNFTVAASolanaNative(signedVAA: Uint8Array) {
const { parse_vaa } = await import("../solana/core/bridge");
const parsedVAA = parse_vaa(signedVAA);
const isSolanaNative =
Buffer.from(new Uint8Array(parsedVAA.payload)).readUInt16BE(33) ===
CHAIN_ID_SOLANA;
return isSolanaNative;
}
export async function redeemOnSolana(
connection: Connection,
bridgeAddress: string,
@ -22,11 +31,7 @@ export async function redeemOnSolana(
payerAddress: string,
signedVAA: Uint8Array
) {
const { parse_vaa } = await import("../solana/core/bridge");
const parsedVAA = parse_vaa(signedVAA);
const isSolanaNative =
Buffer.from(new Uint8Array(parsedVAA.payload)).readUInt16BE(33) ===
CHAIN_ID_SOLANA;
const isSolanaNative = await isNFTVAASolanaNative(signedVAA);
const { complete_transfer_wrapped_ix, complete_transfer_native_ix } =
await import("../solana/nft/nft_bridge");
const ixs = [];
@ -61,3 +66,28 @@ export async function redeemOnSolana(
transaction.feePayer = new PublicKey(payerAddress);
return transaction;
}
export async function createMetaOnSolana(
connection: Connection,
bridgeAddress: string,
tokenBridgeAddress: string,
payerAddress: string,
signedVAA: Uint8Array
) {
const { complete_transfer_wrapped_meta_ix } = await import(
"../solana/nft/nft_bridge"
);
const ix = ixFromRust(
complete_transfer_wrapped_meta_ix(
tokenBridgeAddress,
bridgeAddress,
payerAddress,
signedVAA
)
);
const transaction = new Transaction().add(ix);
const { blockhash } = await connection.getRecentBlockhash();
transaction.recentBlockhash = blockhash;
transaction.feePayer = new PublicKey(payerAddress);
return transaction;
}