nft_bridge fixes

Change-Id: I9420863384e752725cfc75c8b5a21f64be2792b1
This commit is contained in:
Evan Gray 2021-09-10 20:32:46 -04:00
parent 7711abf29a
commit 9ea0369ab0
21 changed files with 535 additions and 73 deletions

View File

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

View File

@ -49,7 +49,7 @@ function Send() {
>
Transfer
</ButtonWithLoader>
<TransferProgress />
<TransferProgress nft />
</>
);
}

View File

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

View File

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

View File

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

View File

@ -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(() => {

View File

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

View File

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

View File

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

View File

@ -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) {

View File

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

View File

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

View File

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

View File

@ -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",

View File

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

View File

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

View File

@ -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).

View File

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

View File

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

View File

@ -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()?,
})

View File

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