bridge_ui: initial NFT bridge support
Change-Id: Iafb0d4f53541cc11c9d42bd432541383274cd2fc
This commit is contained in:
parent
b77751788b
commit
7711abf29a
|
@ -44,7 +44,8 @@
|
|||
"react-redux": "^7.2.4",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"redux": "^3.7.2"
|
||||
"redux": "^3.7.2",
|
||||
"use-debounce": "^7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@craco/craco": "^6.2.0",
|
||||
|
@ -58,8 +59,7 @@
|
|||
},
|
||||
"../sdk/js": {
|
||||
"name": "@certusone/wormhole-sdk",
|
||||
"version": "0.0.1",
|
||||
"hasInstallScript": true,
|
||||
"version": "0.0.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@improbable-eng/grpc-web": "^0.14.0",
|
||||
|
@ -36535,6 +36535,17 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-debounce": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-7.0.0.tgz",
|
||||
"integrity": "sha512-4fvxEEs7ztdNMh+c497HAgysdq2+Ascem6EaDANGlCIap1JzqfL03Xw8xkYc2lShfXm4uO6PA6V5zcXN7gJdFA==",
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/utf-8-validate": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.5.tgz",
|
||||
|
@ -69129,6 +69140,12 @@
|
|||
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
|
||||
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ=="
|
||||
},
|
||||
"use-debounce": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-7.0.0.tgz",
|
||||
"integrity": "sha512-4fvxEEs7ztdNMh+c497HAgysdq2+Ascem6EaDANGlCIap1JzqfL03Xw8xkYc2lShfXm4uO6PA6V5zcXN7gJdFA==",
|
||||
"requires": {}
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.5.tgz",
|
||||
|
|
|
@ -38,7 +38,8 @@
|
|||
"react-redux": "^7.2.4",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"redux": "^3.7.2"
|
||||
"redux": "^3.7.2",
|
||||
"use-debounce": "^7.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"preinstall": "npm ci --prefix ../sdk/js && npm run build --prefix ../sdk/js",
|
||||
|
|
|
@ -6,7 +6,6 @@ import {
|
|||
makeStyles,
|
||||
Toolbar,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
import { GitHub, Publish, Send } from "@material-ui/icons";
|
||||
import {
|
||||
|
@ -18,6 +17,7 @@ import {
|
|||
} from "react-router-dom";
|
||||
import Attest from "./components/Attest";
|
||||
import Home from "./components/Home";
|
||||
import NFT from "./components/NFT";
|
||||
import Transfer from "./components/Transfer";
|
||||
import wormholeLogo from "./icons/wormhole.svg";
|
||||
|
||||
|
@ -75,13 +75,10 @@ function App() {
|
|||
<div className={classes.spacer} />
|
||||
<Hidden implementation="css" xsDown>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<Tooltip title="Coming Soon">
|
||||
<Typography
|
||||
className={classes.link}
|
||||
style={{ color: "#ffffff80", cursor: "default" }}
|
||||
>
|
||||
<Tooltip title="Transfer NFTs to another blockchain">
|
||||
<Link component={NavLink} to="/nft" className={classes.link}>
|
||||
NFTs
|
||||
</Typography>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
<Tooltip title="Transfer tokens to another blockchain">
|
||||
<Link
|
||||
|
@ -139,6 +136,9 @@ function App() {
|
|||
</AppBar>
|
||||
<div className={classes.content}>
|
||||
<Switch>
|
||||
<Route exact path="/nft">
|
||||
<NFT />
|
||||
</Route>
|
||||
<Route exact path="/transfer">
|
||||
<Transfer />
|
||||
</Route>
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import { useSelector } from "react-redux";
|
||||
import { useHandleNFTRedeem } from "../../hooks/useHandleNFTRedeem";
|
||||
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
||||
import { selectNFTTargetChain } from "../../store/selectors";
|
||||
import ButtonWithLoader from "../ButtonWithLoader";
|
||||
import KeyAndBalance from "../KeyAndBalance";
|
||||
import StepDescription from "../StepDescription";
|
||||
|
||||
function Redeem() {
|
||||
const { handleClick, disabled, showLoader } = useHandleNFTRedeem();
|
||||
const targetChain = useSelector(selectNFTTargetChain);
|
||||
const { isReady, statusMessage } = useIsWalletReady(targetChain);
|
||||
return (
|
||||
<>
|
||||
<StepDescription>Receive the NFT on the target chain</StepDescription>
|
||||
<KeyAndBalance chainId={targetChain} />
|
||||
<ButtonWithLoader
|
||||
disabled={!isReady || disabled}
|
||||
onClick={handleClick}
|
||||
showLoader={showLoader}
|
||||
error={statusMessage}
|
||||
>
|
||||
Redeem
|
||||
</ButtonWithLoader>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Redeem;
|
|
@ -0,0 +1,42 @@
|
|||
import { makeStyles, Typography } from "@material-ui/core";
|
||||
import { useCallback } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { selectNFTRedeemTx, selectNFTTargetChain } from "../../store/selectors";
|
||||
import { reset } from "../../store/nftSlice";
|
||||
import ButtonWithLoader from "../ButtonWithLoader";
|
||||
import ShowTx from "../ShowTx";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
description: {
|
||||
textAlign: "center",
|
||||
},
|
||||
}));
|
||||
|
||||
export default function RedeemPreview() {
|
||||
const classes = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
const targetChain = useSelector(selectNFTTargetChain);
|
||||
const redeemTx = useSelector(selectNFTRedeemTx);
|
||||
const handleResetClick = useCallback(() => {
|
||||
dispatch(reset());
|
||||
}, [dispatch]);
|
||||
|
||||
const explainerString =
|
||||
"Success! The redeem transaction was submitted. The NFT will become available once the transaction confirms.";
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography
|
||||
component="div"
|
||||
variant="subtitle2"
|
||||
className={classes.description}
|
||||
>
|
||||
{explainerString}
|
||||
</Typography>
|
||||
{redeemTx ? <ShowTx chainId={targetChain} tx={redeemTx} /> : null}
|
||||
<ButtonWithLoader onClick={handleResetClick}>
|
||||
Transfer Another NFT!
|
||||
</ButtonWithLoader>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import { Alert } from "@material-ui/lab";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useHandleNFTTransfer } from "../../hooks/useHandleNFTTransfer";
|
||||
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
||||
import {
|
||||
selectNFTSourceWalletAddress,
|
||||
selectNFTSourceChain,
|
||||
selectNFTTargetError,
|
||||
} from "../../store/selectors";
|
||||
import { CHAINS_BY_ID } from "../../utils/consts";
|
||||
import ButtonWithLoader from "../ButtonWithLoader";
|
||||
import KeyAndBalance from "../KeyAndBalance";
|
||||
import StepDescription from "../StepDescription";
|
||||
import TransferProgress from "../TransferProgress";
|
||||
|
||||
function Send() {
|
||||
const { handleClick, disabled, showLoader } = useHandleNFTTransfer();
|
||||
const sourceChain = useSelector(selectNFTSourceChain);
|
||||
const error = useSelector(selectNFTTargetError);
|
||||
const { isReady, statusMessage, walletAddress } =
|
||||
useIsWalletReady(sourceChain);
|
||||
const sourceWalletAddress = useSelector(selectNFTSourceWalletAddress);
|
||||
//The chain ID compare is handled implicitly, as the isWalletReady hook should report !isReady if the wallet is on the wrong chain.
|
||||
const isWrongWallet =
|
||||
sourceWalletAddress &&
|
||||
walletAddress &&
|
||||
sourceWalletAddress !== walletAddress;
|
||||
const isDisabled = !isReady || isWrongWallet || disabled;
|
||||
const errorMessage = isWrongWallet
|
||||
? "A different wallet is connected than in Step 1."
|
||||
: statusMessage || error || undefined;
|
||||
return (
|
||||
<>
|
||||
<StepDescription>
|
||||
Transfer the NFT to the Wormhole Token Bridge.
|
||||
</StepDescription>
|
||||
<KeyAndBalance chainId={sourceChain} />
|
||||
<Alert severity="warning">
|
||||
This will initiate the transfer on {CHAINS_BY_ID[sourceChain].name} and
|
||||
wait for finalization. If you navigate away from this page before
|
||||
completing Step 4, you will have to perform the recovery workflow to
|
||||
complete the transfer.
|
||||
</Alert>
|
||||
<ButtonWithLoader
|
||||
disabled={isDisabled}
|
||||
onClick={handleClick}
|
||||
showLoader={showLoader}
|
||||
error={errorMessage}
|
||||
>
|
||||
Transfer
|
||||
</ButtonWithLoader>
|
||||
<TransferProgress />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Send;
|
|
@ -0,0 +1,41 @@
|
|||
import { makeStyles, Typography } from "@material-ui/core";
|
||||
import { useSelector } from "react-redux";
|
||||
import {
|
||||
selectNFTSourceChain,
|
||||
selectNFTTransferTx,
|
||||
} from "../../store/selectors";
|
||||
import ShowTx from "../ShowTx";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
description: {
|
||||
textAlign: "center",
|
||||
},
|
||||
tx: {
|
||||
marginTop: theme.spacing(1),
|
||||
textAlign: "center",
|
||||
},
|
||||
viewButton: {
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function SendPreview() {
|
||||
const classes = useStyles();
|
||||
const sourceChain = useSelector(selectNFTSourceChain);
|
||||
const transferTx = useSelector(selectNFTTransferTx);
|
||||
|
||||
const explainerString = "The NFT has been sent!";
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography
|
||||
component="div"
|
||||
variant="subtitle2"
|
||||
className={classes.description}
|
||||
>
|
||||
{explainerString}
|
||||
</Typography>
|
||||
{transferTx ? <ShowTx chainId={sourceChain} tx={transferTx} /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
|
||||
import { makeStyles, MenuItem, TextField } from "@material-ui/core";
|
||||
import { useCallback } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
||||
import {
|
||||
selectNFTIsSourceComplete,
|
||||
selectNFTShouldLockFields,
|
||||
selectNFTSourceBalanceString,
|
||||
selectNFTSourceChain,
|
||||
selectNFTSourceError,
|
||||
} from "../../store/selectors";
|
||||
import { incrementStep, setSourceChain } from "../../store/nftSlice";
|
||||
import { CHAINS } from "../../utils/consts";
|
||||
import ButtonWithLoader from "../ButtonWithLoader";
|
||||
import KeyAndBalance from "../KeyAndBalance";
|
||||
import StepDescription from "../StepDescription";
|
||||
import { TokenSelector } from "../TokenSelectors/SourceTokenSelector";
|
||||
import { Alert } from "@material-ui/lab";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
transferField: {
|
||||
marginTop: theme.spacing(5),
|
||||
},
|
||||
}));
|
||||
|
||||
function Source() {
|
||||
const classes = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
const sourceChain = useSelector(selectNFTSourceChain);
|
||||
const uiAmountString = useSelector(selectNFTSourceBalanceString);
|
||||
const error = useSelector(selectNFTSourceError);
|
||||
const isSourceComplete = useSelector(selectNFTIsSourceComplete);
|
||||
const shouldLockFields = useSelector(selectNFTShouldLockFields);
|
||||
const { isReady, statusMessage } = useIsWalletReady(sourceChain);
|
||||
const handleSourceChange = useCallback(
|
||||
(event) => {
|
||||
dispatch(setSourceChain(event.target.value));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const handleNextClick = useCallback(() => {
|
||||
dispatch(incrementStep());
|
||||
}, [dispatch]);
|
||||
return (
|
||||
<>
|
||||
<StepDescription>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
Select an NFT to send through the Wormhole NFT Bridge.
|
||||
<div style={{ flexGrow: 1 }} />
|
||||
{/* <Button
|
||||
onClick={() => setIsRecoveryOpen(true)}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
endIcon={<Restore />}
|
||||
>
|
||||
Perform Recovery
|
||||
</Button> */}
|
||||
</div>
|
||||
</StepDescription>
|
||||
<TextField
|
||||
select
|
||||
fullWidth
|
||||
value={sourceChain}
|
||||
onChange={handleSourceChange}
|
||||
disabled={shouldLockFields}
|
||||
>
|
||||
{CHAINS.filter(
|
||||
({ id }) => id === CHAIN_ID_ETH || id === CHAIN_ID_SOLANA
|
||||
).map(({ id, name }) => (
|
||||
<MenuItem key={id} value={id}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
{sourceChain === CHAIN_ID_ETH ? (
|
||||
<Alert severity="info">
|
||||
Only NFTs which implement ERC-721 are supported.
|
||||
</Alert>
|
||||
) : null}
|
||||
<KeyAndBalance chainId={sourceChain} balance={uiAmountString} />
|
||||
{isReady || uiAmountString ? (
|
||||
<div className={classes.transferField}>
|
||||
<TokenSelector disabled={shouldLockFields} nft={true} />
|
||||
</div>
|
||||
) : null}
|
||||
<ButtonWithLoader
|
||||
disabled={!isSourceComplete}
|
||||
onClick={handleNextClick}
|
||||
showLoader={false}
|
||||
error={statusMessage || error}
|
||||
>
|
||||
Next
|
||||
</ButtonWithLoader>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Source;
|
|
@ -0,0 +1,46 @@
|
|||
import { makeStyles, Typography } from "@material-ui/core";
|
||||
import { useSelector } from "react-redux";
|
||||
import {
|
||||
selectNFTSourceChain,
|
||||
selectNFTSourceParsedTokenAccount,
|
||||
} from "../../store/selectors";
|
||||
import { CHAINS_BY_ID } from "../../utils/consts";
|
||||
import { shortenAddress } from "../../utils/solana";
|
||||
import NFTViewer from "../TokenSelectors/NFTViewer";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
description: {
|
||||
textAlign: "center",
|
||||
},
|
||||
}));
|
||||
|
||||
export default function SourcePreview() {
|
||||
const classes = useStyles();
|
||||
const sourceChain = useSelector(selectNFTSourceChain);
|
||||
const sourceParsedTokenAccount = useSelector(
|
||||
selectNFTSourceParsedTokenAccount
|
||||
);
|
||||
|
||||
const explainerString = sourceParsedTokenAccount
|
||||
? `You will transfer 1 NFT of ${shortenAddress(
|
||||
sourceParsedTokenAccount?.mintKey
|
||||
)}, from ${shortenAddress(sourceParsedTokenAccount?.publicKey)} on ${
|
||||
CHAINS_BY_ID[sourceChain].name
|
||||
}`
|
||||
: "Step complete.";
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography
|
||||
component="div"
|
||||
variant="subtitle2"
|
||||
className={classes.description}
|
||||
>
|
||||
{explainerString}
|
||||
</Typography>
|
||||
{sourceParsedTokenAccount ? (
|
||||
<NFTViewer value={sourceParsedTokenAccount} />
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
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 { useDispatch, useSelector } from "react-redux";
|
||||
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
||||
import useSyncTargetAddress from "../../hooks/useSyncTargetAddress";
|
||||
import {
|
||||
selectNFTIsTargetComplete,
|
||||
selectNFTShouldLockFields,
|
||||
selectNFTSourceChain,
|
||||
selectNFTTargetAddressHex,
|
||||
selectNFTTargetAsset,
|
||||
selectNFTTargetBalanceString,
|
||||
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) => ({
|
||||
transferField: {
|
||||
marginTop: theme.spacing(5),
|
||||
},
|
||||
}));
|
||||
|
||||
function Target() {
|
||||
const classes = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
const sourceChain = useSelector(selectNFTSourceChain);
|
||||
const chains = useMemo(
|
||||
() => CHAINS.filter((c) => c.id !== sourceChain),
|
||||
[sourceChain]
|
||||
);
|
||||
const targetChain = useSelector(selectNFTTargetChain);
|
||||
const targetAddressHex = useSelector(selectNFTTargetAddressHex);
|
||||
const targetAsset = useSelector(selectNFTTargetAsset);
|
||||
const readableTargetAddress =
|
||||
hexToNativeString(targetAddressHex, targetChain) || "";
|
||||
const uiAmountString = useSelector(selectNFTTargetBalanceString);
|
||||
const error = useSelector(selectNFTTargetError);
|
||||
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) => {
|
||||
dispatch(setTargetChain(event.target.value));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const handleNextClick = useCallback(() => {
|
||||
dispatch(incrementStep());
|
||||
}, [dispatch]);
|
||||
return (
|
||||
<>
|
||||
<StepDescription>Select a recipient chain and address.</StepDescription>
|
||||
<TextField
|
||||
select
|
||||
fullWidth
|
||||
value={targetChain}
|
||||
onChange={handleTargetChange}
|
||||
disabled={true}
|
||||
>
|
||||
{chains
|
||||
.filter(({ id }) => id === CHAIN_ID_ETH || id === CHAIN_ID_SOLANA)
|
||||
.map(({ id, name }) => (
|
||||
<MenuItem key={id} value={id}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<KeyAndBalance chainId={targetChain} balance={uiAmountString} />
|
||||
<TextField
|
||||
label="Recipient Address"
|
||||
fullWidth
|
||||
className={classes.transferField}
|
||||
value={readableTargetAddress}
|
||||
disabled={true}
|
||||
/>
|
||||
{targetChain === CHAIN_ID_SOLANA && targetAsset ? (
|
||||
<SolanaCreateAssociatedAddress
|
||||
mintAddress={targetAsset}
|
||||
readableTargetAddress={readableTargetAddress}
|
||||
associatedAccountExists={associatedAccountExists}
|
||||
setAssociatedAccountExists={setAssociatedAccountExists}
|
||||
/>
|
||||
) : null}
|
||||
<TextField
|
||||
label="Token Address"
|
||||
fullWidth
|
||||
className={classes.transferField}
|
||||
value={targetAsset || ""}
|
||||
disabled={true}
|
||||
/>
|
||||
<ButtonWithLoader
|
||||
disabled={!isTargetComplete} //|| !associatedAccountExists}
|
||||
onClick={handleNextClick}
|
||||
showLoader={false}
|
||||
error={statusMessage || error}
|
||||
>
|
||||
Next
|
||||
</ButtonWithLoader>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Target;
|
|
@ -0,0 +1,38 @@
|
|||
import { makeStyles, Typography } from "@material-ui/core";
|
||||
import { useSelector } from "react-redux";
|
||||
import {
|
||||
selectNFTTargetAddressHex,
|
||||
selectNFTTargetChain,
|
||||
} from "../../store/selectors";
|
||||
import { hexToNativeString } from "../../utils/array";
|
||||
import { CHAINS_BY_ID } from "../../utils/consts";
|
||||
import { shortenAddress } from "../../utils/solana";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
description: {
|
||||
textAlign: "center",
|
||||
},
|
||||
}));
|
||||
|
||||
export default function TargetPreview() {
|
||||
const classes = useStyles();
|
||||
const targetChain = useSelector(selectNFTTargetChain);
|
||||
const targetAddress = useSelector(selectNFTTargetAddressHex);
|
||||
const targetAddressNative = hexToNativeString(targetAddress, targetChain);
|
||||
|
||||
const explainerString = targetAddressNative
|
||||
? `to ${shortenAddress(targetAddressNative)} on ${
|
||||
CHAINS_BY_ID[targetChain].name
|
||||
}`
|
||||
: "Step complete.";
|
||||
|
||||
return (
|
||||
<Typography
|
||||
component="div"
|
||||
variant="subtitle2"
|
||||
className={classes.description}
|
||||
>
|
||||
{explainerString}
|
||||
</Typography>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
import {
|
||||
Container,
|
||||
makeStyles,
|
||||
Step,
|
||||
StepButton,
|
||||
StepContent,
|
||||
Stepper,
|
||||
} from "@material-ui/core";
|
||||
import { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import useCheckIfWormholeWrapped from "../../hooks/useCheckIfWormholeWrapped";
|
||||
import useFetchTargetAsset from "../../hooks/useFetchTargetAsset";
|
||||
import {
|
||||
selectNFTActiveStep,
|
||||
selectNFTIsRedeemComplete,
|
||||
selectNFTIsRedeeming,
|
||||
selectNFTIsSendComplete,
|
||||
selectNFTIsSending,
|
||||
} from "../../store/selectors";
|
||||
import { setStep } from "../../store/nftSlice";
|
||||
// import Recovery from "./Recovery";
|
||||
import Redeem from "./Redeem";
|
||||
import RedeemPreview from "./RedeemPreview";
|
||||
import Send from "./Send";
|
||||
import SendPreview from "./SendPreview";
|
||||
import Source from "./Source";
|
||||
import SourcePreview from "./SourcePreview";
|
||||
import Target from "./Target";
|
||||
import TargetPreview from "./TargetPreview";
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
rootContainer: {
|
||||
backgroundColor: "rgba(0,0,0,0.2)",
|
||||
},
|
||||
}));
|
||||
|
||||
function NFT() {
|
||||
const classes = useStyles();
|
||||
useCheckIfWormholeWrapped(true);
|
||||
useFetchTargetAsset(true);
|
||||
// const [isRecoveryOpen, setIsRecoveryOpen] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const activeStep = useSelector(selectNFTActiveStep);
|
||||
const isSending = useSelector(selectNFTIsSending);
|
||||
const isSendComplete = useSelector(selectNFTIsSendComplete);
|
||||
const isRedeeming = useSelector(selectNFTIsRedeeming);
|
||||
const isRedeemComplete = useSelector(selectNFTIsRedeemComplete);
|
||||
const preventNavigation = isSending || isSendComplete || isRedeeming;
|
||||
useEffect(() => {
|
||||
if (preventNavigation) {
|
||||
window.onbeforeunload = () => true;
|
||||
return () => {
|
||||
window.onbeforeunload = null;
|
||||
};
|
||||
}
|
||||
}, [preventNavigation]);
|
||||
return (
|
||||
<Container maxWidth="md">
|
||||
<Stepper
|
||||
activeStep={activeStep}
|
||||
orientation="vertical"
|
||||
className={classes.rootContainer}
|
||||
>
|
||||
<Step
|
||||
expanded={activeStep >= 0}
|
||||
disabled={preventNavigation || isRedeemComplete}
|
||||
>
|
||||
<StepButton onClick={() => dispatch(setStep(0))}>Source</StepButton>
|
||||
<StepContent>
|
||||
{activeStep === 0 ? (
|
||||
<Source
|
||||
// setIsRecoveryOpen={setIsRecoveryOpen}
|
||||
/>
|
||||
) : (
|
||||
<SourcePreview />
|
||||
)}
|
||||
</StepContent>
|
||||
</Step>
|
||||
<Step
|
||||
expanded={activeStep >= 1}
|
||||
disabled={preventNavigation || isRedeemComplete}
|
||||
>
|
||||
<StepButton onClick={() => dispatch(setStep(1))}>Target</StepButton>
|
||||
<StepContent>
|
||||
{activeStep === 1 ? <Target /> : <TargetPreview />}
|
||||
</StepContent>
|
||||
</Step>
|
||||
<Step expanded={activeStep >= 2} disabled={isSendComplete}>
|
||||
<StepButton onClick={() => dispatch(setStep(2))}>Send NFT</StepButton>
|
||||
<StepContent>
|
||||
{activeStep === 2 ? <Send /> : <SendPreview />}
|
||||
</StepContent>
|
||||
</Step>
|
||||
<Step expanded={activeStep >= 3}>
|
||||
<StepButton
|
||||
onClick={() => dispatch(setStep(3))}
|
||||
disabled={!isSendComplete}
|
||||
>
|
||||
Redeem NFT
|
||||
</StepButton>
|
||||
<StepContent>
|
||||
{isRedeemComplete ? <RedeemPreview /> : <Redeem />}
|
||||
</StepContent>
|
||||
</Step>
|
||||
</Stepper>
|
||||
{/* <Recovery
|
||||
open={isRecoveryOpen}
|
||||
setOpen={setIsRecoveryOpen}
|
||||
disabled={preventNavigation}
|
||||
/> */}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default NFT;
|
|
@ -12,14 +12,20 @@ import { CovalentData } from "../../hooks/useGetSourceParsedTokenAccounts";
|
|||
import { DataWrapper } from "../../store/helpers";
|
||||
import { ParsedTokenAccount } from "../../store/transferSlice";
|
||||
import {
|
||||
ethNFTToNFTParsedTokenAccount,
|
||||
ethTokenToParsedTokenAccount,
|
||||
getEthereumNFT,
|
||||
getEthereumToken,
|
||||
isNFT,
|
||||
isValidEthereumAddress,
|
||||
} from "../../utils/ethereum";
|
||||
import { shortenAddress } from "../../utils/solana";
|
||||
import OffsetButton from "./OffsetButton";
|
||||
import { WormholeAbi__factory } from "@certusone/wormhole-sdk/lib/ethers-contracts/abi";
|
||||
import { WORMHOLE_V1_ETH_ADDRESS } from "../../utils/consts";
|
||||
import { NFTParsedTokenAccount } from "../../store/nftSlice";
|
||||
import NFTViewer from "./NFTViewer";
|
||||
import { useDebounce } from "use-debounce/lib";
|
||||
|
||||
const useStyles = makeStyles(() =>
|
||||
createStyles({
|
||||
|
@ -50,6 +56,7 @@ type EthereumSourceTokenSelectorProps = {
|
|||
covalent: DataWrapper<CovalentData[]> | undefined;
|
||||
tokenAccounts: DataWrapper<ParsedTokenAccount[]> | undefined;
|
||||
disabled: boolean;
|
||||
nft?: boolean;
|
||||
};
|
||||
|
||||
const renderAccount = (
|
||||
|
@ -79,15 +86,48 @@ const renderAccount = (
|
|||
);
|
||||
};
|
||||
|
||||
const renderNFTAccount = (
|
||||
account: NFTParsedTokenAccount,
|
||||
covalentData: CovalentData | undefined,
|
||||
classes: any
|
||||
) => {
|
||||
const mintPrettyString = shortenAddress(account.mintKey);
|
||||
const tokenId = account.tokenId;
|
||||
const uri = account.image_256;
|
||||
const symbol = covalentData?.contract_ticker_symbol || "Unknown";
|
||||
const name = account.name || "Unknown";
|
||||
return (
|
||||
<div className={classes.tokenOverviewContainer}>
|
||||
<div>
|
||||
{uri && <img alt="" className={classes.tokenImage} src={uri} />}
|
||||
</div>
|
||||
<div>
|
||||
<Typography>{symbol}</Typography>
|
||||
<Typography>{name}</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography>{mintPrettyString}</Typography>
|
||||
<Typography>{tokenId}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function EthereumSourceTokenSelector(
|
||||
props: EthereumSourceTokenSelectorProps
|
||||
) {
|
||||
const { value, onChange, covalent, tokenAccounts, disabled } = props;
|
||||
const { value, onChange, covalent, tokenAccounts, disabled, nft } = props;
|
||||
const classes = useStyles();
|
||||
const [advancedMode, setAdvancedMode] = useState(false);
|
||||
const [advancedModeLoading, setAdvancedModeLoading] = useState(false);
|
||||
const [advancedModeSymbol, setAdvancedModeSymbol] = useState("");
|
||||
const [advancedModeHolderString, setAdvancedModeHolderString] = useState("");
|
||||
const [advancedModeHolderTokenIdRaw, setAdvancedModeHolderTokenId] =
|
||||
useState("");
|
||||
const [advancedModeHolderTokenId] = useDebounce(
|
||||
advancedModeHolderTokenIdRaw,
|
||||
500
|
||||
);
|
||||
const [advancedModeError, setAdvancedModeError] = useState("");
|
||||
|
||||
const [autocompleteHolder, setAutocompleteHolder] =
|
||||
|
@ -104,11 +144,23 @@ export default function EthereumSourceTokenSelector(
|
|||
//This also kicks off the metadata load.
|
||||
if (advancedMode && value && advancedModeHolderString !== value.mintKey) {
|
||||
setAdvancedModeHolderString(value.mintKey);
|
||||
// @ts-ignore // TODO: could be NFTParsedTokenAccount which has a tokenId, nicer way to represent this?
|
||||
if (nft && advancedModeHolderTokenId !== value.tokenId) {
|
||||
// @ts-ignore
|
||||
setAdvancedModeHolderTokenId(value.tokenId || "");
|
||||
}
|
||||
}
|
||||
if (!advancedMode && value && !autocompleteHolder) {
|
||||
setAutocompleteHolder(value);
|
||||
}
|
||||
}, [value, advancedMode, advancedModeHolderString, autocompleteHolder]);
|
||||
}, [
|
||||
value,
|
||||
advancedMode,
|
||||
advancedModeHolderString,
|
||||
autocompleteHolder,
|
||||
nft,
|
||||
advancedModeHolderTokenId,
|
||||
]);
|
||||
|
||||
//This effect is watching the autocomplete selection.
|
||||
//It checks to make sure the token is a valid choice before putting it on the state.
|
||||
|
@ -119,6 +171,10 @@ export default function EthereumSourceTokenSelector(
|
|||
} else {
|
||||
let cancelled = false;
|
||||
setAutocompleteError("");
|
||||
if (nft) {
|
||||
onChange(autocompleteHolder);
|
||||
return;
|
||||
}
|
||||
isWormholev1(provider, autocompleteHolder.mintKey).then(
|
||||
(result) => {
|
||||
if (!cancelled) {
|
||||
|
@ -143,7 +199,7 @@ export default function EthereumSourceTokenSelector(
|
|||
cancelled = true;
|
||||
};
|
||||
}
|
||||
}, [autocompleteHolder, provider, advancedMode, onChange]);
|
||||
}, [autocompleteHolder, provider, advancedMode, onChange, nft]);
|
||||
|
||||
//This effect watches the advancedModeString, and checks that the selected asset is valid before putting
|
||||
// it on the state.
|
||||
|
@ -162,68 +218,111 @@ export default function EthereumSourceTokenSelector(
|
|||
!cancelled && setAdvancedModeError("");
|
||||
!cancelled && setAdvancedModeSymbol("");
|
||||
try {
|
||||
//Validate that the token is not a wormhole v1 asset
|
||||
const isWormholePromise = isWormholev1(
|
||||
provider,
|
||||
advancedModeHolderString
|
||||
).then(
|
||||
(result) => {
|
||||
if (result && !cancelled) {
|
||||
setAdvancedModeError(
|
||||
"Wormhole v1 assets are not eligible for transfer."
|
||||
);
|
||||
setAdvancedModeLoading(false);
|
||||
return Promise.reject();
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
!cancelled &&
|
||||
setAdvancedModeError(
|
||||
"Warning: please verify if this is a Wormhole v1 token address. V1 tokens should not be transferred with this bridge"
|
||||
);
|
||||
!cancelled && setAdvancedModeLoading(false);
|
||||
return Promise.resolve(); //Don't allow an error here to tank the workflow
|
||||
}
|
||||
);
|
||||
|
||||
//Then fetch the asset's information & transform to a parsed token account
|
||||
isWormholePromise.then(() =>
|
||||
getEthereumToken(advancedModeHolderString, provider).then(
|
||||
(token) => {
|
||||
ethTokenToParsedTokenAccount(token, signerAddress).then(
|
||||
(parsedTokenAccount) => {
|
||||
!cancelled && onChange(parsedTokenAccount);
|
||||
!cancelled && setAdvancedModeLoading(false);
|
||||
},
|
||||
(error) => {
|
||||
//These errors can maybe be consolidated
|
||||
if (nft) {
|
||||
getEthereumNFT(advancedModeHolderString, provider)
|
||||
.then((token) => {
|
||||
isNFT(token)
|
||||
.then((result) => {
|
||||
if (result) {
|
||||
ethNFTToNFTParsedTokenAccount(
|
||||
token,
|
||||
advancedModeHolderTokenId,
|
||||
signerAddress
|
||||
)
|
||||
.then((parsedTokenAccount) => {
|
||||
!cancelled && onChange(parsedTokenAccount);
|
||||
!cancelled && setAdvancedModeLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
!cancelled &&
|
||||
setAdvancedModeError(
|
||||
"Failed to find the specified tokenId"
|
||||
);
|
||||
!cancelled && setAdvancedModeLoading(false);
|
||||
});
|
||||
} else {
|
||||
!cancelled &&
|
||||
setAdvancedModeError(
|
||||
"This token does not support ERC-721"
|
||||
);
|
||||
!cancelled && setAdvancedModeLoading(false);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
!cancelled &&
|
||||
setAdvancedModeError(
|
||||
"Failed to find the specified address"
|
||||
);
|
||||
setAdvancedModeError("This token does not support ERC-721");
|
||||
!cancelled && setAdvancedModeLoading(false);
|
||||
}
|
||||
);
|
||||
|
||||
//Also attempt to store off the symbol
|
||||
token.symbol().then(
|
||||
(result) => {
|
||||
!cancelled && setAdvancedModeSymbol(result);
|
||||
},
|
||||
(error) => {
|
||||
!cancelled &&
|
||||
setAdvancedModeError(
|
||||
"Failed to find the specified address"
|
||||
);
|
||||
!cancelled && setAdvancedModeLoading(false);
|
||||
}
|
||||
);
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
!cancelled &&
|
||||
setAdvancedModeError("This token does not support ERC-721");
|
||||
!cancelled && setAdvancedModeLoading(false);
|
||||
});
|
||||
} else {
|
||||
//Validate that the token is not a wormhole v1 asset
|
||||
const isWormholePromise = isWormholev1(
|
||||
provider,
|
||||
advancedModeHolderString
|
||||
).then(
|
||||
(result) => {
|
||||
if (result && !cancelled) {
|
||||
setAdvancedModeError(
|
||||
"Wormhole v1 assets are not eligible for transfer."
|
||||
);
|
||||
setAdvancedModeLoading(false);
|
||||
return Promise.reject();
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
},
|
||||
(error) => {}
|
||||
)
|
||||
);
|
||||
(error) => {
|
||||
!cancelled &&
|
||||
setAdvancedModeError(
|
||||
"Warning: please verify if this is a Wormhole v1 token address. V1 tokens should not be transferred with this bridge"
|
||||
);
|
||||
!cancelled && setAdvancedModeLoading(false);
|
||||
return Promise.resolve(); //Don't allow an error here to tank the workflow
|
||||
}
|
||||
);
|
||||
|
||||
//Then fetch the asset's information & transform to a parsed token account
|
||||
isWormholePromise.then(() =>
|
||||
getEthereumToken(advancedModeHolderString, provider).then(
|
||||
(token) => {
|
||||
ethTokenToParsedTokenAccount(token, signerAddress).then(
|
||||
(parsedTokenAccount) => {
|
||||
!cancelled && onChange(parsedTokenAccount);
|
||||
!cancelled && setAdvancedModeLoading(false);
|
||||
},
|
||||
(error) => {
|
||||
//These errors can maybe be consolidated
|
||||
!cancelled &&
|
||||
setAdvancedModeError(
|
||||
"Failed to find the specified address"
|
||||
);
|
||||
!cancelled && setAdvancedModeLoading(false);
|
||||
}
|
||||
);
|
||||
|
||||
//Also attempt to store off the symbol
|
||||
token.symbol().then(
|
||||
(result) => {
|
||||
!cancelled && setAdvancedModeSymbol(result);
|
||||
},
|
||||
(error) => {
|
||||
!cancelled &&
|
||||
setAdvancedModeError(
|
||||
"Failed to find the specified address"
|
||||
);
|
||||
!cancelled && setAdvancedModeLoading(false);
|
||||
}
|
||||
);
|
||||
},
|
||||
(error) => {}
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
!cancelled &&
|
||||
setAdvancedModeError("Failed to find the specified address");
|
||||
|
@ -239,11 +338,14 @@ export default function EthereumSourceTokenSelector(
|
|||
provider,
|
||||
signerAddress,
|
||||
onChange,
|
||||
nft,
|
||||
advancedModeHolderTokenId,
|
||||
]);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
onChange(null);
|
||||
setAdvancedModeHolderString("");
|
||||
setAdvancedModeHolderTokenId("");
|
||||
}, [onChange]);
|
||||
|
||||
const handleOnChange = useCallback(
|
||||
|
@ -251,6 +353,11 @@ export default function EthereumSourceTokenSelector(
|
|||
[]
|
||||
);
|
||||
|
||||
const handleTokenIdOnChange = useCallback(
|
||||
(event) => setAdvancedModeHolderTokenId(event.target.value),
|
||||
[]
|
||||
);
|
||||
|
||||
const getSymbol = (account: ParsedTokenAccount | null) => {
|
||||
if (!account) {
|
||||
return undefined;
|
||||
|
@ -271,6 +378,18 @@ export default function EthereumSourceTokenSelector(
|
|||
},
|
||||
});
|
||||
|
||||
const filterConfigNFT = createFilterOptions({
|
||||
matchFrom: "any",
|
||||
stringify: (option: NFTParsedTokenAccount) => {
|
||||
const symbol = getSymbol(option) + " " || "";
|
||||
const mint = option.mintKey + " ";
|
||||
const name = option.name ? option.name + " " : "";
|
||||
const id = option.tokenId ? option.tokenId + " " : "";
|
||||
|
||||
return symbol + mint + name + id;
|
||||
},
|
||||
});
|
||||
|
||||
const toggleAdvancedMode = () => {
|
||||
setAdvancedModeHolderString("");
|
||||
setAdvancedModeError("");
|
||||
|
@ -294,29 +413,45 @@ export default function EthereumSourceTokenSelector(
|
|||
blurOnSelect
|
||||
clearOnBlur
|
||||
fullWidth={false}
|
||||
filterOptions={filterConfig}
|
||||
filterOptions={nft ? filterConfigNFT : filterConfig}
|
||||
value={autocompleteHolder}
|
||||
onChange={(event, newValue) => {
|
||||
handleAutocompleteChange(newValue);
|
||||
}}
|
||||
disabled={disabled}
|
||||
noOptionsText={"No ERC20 tokens found at the moment."}
|
||||
noOptionsText={
|
||||
nft
|
||||
? "No ERC-721 tokens found at the moment."
|
||||
: "No ERC-20 tokens found at the moment."
|
||||
}
|
||||
options={tokenAccounts?.data || []}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label="Token Account" variant="outlined" />
|
||||
)}
|
||||
renderOption={(option) => {
|
||||
return renderAccount(
|
||||
option,
|
||||
covalent?.data?.find((x) => x.contract_address === option.mintKey),
|
||||
classes
|
||||
);
|
||||
return nft
|
||||
? renderNFTAccount(
|
||||
option,
|
||||
covalent?.data?.find(
|
||||
(x) => x.contract_address === option.mintKey
|
||||
),
|
||||
classes
|
||||
)
|
||||
: renderAccount(
|
||||
option,
|
||||
covalent?.data?.find(
|
||||
(x) => x.contract_address === option.mintKey
|
||||
),
|
||||
classes
|
||||
);
|
||||
}}
|
||||
getOptionLabel={(option) => {
|
||||
const symbol = getSymbol(option);
|
||||
return `${symbol ? symbol : "Unknown"} (Address: ${shortenAddress(
|
||||
option.mintKey
|
||||
)})`;
|
||||
return `${symbol ? symbol : "Unknown"} ${
|
||||
nft && option.name ? option.name : ""
|
||||
} (Address: ${shortenAddress(option.mintKey)}${
|
||||
nft ? `, ID: ${option.tokenId}` : ""
|
||||
})`;
|
||||
}}
|
||||
/>
|
||||
{autocompleteError && (
|
||||
|
@ -335,7 +470,11 @@ export default function EthereumSourceTokenSelector(
|
|||
|
||||
const content = value ? (
|
||||
<>
|
||||
<Typography>{(symbol ? symbol + " " : "") + value.mintKey}</Typography>
|
||||
{nft ? (
|
||||
<NFTViewer symbol={symbol} value={value} />
|
||||
) : (
|
||||
<Typography>{(symbol ? symbol + " " : "") + value.mintKey}</Typography>
|
||||
)}
|
||||
<OffsetButton onClick={handleClick} disabled={disabled}>
|
||||
Clear
|
||||
</OffsetButton>
|
||||
|
@ -345,8 +484,6 @@ export default function EthereumSourceTokenSelector(
|
|||
<Typography color="error">{advancedModeError}</Typography>
|
||||
) : null}
|
||||
</>
|
||||
) : isLoading ? (
|
||||
<CircularProgress />
|
||||
) : advancedMode ? (
|
||||
<>
|
||||
<TextField
|
||||
|
@ -362,7 +499,21 @@ export default function EthereumSourceTokenSelector(
|
|||
helperText={advancedModeError === "" ? undefined : advancedModeError}
|
||||
disabled={disabled || advancedModeLoading}
|
||||
/>
|
||||
{nft ? (
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Enter a tokenId"
|
||||
value={advancedModeHolderTokenIdRaw}
|
||||
onChange={handleTokenIdOnChange}
|
||||
disabled={disabled || advancedModeLoading}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
) : isLoading ? (
|
||||
<Typography component="div">
|
||||
<CircularProgress size={"1em"} />{" "}
|
||||
{nft ? "Loading (this may take a while)..." : "Loading..."}
|
||||
</Typography>
|
||||
) : (
|
||||
autoComplete
|
||||
);
|
||||
|
@ -370,7 +521,7 @@ export default function EthereumSourceTokenSelector(
|
|||
return (
|
||||
<React.Fragment>
|
||||
{content}
|
||||
{!value && !isLoading && advancedModeToggleButton}
|
||||
{!value && advancedModeToggleButton}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardMedia,
|
||||
makeStyles,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
import { NFTParsedTokenAccount } from "../../store/nftSlice";
|
||||
|
||||
const safeIPFS = (uri: string) =>
|
||||
uri.startsWith("ipfs://ipfs/")
|
||||
? uri.replace("ipfs://", "https://cloudflare-ipfs.com/")
|
||||
: uri.startsWith("ipfs://")
|
||||
? uri.replace("ipfs://", "https://cloudflare-ipfs.com/ipfs/")
|
||||
: uri;
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
card: {
|
||||
background: "transparent",
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
maxWidth: 480,
|
||||
width: 480,
|
||||
margin: `${theme.spacing(1)}px auto`,
|
||||
},
|
||||
textContent: {
|
||||
background: theme.palette.background.paper,
|
||||
},
|
||||
mediaContent: {
|
||||
background: "transparent",
|
||||
},
|
||||
}));
|
||||
|
||||
export default function NFTViewer({
|
||||
value,
|
||||
symbol,
|
||||
}: {
|
||||
value: NFTParsedTokenAccount;
|
||||
symbol?: string;
|
||||
}) {
|
||||
const classes = useStyles();
|
||||
const animLower = value.animation_url?.toLowerCase();
|
||||
// const has3DModel = animLower?.endsWith('gltf') || animLower?.endsWith('glb')
|
||||
const hasVideo =
|
||||
!animLower?.startsWith("ipfs://") && // cloudflare ipfs doesn't support streaming video
|
||||
(animLower?.endsWith("webm") ||
|
||||
animLower?.endsWith("mp4") ||
|
||||
animLower?.endsWith("mov") ||
|
||||
animLower?.endsWith("m4v") ||
|
||||
animLower?.endsWith("ogv") ||
|
||||
animLower?.endsWith("ogg"));
|
||||
const hasAudio =
|
||||
animLower?.endsWith("mp3") ||
|
||||
animLower?.endsWith("flac") ||
|
||||
animLower?.endsWith("wav") ||
|
||||
animLower?.endsWith("oga");
|
||||
const image = (
|
||||
<img
|
||||
src={safeIPFS(value.image || "")}
|
||||
alt={value.name || ""}
|
||||
style={{ maxWidth: "100%" }}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Card className={classes.card} elevation={10}>
|
||||
<CardContent className={classes.textContent}>
|
||||
<Typography>{(symbol ? symbol + " " : "") + value.mintKey}</Typography>
|
||||
<Typography>
|
||||
{value.name} ({value.tokenId})
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardMedia className={classes.mediaContent}>
|
||||
{hasVideo ? (
|
||||
<video controls style={{ maxWidth: "100%" }}>
|
||||
<source src={safeIPFS(value.animation_url || "")} />
|
||||
{image}
|
||||
</video>
|
||||
) : (
|
||||
image
|
||||
)}
|
||||
{hasAudio ? (
|
||||
<audio controls src={safeIPFS(value.animation_url || "")} />
|
||||
) : null}
|
||||
</CardMedia>
|
||||
</Card>
|
||||
);
|
||||
}
|
|
@ -33,12 +33,13 @@ type SolanaSourceTokenSelectorProps = {
|
|||
metaplexData: any; //DataWrapper<(Metadata | undefined)[]> | undefined | null;
|
||||
disabled: boolean;
|
||||
mintAccounts: DataWrapper<Map<String, string | null>> | undefined;
|
||||
nft?: boolean;
|
||||
};
|
||||
|
||||
export default function SolanaSourceTokenSelector(
|
||||
props: SolanaSourceTokenSelectorProps
|
||||
) {
|
||||
const { value, onChange, disabled } = props;
|
||||
const { value, onChange, disabled, nft } = props;
|
||||
const classes = useStyles();
|
||||
|
||||
const memoizedTokenMap: Map<String, TokenInfo> = useMemo(() => {
|
||||
|
@ -196,9 +197,9 @@ export default function SolanaSourceTokenSelector(
|
|||
//TODO, do a better check which likely involves supply or checking masterEdition.
|
||||
const isNFT =
|
||||
x.decimals === 0 && memoizedMetaplex.get(x.mintKey)?.data?.uri;
|
||||
return !isNFT;
|
||||
return nft ? isNFT : !isNFT;
|
||||
});
|
||||
}, [memoizedMetaplex, props.accounts]);
|
||||
}, [memoizedMetaplex, nft, props.accounts]);
|
||||
|
||||
const isOptionDisabled = useMemo(() => {
|
||||
return (value: ParsedTokenAccount) => isWormholev1(value.mintKey);
|
||||
|
@ -220,7 +221,11 @@ export default function SolanaSourceTokenSelector(
|
|||
disabled={disabled}
|
||||
options={filteredOptions}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label="Token Account" variant="outlined" />
|
||||
<TextField
|
||||
{...params}
|
||||
label={nft ? "NFT Account" : "Token Account"}
|
||||
variant="outlined"
|
||||
/>
|
||||
)}
|
||||
renderOption={(option) => {
|
||||
return renderAccount(
|
||||
|
|
|
@ -10,32 +10,50 @@ import { useDispatch, useSelector } from "react-redux";
|
|||
import useGetSourceParsedTokens from "../../hooks/useGetSourceParsedTokenAccounts";
|
||||
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
||||
import {
|
||||
selectNFTSourceChain,
|
||||
selectNFTSourceParsedTokenAccount,
|
||||
selectTransferSourceChain,
|
||||
selectTransferSourceParsedTokenAccount,
|
||||
} from "../../store/selectors";
|
||||
import {
|
||||
ParsedTokenAccount,
|
||||
setSourceParsedTokenAccount,
|
||||
setSourceWalletAddress,
|
||||
setSourceParsedTokenAccount as setTransferSourceParsedTokenAccount,
|
||||
setSourceWalletAddress as setTransferSourceWalletAddress,
|
||||
} from "../../store/transferSlice";
|
||||
import {
|
||||
setSourceParsedTokenAccount as setNFTSourceParsedTokenAccount,
|
||||
setSourceWalletAddress as setNFTSourceWalletAddress,
|
||||
} from "../../store/nftSlice";
|
||||
import EthereumSourceTokenSelector from "./EthereumSourceTokenSelector";
|
||||
import SolanaSourceTokenSelector from "./SolanaSourceTokenSelector";
|
||||
import TerraSourceTokenSelector from "./TerraSourceTokenSelector";
|
||||
|
||||
type TokenSelectorProps = {
|
||||
disabled: boolean;
|
||||
nft?: boolean;
|
||||
};
|
||||
|
||||
export const TokenSelector = (props: TokenSelectorProps) => {
|
||||
const { disabled } = props;
|
||||
const { disabled, nft } = props;
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const lookupChain = useSelector(selectTransferSourceChain);
|
||||
const lookupChain = useSelector(
|
||||
nft ? selectNFTSourceChain : selectTransferSourceChain
|
||||
);
|
||||
const sourceParsedTokenAccount = useSelector(
|
||||
selectTransferSourceParsedTokenAccount
|
||||
nft
|
||||
? selectNFTSourceParsedTokenAccount
|
||||
: selectTransferSourceParsedTokenAccount
|
||||
);
|
||||
const walletIsReady = useIsWalletReady(lookupChain);
|
||||
|
||||
const setSourceParsedTokenAccount = nft
|
||||
? setNFTSourceParsedTokenAccount
|
||||
: setTransferSourceParsedTokenAccount;
|
||||
const setSourceWalletAddress = nft
|
||||
? setNFTSourceWalletAddress
|
||||
: setTransferSourceWalletAddress;
|
||||
|
||||
const handleOnChange = useCallback(
|
||||
(newTokenAccount: ParsedTokenAccount | null) => {
|
||||
if (!newTokenAccount) {
|
||||
|
@ -46,10 +64,15 @@ export const TokenSelector = (props: TokenSelectorProps) => {
|
|||
dispatch(setSourceWalletAddress(walletIsReady.walletAddress));
|
||||
}
|
||||
},
|
||||
[dispatch, walletIsReady]
|
||||
[
|
||||
dispatch,
|
||||
walletIsReady,
|
||||
setSourceParsedTokenAccount,
|
||||
setSourceWalletAddress,
|
||||
]
|
||||
);
|
||||
|
||||
const maps = useGetSourceParsedTokens();
|
||||
const maps = useGetSourceParsedTokens(nft);
|
||||
|
||||
//This is only for errors so bad that we shouldn't even mount the component
|
||||
const fatalError =
|
||||
|
@ -68,6 +91,7 @@ export const TokenSelector = (props: TokenSelectorProps) => {
|
|||
solanaTokenMap={maps?.tokenMap}
|
||||
metaplexData={maps?.metaplex}
|
||||
mintAccounts={maps?.mintAccounts}
|
||||
nft={nft}
|
||||
/>
|
||||
) : lookupChain === CHAIN_ID_ETH ? (
|
||||
<EthereumSourceTokenSelector
|
||||
|
@ -76,6 +100,7 @@ export const TokenSelector = (props: TokenSelectorProps) => {
|
|||
onChange={handleOnChange}
|
||||
covalent={maps?.covalent || undefined}
|
||||
tokenAccounts={maps?.tokenAccounts} //TODO standardize
|
||||
nft={nft}
|
||||
/>
|
||||
) : lookupChain === CHAIN_ID_TERRA ? (
|
||||
<TerraSourceTokenSelector
|
||||
|
|
|
@ -11,7 +11,7 @@ import { CHAINS_BY_ID } from "../../utils/consts";
|
|||
import ButtonWithLoader from "../ButtonWithLoader";
|
||||
import KeyAndBalance from "../KeyAndBalance";
|
||||
import StepDescription from "../StepDescription";
|
||||
import TransferProgress from "./TransferProgress";
|
||||
import TransferProgress from "../TransferProgress";
|
||||
|
||||
function Send() {
|
||||
const { handleClick, disabled, showLoader } = useHandleTransfer();
|
||||
|
|
|
@ -3,13 +3,13 @@ import { LinearProgress, makeStyles, Typography } from "@material-ui/core";
|
|||
import { Connection } from "@solana/web3.js";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
||||
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||
import {
|
||||
selectTransferIsSendComplete,
|
||||
selectTransferSourceChain,
|
||||
selectTransferTransferTx,
|
||||
} from "../../store/selectors";
|
||||
import { CHAINS_BY_ID, SOLANA_HOST } from "../../utils/consts";
|
||||
} from "../store/selectors";
|
||||
import { CHAINS_BY_ID, SOLANA_HOST } from "../utils/consts";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
|
@ -1,29 +1,73 @@
|
|||
import {
|
||||
ChainId,
|
||||
CHAIN_ID_ETH,
|
||||
CHAIN_ID_SOLANA,
|
||||
CHAIN_ID_TERRA,
|
||||
getOriginalAssetEth,
|
||||
getOriginalAssetSol,
|
||||
getOriginalAssetTerra,
|
||||
WormholeWrappedInfo,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import {
|
||||
getOriginalAssetEth as getOriginalAssetEthNFT,
|
||||
getOriginalAssetSol as getOriginalAssetSolNFT,
|
||||
} from "@certusone/wormhole-sdk/lib/nft_bridge";
|
||||
import { Connection } from "@solana/web3.js";
|
||||
import { LCDClient } from "@terra-money/terra.js";
|
||||
import { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||
import {
|
||||
selectNFTSourceAsset,
|
||||
selectNFTSourceChain,
|
||||
selectNFTSourceParsedTokenAccount,
|
||||
selectTransferSourceAsset,
|
||||
selectTransferSourceChain,
|
||||
} from "../store/selectors";
|
||||
import { setSourceWormholeWrappedInfo } from "../store/transferSlice";
|
||||
import { setSourceWormholeWrappedInfo as setNFTSourceWormholeWrappedInfo } from "../store/nftSlice";
|
||||
import { setSourceWormholeWrappedInfo as setTransferSourceWormholeWrappedInfo } from "../store/transferSlice";
|
||||
import { uint8ArrayToHex } from "../utils/array";
|
||||
import {
|
||||
getOriginalAssetEth,
|
||||
getOriginalAssetSol,
|
||||
getOriginalAssetTerra,
|
||||
} from "../utils/getOriginalAsset";
|
||||
ETH_NFT_BRIDGE_ADDRESS,
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
SOLANA_HOST,
|
||||
SOL_NFT_BRIDGE_ADDRESS,
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
TERRA_HOST,
|
||||
} from "../utils/consts";
|
||||
|
||||
export interface StateSafeWormholeWrappedInfo {
|
||||
isWrapped: boolean;
|
||||
chainId: ChainId;
|
||||
assetAddress: string;
|
||||
tokenId?: string;
|
||||
}
|
||||
|
||||
const makeStateSafe = (
|
||||
info: WormholeWrappedInfo
|
||||
): StateSafeWormholeWrappedInfo => ({
|
||||
...info,
|
||||
assetAddress: uint8ArrayToHex(info.assetAddress),
|
||||
});
|
||||
|
||||
// Check if the tokens in the configured source chain/address are wrapped
|
||||
// tokens. Wrapped tokens are tokens that are non-native, I.E, are locked up on
|
||||
// a different chain than this one.
|
||||
function useCheckIfWormholeWrapped() {
|
||||
function useCheckIfWormholeWrapped(nft?: boolean) {
|
||||
const dispatch = useDispatch();
|
||||
const sourceChain = useSelector(selectTransferSourceChain);
|
||||
const sourceAsset = useSelector(selectTransferSourceAsset);
|
||||
const sourceChain = useSelector(
|
||||
nft ? selectNFTSourceChain : selectTransferSourceChain
|
||||
);
|
||||
const sourceAsset = useSelector(
|
||||
nft ? selectNFTSourceAsset : selectTransferSourceAsset
|
||||
);
|
||||
const nftSourceParsedTokenAccount = useSelector(
|
||||
selectNFTSourceParsedTokenAccount
|
||||
);
|
||||
const tokenId = nftSourceParsedTokenAccount?.tokenId || ""; // this should exist by this step for NFT transfers
|
||||
const setSourceWormholeWrappedInfo = nft
|
||||
? setNFTSourceWormholeWrappedInfo
|
||||
: setTransferSourceWormholeWrappedInfo;
|
||||
const { provider } = useEthereumProvider();
|
||||
useEffect(() => {
|
||||
// TODO: loading state, error state
|
||||
|
@ -31,14 +75,42 @@ function useCheckIfWormholeWrapped() {
|
|||
let cancelled = false;
|
||||
(async () => {
|
||||
if (sourceChain === CHAIN_ID_ETH && provider && sourceAsset) {
|
||||
const wrappedInfo = await getOriginalAssetEth(provider, sourceAsset);
|
||||
console.log("getting wrapped info");
|
||||
const wrappedInfo = makeStateSafe(
|
||||
await (nft
|
||||
? getOriginalAssetEthNFT(
|
||||
ETH_NFT_BRIDGE_ADDRESS,
|
||||
provider,
|
||||
sourceAsset,
|
||||
tokenId
|
||||
)
|
||||
: getOriginalAssetEth(
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
provider,
|
||||
sourceAsset
|
||||
))
|
||||
);
|
||||
console.log(wrappedInfo);
|
||||
if (!cancelled) {
|
||||
dispatch(setSourceWormholeWrappedInfo(wrappedInfo));
|
||||
}
|
||||
}
|
||||
if (sourceChain === CHAIN_ID_SOLANA && sourceAsset) {
|
||||
try {
|
||||
const wrappedInfo = await getOriginalAssetSol(sourceAsset);
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
const wrappedInfo = makeStateSafe(
|
||||
await (nft
|
||||
? getOriginalAssetSolNFT(
|
||||
connection,
|
||||
SOL_NFT_BRIDGE_ADDRESS,
|
||||
sourceAsset
|
||||
)
|
||||
: getOriginalAssetSol(
|
||||
connection,
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
sourceAsset
|
||||
))
|
||||
);
|
||||
if (!cancelled) {
|
||||
dispatch(setSourceWormholeWrappedInfo(wrappedInfo));
|
||||
}
|
||||
|
@ -46,7 +118,10 @@ function useCheckIfWormholeWrapped() {
|
|||
}
|
||||
if (sourceChain === CHAIN_ID_TERRA && sourceAsset) {
|
||||
try {
|
||||
const wrappedInfo = await getOriginalAssetTerra(sourceAsset);
|
||||
const lcd = new LCDClient(TERRA_HOST);
|
||||
const wrappedInfo = makeStateSafe(
|
||||
await getOriginalAssetTerra(lcd, sourceAsset)
|
||||
);
|
||||
if (!cancelled) {
|
||||
dispatch(setSourceWormholeWrappedInfo(wrappedInfo));
|
||||
}
|
||||
|
@ -56,7 +131,15 @@ function useCheckIfWormholeWrapped() {
|
|||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [dispatch, sourceChain, sourceAsset, provider]);
|
||||
}, [
|
||||
dispatch,
|
||||
sourceChain,
|
||||
sourceAsset,
|
||||
provider,
|
||||
nft,
|
||||
setSourceWormholeWrappedInfo,
|
||||
tokenId,
|
||||
]);
|
||||
}
|
||||
|
||||
export default useCheckIfWormholeWrapped;
|
||||
|
|
|
@ -2,32 +2,62 @@ import {
|
|||
CHAIN_ID_ETH,
|
||||
CHAIN_ID_SOLANA,
|
||||
CHAIN_ID_TERRA,
|
||||
getForeignAssetEth,
|
||||
getForeignAssetSolana,
|
||||
getForeignAssetTerra,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import {
|
||||
getForeignAssetEth as getForeignAssetEthNFT,
|
||||
getForeignAssetSol as getForeignAssetSolNFT,
|
||||
} from "@certusone/wormhole-sdk/lib/nft_bridge";
|
||||
import { Connection } from "@solana/web3.js";
|
||||
import { LCDClient } from "@terra-money/terra.js";
|
||||
import { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||
import { setTargetAsset as setNFTTargetAsset } from "../store/nftSlice";
|
||||
import {
|
||||
selectNFTIsSourceAssetWormholeWrapped,
|
||||
selectNFTOriginAsset,
|
||||
selectNFTOriginChain,
|
||||
selectNFTOriginTokenId,
|
||||
selectNFTTargetChain,
|
||||
selectTransferIsSourceAssetWormholeWrapped,
|
||||
selectTransferOriginAsset,
|
||||
selectTransferOriginChain,
|
||||
selectTransferTargetChain,
|
||||
} from "../store/selectors";
|
||||
import { setTargetAsset } from "../store/transferSlice";
|
||||
import { hexToNativeString } from "../utils/array";
|
||||
import { setTargetAsset as setTransferTargetAsset } from "../store/transferSlice";
|
||||
import { hexToNativeString, hexToUint8Array } from "../utils/array";
|
||||
import {
|
||||
getForeignAssetEth,
|
||||
getForeignAssetSol,
|
||||
getForeignAssetTerra,
|
||||
} from "../utils/getForeignAsset";
|
||||
ETH_NFT_BRIDGE_ADDRESS,
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
SOLANA_HOST,
|
||||
SOL_NFT_BRIDGE_ADDRESS,
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
TERRA_HOST,
|
||||
TERRA_TOKEN_BRIDGE_ADDRESS,
|
||||
} from "../utils/consts";
|
||||
|
||||
function useFetchTargetAsset() {
|
||||
function useFetchTargetAsset(nft?: boolean) {
|
||||
const dispatch = useDispatch();
|
||||
const isSourceAssetWormholeWrapped = useSelector(
|
||||
selectTransferIsSourceAssetWormholeWrapped
|
||||
nft
|
||||
? selectNFTIsSourceAssetWormholeWrapped
|
||||
: selectTransferIsSourceAssetWormholeWrapped
|
||||
);
|
||||
const originChain = useSelector(selectTransferOriginChain);
|
||||
const originAsset = useSelector(selectTransferOriginAsset);
|
||||
const targetChain = useSelector(selectTransferTargetChain);
|
||||
const originChain = useSelector(
|
||||
nft ? selectNFTOriginChain : selectTransferOriginChain
|
||||
);
|
||||
const originAsset = useSelector(
|
||||
nft ? selectNFTOriginAsset : selectTransferOriginAsset
|
||||
);
|
||||
const originTokenId = useSelector(selectNFTOriginTokenId);
|
||||
const tokenId = originTokenId || ""; // this should exist by this step for NFT transfers
|
||||
const targetChain = useSelector(
|
||||
nft ? selectNFTTargetChain : selectTransferTargetChain
|
||||
);
|
||||
const setTargetAsset = nft ? setNFTTargetAsset : setTransferTargetAsset;
|
||||
const { provider } = useEthereumProvider();
|
||||
useEffect(() => {
|
||||
if (isSourceAssetWormholeWrapped && originChain === targetChain) {
|
||||
|
@ -44,36 +74,74 @@ function useFetchTargetAsset() {
|
|||
originChain &&
|
||||
originAsset
|
||||
) {
|
||||
const asset = await getForeignAssetEth(
|
||||
provider,
|
||||
originChain,
|
||||
originAsset
|
||||
);
|
||||
if (!cancelled) {
|
||||
dispatch(setTargetAsset(asset));
|
||||
}
|
||||
}
|
||||
if (targetChain === CHAIN_ID_SOLANA && originChain && originAsset) {
|
||||
try {
|
||||
const asset = await getForeignAssetSol(originChain, originAsset);
|
||||
const asset = await (nft
|
||||
? getForeignAssetEthNFT(
|
||||
ETH_NFT_BRIDGE_ADDRESS,
|
||||
provider,
|
||||
originChain,
|
||||
hexToUint8Array(originAsset)
|
||||
)
|
||||
: getForeignAssetEth(
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
provider,
|
||||
originChain,
|
||||
hexToUint8Array(originAsset)
|
||||
));
|
||||
if (!cancelled) {
|
||||
dispatch(setTargetAsset(asset));
|
||||
}
|
||||
} catch (e) {
|
||||
if (!cancelled) {
|
||||
// TODO: warning for this
|
||||
dispatch(setTargetAsset(null));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (targetChain === CHAIN_ID_SOLANA && originChain && originAsset) {
|
||||
try {
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
const asset = await (nft
|
||||
? getForeignAssetSolNFT(
|
||||
SOL_NFT_BRIDGE_ADDRESS,
|
||||
originChain,
|
||||
hexToUint8Array(originAsset),
|
||||
new Uint8Array([0, 0, 0, 0]) //tokenId // TODO: string
|
||||
)
|
||||
: getForeignAssetSolana(
|
||||
connection,
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (targetChain === CHAIN_ID_TERRA && originChain && originAsset) {
|
||||
try {
|
||||
const asset = await getForeignAssetTerra(originChain, originAsset);
|
||||
const lcd = new LCDClient(TERRA_HOST);
|
||||
const asset = await getForeignAssetTerra(
|
||||
TERRA_TOKEN_BRIDGE_ADDRESS,
|
||||
lcd,
|
||||
originChain,
|
||||
hexToUint8Array(originAsset)
|
||||
);
|
||||
if (!cancelled) {
|
||||
dispatch(setTargetAsset(asset));
|
||||
}
|
||||
} catch (e) {
|
||||
if (!cancelled) {
|
||||
// TODO: warning for this
|
||||
dispatch(setTargetAsset(null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -88,6 +156,9 @@ function useFetchTargetAsset() {
|
|||
originAsset,
|
||||
targetChain,
|
||||
provider,
|
||||
nft,
|
||||
setTargetAsset,
|
||||
tokenId,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,18 @@ import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
|||
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
||||
import { DataWrapper } from "../store/helpers";
|
||||
import {
|
||||
errorSourceParsedTokenAccounts as errorSourceParsedTokenAccountsNFT,
|
||||
fetchSourceParsedTokenAccounts as fetchSourceParsedTokenAccountsNFT,
|
||||
NFTParsedTokenAccount,
|
||||
receiveSourceParsedTokenAccounts as receiveSourceParsedTokenAccountsNFT,
|
||||
setSourceParsedTokenAccount as setSourceParsedTokenAccountNFT,
|
||||
setSourceParsedTokenAccounts as setSourceParsedTokenAccountsNFT,
|
||||
setSourceWalletAddress as setSourceWalletAddressNFT,
|
||||
} from "../store/nftSlice";
|
||||
import {
|
||||
selectNFTSourceChain,
|
||||
selectNFTSourceParsedTokenAccounts,
|
||||
selectNFTSourceWalletAddress,
|
||||
selectSolanaTokenMap,
|
||||
selectSourceWalletAddress,
|
||||
selectTerraTokenMap,
|
||||
|
@ -78,6 +90,36 @@ export function createParsedTokenAccount(
|
|||
};
|
||||
}
|
||||
|
||||
export function createNFTParsedTokenAccount(
|
||||
publicKey: string,
|
||||
mintKey: string,
|
||||
amount: string,
|
||||
decimals: number,
|
||||
uiAmount: number,
|
||||
uiAmountString: string,
|
||||
tokenId: string,
|
||||
animation_url?: string,
|
||||
external_url?: string,
|
||||
image?: string,
|
||||
image_256?: string,
|
||||
name?: string
|
||||
): NFTParsedTokenAccount {
|
||||
return {
|
||||
publicKey,
|
||||
mintKey,
|
||||
amount,
|
||||
decimals,
|
||||
uiAmount,
|
||||
uiAmountString,
|
||||
tokenId,
|
||||
animation_url,
|
||||
external_url,
|
||||
image,
|
||||
image_256,
|
||||
name,
|
||||
};
|
||||
}
|
||||
|
||||
export type TerraTokenMetadata = {
|
||||
protocol: string;
|
||||
symbol: string;
|
||||
|
@ -119,6 +161,32 @@ const createParsedTokenAccountFromCovalent = (
|
|||
};
|
||||
};
|
||||
|
||||
const createNFTParsedTokenAccountFromCovalent = (
|
||||
walletAddress: string,
|
||||
covalent: CovalentData,
|
||||
nft_data: CovalentNFTData
|
||||
): NFTParsedTokenAccount => {
|
||||
return {
|
||||
publicKey: walletAddress,
|
||||
mintKey: covalent.contract_address,
|
||||
amount: nft_data.token_balance,
|
||||
decimals: covalent.contract_decimals,
|
||||
uiAmount: Number(
|
||||
formatUnits(nft_data.token_balance, covalent.contract_decimals)
|
||||
),
|
||||
uiAmountString: formatUnits(
|
||||
nft_data.token_balance,
|
||||
covalent.contract_decimals
|
||||
),
|
||||
tokenId: nft_data.token_id,
|
||||
animation_url: nft_data.external_data.animation_url,
|
||||
external_url: nft_data.external_data.external_url,
|
||||
image: nft_data.external_data.image,
|
||||
image_256: nft_data.external_data.image_256,
|
||||
name: nft_data.external_data.name,
|
||||
};
|
||||
};
|
||||
|
||||
export type CovalentData = {
|
||||
contract_decimals: number;
|
||||
contract_ticker_symbol: string;
|
||||
|
@ -128,12 +196,28 @@ export type CovalentData = {
|
|||
balance: string;
|
||||
quote: number | undefined;
|
||||
quote_rate: number | undefined;
|
||||
nft_data?: CovalentNFTData[];
|
||||
};
|
||||
|
||||
export type CovalentNFTExternalData = {
|
||||
animation_url: string | null;
|
||||
external_url: string | null;
|
||||
image: string;
|
||||
image_256: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type CovalentNFTData = {
|
||||
token_id: string;
|
||||
token_balance: string;
|
||||
external_data: CovalentNFTExternalData;
|
||||
};
|
||||
|
||||
const getEthereumAccountsCovalent = async (
|
||||
walletAddress: string
|
||||
walletAddress: string,
|
||||
nft?: boolean
|
||||
): Promise<CovalentData[]> => {
|
||||
const url = COVALENT_GET_TOKENS_URL(CHAIN_ID_ETH, walletAddress);
|
||||
const url = COVALENT_GET_TOKENS_URL(CHAIN_ID_ETH, walletAddress, nft);
|
||||
|
||||
try {
|
||||
const output = [] as CovalentData[];
|
||||
|
@ -144,11 +228,13 @@ const getEthereumAccountsCovalent = async (
|
|||
for (const item of tokens) {
|
||||
// TODO: filter?
|
||||
if (
|
||||
item.contract_decimals &&
|
||||
item.contract_decimals !== undefined &&
|
||||
item.contract_ticker_symbol &&
|
||||
item.contract_address &&
|
||||
item.balance &&
|
||||
item.supports_erc?.includes("erc20")
|
||||
(nft
|
||||
? item.supports_erc?.includes("erc721")
|
||||
: item.supports_erc?.includes("erc20"))
|
||||
) {
|
||||
output.push({ ...item } as CovalentData);
|
||||
}
|
||||
|
@ -199,10 +285,13 @@ const getMetaplexData = async (mintAddresses: string[]) => {
|
|||
|
||||
const getSolanaParsedTokenAccounts = (
|
||||
walletAddress: string,
|
||||
dispatch: Dispatch
|
||||
dispatch: Dispatch,
|
||||
nft: boolean
|
||||
) => {
|
||||
const connection = new Connection(SOLANA_HOST, "finalized");
|
||||
dispatch(fetchSourceParsedTokenAccounts());
|
||||
dispatch(
|
||||
nft ? fetchSourceParsedTokenAccountsNFT() : fetchSourceParsedTokenAccounts()
|
||||
);
|
||||
return connection
|
||||
.getParsedTokenAccountsByOwner(new PublicKey(walletAddress), {
|
||||
programId: new PublicKey(TOKEN_PROGRAM_ID),
|
||||
|
@ -212,11 +301,17 @@ const getSolanaParsedTokenAccounts = (
|
|||
const mappedItems = result.value.map((item) =>
|
||||
createParsedTokenAccountFromInfo(item.pubkey, item.account)
|
||||
);
|
||||
dispatch(receiveSourceParsedTokenAccounts(mappedItems));
|
||||
dispatch(
|
||||
nft
|
||||
? receiveSourceParsedTokenAccountsNFT(mappedItems)
|
||||
: receiveSourceParsedTokenAccounts(mappedItems)
|
||||
);
|
||||
},
|
||||
(error) => {
|
||||
dispatch(
|
||||
errorSourceParsedTokenAccounts("Failed to load token metadata.")
|
||||
nft
|
||||
? errorSourceParsedTokenAccountsNFT("Failed to load NFT metadata")
|
||||
: errorSourceParsedTokenAccounts("Failed to load token metadata.")
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -240,14 +335,20 @@ const getSolanaTokenMap = (dispatch: Dispatch) => {
|
|||
* Fetches the balance of an asset for the connected wallet
|
||||
* This should handle every type of chain in the future, but only reads the Transfer state.
|
||||
*/
|
||||
function useGetAvailableTokens() {
|
||||
function useGetAvailableTokens(nft: boolean = false) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const tokenAccounts = useSelector(selectTransferSourceParsedTokenAccounts);
|
||||
const tokenAccounts = useSelector(
|
||||
nft
|
||||
? selectNFTSourceParsedTokenAccounts
|
||||
: selectTransferSourceParsedTokenAccounts
|
||||
);
|
||||
const solanaTokenMap = useSelector(selectSolanaTokenMap);
|
||||
const terraTokenMap = useSelector(selectTerraTokenMap);
|
||||
|
||||
const lookupChain = useSelector(selectTransferSourceChain);
|
||||
const lookupChain = useSelector(
|
||||
nft ? selectNFTSourceChain : selectTransferSourceChain
|
||||
);
|
||||
const solanaWallet = useSolanaWallet();
|
||||
const solPK = solanaWallet?.publicKey;
|
||||
//const terraWallet = useConnectedWallet(); //TODO
|
||||
|
@ -270,7 +371,9 @@ function useGetAvailableTokens() {
|
|||
string | undefined
|
||||
>(undefined);
|
||||
|
||||
const selectedSourceWalletAddress = useSelector(selectSourceWalletAddress);
|
||||
const selectedSourceWalletAddress = useSelector(
|
||||
nft ? selectNFTSourceWalletAddress : selectSourceWalletAddress
|
||||
);
|
||||
const currentSourceWalletAddress: string | undefined =
|
||||
lookupChain === CHAIN_ID_ETH
|
||||
? signerAddress
|
||||
|
@ -286,14 +389,26 @@ function useGetAvailableTokens() {
|
|||
currentSourceWalletAddress !== undefined &&
|
||||
currentSourceWalletAddress !== selectedSourceWalletAddress
|
||||
) {
|
||||
dispatch(setSourceWalletAddress(undefined));
|
||||
dispatch(setSourceParsedTokenAccount(undefined));
|
||||
dispatch(setSourceParsedTokenAccounts(undefined));
|
||||
dispatch(setAmount(""));
|
||||
dispatch(
|
||||
nft
|
||||
? setSourceWalletAddressNFT(undefined)
|
||||
: setSourceWalletAddress(undefined)
|
||||
);
|
||||
dispatch(
|
||||
nft
|
||||
? setSourceParsedTokenAccountNFT(undefined)
|
||||
: setSourceParsedTokenAccount(undefined)
|
||||
);
|
||||
dispatch(
|
||||
nft
|
||||
? setSourceParsedTokenAccountsNFT(undefined)
|
||||
: setSourceParsedTokenAccounts(undefined)
|
||||
);
|
||||
!nft && dispatch(setAmount(""));
|
||||
return;
|
||||
} else {
|
||||
}
|
||||
}, [selectedSourceWalletAddress, currentSourceWalletAddress, dispatch]);
|
||||
}, [selectedSourceWalletAddress, currentSourceWalletAddress, dispatch, nft]);
|
||||
|
||||
// Solana metaplex load
|
||||
useEffect(() => {
|
||||
|
@ -333,7 +448,7 @@ function useGetAvailableTokens() {
|
|||
if (
|
||||
!(tokenAccounts.data || tokenAccounts.isFetching || tokenAccounts.error)
|
||||
) {
|
||||
getSolanaParsedTokenAccounts(solPK.toString(), dispatch);
|
||||
getSolanaParsedTokenAccounts(solPK.toString(), dispatch, nft);
|
||||
}
|
||||
if (
|
||||
!(
|
||||
|
@ -354,6 +469,7 @@ function useGetAvailableTokens() {
|
|||
solPK,
|
||||
tokenAccounts,
|
||||
solanaTokenMap,
|
||||
nft,
|
||||
]);
|
||||
|
||||
//Solana Mint Accounts lookup
|
||||
|
@ -406,6 +522,8 @@ function useGetAvailableTokens() {
|
|||
//Ethereum accounts load
|
||||
useEffect(() => {
|
||||
//const testWallet = "0xf60c2ea62edbfe808163751dd0d8693dcb30019c";
|
||||
// const nftTestWallet1 = "0x3f304c6721f35ff9af00fd32650c8e0a982180ab";
|
||||
// const nftTestWallet2 = "0x98ed231428088eb440e8edb5cc8d66dcf913b86e";
|
||||
let cancelled = false;
|
||||
const walletAddress = signerAddress;
|
||||
if (!walletAddress || lookupChain !== CHAIN_ID_ETH) {
|
||||
|
@ -413,27 +531,53 @@ function useGetAvailableTokens() {
|
|||
}
|
||||
//TODO less cancel
|
||||
!cancelled && setCovalentLoading(true);
|
||||
!cancelled && dispatch(fetchSourceParsedTokenAccounts());
|
||||
getEthereumAccountsCovalent(walletAddress).then(
|
||||
!cancelled &&
|
||||
dispatch(
|
||||
nft
|
||||
? fetchSourceParsedTokenAccountsNFT()
|
||||
: fetchSourceParsedTokenAccounts()
|
||||
);
|
||||
getEthereumAccountsCovalent(walletAddress, nft).then(
|
||||
(accounts) => {
|
||||
!cancelled && setCovalentLoading(false);
|
||||
!cancelled && setCovalentError(undefined);
|
||||
!cancelled && setCovalent(accounts);
|
||||
!cancelled &&
|
||||
dispatch(
|
||||
receiveSourceParsedTokenAccounts(
|
||||
accounts.map((x) =>
|
||||
createParsedTokenAccountFromCovalent(walletAddress, x)
|
||||
)
|
||||
)
|
||||
nft
|
||||
? receiveSourceParsedTokenAccountsNFT(
|
||||
accounts.reduce((arr, current) => {
|
||||
if (current.nft_data) {
|
||||
current.nft_data.forEach((x) =>
|
||||
arr.push(
|
||||
createNFTParsedTokenAccountFromCovalent(
|
||||
walletAddress,
|
||||
current,
|
||||
x
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
return arr;
|
||||
}, [] as NFTParsedTokenAccount[])
|
||||
)
|
||||
: receiveSourceParsedTokenAccounts(
|
||||
accounts.map((x) =>
|
||||
createParsedTokenAccountFromCovalent(walletAddress, x)
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
() => {
|
||||
!cancelled &&
|
||||
dispatch(
|
||||
errorSourceParsedTokenAccounts(
|
||||
"Cannot load your Ethereum tokens at the moment."
|
||||
)
|
||||
nft
|
||||
? errorSourceParsedTokenAccountsNFT(
|
||||
"Cannot load your Ethereum NFTs at the moment."
|
||||
)
|
||||
: errorSourceParsedTokenAccounts(
|
||||
"Cannot load your Ethereum tokens at the moment."
|
||||
)
|
||||
);
|
||||
!cancelled &&
|
||||
setCovalentError("Cannot load your Ethereum tokens at the moment.");
|
||||
|
@ -444,7 +588,7 @@ function useGetAvailableTokens() {
|
|||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [lookupChain, provider, signerAddress, dispatch]);
|
||||
}, [lookupChain, provider, signerAddress, dispatch, nft]);
|
||||
|
||||
//Terra accounts load
|
||||
//At present, we don't have any mechanism for doing this.
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
import {
|
||||
CHAIN_ID_ETH,
|
||||
CHAIN_ID_SOLANA,
|
||||
postVaaSolana,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import {
|
||||
redeemOnEth,
|
||||
redeemOnSolana,
|
||||
} 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 { useSnackbar } from "notistack";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
||||
import { setIsRedeeming, setRedeemTx } from "../store/nftSlice";
|
||||
import { selectNFTIsRedeeming, selectNFTTargetChain } from "../store/selectors";
|
||||
import {
|
||||
ETH_NFT_BRIDGE_ADDRESS,
|
||||
SOLANA_HOST,
|
||||
SOL_BRIDGE_ADDRESS,
|
||||
SOL_NFT_BRIDGE_ADDRESS,
|
||||
} from "../utils/consts";
|
||||
import parseError from "../utils/parseError";
|
||||
import { signSendAndConfirm } from "../utils/solana";
|
||||
import useNFTSignedVAA from "./useNFTSignedVAA";
|
||||
|
||||
async function eth(
|
||||
dispatch: any,
|
||||
enqueueSnackbar: any,
|
||||
signer: Signer,
|
||||
signedVAA: Uint8Array
|
||||
) {
|
||||
dispatch(setIsRedeeming(true));
|
||||
try {
|
||||
const receipt = await redeemOnEth(
|
||||
ETH_NFT_BRIDGE_ADDRESS,
|
||||
signer,
|
||||
signedVAA
|
||||
);
|
||||
dispatch(
|
||||
setRedeemTx({ id: receipt.transactionHash, block: receipt.blockNumber })
|
||||
);
|
||||
enqueueSnackbar("Transaction confirmed", { variant: "success" });
|
||||
} catch (e) {
|
||||
enqueueSnackbar(parseError(e), { variant: "error" });
|
||||
dispatch(setIsRedeeming(false));
|
||||
}
|
||||
}
|
||||
|
||||
async function solana(
|
||||
dispatch: any,
|
||||
enqueueSnackbar: any,
|
||||
wallet: WalletContextState,
|
||||
payerAddress: string, //TODO: we may not need this since we have wallet
|
||||
signedVAA: Uint8Array
|
||||
) {
|
||||
dispatch(setIsRedeeming(true));
|
||||
try {
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
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
|
||||
);
|
||||
const 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 }));
|
||||
enqueueSnackbar("Transaction confirmed", { variant: "success" });
|
||||
} catch (e) {
|
||||
enqueueSnackbar(parseError(e), { variant: "error" });
|
||||
dispatch(setIsRedeeming(false));
|
||||
}
|
||||
}
|
||||
|
||||
export function useHandleNFTRedeem() {
|
||||
const dispatch = useDispatch();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const targetChain = useSelector(selectNFTTargetChain);
|
||||
const solanaWallet = useSolanaWallet();
|
||||
const solPK = solanaWallet?.publicKey;
|
||||
const { signer } = useEthereumProvider();
|
||||
const signedVAA = useNFTSignedVAA();
|
||||
const isRedeeming = useSelector(selectNFTIsRedeeming);
|
||||
const handleRedeemClick = useCallback(() => {
|
||||
if (targetChain === CHAIN_ID_ETH && !!signer && signedVAA) {
|
||||
eth(dispatch, enqueueSnackbar, signer, signedVAA);
|
||||
} else if (
|
||||
targetChain === CHAIN_ID_SOLANA &&
|
||||
!!solanaWallet &&
|
||||
!!solPK &&
|
||||
signedVAA
|
||||
) {
|
||||
solana(
|
||||
dispatch,
|
||||
enqueueSnackbar,
|
||||
solanaWallet,
|
||||
solPK.toString(),
|
||||
signedVAA
|
||||
);
|
||||
} else {
|
||||
// enqueueSnackbar("Redeeming on this chain is not yet supported", {
|
||||
// variant: "error",
|
||||
// });
|
||||
}
|
||||
}, [
|
||||
dispatch,
|
||||
enqueueSnackbar,
|
||||
targetChain,
|
||||
signer,
|
||||
signedVAA,
|
||||
solanaWallet,
|
||||
solPK,
|
||||
]);
|
||||
return useMemo(
|
||||
() => ({
|
||||
handleClick: handleRedeemClick,
|
||||
disabled: !!isRedeeming,
|
||||
showLoader: !!isRedeeming,
|
||||
}),
|
||||
[handleRedeemClick, isRedeeming]
|
||||
);
|
||||
}
|
|
@ -0,0 +1,245 @@
|
|||
import {
|
||||
ChainId,
|
||||
CHAIN_ID_ETH,
|
||||
CHAIN_ID_SOLANA,
|
||||
getEmitterAddressEth,
|
||||
getEmitterAddressSolana,
|
||||
parseSequenceFromLogEth,
|
||||
parseSequenceFromLogSolana,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import {
|
||||
transferFromEth,
|
||||
transferFromSolana,
|
||||
} 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 { useSnackbar } from "notistack";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
||||
import {
|
||||
setIsSending,
|
||||
setSignedVAAHex,
|
||||
setTransferTx,
|
||||
} from "../store/nftSlice";
|
||||
import {
|
||||
selectNFTIsSendComplete,
|
||||
selectNFTIsSending,
|
||||
selectNFTIsTargetComplete,
|
||||
selectNFTOriginAsset,
|
||||
selectNFTOriginChain,
|
||||
selectNFTOriginTokenId,
|
||||
selectNFTSourceAsset,
|
||||
selectNFTSourceChain,
|
||||
selectNFTSourceParsedTokenAccount,
|
||||
selectNFTTargetChain,
|
||||
} from "../store/selectors";
|
||||
import { hexToUint8Array, uint8ArrayToHex } from "../utils/array";
|
||||
import {
|
||||
ETH_BRIDGE_ADDRESS,
|
||||
ETH_NFT_BRIDGE_ADDRESS,
|
||||
SOLANA_HOST,
|
||||
SOL_BRIDGE_ADDRESS,
|
||||
SOL_NFT_BRIDGE_ADDRESS,
|
||||
} from "../utils/consts";
|
||||
import { getSignedVAAWithRetry } from "../utils/getSignedVAAWithRetry";
|
||||
import parseError from "../utils/parseError";
|
||||
import { signSendAndConfirm } from "../utils/solana";
|
||||
import useNFTTargetAddressHex from "./useNFTTargetAddress";
|
||||
|
||||
async function eth(
|
||||
dispatch: any,
|
||||
enqueueSnackbar: any,
|
||||
signer: Signer,
|
||||
tokenAddress: string,
|
||||
tokenId: string,
|
||||
recipientChain: ChainId,
|
||||
recipientAddress: Uint8Array
|
||||
) {
|
||||
dispatch(setIsSending(true));
|
||||
try {
|
||||
const receipt = await transferFromEth(
|
||||
ETH_NFT_BRIDGE_ADDRESS,
|
||||
signer,
|
||||
tokenAddress,
|
||||
tokenId,
|
||||
recipientChain,
|
||||
recipientAddress
|
||||
);
|
||||
dispatch(
|
||||
setTransferTx({ id: receipt.transactionHash, block: receipt.blockNumber })
|
||||
);
|
||||
enqueueSnackbar("Transaction confirmed", { variant: "success" });
|
||||
const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS);
|
||||
const emitterAddress = getEmitterAddressEth(ETH_NFT_BRIDGE_ADDRESS);
|
||||
enqueueSnackbar("Fetching VAA", { variant: "info" });
|
||||
const { vaaBytes } = await getSignedVAAWithRetry(
|
||||
CHAIN_ID_ETH,
|
||||
emitterAddress,
|
||||
sequence.toString()
|
||||
);
|
||||
dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
|
||||
enqueueSnackbar("Fetched Signed VAA", { variant: "success" });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
enqueueSnackbar(parseError(e), { variant: "error" });
|
||||
dispatch(setIsSending(false));
|
||||
}
|
||||
}
|
||||
|
||||
async function solana(
|
||||
dispatch: any,
|
||||
enqueueSnackbar: any,
|
||||
wallet: WalletContextState,
|
||||
payerAddress: string, //TODO: we may not need this since we have wallet
|
||||
fromAddress: string,
|
||||
mintAddress: string,
|
||||
targetChain: ChainId,
|
||||
targetAddress: Uint8Array,
|
||||
originAddressStr?: string,
|
||||
originChain?: ChainId,
|
||||
originTokenId?: string
|
||||
) {
|
||||
dispatch(setIsSending(true));
|
||||
try {
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
const originAddress = originAddressStr
|
||||
? zeroPad(hexToUint8Array(originAddressStr), 32)
|
||||
: undefined;
|
||||
const transaction = await transferFromSolana(
|
||||
connection,
|
||||
SOL_BRIDGE_ADDRESS,
|
||||
SOL_NFT_BRIDGE_ADDRESS,
|
||||
payerAddress,
|
||||
fromAddress,
|
||||
mintAddress,
|
||||
targetAddress,
|
||||
targetChain,
|
||||
originAddress,
|
||||
originChain,
|
||||
new Uint8Array([0, 0, 0, 0]) //originTokenId //TODO: string
|
||||
);
|
||||
const txid = await signSendAndConfirm(wallet, connection, transaction);
|
||||
enqueueSnackbar("Transaction confirmed", { variant: "success" });
|
||||
const info = await connection.getTransaction(txid);
|
||||
if (!info) {
|
||||
throw new Error("An error occurred while fetching the transaction info");
|
||||
}
|
||||
dispatch(setTransferTx({ id: txid, block: info.slot }));
|
||||
const sequence = parseSequenceFromLogSolana(info);
|
||||
const emitterAddress = await getEmitterAddressSolana(
|
||||
SOL_NFT_BRIDGE_ADDRESS
|
||||
);
|
||||
enqueueSnackbar("Fetching VAA", { variant: "info" });
|
||||
const { vaaBytes } = await getSignedVAAWithRetry(
|
||||
CHAIN_ID_SOLANA,
|
||||
emitterAddress,
|
||||
sequence
|
||||
);
|
||||
|
||||
dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
|
||||
enqueueSnackbar("Fetched Signed VAA", { variant: "success" });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
enqueueSnackbar(parseError(e), { variant: "error" });
|
||||
dispatch(setIsSending(false));
|
||||
}
|
||||
}
|
||||
|
||||
export function useHandleNFTTransfer() {
|
||||
const dispatch = useDispatch();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const sourceChain = useSelector(selectNFTSourceChain);
|
||||
const sourceAsset = useSelector(selectNFTSourceAsset);
|
||||
const nftSourceParsedTokenAccount = useSelector(
|
||||
selectNFTSourceParsedTokenAccount
|
||||
);
|
||||
const sourceTokenId = nftSourceParsedTokenAccount?.tokenId || ""; // this should exist by this step for NFT transfers
|
||||
const originChain = useSelector(selectNFTOriginChain);
|
||||
const originAsset = useSelector(selectNFTOriginAsset);
|
||||
const originTokenId = useSelector(selectNFTOriginTokenId);
|
||||
const targetChain = useSelector(selectNFTTargetChain);
|
||||
const targetAddress = useNFTTargetAddressHex();
|
||||
const isTargetComplete = useSelector(selectNFTIsTargetComplete);
|
||||
const isSending = useSelector(selectNFTIsSending);
|
||||
const isSendComplete = useSelector(selectNFTIsSendComplete);
|
||||
const { signer } = useEthereumProvider();
|
||||
const solanaWallet = useSolanaWallet();
|
||||
const solPK = solanaWallet?.publicKey;
|
||||
const sourceParsedTokenAccount = useSelector(
|
||||
selectNFTSourceParsedTokenAccount
|
||||
);
|
||||
const sourceTokenPublicKey = sourceParsedTokenAccount?.publicKey;
|
||||
const disabled = !isTargetComplete || isSending || isSendComplete;
|
||||
const handleTransferClick = useCallback(() => {
|
||||
// TODO: we should separate state for transaction vs fetching vaa
|
||||
if (
|
||||
sourceChain === CHAIN_ID_ETH &&
|
||||
!!signer &&
|
||||
!!sourceAsset &&
|
||||
!!sourceTokenId &&
|
||||
!!targetAddress
|
||||
) {
|
||||
eth(
|
||||
dispatch,
|
||||
enqueueSnackbar,
|
||||
signer,
|
||||
sourceAsset,
|
||||
sourceTokenId,
|
||||
targetChain,
|
||||
targetAddress
|
||||
);
|
||||
} else if (
|
||||
sourceChain === CHAIN_ID_SOLANA &&
|
||||
!!solanaWallet &&
|
||||
!!solPK &&
|
||||
!!sourceAsset &&
|
||||
!!sourceTokenPublicKey &&
|
||||
!!targetAddress
|
||||
) {
|
||||
solana(
|
||||
dispatch,
|
||||
enqueueSnackbar,
|
||||
solanaWallet,
|
||||
solPK.toString(),
|
||||
sourceTokenPublicKey,
|
||||
sourceAsset,
|
||||
targetChain,
|
||||
targetAddress,
|
||||
originAsset,
|
||||
originChain,
|
||||
originTokenId
|
||||
);
|
||||
} else {
|
||||
// enqueueSnackbar("Transfers from this chain are not yet supported", {
|
||||
// variant: "error",
|
||||
// });
|
||||
}
|
||||
}, [
|
||||
dispatch,
|
||||
enqueueSnackbar,
|
||||
sourceChain,
|
||||
signer,
|
||||
solanaWallet,
|
||||
solPK,
|
||||
sourceTokenPublicKey,
|
||||
sourceAsset,
|
||||
sourceTokenId,
|
||||
targetChain,
|
||||
targetAddress,
|
||||
originAsset,
|
||||
originChain,
|
||||
originTokenId,
|
||||
]);
|
||||
return useMemo(
|
||||
() => ({
|
||||
handleClick: handleTransferClick,
|
||||
disabled,
|
||||
showLoader: isSending,
|
||||
}),
|
||||
[handleTransferClick, disabled, isSending]
|
||||
);
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import { useMemo } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { selectNFTSignedVAAHex } from "../store/selectors";
|
||||
import { hexToUint8Array } from "../utils/array";
|
||||
|
||||
export default function useNFTSignedVAA() {
|
||||
const signedVAAHex = useSelector(selectNFTSignedVAAHex);
|
||||
const signedVAA = useMemo(
|
||||
() => (signedVAAHex ? hexToUint8Array(signedVAAHex) : undefined),
|
||||
[signedVAAHex]
|
||||
);
|
||||
return signedVAA;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import { useMemo } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { selectNFTTargetAddressHex } from "../store/selectors";
|
||||
import { hexToUint8Array } from "../utils/array";
|
||||
|
||||
export default function useNFTTargetAddressHex() {
|
||||
const targetAddressHex = useSelector(selectNFTTargetAddressHex);
|
||||
const targetAddress = useMemo(
|
||||
() => (targetAddressHex ? hexToUint8Array(targetAddressHex) : undefined),
|
||||
[targetAddressHex]
|
||||
);
|
||||
return targetAddress;
|
||||
}
|
|
@ -17,25 +17,35 @@ import { useDispatch, useSelector } from "react-redux";
|
|||
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
||||
import {
|
||||
selectNFTTargetAsset,
|
||||
selectNFTTargetChain,
|
||||
selectTransferTargetAsset,
|
||||
selectTransferTargetChain,
|
||||
selectTransferTargetParsedTokenAccount,
|
||||
} from "../store/selectors";
|
||||
import { setTargetAddressHex } from "../store/transferSlice";
|
||||
import { setTargetAddressHex as setNFTTargetAddressHex } from "../store/nftSlice";
|
||||
import { setTargetAddressHex as setTransferTargetAddressHex } from "../store/transferSlice";
|
||||
import { uint8ArrayToHex } from "../utils/array";
|
||||
|
||||
function useSyncTargetAddress(shouldFire: boolean) {
|
||||
function useSyncTargetAddress(shouldFire: boolean, nft?: boolean) {
|
||||
const dispatch = useDispatch();
|
||||
const targetChain = useSelector(selectTransferTargetChain);
|
||||
const targetChain = useSelector(
|
||||
nft ? selectNFTTargetChain : selectTransferTargetChain
|
||||
);
|
||||
const { signerAddress } = useEthereumProvider();
|
||||
const solanaWallet = useSolanaWallet();
|
||||
const solPK = solanaWallet?.publicKey;
|
||||
const targetAsset = useSelector(selectTransferTargetAsset);
|
||||
const targetAsset = useSelector(
|
||||
nft ? selectNFTTargetAsset : selectTransferTargetAsset
|
||||
);
|
||||
const targetParsedTokenAccount = useSelector(
|
||||
selectTransferTargetParsedTokenAccount
|
||||
);
|
||||
const targetTokenAccountPublicKey = targetParsedTokenAccount?.publicKey;
|
||||
const terraWallet = useConnectedWallet();
|
||||
const setTargetAddressHex = nft
|
||||
? setNFTTargetAddressHex
|
||||
: setTransferTargetAddressHex;
|
||||
useEffect(() => {
|
||||
if (shouldFire) {
|
||||
let cancelled = false;
|
||||
|
@ -47,7 +57,11 @@ function useSyncTargetAddress(shouldFire: boolean) {
|
|||
);
|
||||
}
|
||||
// TODO: have the user explicitly select an account on solana
|
||||
else if (targetChain === CHAIN_ID_SOLANA && targetTokenAccountPublicKey) {
|
||||
else if (
|
||||
!nft && // only support existing, non-derived token accounts for token transfers (nft flow doesn't check balance)
|
||||
targetChain === CHAIN_ID_SOLANA &&
|
||||
targetTokenAccountPublicKey
|
||||
) {
|
||||
// use the target's TokenAccount if it exists
|
||||
dispatch(
|
||||
setTargetAddressHex(
|
||||
|
@ -74,7 +88,11 @@ function useSyncTargetAddress(shouldFire: boolean) {
|
|||
)
|
||||
);
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
if (!cancelled) {
|
||||
dispatch(setTargetAddressHex(undefined));
|
||||
}
|
||||
}
|
||||
})();
|
||||
} else if (
|
||||
targetChain === CHAIN_ID_TERRA &&
|
||||
|
@ -104,6 +122,8 @@ function useSyncTargetAddress(shouldFire: boolean) {
|
|||
targetAsset,
|
||||
targetTokenAccountPublicKey,
|
||||
terraWallet,
|
||||
nft,
|
||||
setTargetAddressHex,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import attestReducer from "./attestSlice";
|
||||
import nftReducer from "./nftSlice";
|
||||
import transferReducer from "./transferSlice";
|
||||
import tokenReducer from "./tokenSlice";
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
attest: attestReducer,
|
||||
nft: nftReducer,
|
||||
transfer: transferReducer,
|
||||
tokens: tokenReducer,
|
||||
},
|
||||
|
|
|
@ -0,0 +1,231 @@
|
|||
import {
|
||||
ChainId,
|
||||
CHAIN_ID_ETH,
|
||||
CHAIN_ID_SOLANA,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { StateSafeWormholeWrappedInfo } from "../hooks/useCheckIfWormholeWrapped";
|
||||
import {
|
||||
DataWrapper,
|
||||
errorDataWrapper,
|
||||
fetchDataWrapper,
|
||||
getEmptyDataWrapper,
|
||||
receiveDataWrapper,
|
||||
} from "./helpers";
|
||||
import { ParsedTokenAccount, Transaction } from "./transferSlice";
|
||||
|
||||
const LAST_STEP = 3;
|
||||
|
||||
type Steps = 0 | 1 | 2 | 3;
|
||||
|
||||
// these all are optional so NFT could share TokenSelectors
|
||||
export interface NFTParsedTokenAccount extends ParsedTokenAccount {
|
||||
tokenId?: string;
|
||||
animation_url?: string | null;
|
||||
external_url?: string | null;
|
||||
image?: string;
|
||||
image_256?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface NFTState {
|
||||
activeStep: Steps;
|
||||
sourceChain: ChainId;
|
||||
isSourceAssetWormholeWrapped: boolean | undefined;
|
||||
originChain: ChainId | undefined;
|
||||
originAsset: string | undefined;
|
||||
originTokenId: string | undefined;
|
||||
sourceWalletAddress: string | undefined;
|
||||
sourceParsedTokenAccount: NFTParsedTokenAccount | undefined;
|
||||
sourceParsedTokenAccounts: DataWrapper<NFTParsedTokenAccount[]>;
|
||||
targetChain: ChainId;
|
||||
targetAddressHex: string | undefined;
|
||||
targetAsset: string | null | undefined;
|
||||
targetParsedTokenAccount: NFTParsedTokenAccount | undefined;
|
||||
transferTx: Transaction | undefined;
|
||||
signedVAAHex: string | undefined;
|
||||
isSending: boolean;
|
||||
isRedeeming: boolean;
|
||||
redeemTx: Transaction | undefined;
|
||||
}
|
||||
|
||||
const initialState: NFTState = {
|
||||
activeStep: 0,
|
||||
sourceChain: CHAIN_ID_SOLANA,
|
||||
isSourceAssetWormholeWrapped: false,
|
||||
sourceWalletAddress: undefined,
|
||||
sourceParsedTokenAccount: undefined,
|
||||
sourceParsedTokenAccounts: getEmptyDataWrapper(),
|
||||
originChain: undefined,
|
||||
originAsset: undefined,
|
||||
originTokenId: undefined,
|
||||
targetChain: CHAIN_ID_ETH,
|
||||
targetAddressHex: undefined,
|
||||
targetAsset: undefined,
|
||||
targetParsedTokenAccount: undefined,
|
||||
transferTx: undefined,
|
||||
signedVAAHex: undefined,
|
||||
isSending: false,
|
||||
isRedeeming: false,
|
||||
redeemTx: undefined,
|
||||
};
|
||||
|
||||
export const nftSlice = createSlice({
|
||||
name: "nft",
|
||||
initialState,
|
||||
reducers: {
|
||||
incrementStep: (state) => {
|
||||
if (state.activeStep < LAST_STEP) state.activeStep++;
|
||||
},
|
||||
decrementStep: (state) => {
|
||||
if (state.activeStep > 0) state.activeStep--;
|
||||
},
|
||||
setStep: (state, action: PayloadAction<Steps>) => {
|
||||
state.activeStep = action.payload;
|
||||
},
|
||||
setSourceChain: (state, action: PayloadAction<ChainId>) => {
|
||||
const prevSourceChain = state.sourceChain;
|
||||
state.sourceChain = action.payload;
|
||||
state.sourceParsedTokenAccount = undefined;
|
||||
state.sourceParsedTokenAccounts = getEmptyDataWrapper();
|
||||
if (state.targetChain === action.payload) {
|
||||
state.targetChain = prevSourceChain;
|
||||
state.targetAddressHex = undefined;
|
||||
// clear targetAsset so that components that fire before useFetchTargetAsset don't get stale data
|
||||
state.targetAsset = undefined;
|
||||
state.targetParsedTokenAccount = undefined;
|
||||
}
|
||||
},
|
||||
setSourceWormholeWrappedInfo: (
|
||||
state,
|
||||
action: PayloadAction<StateSafeWormholeWrappedInfo | undefined>
|
||||
) => {
|
||||
if (action.payload) {
|
||||
state.isSourceAssetWormholeWrapped = action.payload.isWrapped;
|
||||
state.originChain = action.payload.chainId;
|
||||
state.originAsset = action.payload.assetAddress;
|
||||
state.originTokenId = action.payload.tokenId;
|
||||
} else {
|
||||
state.isSourceAssetWormholeWrapped = undefined;
|
||||
state.originChain = undefined;
|
||||
state.originAsset = undefined;
|
||||
state.originTokenId = undefined;
|
||||
}
|
||||
},
|
||||
setSourceWalletAddress: (
|
||||
state,
|
||||
action: PayloadAction<string | undefined>
|
||||
) => {
|
||||
state.sourceWalletAddress = action.payload;
|
||||
},
|
||||
setSourceParsedTokenAccount: (
|
||||
state,
|
||||
action: PayloadAction<NFTParsedTokenAccount | undefined>
|
||||
) => {
|
||||
state.sourceParsedTokenAccount = action.payload;
|
||||
},
|
||||
setSourceParsedTokenAccounts: (
|
||||
state,
|
||||
action: PayloadAction<NFTParsedTokenAccount[] | undefined>
|
||||
) => {
|
||||
state.sourceParsedTokenAccounts = action.payload
|
||||
? receiveDataWrapper(action.payload)
|
||||
: getEmptyDataWrapper();
|
||||
},
|
||||
fetchSourceParsedTokenAccounts: (state) => {
|
||||
state.sourceParsedTokenAccounts = fetchDataWrapper();
|
||||
},
|
||||
errorSourceParsedTokenAccounts: (
|
||||
state,
|
||||
action: PayloadAction<string | undefined>
|
||||
) => {
|
||||
state.sourceParsedTokenAccounts = errorDataWrapper(
|
||||
action.payload || "An unknown error occurred."
|
||||
);
|
||||
},
|
||||
receiveSourceParsedTokenAccounts: (
|
||||
state,
|
||||
action: PayloadAction<NFTParsedTokenAccount[]>
|
||||
) => {
|
||||
state.sourceParsedTokenAccounts = receiveDataWrapper(action.payload);
|
||||
},
|
||||
setTargetChain: (state, action: PayloadAction<ChainId>) => {
|
||||
const prevTargetChain = state.targetChain;
|
||||
state.targetChain = action.payload;
|
||||
state.targetAddressHex = undefined;
|
||||
// clear targetAsset so that components that fire before useFetchTargetAsset don't get stale data
|
||||
state.targetAsset = undefined;
|
||||
state.targetParsedTokenAccount = undefined;
|
||||
if (state.sourceChain === action.payload) {
|
||||
state.sourceChain = prevTargetChain;
|
||||
state.activeStep = 0;
|
||||
state.sourceParsedTokenAccount = undefined;
|
||||
state.sourceParsedTokenAccounts = getEmptyDataWrapper();
|
||||
}
|
||||
},
|
||||
setTargetAddressHex: (state, action: PayloadAction<string | undefined>) => {
|
||||
state.targetAddressHex = action.payload;
|
||||
},
|
||||
setTargetAsset: (
|
||||
state,
|
||||
action: PayloadAction<string | null | undefined>
|
||||
) => {
|
||||
state.targetAsset = action.payload;
|
||||
},
|
||||
setTargetParsedTokenAccount: (
|
||||
state,
|
||||
action: PayloadAction<NFTParsedTokenAccount | undefined>
|
||||
) => {
|
||||
state.targetParsedTokenAccount = action.payload;
|
||||
},
|
||||
setTransferTx: (state, action: PayloadAction<Transaction>) => {
|
||||
state.transferTx = action.payload;
|
||||
},
|
||||
setSignedVAAHex: (state, action: PayloadAction<string>) => {
|
||||
state.signedVAAHex = action.payload;
|
||||
state.isSending = false;
|
||||
state.activeStep = 3;
|
||||
},
|
||||
setIsSending: (state, action: PayloadAction<boolean>) => {
|
||||
state.isSending = action.payload;
|
||||
},
|
||||
setIsRedeeming: (state, action: PayloadAction<boolean>) => {
|
||||
state.isRedeeming = action.payload;
|
||||
},
|
||||
setRedeemTx: (state, action: PayloadAction<Transaction>) => {
|
||||
state.redeemTx = action.payload;
|
||||
state.isRedeeming = false;
|
||||
},
|
||||
reset: (state) => ({
|
||||
...initialState,
|
||||
sourceChain: state.sourceChain,
|
||||
targetChain: state.targetChain,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
incrementStep,
|
||||
decrementStep,
|
||||
setStep,
|
||||
setSourceChain,
|
||||
setSourceWormholeWrappedInfo,
|
||||
setSourceWalletAddress,
|
||||
setSourceParsedTokenAccount,
|
||||
setSourceParsedTokenAccounts,
|
||||
receiveSourceParsedTokenAccounts,
|
||||
errorSourceParsedTokenAccounts,
|
||||
fetchSourceParsedTokenAccounts,
|
||||
setTargetChain,
|
||||
setTargetAddressHex,
|
||||
setTargetAsset,
|
||||
setTargetParsedTokenAccount,
|
||||
setTransferTx,
|
||||
setSignedVAAHex,
|
||||
setIsSending,
|
||||
setIsRedeeming,
|
||||
setRedeemTx,
|
||||
reset,
|
||||
} = nftSlice.actions;
|
||||
|
||||
export default nftSlice.reducer;
|
|
@ -31,6 +31,113 @@ export const selectAttestIsSendComplete = (state: RootState) =>
|
|||
export const selectAttestShouldLockFields = (state: RootState) =>
|
||||
selectAttestIsSending(state) || selectAttestIsSendComplete(state);
|
||||
|
||||
/*
|
||||
* NFT
|
||||
*/
|
||||
|
||||
export const selectNFTActiveStep = (state: RootState) => state.nft.activeStep;
|
||||
export const selectNFTSourceChain = (state: RootState) => state.nft.sourceChain;
|
||||
export const selectNFTSourceAsset = (state: RootState) => {
|
||||
return state.nft.sourceParsedTokenAccount?.mintKey || undefined;
|
||||
};
|
||||
export const selectNFTIsSourceAssetWormholeWrapped = (state: RootState) =>
|
||||
state.nft.isSourceAssetWormholeWrapped;
|
||||
export const selectNFTOriginChain = (state: RootState) => state.nft.originChain;
|
||||
export const selectNFTOriginAsset = (state: RootState) => state.nft.originAsset;
|
||||
export const selectNFTOriginTokenId = (state: RootState) =>
|
||||
state.nft.originTokenId;
|
||||
export const selectNFTSourceWalletAddress = (state: RootState) =>
|
||||
state.nft.sourceWalletAddress;
|
||||
export const selectNFTSourceParsedTokenAccount = (state: RootState) =>
|
||||
state.nft.sourceParsedTokenAccount;
|
||||
export const selectNFTSourceParsedTokenAccounts = (state: RootState) =>
|
||||
state.nft.sourceParsedTokenAccounts;
|
||||
export const selectNFTSourceBalanceString = (state: RootState) =>
|
||||
state.nft.sourceParsedTokenAccount?.uiAmountString || "";
|
||||
export const selectNFTTargetChain = (state: RootState) => state.nft.targetChain;
|
||||
export const selectNFTTargetAddressHex = (state: RootState) =>
|
||||
state.nft.targetAddressHex;
|
||||
export const selectNFTTargetAsset = (state: RootState) => state.nft.targetAsset;
|
||||
export const selectNFTTargetParsedTokenAccount = (state: RootState) =>
|
||||
state.nft.targetParsedTokenAccount;
|
||||
export const selectNFTTargetBalanceString = (state: RootState) =>
|
||||
state.nft.targetParsedTokenAccount?.uiAmountString || "";
|
||||
export const selectNFTTransferTx = (state: RootState) => state.nft.transferTx;
|
||||
export const selectNFTSignedVAAHex = (state: RootState) =>
|
||||
state.nft.signedVAAHex;
|
||||
export const selectNFTIsSending = (state: RootState) => state.nft.isSending;
|
||||
export const selectNFTIsRedeeming = (state: RootState) => state.nft.isRedeeming;
|
||||
export const selectNFTRedeemTx = (state: RootState) => state.nft.redeemTx;
|
||||
export const selectNFTSourceError = (state: RootState): string | undefined => {
|
||||
if (!state.nft.sourceChain) {
|
||||
return "Select a source chain";
|
||||
}
|
||||
if (!state.nft.sourceParsedTokenAccount) {
|
||||
return "Select an NFT";
|
||||
}
|
||||
if (
|
||||
state.nft.sourceChain === CHAIN_ID_SOLANA &&
|
||||
!state.nft.sourceParsedTokenAccount.publicKey
|
||||
) {
|
||||
return "Token account unavailable";
|
||||
}
|
||||
if (!state.nft.sourceParsedTokenAccount.uiAmountString) {
|
||||
return "Token amount unavailable";
|
||||
}
|
||||
if (state.nft.sourceParsedTokenAccount.decimals !== 0) {
|
||||
// TODO: more advanced NFT check - also check supply and uri
|
||||
return "For non-NFTs, use the Transfer flow";
|
||||
}
|
||||
try {
|
||||
// these may trigger error: fractional component exceeds decimals
|
||||
if (
|
||||
parseUnits(
|
||||
state.nft.sourceParsedTokenAccount.uiAmountString,
|
||||
state.nft.sourceParsedTokenAccount.decimals
|
||||
).lte(0)
|
||||
) {
|
||||
return "Balance must be greater than zero";
|
||||
}
|
||||
} catch (e) {
|
||||
if (e?.message) {
|
||||
return e.message.substring(0, e.message.indexOf("("));
|
||||
}
|
||||
return "Invalid amount";
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
export const selectNFTIsSourceComplete = (state: RootState) =>
|
||||
!selectNFTSourceError(state);
|
||||
export const selectNFTTargetError = (state: RootState) => {
|
||||
const sourceError = selectNFTSourceError(state);
|
||||
if (sourceError) {
|
||||
return `Error in source: ${sourceError}`;
|
||||
}
|
||||
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
|
||||
) {
|
||||
return UNREGISTERED_ERROR_MESSAGE;
|
||||
}
|
||||
if (!state.nft.targetAddressHex) {
|
||||
return "Target account unavailable";
|
||||
}
|
||||
};
|
||||
export const selectNFTIsTargetComplete = (state: RootState) =>
|
||||
!selectNFTTargetError(state);
|
||||
export const selectNFTIsSendComplete = (state: RootState) =>
|
||||
!!selectNFTSignedVAAHex(state);
|
||||
export const selectNFTIsRedeemComplete = (state: RootState) =>
|
||||
!!selectNFTRedeemTx(state);
|
||||
export const selectNFTShouldLockFields = (state: RootState) =>
|
||||
selectNFTIsSending(state) || selectNFTIsSendComplete(state);
|
||||
|
||||
/*
|
||||
* Transfer
|
||||
*/
|
||||
|
@ -100,7 +207,7 @@ export const selectTransferSourceError = (
|
|||
}
|
||||
if (state.transfer.sourceParsedTokenAccount.decimals === 0) {
|
||||
// TODO: more advanced NFT check - also check supply and uri
|
||||
return "NFTs are not currently supported";
|
||||
return "For NFTs, use the NFT flow";
|
||||
}
|
||||
try {
|
||||
// these may trigger error: fractional component exceeds decimals
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
CHAIN_ID_SOLANA,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { StateSafeWormholeWrappedInfo } from "../utils/getOriginalAsset";
|
||||
import { StateSafeWormholeWrappedInfo } from "../hooks/useCheckIfWormholeWrapped";
|
||||
import {
|
||||
DataWrapper,
|
||||
errorDataWrapper,
|
||||
|
|
|
@ -30,10 +30,10 @@ export const CHAINS =
|
|||
id: CHAIN_ID_SOLANA,
|
||||
name: "Solana",
|
||||
},
|
||||
{
|
||||
id: CHAIN_ID_TERRA,
|
||||
name: "Terra",
|
||||
},
|
||||
// {
|
||||
// id: CHAIN_ID_TERRA,
|
||||
// name: "Terra",
|
||||
// },
|
||||
]
|
||||
: [
|
||||
{
|
||||
|
@ -89,6 +89,11 @@ export const ETH_BRIDGE_ADDRESS = getAddress(
|
|||
? "0x44F3e7c20850B3B5f3031114726A9240911D912a"
|
||||
: "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550"
|
||||
);
|
||||
export const ETH_NFT_BRIDGE_ADDRESS = getAddress(
|
||||
CLUSTER === "testnet"
|
||||
? "0x26b4afb60d6c903165150c6f0aa14f8016be4aec" // TODO: test address
|
||||
: "0x26b4afb60d6c903165150c6f0aa14f8016be4aec"
|
||||
);
|
||||
export const ETH_TOKEN_BRIDGE_ADDRESS = getAddress(
|
||||
CLUSTER === "testnet"
|
||||
? "0xa6CDAddA6e4B6704705b065E01E52e2486c0FBf6"
|
||||
|
@ -102,6 +107,10 @@ export const SOL_BRIDGE_ADDRESS =
|
|||
CLUSTER === "testnet"
|
||||
? "Brdguy7BmNB4qwEbcqqMbyV5CyJd2sxQNUn6NEpMSsUb"
|
||||
: "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o";
|
||||
export const SOL_NFT_BRIDGE_ADDRESS =
|
||||
CLUSTER === "testnet"
|
||||
? "NFTWqJR8YnRVqPDvTJrYuLrQDitTG5AScqbeghi4zSA" // TODO: test address
|
||||
: "NFTWqJR8YnRVqPDvTJrYuLrQDitTG5AScqbeghi4zSA";
|
||||
export const SOL_TOKEN_BRIDGE_ADDRESS =
|
||||
CLUSTER === "testnet"
|
||||
? "A4Us8EhCC76XdGAN17L4KpRNEK423nMivVHZzZqFqqBg"
|
||||
|
@ -119,14 +128,17 @@ export const COVALENT_API_KEY = process.env.REACT_APP_COVALENT_API_KEY
|
|||
|
||||
export const COVALENT_GET_TOKENS_URL = (
|
||||
chainId: ChainId,
|
||||
walletAddress: string
|
||||
walletAddress: string,
|
||||
nft?: boolean
|
||||
) => {
|
||||
let chainNum = "";
|
||||
if (chainId === CHAIN_ID_ETH) {
|
||||
chainNum = COVALENT_ETHEREUM_MAINNET;
|
||||
}
|
||||
|
||||
return `https://api.covalenthq.com/v1/${chainNum}/address/${walletAddress}/balances_v2/?key=${COVALENT_API_KEY}`;
|
||||
// https://www.covalenthq.com/docs/api/#get-/v1/{chain_id}/address/{address}/balances_v2/
|
||||
return `https://api.covalenthq.com/v1/${chainNum}/address/${walletAddress}/balances_v2/?key=${COVALENT_API_KEY}${
|
||||
nft ? "&nft=true" : ""
|
||||
}`;
|
||||
};
|
||||
|
||||
export const COVALENT_ETHEREUM_MAINNET = "1";
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
import {
|
||||
NFTImplementation,
|
||||
NFTImplementation__factory,
|
||||
TokenImplementation,
|
||||
TokenImplementation__factory,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import { ethers } from "ethers";
|
||||
import { formatUnits } from "ethers/lib/utils";
|
||||
import { createParsedTokenAccount } from "../hooks/useGetSourceParsedTokenAccounts";
|
||||
import { arrayify, formatUnits } from "ethers/lib/utils";
|
||||
import {
|
||||
createNFTParsedTokenAccount,
|
||||
createParsedTokenAccount,
|
||||
} from "../hooks/useGetSourceParsedTokenAccounts";
|
||||
|
||||
//This is a valuable intermediate step to the parsed token account, as the token has metadata information on it.
|
||||
export async function getEthereumToken(
|
||||
|
@ -31,6 +36,43 @@ export async function ethTokenToParsedTokenAccount(
|
|||
);
|
||||
}
|
||||
|
||||
//This is a valuable intermediate step to the parsed token account, as the token has metadata information on it.
|
||||
export async function getEthereumNFT(
|
||||
tokenAddress: string,
|
||||
provider: ethers.providers.Web3Provider
|
||||
) {
|
||||
const token = NFTImplementation__factory.connect(tokenAddress, provider);
|
||||
return token;
|
||||
}
|
||||
|
||||
export async function isNFT(token: NFTImplementation) {
|
||||
const erc721 = "0x80ac58cd";
|
||||
const erc721metadata = "0x5b5e139f";
|
||||
return (
|
||||
(await token.supportsInterface(arrayify(erc721))) &&
|
||||
(await token.supportsInterface(arrayify(erc721metadata)))
|
||||
);
|
||||
}
|
||||
|
||||
export async function ethNFTToNFTParsedTokenAccount(
|
||||
token: NFTImplementation,
|
||||
tokenId: string,
|
||||
signerAddress: string
|
||||
) {
|
||||
const decimals = 0;
|
||||
const balance = (await token.ownerOf(tokenId)) === signerAddress ? 1 : 0;
|
||||
// const uri = await token.tokenURI(tokenId);
|
||||
return createNFTParsedTokenAccount(
|
||||
signerAddress,
|
||||
token.address,
|
||||
balance.toString(),
|
||||
decimals,
|
||||
Number(formatUnits(balance, decimals)),
|
||||
formatUnits(balance, decimals),
|
||||
tokenId
|
||||
);
|
||||
}
|
||||
|
||||
export function isValidEthereumAddress(address: string) {
|
||||
return ethers.utils.isAddress(address);
|
||||
}
|
||||
|
|
|
@ -1,70 +0,0 @@
|
|||
import {
|
||||
ChainId,
|
||||
getForeignAssetEth as getForeignAssetEthTx,
|
||||
getForeignAssetSolana as getForeignAssetSolanaTx,
|
||||
getForeignAssetTerra as getForeignAssetTerraTx,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import { Connection } from "@solana/web3.js";
|
||||
import { LCDClient } from "@terra-money/terra.js";
|
||||
import { ethers } from "ethers";
|
||||
import { hexToUint8Array } from "./array";
|
||||
import {
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
SOLANA_HOST,
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
TERRA_HOST,
|
||||
TERRA_TOKEN_BRIDGE_ADDRESS,
|
||||
} from "./consts";
|
||||
|
||||
export async function getForeignAssetEth(
|
||||
provider: ethers.providers.Web3Provider,
|
||||
originChain: ChainId,
|
||||
originAsset: string
|
||||
) {
|
||||
try {
|
||||
return await getForeignAssetEthTx(
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
provider,
|
||||
originChain,
|
||||
hexToUint8Array(originAsset)
|
||||
);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getForeignAssetSol(
|
||||
originChain: ChainId,
|
||||
originAsset: string
|
||||
) {
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
return await getForeignAssetSolanaTx(
|
||||
connection,
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
originChain,
|
||||
hexToUint8Array(originAsset)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a foreign asset address on Terra for a provided native chain and asset address
|
||||
* @param originChain
|
||||
* @param originAsset
|
||||
* @returns
|
||||
*/
|
||||
export async function getForeignAssetTerra(
|
||||
originChain: ChainId,
|
||||
originAsset: string
|
||||
) {
|
||||
try {
|
||||
const lcd = new LCDClient(TERRA_HOST);
|
||||
return await getForeignAssetTerraTx(
|
||||
TERRA_TOKEN_BRIDGE_ADDRESS,
|
||||
lcd,
|
||||
originChain,
|
||||
hexToUint8Array(originAsset)
|
||||
);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
import {
|
||||
ChainId,
|
||||
getOriginalAssetEth as getOriginalAssetEthTx,
|
||||
getOriginalAssetSol as getOriginalAssetSolTx,
|
||||
getOriginalAssetTerra as getOriginalAssetTerraTx,
|
||||
WormholeWrappedInfo,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import { Connection } from "@solana/web3.js";
|
||||
import { LCDClient } from "@terra-money/terra.js";
|
||||
import { ethers } from "ethers";
|
||||
import { uint8ArrayToHex } from "./array";
|
||||
import {
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
SOLANA_HOST,
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
TERRA_HOST,
|
||||
} from "./consts";
|
||||
|
||||
export interface StateSafeWormholeWrappedInfo {
|
||||
isWrapped: boolean;
|
||||
chainId: ChainId;
|
||||
assetAddress: string;
|
||||
}
|
||||
|
||||
const makeStateSafe = (
|
||||
info: WormholeWrappedInfo
|
||||
): StateSafeWormholeWrappedInfo => ({
|
||||
...info,
|
||||
assetAddress: uint8ArrayToHex(info.assetAddress),
|
||||
});
|
||||
|
||||
export async function getOriginalAssetEth(
|
||||
provider: ethers.providers.Web3Provider,
|
||||
wrappedAddress: string
|
||||
): Promise<StateSafeWormholeWrappedInfo> {
|
||||
return makeStateSafe(
|
||||
await getOriginalAssetEthTx(
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
provider,
|
||||
wrappedAddress
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export async function getOriginalAssetSol(
|
||||
mintAddress: string
|
||||
): Promise<StateSafeWormholeWrappedInfo> {
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
return makeStateSafe(
|
||||
await getOriginalAssetSolTx(
|
||||
connection,
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
mintAddress
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export async function getOriginalAssetTerra(
|
||||
mintAddress: string
|
||||
): Promise<StateSafeWormholeWrappedInfo> {
|
||||
const lcd = new LCDClient(TERRA_HOST);
|
||||
return makeStateSafe(await getOriginalAssetTerraTx(lcd, mintAddress));
|
||||
}
|
|
@ -18,7 +18,7 @@
|
|||
"devDependencies": {
|
||||
"@chainsafe/truffle-plugin-abigen": "0.0.1",
|
||||
"@openzeppelin/cli": "^2.8.2",
|
||||
"@openzeppelin/contracts": "^4.1.0",
|
||||
"@openzeppelin/contracts": "^4.3.1",
|
||||
"@openzeppelin/test-environment": "^0.1.6",
|
||||
"@openzeppelin/test-helpers": "^0.5.9",
|
||||
"@truffle/hdwallet-provider": "^1.2.0",
|
||||
|
@ -4095,9 +4095,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@openzeppelin/contracts": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.1.0.tgz",
|
||||
"integrity": "sha512-TihZitscnaHNcZgXGj9zDLDyCqjziytB4tMCwXq0XimfWkAjBYyk5/pOsDbbwcavhlc79HhpTEpQcrMnPVa1mw==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.3.1.tgz",
|
||||
"integrity": "sha512-QjgbPPlmDK2clK1hzjw2ROfY8KA5q+PfhDUUxZFEBCZP9fi6d5FuNoh/Uq0oCTMEKPmue69vhX2jcl0N/tFKGw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@openzeppelin/fuzzy-solidity-import-parser": {
|
||||
|
@ -43868,9 +43868,9 @@
|
|||
}
|
||||
},
|
||||
"@openzeppelin/contracts": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.1.0.tgz",
|
||||
"integrity": "sha512-TihZitscnaHNcZgXGj9zDLDyCqjziytB4tMCwXq0XimfWkAjBYyk5/pOsDbbwcavhlc79HhpTEpQcrMnPVa1mw==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.3.1.tgz",
|
||||
"integrity": "sha512-QjgbPPlmDK2clK1hzjw2ROfY8KA5q+PfhDUUxZFEBCZP9fi6d5FuNoh/Uq0oCTMEKPmue69vhX2jcl0N/tFKGw==",
|
||||
"dev": true
|
||||
},
|
||||
"@openzeppelin/fuzzy-solidity-import-parser": {
|
||||
|
@ -50815,6 +50815,7 @@
|
|||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"bitcore-lib": "^8.25.10",
|
||||
"unorm": "^1.4.1"
|
||||
}
|
||||
},
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"devDependencies": {
|
||||
"@chainsafe/truffle-plugin-abigen": "0.0.1",
|
||||
"@openzeppelin/cli": "^2.8.2",
|
||||
"@openzeppelin/contracts": "^4.1.0",
|
||||
"@openzeppelin/contracts": "^4.3.1",
|
||||
"@openzeppelin/test-environment": "^0.1.6",
|
||||
"@openzeppelin/test-helpers": "^0.5.9",
|
||||
"@truffle/hdwallet-provider": "^1.2.0",
|
||||
|
|
|
@ -1,35 +1,42 @@
|
|||
// run this script with truffle exec
|
||||
|
||||
const ERC20 = artifacts.require("ERC20PresetMinterPauser")
|
||||
const ERC721 = artifacts.require("ERC721PresetMinterPauserAutoId")
|
||||
const ERC20 = artifacts.require("ERC20PresetMinterPauser");
|
||||
const ERC721 = artifacts.require("ERC721PresetMinterPauserAutoId");
|
||||
|
||||
module.exports = async function (callback) {
|
||||
try {
|
||||
const accounts = await web3.eth.getAccounts();
|
||||
module.exports = async function(callback) {
|
||||
try {
|
||||
const accounts = await web3.eth.getAccounts();
|
||||
|
||||
// deploy token contract
|
||||
const tokenAddress = (await ERC20.new("Ethereum Test Token", "TKN")).address;
|
||||
const token = new web3.eth.Contract(ERC20.abi, tokenAddress);
|
||||
// deploy token contract
|
||||
const tokenAddress = (await ERC20.new("Ethereum Test Token", "TKN"))
|
||||
.address;
|
||||
const token = new web3.eth.Contract(ERC20.abi, tokenAddress);
|
||||
|
||||
console.log("Token deployed at: " + tokenAddress);
|
||||
console.log("Token deployed at: " + tokenAddress);
|
||||
|
||||
// mint 1000 units
|
||||
await token.methods.mint(accounts[0], "1000000000000000000000").send({
|
||||
from: accounts[0],
|
||||
gas: 1000000
|
||||
});
|
||||
// mint 1000 units
|
||||
await token.methods.mint(accounts[0], "1000000000000000000000").send({
|
||||
from: accounts[0],
|
||||
gas: 1000000,
|
||||
});
|
||||
|
||||
const nftAddress = (await ERC721.new("Not an APE", "APE", "https://cloudflare-ipfs.com/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/3287")).address;
|
||||
const nft = new web3.eth.Contract(ERC721.abi, nftAddress);
|
||||
await nft.methods.mint(accounts[0]).send({
|
||||
from: accounts[0],
|
||||
gas: 1000000
|
||||
});
|
||||
const nftAddress = (
|
||||
await ERC721.new(
|
||||
"Not an APE",
|
||||
"APE",
|
||||
"https://cloudflare-ipfs.com/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/"
|
||||
)
|
||||
).address;
|
||||
const nft = new web3.eth.Contract(ERC721.abi, nftAddress);
|
||||
await nft.methods.mint(accounts[0]).send({
|
||||
from: accounts[0],
|
||||
gas: 1000000,
|
||||
});
|
||||
|
||||
console.log("NFT deployed at: " + nftAddress);
|
||||
console.log("NFT deployed at: " + nftAddress);
|
||||
|
||||
callback();
|
||||
} catch (e) {
|
||||
callback(e);
|
||||
}
|
||||
}
|
||||
callback();
|
||||
} catch (e) {
|
||||
callback(e);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -7,6 +7,14 @@ fs.copyFileSync(
|
|||
"src/solana/core/bridge_bg.wasm.d.ts",
|
||||
"lib/solana/core/bridge_bg.wasm.d.ts"
|
||||
);
|
||||
fs.copyFileSync(
|
||||
"src/solana/nft/nft_bridge_bg.wasm",
|
||||
"lib/solana/nft/nft_bridge_bg.wasm"
|
||||
);
|
||||
fs.copyFileSync(
|
||||
"src/solana/nft/nft_bridge_bg.wasm.d.ts",
|
||||
"lib/solana/nft/nft_bridge_bg.wasm.d.ts"
|
||||
);
|
||||
fs.copyFileSync(
|
||||
"src/solana/token/token_bridge_bg.wasm",
|
||||
"lib/solana/token/token_bridge_bg.wasm"
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import { Connection, PublicKey } from "@solana/web3.js";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import { ethers } from "ethers";
|
||||
import { Bridge__factory } from "../ethers-contracts";
|
||||
import { ChainId } from "../utils";
|
||||
import { LCDClient } from "@terra-money/terra.js";
|
||||
import { fromUint8Array } from "js-base64";
|
||||
|
||||
/**
|
||||
* Returns a foreign asset address on Ethereum for a provided native chain and asset address, AddressZero if it does not exist
|
||||
|
@ -23,48 +21,30 @@ export async function getForeignAssetEth(
|
|||
try {
|
||||
return await tokenBridge.wrappedAsset(originChain, originAsset);
|
||||
} catch (e) {
|
||||
return ethers.constants.AddressZero;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getForeignAssetTerra(
|
||||
tokenBridgeAddress: string,
|
||||
client: LCDClient,
|
||||
originChain: ChainId,
|
||||
originAsset: Uint8Array
|
||||
) {
|
||||
const result: { address: string } = await client.wasm.contractQuery(tokenBridgeAddress, {
|
||||
wrapped_registry: {
|
||||
chain: originChain,
|
||||
address: fromUint8Array(originAsset),
|
||||
},
|
||||
});
|
||||
return result.address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a foreign asset address on Solana for a provided native chain and asset address
|
||||
* @param connection
|
||||
* @param tokenBridgeAddress
|
||||
* @param originChain
|
||||
* @param originAsset zero pad to 32 bytes
|
||||
* @returns
|
||||
*/
|
||||
export async function getForeignAssetSol(
|
||||
connection: Connection,
|
||||
tokenBridgeAddress: string,
|
||||
originChain: ChainId,
|
||||
originAsset: Uint8Array
|
||||
originAsset: Uint8Array,
|
||||
tokenId: Uint8Array
|
||||
) {
|
||||
const { wrapped_address } = await import("../solana/nft/nft_bridge");
|
||||
const wrappedAddress = wrapped_address(
|
||||
tokenBridgeAddress,
|
||||
originAsset,
|
||||
originChain
|
||||
originChain,
|
||||
tokenId
|
||||
);
|
||||
const wrappedAddressPK = new PublicKey(wrappedAddress);
|
||||
const wrappedAssetAccountInfo = await connection.getAccountInfo(
|
||||
wrappedAddressPK
|
||||
);
|
||||
return wrappedAssetAccountInfo ? wrappedAddressPK.toString() : null;
|
||||
// we don't require NFT accounts to exist, so don't check them.
|
||||
return wrappedAddressPK.toString();
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { Connection, PublicKey } from "@solana/web3.js";
|
||||
import { ethers } from "ethers";
|
||||
import { Bridge__factory } from "../ethers-contracts";
|
||||
import { ConnectedWallet as TerraConnectedWallet } from "@terra-money/wallet-provider";
|
||||
|
||||
/**
|
||||
* Returns whether or not an asset address on Ethereum is a wormhole wrapped asset
|
||||
|
@ -20,14 +19,6 @@ export async function getIsWrappedAssetEth(
|
|||
return await tokenBridge.isWrappedAsset(assetAddress);
|
||||
}
|
||||
|
||||
export async function getIsWrappedAssetTerra(
|
||||
tokenBridgeAddress: string,
|
||||
wallet: TerraConnectedWallet,
|
||||
assetAddress: string
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not an asset on Solana is a wormhole wrapped asset
|
||||
* @param connection
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { Connection, PublicKey } from "@solana/web3.js";
|
||||
import { ethers } from "ethers";
|
||||
import { arrayify } from "ethers/lib/utils";
|
||||
import { arrayify, zeroPad } from "ethers/lib/utils";
|
||||
import { TokenImplementation__factory } from "../ethers-contracts";
|
||||
import { ChainId, CHAIN_ID_ETH, CHAIN_ID_SOLANA, CHAIN_ID_TERRA } from "../utils";
|
||||
import { ChainId, CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "../utils";
|
||||
import { getIsWrappedAssetEth } from "./getIsWrappedAsset";
|
||||
import { ConnectedWallet as TerraConnectedWallet } from "@terra-money/wallet-provider";
|
||||
|
||||
export interface WormholeWrappedInfo {
|
||||
export interface WormholeWrappedNFTInfo {
|
||||
isWrapped: boolean;
|
||||
chainId: ChainId;
|
||||
assetAddress: Uint8Array;
|
||||
tokenId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -22,8 +22,9 @@ export interface WormholeWrappedInfo {
|
|||
export async function getOriginalAssetEth(
|
||||
tokenBridgeAddress: string,
|
||||
provider: ethers.providers.Web3Provider,
|
||||
wrappedAddress: string
|
||||
): Promise<WormholeWrappedInfo> {
|
||||
wrappedAddress: string,
|
||||
tokenId: string
|
||||
): Promise<WormholeWrappedNFTInfo> {
|
||||
const isWrapped = await getIsWrappedAssetEth(
|
||||
tokenBridgeAddress,
|
||||
provider,
|
||||
|
@ -36,6 +37,7 @@ 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,
|
||||
|
@ -45,19 +47,8 @@ export async function getOriginalAssetEth(
|
|||
return {
|
||||
isWrapped: false,
|
||||
chainId: CHAIN_ID_ETH,
|
||||
assetAddress: arrayify(wrappedAddress),
|
||||
};
|
||||
}
|
||||
|
||||
export async function getOriginalAssetTerra(
|
||||
tokenBridgeAddress: string,
|
||||
wallet: TerraConnectedWallet,
|
||||
wrappedAddress: string
|
||||
): Promise<WormholeWrappedInfo> {
|
||||
return {
|
||||
isWrapped: false,
|
||||
chainId: CHAIN_ID_TERRA,
|
||||
assetAddress: arrayify(""),
|
||||
assetAddress: zeroPad(arrayify(wrappedAddress), 32),
|
||||
tokenId,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -72,7 +63,7 @@ export async function getOriginalAssetSol(
|
|||
connection: Connection,
|
||||
tokenBridgeAddress: string,
|
||||
mintAddress: string
|
||||
): Promise<WormholeWrappedInfo> {
|
||||
): Promise<WormholeWrappedNFTInfo> {
|
||||
if (mintAddress) {
|
||||
// TODO: share some of this with getIsWrappedAssetSol, like a getWrappedMetaAccountAddress or something
|
||||
const { parse_wrapped_meta, wrapped_meta_address } = await import(
|
||||
|
@ -92,9 +83,17 @@ export async function getOriginalAssetSol(
|
|||
isWrapped: true,
|
||||
chainId: parsed.chain,
|
||||
assetAddress: parsed.token_address,
|
||||
tokenId: parsed.token_id,
|
||||
};
|
||||
}
|
||||
}
|
||||
try {
|
||||
return {
|
||||
isWrapped: false,
|
||||
chainId: CHAIN_ID_SOLANA,
|
||||
assetAddress: new PublicKey(mintAddress).toBytes(),
|
||||
};
|
||||
} catch (e) {}
|
||||
return {
|
||||
isWrapped: false,
|
||||
chainId: CHAIN_ID_SOLANA,
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
import {
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
Token,
|
||||
TOKEN_PROGRAM_ID,
|
||||
} from "@solana/spl-token";
|
||||
import { Connection, PublicKey, Transaction } from "@solana/web3.js";
|
||||
import { ethers } from "ethers";
|
||||
import { CHAIN_ID_SOLANA } from "..";
|
||||
import { Bridge__factory } from "../ethers-contracts";
|
||||
import { ixFromRust } from "../solana";
|
||||
|
||||
|
@ -24,16 +20,13 @@ export async function redeemOnSolana(
|
|||
bridgeAddress: string,
|
||||
tokenBridgeAddress: string,
|
||||
payerAddress: string,
|
||||
signedVAA: Uint8Array,
|
||||
isSolanaNative: boolean,
|
||||
mintAddress?: string // TODO: read the signedVAA and create the account if it doesn't exist
|
||||
signedVAA: Uint8Array
|
||||
) {
|
||||
// TODO: this gets the target account off the vaa, but is there a way to do this via wasm?
|
||||
// also, would this always be safe to do?
|
||||
// should we rely on this function to create accounts at all?
|
||||
// const { parse_vaa } = await import("../solana/core/bridge")
|
||||
// const parsedVAA = parse_vaa(signedVAA);
|
||||
// const targetAddress = new PublicKey(parsedVAA.payload.slice(67, 67 + 32)).toString()
|
||||
const { parse_vaa } = await import("../solana/core/bridge");
|
||||
const parsedVAA = parse_vaa(signedVAA);
|
||||
const isSolanaNative =
|
||||
Buffer.from(new Uint8Array(parsedVAA.payload)).readUInt16BE(65) ===
|
||||
CHAIN_ID_SOLANA;
|
||||
const { complete_transfer_wrapped_ix, complete_transfer_native_ix } =
|
||||
await import("../solana/nft/nft_bridge");
|
||||
const ixs = [];
|
||||
|
@ -49,33 +42,6 @@ export async function redeemOnSolana(
|
|||
)
|
||||
);
|
||||
} else {
|
||||
// TODO: we should always do this, they could buy wrapped somewhere else and transfer it back for the first time, but again, do it based on vaa
|
||||
if (mintAddress) {
|
||||
const mintPublicKey = new PublicKey(mintAddress);
|
||||
// TODO: re: todo above, this should be swapped for the address from the vaa (may not be the same as the payer)
|
||||
const payerPublicKey = new PublicKey(payerAddress);
|
||||
const associatedAddress = await Token.getAssociatedTokenAddress(
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
TOKEN_PROGRAM_ID,
|
||||
mintPublicKey,
|
||||
payerPublicKey
|
||||
);
|
||||
const associatedAddressInfo = await connection.getAccountInfo(
|
||||
associatedAddress
|
||||
);
|
||||
if (!associatedAddressInfo) {
|
||||
ixs.push(
|
||||
await Token.createAssociatedTokenAccountInstruction(
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
TOKEN_PROGRAM_ID,
|
||||
mintPublicKey,
|
||||
associatedAddress,
|
||||
payerPublicKey, // owner
|
||||
payerPublicKey // payer
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
ixs.push(
|
||||
ixFromRust(
|
||||
complete_transfer_wrapped_ix(
|
||||
|
|
|
@ -41,7 +41,8 @@ export async function transferFromSolana(
|
|||
targetAddress: Uint8Array,
|
||||
targetChain: ChainId,
|
||||
originAddress?: Uint8Array,
|
||||
originChain?: ChainId
|
||||
originChain?: ChainId,
|
||||
originTokenId?: Uint8Array
|
||||
) {
|
||||
const nonce = createNonce().readUInt32LE(0);
|
||||
const transferIx = await getBridgeFeeIx(
|
||||
|
@ -65,8 +66,8 @@ export async function transferFromSolana(
|
|||
let messageKey = Keypair.generate();
|
||||
const isSolanaNative =
|
||||
originChain === undefined || originChain === CHAIN_ID_SOLANA;
|
||||
if (!isSolanaNative && !originAddress) {
|
||||
throw new Error("originAddress is required when specifying originChain");
|
||||
if (!isSolanaNative && !originAddress && !originTokenId) {
|
||||
throw new Error("originAddress and originTokenId are required when specifying originChain");
|
||||
}
|
||||
const ix = ixFromRust(
|
||||
isSolanaNative
|
||||
|
@ -90,6 +91,7 @@ export async function transferFromSolana(
|
|||
payerAddress,
|
||||
originChain as number, // checked by isSolanaNative
|
||||
originAddress as Uint8Array, // checked by throw
|
||||
originTokenId as Uint8Array, // checked by throw
|
||||
nonce,
|
||||
targetAddress,
|
||||
targetChain
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
import {
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
Token,
|
||||
TOKEN_PROGRAM_ID,
|
||||
} from "@solana/spl-token";
|
||||
import { MsgExecuteContract } from "@terra-money/terra.js";
|
||||
import { Connection, PublicKey, Transaction } from "@solana/web3.js";
|
||||
import { fromUint8Array } from "js-base64";
|
||||
import { MsgExecuteContract } from "@terra-money/terra.js";
|
||||
import { ethers } from "ethers";
|
||||
import { fromUint8Array } from "js-base64";
|
||||
import { Bridge__factory } from "../ethers-contracts";
|
||||
import { ixFromRust } from "../solana";
|
||||
import { CHAIN_ID_SOLANA } from "../utils";
|
||||
|
|
Loading…
Reference in New Issue