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(
|
const connection = useMemo(
|
||||||
() => new Connection(SOLANA_HOST, "confirmed"),
|
() => new Connection(SOLANA_HOST, "confirmed"),
|
||||||
[]
|
[]
|
||||||
); //TODO confirmed or finalized?
|
);
|
||||||
const wallet = useSolanaWallet();
|
const wallet = useSolanaWallet();
|
||||||
const { isReady } = useIsWalletReady(CHAIN_ID_SOLANA);
|
const { isReady } = useIsWalletReady(CHAIN_ID_SOLANA);
|
||||||
const solanaTokenMap = useSolanaTokenMap();
|
const solanaTokenMap = useSolanaTokenMap();
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
CircularProgress,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
@ -27,6 +28,7 @@ import { Restore } from "@material-ui/icons";
|
||||||
import { Alert } from "@material-ui/lab";
|
import { Alert } from "@material-ui/lab";
|
||||||
import { Connection } from "@solana/web3.js";
|
import { Connection } from "@solana/web3.js";
|
||||||
import { BigNumber, ethers } from "ethers";
|
import { BigNumber, ethers } from "ethers";
|
||||||
|
import { useSnackbar } from "notistack";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
||||||
|
@ -50,6 +52,7 @@ import {
|
||||||
} from "../../utils/consts";
|
} from "../../utils/consts";
|
||||||
import { getSignedVAAWithRetry } from "../../utils/getSignedVAAWithRetry";
|
import { getSignedVAAWithRetry } from "../../utils/getSignedVAAWithRetry";
|
||||||
import { METADATA_REPLACE } from "../../utils/metaplex";
|
import { METADATA_REPLACE } from "../../utils/metaplex";
|
||||||
|
import parseError from "../../utils/parseError";
|
||||||
import KeyAndBalance from "../KeyAndBalance";
|
import KeyAndBalance from "../KeyAndBalance";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
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 {
|
try {
|
||||||
const receipt = await provider.getTransactionReceipt(tx);
|
const receipt = await provider.getTransactionReceipt(tx);
|
||||||
const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS);
|
const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS);
|
||||||
|
@ -71,14 +78,15 @@ async function eth(provider: ethers.providers.Web3Provider, tx: string) {
|
||||||
sequence.toString(),
|
sequence.toString(),
|
||||||
WORMHOLE_RPC_HOSTS.length
|
WORMHOLE_RPC_HOSTS.length
|
||||||
);
|
);
|
||||||
return uint8ArrayToHex(vaaBytes);
|
return { vaa: uint8ArrayToHex(vaaBytes), error: null };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(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 {
|
try {
|
||||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||||
const info = await connection.getTransaction(tx);
|
const info = await connection.getTransaction(tx);
|
||||||
|
@ -95,11 +103,12 @@ async function solana(tx: string) {
|
||||||
sequence.toString(),
|
sequence.toString(),
|
||||||
WORMHOLE_RPC_HOSTS.length
|
WORMHOLE_RPC_HOSTS.length
|
||||||
);
|
);
|
||||||
return uint8ArrayToHex(vaaBytes);
|
return { vaa: uint8ArrayToHex(vaaBytes), error: null };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
enqueueSnackbar(parseError(e), { variant: "error" });
|
||||||
|
return { vaa: null, error: parseError(e) };
|
||||||
}
|
}
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// note: actual first byte is message type
|
// note: actual first byte is message type
|
||||||
|
@ -114,7 +123,7 @@ async function solana(tx: string) {
|
||||||
// ? u16 recipient_chain
|
// ? u16 recipient_chain
|
||||||
|
|
||||||
// TODO: move to wasm / sdk, share with solana
|
// 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 originAddress = arr.slice(1, 1 + 32).toString("hex");
|
||||||
const originChain = arr.readUInt16BE(33) as ChainId;
|
const originChain = arr.readUInt16BE(33) as ChainId;
|
||||||
const symbol = Buffer.from(arr.slice(35, 35 + 32))
|
const symbol = Buffer.from(arr.slice(35, 35 + 32))
|
||||||
|
@ -152,12 +161,16 @@ function RecoveryDialogContent({
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { provider } = useEthereumProvider();
|
const { provider } = useEthereumProvider();
|
||||||
const currentSourceChain = useSelector(selectNFTSourceChain);
|
const currentSourceChain = useSelector(selectNFTSourceChain);
|
||||||
const [recoverySourceChain, setRecoverySourceChain] =
|
const [recoverySourceChain, setRecoverySourceChain] =
|
||||||
useState(currentSourceChain);
|
useState(currentSourceChain);
|
||||||
const [recoverySourceTx, setRecoverySourceTx] = useState("");
|
const [recoverySourceTx, setRecoverySourceTx] = useState("");
|
||||||
|
const [recoverySourceTxIsLoading, setRecoverySourceTxIsLoading] =
|
||||||
|
useState(false);
|
||||||
|
const [recoverySourceTxError, setRecoverySourceTxError] = useState("");
|
||||||
const currentSignedVAA = useSelector(selectNFTSignedVAAHex);
|
const currentSignedVAA = useSelector(selectNFTSignedVAAHex);
|
||||||
const [recoverySignedVAA, setRecoverySignedVAA] = useState(currentSignedVAA);
|
const [recoverySignedVAA, setRecoverySignedVAA] = useState(currentSignedVAA);
|
||||||
const [recoveryParsedVAA, setRecoveryParsedVAA] = useState<any>(null);
|
const [recoveryParsedVAA, setRecoveryParsedVAA] = useState<any>(null);
|
||||||
|
@ -171,17 +184,40 @@ function RecoveryDialogContent({
|
||||||
if (recoverySourceTx) {
|
if (recoverySourceTx) {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
if (recoverySourceChain === CHAIN_ID_ETH && provider) {
|
if (recoverySourceChain === CHAIN_ID_ETH && provider) {
|
||||||
|
setRecoverySourceTxError("");
|
||||||
|
setRecoverySourceTxIsLoading(true);
|
||||||
(async () => {
|
(async () => {
|
||||||
const vaa = await eth(provider, recoverySourceTx);
|
const { vaa, error } = await eth(
|
||||||
|
provider,
|
||||||
|
recoverySourceTx,
|
||||||
|
enqueueSnackbar
|
||||||
|
);
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setRecoverySignedVAA(vaa);
|
setRecoverySourceTxIsLoading(false);
|
||||||
|
if (vaa) {
|
||||||
|
setRecoverySignedVAA(vaa);
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
setRecoverySourceTxError(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
} else if (recoverySourceChain === CHAIN_ID_SOLANA) {
|
} else if (recoverySourceChain === CHAIN_ID_SOLANA) {
|
||||||
|
setRecoverySourceTxError("");
|
||||||
|
setRecoverySourceTxIsLoading(true);
|
||||||
(async () => {
|
(async () => {
|
||||||
const vaa = await solana(recoverySourceTx);
|
const { vaa, error } = await solana(
|
||||||
|
recoverySourceTx,
|
||||||
|
enqueueSnackbar
|
||||||
|
);
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setRecoverySignedVAA(vaa);
|
setRecoverySourceTxIsLoading(false);
|
||||||
|
if (vaa) {
|
||||||
|
setRecoverySignedVAA(vaa);
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
setRecoverySourceTxError(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
@ -189,7 +225,7 @@ function RecoveryDialogContent({
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [recoverySourceChain, recoverySourceTx, provider]);
|
}, [recoverySourceChain, recoverySourceTx, provider, enqueueSnackbar]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRecoverySignedVAA(currentSignedVAA);
|
setRecoverySignedVAA(currentSignedVAA);
|
||||||
}, [currentSignedVAA]);
|
}, [currentSignedVAA]);
|
||||||
|
@ -280,23 +316,45 @@ function RecoveryDialogContent({
|
||||||
<KeyAndBalance chainId={recoverySourceChain} />
|
<KeyAndBalance chainId={recoverySourceChain} />
|
||||||
) : null}
|
) : null}
|
||||||
<TextField
|
<TextField
|
||||||
label="Source Tx"
|
label="Source Tx (paste here)"
|
||||||
disabled={!!recoverySignedVAA}
|
disabled={!!recoverySignedVAA || recoverySourceTxIsLoading}
|
||||||
value={recoverySourceTx}
|
value={recoverySourceTx}
|
||||||
onChange={handleSourceTxChange}
|
onChange={handleSourceTxChange}
|
||||||
|
error={!!recoverySourceTxError}
|
||||||
|
helperText={recoverySourceTxError}
|
||||||
fullWidth
|
fullWidth
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<Box mt={4}>
|
<Box position="relative">
|
||||||
<Typography>or</Typography>
|
<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>
|
||||||
<TextField
|
|
||||||
label="Signed VAA (Hex)"
|
|
||||||
value={recoverySignedVAA || ""}
|
|
||||||
onChange={handleSignedVAAChange}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<Box my={4}>
|
<Box my={4}>
|
||||||
<Divider />
|
<Divider />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
import { CHAINS_BY_ID } from "../../utils/consts";
|
import { CHAINS_BY_ID } from "../../utils/consts";
|
||||||
import ButtonWithLoader from "../ButtonWithLoader";
|
import ButtonWithLoader from "../ButtonWithLoader";
|
||||||
import KeyAndBalance from "../KeyAndBalance";
|
import KeyAndBalance from "../KeyAndBalance";
|
||||||
|
import ShowTx from "../ShowTx";
|
||||||
import StepDescription from "../StepDescription";
|
import StepDescription from "../StepDescription";
|
||||||
import TransactionProgress from "../TransactionProgress";
|
import TransactionProgress from "../TransactionProgress";
|
||||||
import WaitingForWalletMessage from "./WaitingForWalletMessage";
|
import WaitingForWalletMessage from "./WaitingForWalletMessage";
|
||||||
|
@ -55,6 +56,7 @@ function Send() {
|
||||||
Transfer
|
Transfer
|
||||||
</ButtonWithLoader>
|
</ButtonWithLoader>
|
||||||
<WaitingForWalletMessage />
|
<WaitingForWalletMessage />
|
||||||
|
{transferTx ? <ShowTx chainId={sourceChain} tx={transferTx} /> : null}
|
||||||
<TransactionProgress
|
<TransactionProgress
|
||||||
chainId={sourceChain}
|
chainId={sourceChain}
|
||||||
tx={transferTx}
|
tx={transferTx}
|
||||||
|
|
|
@ -173,7 +173,7 @@ export default function NFTViewer({
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [uri, metadata.image]);
|
}, [uri]);
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const animLower = metadata.animation_url?.toLowerCase();
|
const animLower = metadata.animation_url?.toLowerCase();
|
||||||
// const has3DModel = animLower?.endsWith('gltf') || animLower?.endsWith('glb')
|
// const has3DModel = animLower?.endsWith('gltf') || animLower?.endsWith('glb')
|
||||||
|
|
|
@ -97,7 +97,7 @@ function useGetBalanceEffect(sourceOrTarget: "source" | "target") {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const connection = new Connection(SOLANA_HOST, "finalized");
|
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||||
connection
|
connection
|
||||||
.getParsedTokenAccountsByOwner(solPK, { mint })
|
.getParsedTokenAccountsByOwner(solPK, { mint })
|
||||||
.then(({ value }) => {
|
.then(({ value }) => {
|
||||||
|
|
|
@ -275,7 +275,7 @@ const getSolanaParsedTokenAccounts = (
|
||||||
dispatch: Dispatch,
|
dispatch: Dispatch,
|
||||||
nft: boolean
|
nft: boolean
|
||||||
) => {
|
) => {
|
||||||
const connection = new Connection(SOLANA_HOST, "finalized");
|
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||||
dispatch(
|
dispatch(
|
||||||
nft ? fetchSourceParsedTokenAccountsNFT() : fetchSourceParsedTokenAccounts()
|
nft ? fetchSourceParsedTokenAccountsNFT() : fetchSourceParsedTokenAccounts()
|
||||||
);
|
);
|
||||||
|
@ -426,7 +426,7 @@ function useGetAvailableTokens(nft: boolean = false) {
|
||||||
//SOLT devnet token
|
//SOLT devnet token
|
||||||
// mintAddresses.push("2WDq7wSs9zYrpx2kbHDA4RUTRch2CCTP6ZWaH4GNfnQQ");
|
// mintAddresses.push("2WDq7wSs9zYrpx2kbHDA4RUTRch2CCTP6ZWaH4GNfnQQ");
|
||||||
|
|
||||||
const connection = new Connection(SOLANA_HOST, "finalized");
|
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||||
getMultipleAccountsRPC(
|
getMultipleAccountsRPC(
|
||||||
connection,
|
connection,
|
||||||
mintAddresses.map((x) => new PublicKey(x))
|
mintAddresses.map((x) => new PublicKey(x))
|
||||||
|
|
|
@ -1,28 +1,36 @@
|
||||||
import {
|
import {
|
||||||
CHAIN_ID_ETH,
|
CHAIN_ID_ETH,
|
||||||
CHAIN_ID_SOLANA,
|
CHAIN_ID_SOLANA,
|
||||||
|
getClaimAddressSolana,
|
||||||
postVaaSolana,
|
postVaaSolana,
|
||||||
} from "@certusone/wormhole-sdk";
|
} from "@certusone/wormhole-sdk";
|
||||||
import {
|
import {
|
||||||
|
createMetaOnSolana,
|
||||||
|
getForeignAssetSol,
|
||||||
|
isNFTVAASolanaNative,
|
||||||
redeemOnEth,
|
redeemOnEth,
|
||||||
redeemOnSolana,
|
redeemOnSolana,
|
||||||
} from "@certusone/wormhole-sdk/lib/nft_bridge";
|
} from "@certusone/wormhole-sdk/lib/nft_bridge";
|
||||||
|
import { arrayify } from "@ethersproject/bytes";
|
||||||
import { WalletContextState } from "@solana/wallet-adapter-react";
|
import { WalletContextState } from "@solana/wallet-adapter-react";
|
||||||
import { Connection } from "@solana/web3.js";
|
import { Connection } from "@solana/web3.js";
|
||||||
import { Signer } from "ethers";
|
import { Signer } from "ethers";
|
||||||
import { useSnackbar } from "notistack";
|
import { useSnackbar } from "notistack";
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { parsePayload } from "../components/NFT/Recovery";
|
||||||
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||||
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
||||||
import { setIsRedeeming, setRedeemTx } from "../store/nftSlice";
|
import { setIsRedeeming, setRedeemTx } from "../store/nftSlice";
|
||||||
import { selectNFTIsRedeeming, selectNFTTargetChain } from "../store/selectors";
|
import { selectNFTIsRedeeming, selectNFTTargetChain } from "../store/selectors";
|
||||||
|
import { hexToUint8Array } from "../utils/array";
|
||||||
import {
|
import {
|
||||||
ETH_NFT_BRIDGE_ADDRESS,
|
ETH_NFT_BRIDGE_ADDRESS,
|
||||||
SOLANA_HOST,
|
SOLANA_HOST,
|
||||||
SOL_BRIDGE_ADDRESS,
|
SOL_BRIDGE_ADDRESS,
|
||||||
SOL_NFT_BRIDGE_ADDRESS,
|
SOL_NFT_BRIDGE_ADDRESS,
|
||||||
} from "../utils/consts";
|
} from "../utils/consts";
|
||||||
|
import { getMetadataAddress } from "../utils/metaplex";
|
||||||
import parseError from "../utils/parseError";
|
import parseError from "../utils/parseError";
|
||||||
import { signSendAndConfirm } from "../utils/solana";
|
import { signSendAndConfirm } from "../utils/solana";
|
||||||
import useNFTSignedVAA from "./useNFTSignedVAA";
|
import useNFTSignedVAA from "./useNFTSignedVAA";
|
||||||
|
@ -63,24 +71,60 @@ async function solana(
|
||||||
throw new Error("wallet.signTransaction is undefined");
|
throw new Error("wallet.signTransaction is undefined");
|
||||||
}
|
}
|
||||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||||
await postVaaSolana(
|
const claimAddress = await getClaimAddressSolana(
|
||||||
connection,
|
|
||||||
wallet.signTransaction,
|
|
||||||
SOL_BRIDGE_ADDRESS,
|
|
||||||
payerAddress,
|
|
||||||
Buffer.from(signedVAA)
|
|
||||||
);
|
|
||||||
// TODO: how do we retry in between these steps
|
|
||||||
const transaction = await redeemOnSolana(
|
|
||||||
connection,
|
|
||||||
SOL_BRIDGE_ADDRESS,
|
|
||||||
SOL_NFT_BRIDGE_ADDRESS,
|
SOL_NFT_BRIDGE_ADDRESS,
|
||||||
payerAddress,
|
|
||||||
signedVAA
|
signedVAA
|
||||||
);
|
);
|
||||||
const txid = await signSendAndConfirm(wallet, connection, transaction);
|
const claimInfo = await connection.getAccountInfo(claimAddress);
|
||||||
// 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?
|
let txid;
|
||||||
dispatch(setRedeemTx({ id: txid, block: 1 }));
|
if (!claimInfo) {
|
||||||
|
await postVaaSolana(
|
||||||
|
connection,
|
||||||
|
wallet.signTransaction,
|
||||||
|
SOL_BRIDGE_ADDRESS,
|
||||||
|
payerAddress,
|
||||||
|
Buffer.from(signedVAA)
|
||||||
|
);
|
||||||
|
// TODO: how do we retry in between these steps
|
||||||
|
const transaction = await redeemOnSolana(
|
||||||
|
connection,
|
||||||
|
SOL_BRIDGE_ADDRESS,
|
||||||
|
SOL_NFT_BRIDGE_ADDRESS,
|
||||||
|
payerAddress,
|
||||||
|
signedVAA
|
||||||
|
);
|
||||||
|
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?
|
||||||
|
}
|
||||||
|
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" });
|
enqueueSnackbar("Transaction confirmed", { variant: "success" });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
enqueueSnackbar(parseError(e), { variant: "error" });
|
enqueueSnackbar(parseError(e), { variant: "error" });
|
||||||
|
|
|
@ -15,7 +15,7 @@ const getMetaplexData = async (mintAddresses: string[]) => {
|
||||||
promises.push(getMetadataAddress(address));
|
promises.push(getMetadataAddress(address));
|
||||||
}
|
}
|
||||||
const metaAddresses = await Promise.all(promises);
|
const metaAddresses = await Promise.all(promises);
|
||||||
const connection = new Connection(SOLANA_HOST, "finalized");
|
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||||
const results = await getMultipleAccountsRPC(
|
const results = await getMultipleAccountsRPC(
|
||||||
connection,
|
connection,
|
||||||
metaAddresses.map((pair) => pair && pair[0])
|
metaAddresses.map((pair) => pair && pair[0])
|
||||||
|
|
|
@ -38,7 +38,7 @@ export async function getMultipleAccountsRPC(
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
pubkeys: PublicKey[]
|
pubkeys: PublicKey[]
|
||||||
): Promise<(AccountInfo<Buffer> | null)[]> {
|
): Promise<(AccountInfo<Buffer> | null)[]> {
|
||||||
return getMultipleAccounts(connection, pubkeys, "finalized");
|
return getMultipleAccounts(connection, pubkeys, "confirmed");
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getMultipleAccounts = async (
|
export const getMultipleAccounts = async (
|
||||||
|
|
|
@ -2,6 +2,14 @@
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
## 0.0.5
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
NFT Bridge Support
|
||||||
|
getClaimAddressSolana
|
||||||
|
createMetaOnSolana
|
||||||
|
|
||||||
## 0.0.4
|
## 0.0.4
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@certusone/wormhole-sdk",
|
"name": "@certusone/wormhole-sdk",
|
||||||
"version": "0.0.4",
|
"version": "0.0.5",
|
||||||
"description": "SDK for interacting with Wormhole",
|
"description": "SDK for interacting with Wormhole",
|
||||||
"homepage": "https://wormholenetwork.com",
|
"homepage": "https://wormholenetwork.com",
|
||||||
"main": "lib/index.js",
|
"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 "./getEmitterAddress";
|
||||||
export * from "./parseSequenceFromLog";
|
export * from "./parseSequenceFromLog";
|
||||||
|
|
|
@ -15,6 +15,15 @@ export async function redeemOnEth(
|
||||||
return receipt;
|
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(
|
export async function redeemOnSolana(
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
bridgeAddress: string,
|
bridgeAddress: string,
|
||||||
|
@ -22,11 +31,7 @@ export async function redeemOnSolana(
|
||||||
payerAddress: string,
|
payerAddress: string,
|
||||||
signedVAA: Uint8Array
|
signedVAA: Uint8Array
|
||||||
) {
|
) {
|
||||||
const { parse_vaa } = await import("../solana/core/bridge");
|
const isSolanaNative = await isNFTVAASolanaNative(signedVAA);
|
||||||
const parsedVAA = parse_vaa(signedVAA);
|
|
||||||
const isSolanaNative =
|
|
||||||
Buffer.from(new Uint8Array(parsedVAA.payload)).readUInt16BE(33) ===
|
|
||||||
CHAIN_ID_SOLANA;
|
|
||||||
const { complete_transfer_wrapped_ix, complete_transfer_native_ix } =
|
const { complete_transfer_wrapped_ix, complete_transfer_native_ix } =
|
||||||
await import("../solana/nft/nft_bridge");
|
await import("../solana/nft/nft_bridge");
|
||||||
const ixs = [];
|
const ixs = [];
|
||||||
|
@ -61,3 +66,28 @@ export async function redeemOnSolana(
|
||||||
transaction.feePayer = new PublicKey(payerAddress);
|
transaction.feePayer = new PublicKey(payerAddress);
|
||||||
return transaction;
|
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