bridge_ui: support 2 step solana redeem
Change-Id: Ic2e3fdd97a8fdfb6aae7e22678f2d82a08fed174
This commit is contained in:
parent
1f917c56d8
commit
c67410cd15
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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" });
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -2,6 +2,14 @@
|
|||
|
||||
## Unreleased
|
||||
|
||||
## 0.0.5
|
||||
|
||||
### Added
|
||||
|
||||
NFT Bridge Support
|
||||
getClaimAddressSolana
|
||||
createMetaOnSolana
|
||||
|
||||
## 0.0.4
|
||||
|
||||
### Added
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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));
|
||||
}
|
|
@ -1,2 +1,3 @@
|
|||
export * from "./getClaimAddress";
|
||||
export * from "./getEmitterAddress";
|
||||
export * from "./parseSequenceFromLog";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue