nft_bridge fixes
Change-Id: I9420863384e752725cfc75c8b5a21f64be2792b1
This commit is contained in:
parent
7711abf29a
commit
9ea0369ab0
|
@ -0,0 +1,444 @@
|
|||
import {
|
||||
ChainId,
|
||||
CHAIN_ID_BSC,
|
||||
CHAIN_ID_ETH,
|
||||
CHAIN_ID_SOLANA,
|
||||
getEmitterAddressEth,
|
||||
getEmitterAddressSolana,
|
||||
getSignedVAA,
|
||||
parseSequenceFromLogEth,
|
||||
parseSequenceFromLogSolana,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Divider,
|
||||
Fab,
|
||||
makeStyles,
|
||||
MenuItem,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
import { Restore } from "@material-ui/icons";
|
||||
import { Alert } from "@material-ui/lab";
|
||||
import { Connection } from "@solana/web3.js";
|
||||
import { BigNumber, ethers } from "ethers";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
||||
import {
|
||||
selectNFTSignedVAAHex,
|
||||
selectNFTSourceChain,
|
||||
} from "../../store/selectors";
|
||||
import { setSignedVAAHex, setStep, setTargetChain } from "../../store/nftSlice";
|
||||
import {
|
||||
hexToNativeString,
|
||||
hexToUint8Array,
|
||||
uint8ArrayToHex,
|
||||
} from "../../utils/array";
|
||||
import {
|
||||
CHAINS,
|
||||
ETH_BRIDGE_ADDRESS,
|
||||
ETH_NFT_BRIDGE_ADDRESS,
|
||||
SOLANA_HOST,
|
||||
SOL_NFT_BRIDGE_ADDRESS,
|
||||
WORMHOLE_RPC_HOST,
|
||||
} from "../../utils/consts";
|
||||
import KeyAndBalance from "../KeyAndBalance";
|
||||
import { METADATA_REPLACE } from "../../utils/metaplex";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
fab: {
|
||||
position: "fixed",
|
||||
bottom: theme.spacing(2),
|
||||
right: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
|
||||
async function eth(provider: ethers.providers.Web3Provider, tx: string) {
|
||||
try {
|
||||
const receipt = await provider.getTransactionReceipt(tx);
|
||||
const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS);
|
||||
const emitterAddress = getEmitterAddressEth(ETH_NFT_BRIDGE_ADDRESS);
|
||||
const { vaaBytes } = await getSignedVAA(
|
||||
WORMHOLE_RPC_HOST,
|
||||
CHAIN_ID_ETH,
|
||||
emitterAddress,
|
||||
sequence.toString()
|
||||
);
|
||||
return uint8ArrayToHex(vaaBytes);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
async function solana(tx: string) {
|
||||
try {
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
const info = await connection.getTransaction(tx);
|
||||
if (!info) {
|
||||
throw new Error("An error occurred while fetching the transaction info");
|
||||
}
|
||||
const sequence = parseSequenceFromLogSolana(info);
|
||||
const emitterAddress = await getEmitterAddressSolana(
|
||||
SOL_NFT_BRIDGE_ADDRESS
|
||||
);
|
||||
const { vaaBytes } = await getSignedVAA(
|
||||
WORMHOLE_RPC_HOST,
|
||||
CHAIN_ID_SOLANA,
|
||||
emitterAddress,
|
||||
sequence.toString()
|
||||
);
|
||||
return uint8ArrayToHex(vaaBytes);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// note: actual first byte is message type
|
||||
// 0 [u8; 32] token_address
|
||||
// 32 u16 token_chain
|
||||
// 34 [u8; 32] symbol
|
||||
// 66 [u8; 32] name
|
||||
// 98 u256 tokenId
|
||||
// 130 u8 uri_len
|
||||
// 131 [u8;len] uri
|
||||
// ? [u8; 32] recipient
|
||||
// ? u16 recipient_chain
|
||||
|
||||
// TODO: move to wasm / sdk, share with solana
|
||||
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))
|
||||
.toString("utf8")
|
||||
.replace(METADATA_REPLACE, "");
|
||||
const name = Buffer.from(arr.slice(67, 67 + 32))
|
||||
.toString("utf8")
|
||||
.replace(METADATA_REPLACE, "");
|
||||
const tokenId = BigNumber.from(arr.slice(99, 99 + 32));
|
||||
const uri_len = arr.readUInt8(131);
|
||||
const uri = Buffer.from(arr.slice(132, 132 + uri_len))
|
||||
.toString("utf8")
|
||||
.replace(METADATA_REPLACE, "");
|
||||
const target_offset = 132 + uri_len;
|
||||
const targetAddress = arr
|
||||
.slice(target_offset, target_offset + 32)
|
||||
.toString("hex");
|
||||
const targetChain = arr.readUInt16BE(target_offset + 32) as ChainId;
|
||||
return {
|
||||
originAddress,
|
||||
originChain,
|
||||
symbol,
|
||||
name,
|
||||
tokenId,
|
||||
uri,
|
||||
targetAddress,
|
||||
targetChain,
|
||||
};
|
||||
};
|
||||
|
||||
function RecoveryDialogContent({
|
||||
onClose,
|
||||
disabled,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
disabled: boolean;
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
const { provider } = useEthereumProvider();
|
||||
const currentSourceChain = useSelector(selectNFTSourceChain);
|
||||
const [recoverySourceChain, setRecoverySourceChain] =
|
||||
useState(currentSourceChain);
|
||||
const [recoverySourceTx, setRecoverySourceTx] = useState("");
|
||||
const currentSignedVAA = useSelector(selectNFTSignedVAAHex);
|
||||
const [recoverySignedVAA, setRecoverySignedVAA] = useState(currentSignedVAA);
|
||||
const [recoveryParsedVAA, setRecoveryParsedVAA] = useState<any>(null);
|
||||
useEffect(() => {
|
||||
if (!recoverySignedVAA) {
|
||||
setRecoverySourceTx("");
|
||||
setRecoverySourceChain(currentSourceChain);
|
||||
}
|
||||
}, [recoverySignedVAA, currentSourceChain]);
|
||||
useEffect(() => {
|
||||
if (recoverySourceTx) {
|
||||
let cancelled = false;
|
||||
if (recoverySourceChain === CHAIN_ID_ETH && provider) {
|
||||
(async () => {
|
||||
const vaa = await eth(provider, recoverySourceTx);
|
||||
if (!cancelled) {
|
||||
setRecoverySignedVAA(vaa);
|
||||
}
|
||||
})();
|
||||
} else if (recoverySourceChain === CHAIN_ID_SOLANA) {
|
||||
(async () => {
|
||||
const vaa = await solana(recoverySourceTx);
|
||||
if (!cancelled) {
|
||||
setRecoverySignedVAA(vaa);
|
||||
}
|
||||
})();
|
||||
}
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}
|
||||
}, [recoverySourceChain, recoverySourceTx, provider]);
|
||||
useEffect(() => {
|
||||
setRecoverySignedVAA(currentSignedVAA);
|
||||
}, [currentSignedVAA]);
|
||||
const handleSourceChainChange = useCallback((event) => {
|
||||
setRecoverySourceTx("");
|
||||
setRecoverySourceChain(event.target.value);
|
||||
}, []);
|
||||
const handleSourceTxChange = useCallback((event) => {
|
||||
setRecoverySourceTx(event.target.value);
|
||||
}, []);
|
||||
const handleSignedVAAChange = useCallback((event) => {
|
||||
setRecoverySignedVAA(event.target.value);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
if (recoverySignedVAA) {
|
||||
(async () => {
|
||||
try {
|
||||
const { parse_vaa } = await import(
|
||||
"@certusone/wormhole-sdk/lib/solana/core/bridge"
|
||||
);
|
||||
const parsedVAA = parse_vaa(hexToUint8Array(recoverySignedVAA));
|
||||
if (!cancelled) {
|
||||
setRecoveryParsedVAA(parsedVAA);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
if (!cancelled) {
|
||||
setRecoveryParsedVAA(null);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [recoverySignedVAA]);
|
||||
const parsedPayload = useMemo(
|
||||
() =>
|
||||
recoveryParsedVAA?.payload
|
||||
? parsePayload(Buffer.from(new Uint8Array(recoveryParsedVAA.payload)))
|
||||
: null,
|
||||
[recoveryParsedVAA]
|
||||
);
|
||||
const parsedPayloadTargetChain = parsedPayload?.targetChain;
|
||||
const enableRecovery = recoverySignedVAA && parsedPayloadTargetChain;
|
||||
const handleRecoverClick = useCallback(() => {
|
||||
if (enableRecovery && recoverySignedVAA && parsedPayloadTargetChain) {
|
||||
// TODO: make recovery reducer
|
||||
dispatch(setSignedVAAHex(recoverySignedVAA));
|
||||
dispatch(setTargetChain(parsedPayloadTargetChain));
|
||||
dispatch(setStep(3));
|
||||
onClose();
|
||||
}
|
||||
}, [
|
||||
dispatch,
|
||||
enableRecovery,
|
||||
recoverySignedVAA,
|
||||
parsedPayloadTargetChain,
|
||||
onClose,
|
||||
]);
|
||||
return (
|
||||
<>
|
||||
<DialogContent>
|
||||
<Alert severity="info">
|
||||
If you have sent your tokens but have not redeemed them, you may paste
|
||||
in the Source Transaction ID (from Step 3) to resume your transfer.
|
||||
</Alert>
|
||||
<TextField
|
||||
select
|
||||
label="Source Chain"
|
||||
disabled={!!recoverySignedVAA}
|
||||
value={recoverySourceChain}
|
||||
onChange={handleSourceChainChange}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
>
|
||||
{CHAINS.filter(
|
||||
({ id }) => id === CHAIN_ID_ETH || id === CHAIN_ID_SOLANA
|
||||
).map(({ id, name }) => (
|
||||
<MenuItem key={id} value={id}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
{recoverySourceChain === CHAIN_ID_ETH ||
|
||||
recoverySourceChain === CHAIN_ID_BSC ? (
|
||||
<KeyAndBalance chainId={recoverySourceChain} />
|
||||
) : null}
|
||||
<TextField
|
||||
label="Source Tx"
|
||||
disabled={!!recoverySignedVAA}
|
||||
value={recoverySourceTx}
|
||||
onChange={handleSourceTxChange}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
/>
|
||||
<Box mt={4}>
|
||||
<Typography>or</Typography>
|
||||
</Box>
|
||||
<TextField
|
||||
label="Signed VAA (Hex)"
|
||||
value={recoverySignedVAA || ""}
|
||||
onChange={handleSignedVAAChange}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
/>
|
||||
<Box my={4}>
|
||||
<Divider />
|
||||
</Box>
|
||||
<TextField
|
||||
label="Emitter Chain"
|
||||
disabled
|
||||
value={recoveryParsedVAA?.emitter_chain || ""}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
/>
|
||||
<TextField
|
||||
label="Emitter Address"
|
||||
disabled
|
||||
value={
|
||||
(recoveryParsedVAA &&
|
||||
hexToNativeString(
|
||||
recoveryParsedVAA.emitter_address,
|
||||
recoveryParsedVAA.emitter_chain
|
||||
)) ||
|
||||
""
|
||||
}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
/>
|
||||
<TextField
|
||||
label="Sequence"
|
||||
disabled
|
||||
value={recoveryParsedVAA?.sequence || ""}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
/>
|
||||
<TextField
|
||||
label="Timestamp"
|
||||
disabled
|
||||
value={
|
||||
(recoveryParsedVAA &&
|
||||
new Date(recoveryParsedVAA.timestamp * 1000).toLocaleString()) ||
|
||||
""
|
||||
}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
/>
|
||||
<Box my={4}>
|
||||
<Divider />
|
||||
</Box>
|
||||
<TextField
|
||||
label="Origin Chain"
|
||||
disabled
|
||||
value={parsedPayload?.originChain.toString() || ""}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
/>
|
||||
<TextField
|
||||
label="Origin Token Address"
|
||||
disabled
|
||||
value={
|
||||
(parsedPayload &&
|
||||
hexToNativeString(
|
||||
parsedPayload.originAddress,
|
||||
parsedPayload.originChain
|
||||
)) ||
|
||||
""
|
||||
}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
/>
|
||||
<TextField
|
||||
label="Origin Token ID"
|
||||
disabled
|
||||
value={parsedPayload?.tokenId || ""}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
/>
|
||||
<TextField
|
||||
label="Target Chain"
|
||||
disabled
|
||||
value={parsedPayload?.targetChain.toString() || ""}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
/>
|
||||
<TextField
|
||||
label="Target Address"
|
||||
disabled
|
||||
value={
|
||||
(parsedPayload &&
|
||||
hexToNativeString(
|
||||
parsedPayload.targetAddress,
|
||||
parsedPayload.targetChain
|
||||
)) ||
|
||||
""
|
||||
}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
/>
|
||||
<Box my={4}>
|
||||
<Divider />
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} variant="outlined" color="default">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleRecoverClick}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={!enableRecovery || disabled}
|
||||
>
|
||||
Recover
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Recovery({
|
||||
open,
|
||||
setOpen,
|
||||
disabled,
|
||||
}: {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
disabled: boolean;
|
||||
}) {
|
||||
const classes = useStyles();
|
||||
const handleOpenClick = useCallback(() => {
|
||||
setOpen(true);
|
||||
}, [setOpen]);
|
||||
const handleCloseClick = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, [setOpen]);
|
||||
return (
|
||||
<>
|
||||
<Tooltip title="Open Recovery Dialog">
|
||||
<Fab className={classes.fab} onClick={handleOpenClick}>
|
||||
<Restore />
|
||||
</Fab>
|
||||
</Tooltip>
|
||||
<Dialog open={open} onClose={handleCloseClick} maxWidth="md" fullWidth>
|
||||
<DialogTitle>Recovery</DialogTitle>
|
||||
<RecoveryDialogContent onClose={handleCloseClick} disabled={disabled} />
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -49,7 +49,7 @@ function Send() {
|
|||
>
|
||||
Transfer
|
||||
</ButtonWithLoader>
|
||||
<TransferProgress />
|
||||
<TransferProgress nft />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
|
||||
import { makeStyles, MenuItem, TextField } from "@material-ui/core";
|
||||
import { Button, makeStyles, MenuItem, TextField } from "@material-ui/core";
|
||||
import { Restore } from "@material-ui/icons";
|
||||
import { useCallback } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
||||
|
@ -24,7 +25,11 @@ const useStyles = makeStyles((theme) => ({
|
|||
},
|
||||
}));
|
||||
|
||||
function Source() {
|
||||
function Source({
|
||||
setIsRecoveryOpen,
|
||||
}: {
|
||||
setIsRecoveryOpen: (open: boolean) => void;
|
||||
}) {
|
||||
const classes = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
const sourceChain = useSelector(selectNFTSourceChain);
|
||||
|
@ -48,14 +53,14 @@ function Source() {
|
|||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
Select an NFT to send through the Wormhole NFT Bridge.
|
||||
<div style={{ flexGrow: 1 }} />
|
||||
{/* <Button
|
||||
<Button
|
||||
onClick={() => setIsRecoveryOpen(true)}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
endIcon={<Restore />}
|
||||
>
|
||||
Perform Recovery
|
||||
</Button> */}
|
||||
</Button>
|
||||
</div>
|
||||
</StepDescription>
|
||||
<TextField
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
|
||||
import { makeStyles, MenuItem, TextField } from "@material-ui/core";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { ethers } from "ethers";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
||||
import useSyncTargetAddress from "../../hooks/useSyncTargetAddress";
|
||||
import { incrementStep, setTargetChain } from "../../store/nftSlice";
|
||||
import {
|
||||
selectNFTIsTargetComplete,
|
||||
selectNFTOriginTokenId,
|
||||
selectNFTShouldLockFields,
|
||||
selectNFTSourceChain,
|
||||
selectNFTTargetAddressHex,
|
||||
|
@ -14,14 +17,10 @@ import {
|
|||
selectNFTTargetChain,
|
||||
selectNFTTargetError,
|
||||
} from "../../store/selectors";
|
||||
import { incrementStep, setTargetChain } from "../../store/nftSlice";
|
||||
import { hexToNativeString } from "../../utils/array";
|
||||
import { CHAINS } from "../../utils/consts";
|
||||
import ButtonWithLoader from "../ButtonWithLoader";
|
||||
import KeyAndBalance from "../KeyAndBalance";
|
||||
import SolanaCreateAssociatedAddress, {
|
||||
useAssociatedAccountExistsState,
|
||||
} from "../SolanaCreateAssociatedAddress";
|
||||
import StepDescription from "../StepDescription";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
|
@ -41,6 +40,7 @@ function Target() {
|
|||
const targetChain = useSelector(selectNFTTargetChain);
|
||||
const targetAddressHex = useSelector(selectNFTTargetAddressHex);
|
||||
const targetAsset = useSelector(selectNFTTargetAsset);
|
||||
const originTokenId = useSelector(selectNFTOriginTokenId);
|
||||
const readableTargetAddress =
|
||||
hexToNativeString(targetAddressHex, targetChain) || "";
|
||||
const uiAmountString = useSelector(selectNFTTargetBalanceString);
|
||||
|
@ -48,12 +48,6 @@ function Target() {
|
|||
const isTargetComplete = useSelector(selectNFTIsTargetComplete);
|
||||
const shouldLockFields = useSelector(selectNFTShouldLockFields);
|
||||
const { statusMessage } = useIsWalletReady(targetChain);
|
||||
const { associatedAccountExists, setAssociatedAccountExists } =
|
||||
useAssociatedAccountExistsState(
|
||||
targetChain,
|
||||
targetAsset,
|
||||
readableTargetAddress
|
||||
);
|
||||
useSyncTargetAddress(!shouldLockFields, true);
|
||||
const handleTargetChange = useCallback(
|
||||
(event) => {
|
||||
|
@ -90,21 +84,26 @@ function Target() {
|
|||
value={readableTargetAddress}
|
||||
disabled={true}
|
||||
/>
|
||||
{targetChain === CHAIN_ID_SOLANA && targetAsset ? (
|
||||
<SolanaCreateAssociatedAddress
|
||||
mintAddress={targetAsset}
|
||||
readableTargetAddress={readableTargetAddress}
|
||||
associatedAccountExists={associatedAccountExists}
|
||||
setAssociatedAccountExists={setAssociatedAccountExists}
|
||||
/>
|
||||
{targetAsset !== ethers.constants.AddressZero ? (
|
||||
<>
|
||||
<TextField
|
||||
label="Token Address"
|
||||
fullWidth
|
||||
className={classes.transferField}
|
||||
value={targetAsset || ""}
|
||||
disabled={true}
|
||||
/>
|
||||
{targetChain === CHAIN_ID_ETH ? (
|
||||
<TextField
|
||||
label="TokenId"
|
||||
fullWidth
|
||||
className={classes.transferField}
|
||||
value={originTokenId || ""}
|
||||
disabled={true}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
<TextField
|
||||
label="Token Address"
|
||||
fullWidth
|
||||
className={classes.transferField}
|
||||
value={targetAsset || ""}
|
||||
disabled={true}
|
||||
/>
|
||||
<ButtonWithLoader
|
||||
disabled={!isTargetComplete} //|| !associatedAccountExists}
|
||||
onClick={handleNextClick}
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
StepContent,
|
||||
Stepper,
|
||||
} from "@material-ui/core";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import useCheckIfWormholeWrapped from "../../hooks/useCheckIfWormholeWrapped";
|
||||
import useFetchTargetAsset from "../../hooks/useFetchTargetAsset";
|
||||
|
@ -18,7 +18,7 @@ import {
|
|||
selectNFTIsSending,
|
||||
} from "../../store/selectors";
|
||||
import { setStep } from "../../store/nftSlice";
|
||||
// import Recovery from "./Recovery";
|
||||
import Recovery from "./Recovery";
|
||||
import Redeem from "./Redeem";
|
||||
import RedeemPreview from "./RedeemPreview";
|
||||
import Send from "./Send";
|
||||
|
@ -38,7 +38,7 @@ function NFT() {
|
|||
const classes = useStyles();
|
||||
useCheckIfWormholeWrapped(true);
|
||||
useFetchTargetAsset(true);
|
||||
// const [isRecoveryOpen, setIsRecoveryOpen] = useState(false);
|
||||
const [isRecoveryOpen, setIsRecoveryOpen] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const activeStep = useSelector(selectNFTActiveStep);
|
||||
const isSending = useSelector(selectNFTIsSending);
|
||||
|
@ -68,9 +68,7 @@ function NFT() {
|
|||
<StepButton onClick={() => dispatch(setStep(0))}>Source</StepButton>
|
||||
<StepContent>
|
||||
{activeStep === 0 ? (
|
||||
<Source
|
||||
// setIsRecoveryOpen={setIsRecoveryOpen}
|
||||
/>
|
||||
<Source setIsRecoveryOpen={setIsRecoveryOpen} />
|
||||
) : (
|
||||
<SourcePreview />
|
||||
)}
|
||||
|
@ -103,11 +101,11 @@ function NFT() {
|
|||
</StepContent>
|
||||
</Step>
|
||||
</Stepper>
|
||||
{/* <Recovery
|
||||
<Recovery
|
||||
open={isRecoveryOpen}
|
||||
setOpen={setIsRecoveryOpen}
|
||||
disabled={preventNavigation}
|
||||
/> */}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,9 @@ import { useEffect, useState } from "react";
|
|||
import { useSelector } from "react-redux";
|
||||
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||
import {
|
||||
selectNFTIsSendComplete,
|
||||
selectNFTSourceChain,
|
||||
selectNFTTransferTx,
|
||||
selectTransferIsSendComplete,
|
||||
selectTransferSourceChain,
|
||||
selectTransferTransferTx,
|
||||
|
@ -21,11 +24,17 @@ const useStyles = makeStyles((theme) => ({
|
|||
},
|
||||
}));
|
||||
|
||||
export default function TransferProgress() {
|
||||
export default function TransferProgress({ nft }: { nft?: boolean }) {
|
||||
const classes = useStyles();
|
||||
const sourceChain = useSelector(selectTransferSourceChain);
|
||||
const transferTx = useSelector(selectTransferTransferTx);
|
||||
const isSendComplete = useSelector(selectTransferIsSendComplete);
|
||||
const sourceChain = useSelector(
|
||||
nft ? selectNFTSourceChain : selectTransferSourceChain
|
||||
);
|
||||
const transferTx = useSelector(
|
||||
nft ? selectNFTTransferTx : selectTransferTransferTx
|
||||
);
|
||||
const isSendComplete = useSelector(
|
||||
nft ? selectNFTIsSendComplete : selectTransferIsSendComplete
|
||||
);
|
||||
const { provider } = useEthereumProvider();
|
||||
const [currentBlock, setCurrentBlock] = useState(0);
|
||||
useEffect(() => {
|
||||
|
|
|
@ -75,7 +75,6 @@ function useCheckIfWormholeWrapped(nft?: boolean) {
|
|||
let cancelled = false;
|
||||
(async () => {
|
||||
if (sourceChain === CHAIN_ID_ETH && provider && sourceAsset) {
|
||||
console.log("getting wrapped info");
|
||||
const wrappedInfo = makeStateSafe(
|
||||
await (nft
|
||||
? getOriginalAssetEthNFT(
|
||||
|
@ -90,7 +89,6 @@ function useCheckIfWormholeWrapped(nft?: boolean) {
|
|||
sourceAsset
|
||||
))
|
||||
);
|
||||
console.log(wrappedInfo);
|
||||
if (!cancelled) {
|
||||
dispatch(setSourceWormholeWrappedInfo(wrappedInfo));
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@ import {
|
|||
getForeignAssetEth as getForeignAssetEthNFT,
|
||||
getForeignAssetSol as getForeignAssetSolNFT,
|
||||
} from "@certusone/wormhole-sdk/lib/nft_bridge";
|
||||
import { BigNumber } from "@ethersproject/bignumber";
|
||||
import { arrayify } from "@ethersproject/bytes";
|
||||
import { Connection } from "@solana/web3.js";
|
||||
import { LCDClient } from "@terra-money/terra.js";
|
||||
import { useEffect } from "react";
|
||||
|
@ -106,7 +108,7 @@ function useFetchTargetAsset(nft?: boolean) {
|
|||
SOL_NFT_BRIDGE_ADDRESS,
|
||||
originChain,
|
||||
hexToUint8Array(originAsset),
|
||||
new Uint8Array([0, 0, 0, 0]) //tokenId // TODO: string
|
||||
arrayify(BigNumber.from(tokenId || "0"))
|
||||
)
|
||||
: getForeignAssetSolana(
|
||||
connection,
|
||||
|
@ -114,12 +116,10 @@ function useFetchTargetAsset(nft?: boolean) {
|
|||
originChain,
|
||||
hexToUint8Array(originAsset)
|
||||
));
|
||||
console.log("asset", asset);
|
||||
if (!cancelled) {
|
||||
dispatch(setTargetAsset(asset));
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
if (!cancelled) {
|
||||
// TODO: warning for this
|
||||
dispatch(setTargetAsset(null));
|
||||
|
|
|
@ -13,8 +13,8 @@ import {
|
|||
} from "@certusone/wormhole-sdk/lib/nft_bridge";
|
||||
import { WalletContextState } from "@solana/wallet-adapter-react";
|
||||
import { Connection } from "@solana/web3.js";
|
||||
import { Signer } from "ethers";
|
||||
import { zeroPad } from "ethers/lib/utils";
|
||||
import { BigNumber, Signer } from "ethers";
|
||||
import { arrayify, zeroPad } from "ethers/lib/utils";
|
||||
import { useSnackbar } from "notistack";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
@ -120,7 +120,7 @@ async function solana(
|
|||
targetChain,
|
||||
originAddress,
|
||||
originChain,
|
||||
new Uint8Array([0, 0, 0, 0]) //originTokenId //TODO: string
|
||||
arrayify(BigNumber.from(originTokenId || "0"))
|
||||
);
|
||||
const txid = await signSendAndConfirm(wallet, connection, transaction);
|
||||
enqueueSnackbar("Transaction confirmed", { variant: "success" });
|
||||
|
|
|
@ -116,13 +116,10 @@ export const selectNFTTargetError = (state: RootState) => {
|
|||
if (!state.nft.targetChain) {
|
||||
return "Select a target chain";
|
||||
}
|
||||
if (!state.nft.targetAsset) {
|
||||
return UNREGISTERED_ERROR_MESSAGE;
|
||||
}
|
||||
if (
|
||||
state.nft.targetChain === CHAIN_ID_ETH &&
|
||||
state.nft.targetAsset === ethers.constants.AddressZero
|
||||
) {
|
||||
if (state.nft.targetChain === CHAIN_ID_SOLANA && !state.nft.targetAsset) {
|
||||
// target asset is only required for solana
|
||||
// in the cases of new transfers, target asset will not exist and be created on redeem
|
||||
// Solana requires the derived address to derive the associated token account which is the target on the vaa
|
||||
return UNREGISTERED_ERROR_MESSAGE;
|
||||
}
|
||||
if (!state.nft.targetAddressHex) {
|
||||
|
|
|
@ -5,7 +5,7 @@ import { BinaryReader, BinaryWriter } from "borsh";
|
|||
const base58: any = require("bs58");
|
||||
|
||||
// eslint-disable-next-line
|
||||
const METADATA_REPLACE = new RegExp("\u0000", "g");
|
||||
export const METADATA_REPLACE = new RegExp("\u0000", "g");
|
||||
export const EDITION_MARKER_BIT_SIZE = 248;
|
||||
export const METADATA_PREFIX = "metadata";
|
||||
export const EDITION = "edition";
|
||||
|
|
|
@ -22,8 +22,8 @@ module.exports = async function(callback) {
|
|||
|
||||
const nftAddress = (
|
||||
await ERC721.new(
|
||||
"Not an APE",
|
||||
"APE",
|
||||
"Not an APE 🐒",
|
||||
"APE🐒",
|
||||
"https://cloudflare-ipfs.com/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/"
|
||||
)
|
||||
).address;
|
||||
|
@ -32,6 +32,10 @@ module.exports = async function(callback) {
|
|||
from: accounts[0],
|
||||
gas: 1000000,
|
||||
});
|
||||
await nft.methods.mint(accounts[0]).send({
|
||||
from: accounts[0],
|
||||
gas: 1000000,
|
||||
});
|
||||
|
||||
console.log("NFT deployed at: " + nftAddress);
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ module.exports = async function (callback) {
|
|||
gasLimit: 2000000
|
||||
});
|
||||
|
||||
await nftBridge.methods.registerChain("0x010000000001008ac4e21c24172fd5f4bdf0b5211f0232cd350a407751a64900fe65d7555384767440eeeae8541dc777e994feb343d59c61dfe72d14206ef77591f8b2b3d8e6280000000001000000010001000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000004e4654427269646765010000000105718b324065244262a50875000f903525d5204dc7feff0fd5a26270682cd7ff").send({
|
||||
await nftBridge.methods.registerChain("0x010000000001007985ba742002ae745c19722fea4d82102e68526c7c9d94d0e5d0a809071c98451c9693b230b3390f4ca9555a3ba9a9abbe87cf6f9e400682213e4fbbe1dabb9e0100000001000000010001000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000004e4654427269646765010000000196ee982293251b48729804c8e8b24b553eb6b887867024948d2236fd37a577ab").send({
|
||||
value: 0,
|
||||
from: accounts[0],
|
||||
gasLimit: 2000000
|
||||
|
|
|
@ -12,8 +12,11 @@
|
|||
"scripts": {
|
||||
"build-contracts": "npm run build --prefix ../../ethereum && node scripts/copyContracts.js && typechain --target=ethers-v5 --out-dir=src/ethers-contracts contracts/*.json",
|
||||
"build-abis": "typechain --target=ethers-v5 --out-dir=src/ethers-contracts/abi src/abi/Wormhole.abi.json",
|
||||
"build-deps": "npm run build-abis && npm run build-contracts",
|
||||
"build-lib": "tsc && node scripts/copyEthersTypes.js && node scripts/copyWasm.js",
|
||||
"build-all": "npm run build-deps && npm run build-lib",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "npm run build-abis && npm run build-contracts && tsc && node scripts/copyEthersTypes.js && node scripts/copyWasm.js",
|
||||
"build": "npm run build-all",
|
||||
"format": "echo \"disabled: prettier --write \"src/**/*.ts\"\"",
|
||||
"lint": "tslint -p tsconfig.json",
|
||||
"prepare": "npm run build",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Connection, PublicKey } from "@solana/web3.js";
|
||||
import { ethers } from "ethers";
|
||||
import { BigNumber, ethers } from "ethers";
|
||||
import { arrayify, zeroPad } from "ethers/lib/utils";
|
||||
import { TokenImplementation__factory } from "../ethers-contracts";
|
||||
import { ChainId, CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "../utils";
|
||||
|
@ -37,11 +37,11 @@ export async function getOriginalAssetEth(
|
|||
);
|
||||
const chainId = (await token.chainId()) as ChainId; // origin chain
|
||||
const assetAddress = await token.nativeContract(); // origin address
|
||||
// TODO: tokenId
|
||||
return {
|
||||
isWrapped: true,
|
||||
chainId,
|
||||
assetAddress: arrayify(assetAddress),
|
||||
tokenId, // tokenIds are maintained across EVM chains
|
||||
};
|
||||
}
|
||||
return {
|
||||
|
@ -79,11 +79,13 @@ export async function getOriginalAssetSol(
|
|||
);
|
||||
if (wrappedMetaAccountInfo) {
|
||||
const parsed = parse_wrapped_meta(wrappedMetaAccountInfo.data);
|
||||
const token_id_arr = parsed.token_id as BigUint64Array;
|
||||
const token_id = BigNumber.from(token_id_arr.reverse()).toString();
|
||||
return {
|
||||
isWrapped: true,
|
||||
chainId: parsed.chain,
|
||||
assetAddress: parsed.token_address,
|
||||
tokenId: parsed.token_id,
|
||||
tokenId: token_id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ export async function redeemOnSolana(
|
|||
const { parse_vaa } = await import("../solana/core/bridge");
|
||||
const parsedVAA = parse_vaa(signedVAA);
|
||||
const isSolanaNative =
|
||||
Buffer.from(new Uint8Array(parsedVAA.payload)).readUInt16BE(65) ===
|
||||
Buffer.from(new Uint8Array(parsedVAA.payload)).readUInt16BE(33) ===
|
||||
CHAIN_ID_SOLANA;
|
||||
const { complete_transfer_wrapped_ix, complete_transfer_native_ix } =
|
||||
await import("../solana/nft/nft_bridge");
|
||||
|
@ -37,6 +37,7 @@ export async function redeemOnSolana(
|
|||
tokenBridgeAddress,
|
||||
bridgeAddress,
|
||||
payerAddress,
|
||||
payerAddress, //TODO: allow for a different address than payer
|
||||
signedVAA
|
||||
)
|
||||
)
|
||||
|
@ -48,6 +49,7 @@ export async function redeemOnSolana(
|
|||
tokenBridgeAddress,
|
||||
bridgeAddress,
|
||||
payerAddress,
|
||||
payerAddress, //TODO: allow for a different address than payer
|
||||
signedVAA
|
||||
)
|
||||
)
|
||||
|
|
|
@ -64,7 +64,7 @@ echo "Created NFT account $nft_account"
|
|||
spl-token mint "$nft" 1 "$nft_account"
|
||||
|
||||
# Create meta for token
|
||||
token-bridge-client create-meta "$nft" "Not a PUNK" "PUNK" "https://wrappedpunks.com:3000/api/punks/metadata/39"
|
||||
token-bridge-client create-meta "$nft" "Not a PUNK🎸" "PUNK🎸" "https://wrappedpunks.com:3000/api/punks/metadata/39"
|
||||
|
||||
# Create the bridge contract at a known address
|
||||
# OK to fail on subsequent attempts (already created).
|
||||
|
|
|
@ -129,7 +129,7 @@ pub fn complete_native(
|
|||
accs.to_authority.info().key,
|
||||
accs.mint.info().key,
|
||||
);
|
||||
if *accs.to_authority.info().key != associated_addr {
|
||||
if *accs.to.info().key != associated_addr {
|
||||
return Err(InvalidAssociatedAccount.into());
|
||||
}
|
||||
// Create associated token account
|
||||
|
@ -294,7 +294,7 @@ pub fn complete_wrapped(
|
|||
accs.to_authority.info().key,
|
||||
accs.mint.info().key,
|
||||
);
|
||||
if *accs.to_authority.info().key != associated_addr {
|
||||
if *accs.to.info().key != associated_addr {
|
||||
return Err(InvalidAssociatedAccount.into());
|
||||
}
|
||||
// Create associated token account
|
||||
|
@ -303,7 +303,7 @@ pub fn complete_wrapped(
|
|||
accs.to_authority.info().key,
|
||||
accs.mint.info().key,
|
||||
);
|
||||
invoke(&ix, ctx.accounts)?;
|
||||
invoke_signed(&ix, ctx.accounts, &[])?;
|
||||
} else if *accs.mint.info().key != accs.to.mint {
|
||||
return Err(InvalidMint.into());
|
||||
}
|
||||
|
|
|
@ -154,7 +154,7 @@ pub fn transfer_native(
|
|||
}
|
||||
|
||||
// Token must have metadata
|
||||
if !accs.spl_metadata.data_is_empty() {
|
||||
if accs.spl_metadata.data_is_empty() {
|
||||
return Err(TokenNotNFT.into());
|
||||
}
|
||||
|
||||
|
@ -348,7 +348,7 @@ pub fn transfer_wrapped(
|
|||
.verify_derivation(ctx.program_id, &derivation_data)?;
|
||||
|
||||
// Token must have metadata
|
||||
if !accs.spl_metadata.data_is_empty() {
|
||||
if accs.spl_metadata.data_is_empty() {
|
||||
return Err(TokenNotNFT.into());
|
||||
}
|
||||
|
||||
|
|
|
@ -206,6 +206,7 @@ pub fn complete_wrapped(
|
|||
AccountMeta::new_readonly(bridge_id, false),
|
||||
AccountMeta::new_readonly(spl_token::id(), false),
|
||||
AccountMeta::new_readonly(spl_associated_token_account::id(), false),
|
||||
AccountMeta::new_readonly(spl_token_metadata::id(), false),
|
||||
],
|
||||
data: (crate::instruction::Instruction::CompleteWrapped, data).try_to_vec()?,
|
||||
})
|
||||
|
|
|
@ -113,7 +113,7 @@ pub fn transfer_wrapped_ix(
|
|||
target_addr.copy_from_slice(target_address.as_slice());
|
||||
let mut token_addr = [0u8; 32];
|
||||
token_addr.copy_from_slice(token_address.as_slice());
|
||||
let token_id = U256::from_little_endian(token_id.as_slice());
|
||||
let token_id = U256::from_big_endian(token_id.as_slice());
|
||||
|
||||
let ix = transfer_wrapped(
|
||||
program_id,
|
||||
|
@ -329,7 +329,7 @@ pub fn wrapped_address(
|
|||
let program_id = Pubkey::from_str(program_id.as_str()).unwrap();
|
||||
let mut t_addr = [0u8; 32];
|
||||
t_addr.copy_from_slice(&token_address);
|
||||
let token_id = U256::from_little_endian(token_id.as_slice());
|
||||
let token_id = U256::from_big_endian(token_id.as_slice());
|
||||
|
||||
let wrapped_addr = WrappedMint::<'_, { AccountState::Initialized }>::key(
|
||||
&WrappedDerivationData {
|
||||
|
|
Loading…
Reference in New Issue