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-redux": "^7.2.4",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "4.0.3",
|
"react-scripts": "4.0.3",
|
||||||
"redux": "^3.7.2"
|
"redux": "^3.7.2",
|
||||||
|
"use-debounce": "^7.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@craco/craco": "^6.2.0",
|
"@craco/craco": "^6.2.0",
|
||||||
|
@ -58,8 +59,7 @@
|
||||||
},
|
},
|
||||||
"../sdk/js": {
|
"../sdk/js": {
|
||||||
"name": "@certusone/wormhole-sdk",
|
"name": "@certusone/wormhole-sdk",
|
||||||
"version": "0.0.1",
|
"version": "0.0.2",
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@improbable-eng/grpc-web": "^0.14.0",
|
"@improbable-eng/grpc-web": "^0.14.0",
|
||||||
|
@ -36535,6 +36535,17 @@
|
||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/utf-8-validate": {
|
||||||
"version": "5.0.5",
|
"version": "5.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.5.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
|
||||||
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ=="
|
"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": {
|
"utf-8-validate": {
|
||||||
"version": "5.0.5",
|
"version": "5.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.5.tgz",
|
"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-redux": "^7.2.4",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "4.0.3",
|
"react-scripts": "4.0.3",
|
||||||
"redux": "^3.7.2"
|
"redux": "^3.7.2",
|
||||||
|
"use-debounce": "^7.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "npm ci --prefix ../sdk/js && npm run build --prefix ../sdk/js",
|
"preinstall": "npm ci --prefix ../sdk/js && npm run build --prefix ../sdk/js",
|
||||||
|
|
|
@ -6,7 +6,6 @@ import {
|
||||||
makeStyles,
|
makeStyles,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Typography,
|
|
||||||
} from "@material-ui/core";
|
} from "@material-ui/core";
|
||||||
import { GitHub, Publish, Send } from "@material-ui/icons";
|
import { GitHub, Publish, Send } from "@material-ui/icons";
|
||||||
import {
|
import {
|
||||||
|
@ -18,6 +17,7 @@ import {
|
||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
import Attest from "./components/Attest";
|
import Attest from "./components/Attest";
|
||||||
import Home from "./components/Home";
|
import Home from "./components/Home";
|
||||||
|
import NFT from "./components/NFT";
|
||||||
import Transfer from "./components/Transfer";
|
import Transfer from "./components/Transfer";
|
||||||
import wormholeLogo from "./icons/wormhole.svg";
|
import wormholeLogo from "./icons/wormhole.svg";
|
||||||
|
|
||||||
|
@ -75,13 +75,10 @@ function App() {
|
||||||
<div className={classes.spacer} />
|
<div className={classes.spacer} />
|
||||||
<Hidden implementation="css" xsDown>
|
<Hidden implementation="css" xsDown>
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
<Tooltip title="Coming Soon">
|
<Tooltip title="Transfer NFTs to another blockchain">
|
||||||
<Typography
|
<Link component={NavLink} to="/nft" className={classes.link}>
|
||||||
className={classes.link}
|
|
||||||
style={{ color: "#ffffff80", cursor: "default" }}
|
|
||||||
>
|
|
||||||
NFTs
|
NFTs
|
||||||
</Typography>
|
</Link>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="Transfer tokens to another blockchain">
|
<Tooltip title="Transfer tokens to another blockchain">
|
||||||
<Link
|
<Link
|
||||||
|
@ -139,6 +136,9 @@ function App() {
|
||||||
</AppBar>
|
</AppBar>
|
||||||
<div className={classes.content}>
|
<div className={classes.content}>
|
||||||
<Switch>
|
<Switch>
|
||||||
|
<Route exact path="/nft">
|
||||||
|
<NFT />
|
||||||
|
</Route>
|
||||||
<Route exact path="/transfer">
|
<Route exact path="/transfer">
|
||||||
<Transfer />
|
<Transfer />
|
||||||
</Route>
|
</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 { DataWrapper } from "../../store/helpers";
|
||||||
import { ParsedTokenAccount } from "../../store/transferSlice";
|
import { ParsedTokenAccount } from "../../store/transferSlice";
|
||||||
import {
|
import {
|
||||||
|
ethNFTToNFTParsedTokenAccount,
|
||||||
ethTokenToParsedTokenAccount,
|
ethTokenToParsedTokenAccount,
|
||||||
|
getEthereumNFT,
|
||||||
getEthereumToken,
|
getEthereumToken,
|
||||||
|
isNFT,
|
||||||
isValidEthereumAddress,
|
isValidEthereumAddress,
|
||||||
} from "../../utils/ethereum";
|
} from "../../utils/ethereum";
|
||||||
import { shortenAddress } from "../../utils/solana";
|
import { shortenAddress } from "../../utils/solana";
|
||||||
import OffsetButton from "./OffsetButton";
|
import OffsetButton from "./OffsetButton";
|
||||||
import { WormholeAbi__factory } from "@certusone/wormhole-sdk/lib/ethers-contracts/abi";
|
import { WormholeAbi__factory } from "@certusone/wormhole-sdk/lib/ethers-contracts/abi";
|
||||||
import { WORMHOLE_V1_ETH_ADDRESS } from "../../utils/consts";
|
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(() =>
|
const useStyles = makeStyles(() =>
|
||||||
createStyles({
|
createStyles({
|
||||||
|
@ -50,6 +56,7 @@ type EthereumSourceTokenSelectorProps = {
|
||||||
covalent: DataWrapper<CovalentData[]> | undefined;
|
covalent: DataWrapper<CovalentData[]> | undefined;
|
||||||
tokenAccounts: DataWrapper<ParsedTokenAccount[]> | undefined;
|
tokenAccounts: DataWrapper<ParsedTokenAccount[]> | undefined;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
nft?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderAccount = (
|
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(
|
export default function EthereumSourceTokenSelector(
|
||||||
props: EthereumSourceTokenSelectorProps
|
props: EthereumSourceTokenSelectorProps
|
||||||
) {
|
) {
|
||||||
const { value, onChange, covalent, tokenAccounts, disabled } = props;
|
const { value, onChange, covalent, tokenAccounts, disabled, nft } = props;
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const [advancedMode, setAdvancedMode] = useState(false);
|
const [advancedMode, setAdvancedMode] = useState(false);
|
||||||
const [advancedModeLoading, setAdvancedModeLoading] = useState(false);
|
const [advancedModeLoading, setAdvancedModeLoading] = useState(false);
|
||||||
const [advancedModeSymbol, setAdvancedModeSymbol] = useState("");
|
const [advancedModeSymbol, setAdvancedModeSymbol] = useState("");
|
||||||
const [advancedModeHolderString, setAdvancedModeHolderString] = useState("");
|
const [advancedModeHolderString, setAdvancedModeHolderString] = useState("");
|
||||||
|
const [advancedModeHolderTokenIdRaw, setAdvancedModeHolderTokenId] =
|
||||||
|
useState("");
|
||||||
|
const [advancedModeHolderTokenId] = useDebounce(
|
||||||
|
advancedModeHolderTokenIdRaw,
|
||||||
|
500
|
||||||
|
);
|
||||||
const [advancedModeError, setAdvancedModeError] = useState("");
|
const [advancedModeError, setAdvancedModeError] = useState("");
|
||||||
|
|
||||||
const [autocompleteHolder, setAutocompleteHolder] =
|
const [autocompleteHolder, setAutocompleteHolder] =
|
||||||
|
@ -104,11 +144,23 @@ export default function EthereumSourceTokenSelector(
|
||||||
//This also kicks off the metadata load.
|
//This also kicks off the metadata load.
|
||||||
if (advancedMode && value && advancedModeHolderString !== value.mintKey) {
|
if (advancedMode && value && advancedModeHolderString !== value.mintKey) {
|
||||||
setAdvancedModeHolderString(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) {
|
if (!advancedMode && value && !autocompleteHolder) {
|
||||||
setAutocompleteHolder(value);
|
setAutocompleteHolder(value);
|
||||||
}
|
}
|
||||||
}, [value, advancedMode, advancedModeHolderString, autocompleteHolder]);
|
}, [
|
||||||
|
value,
|
||||||
|
advancedMode,
|
||||||
|
advancedModeHolderString,
|
||||||
|
autocompleteHolder,
|
||||||
|
nft,
|
||||||
|
advancedModeHolderTokenId,
|
||||||
|
]);
|
||||||
|
|
||||||
//This effect is watching the autocomplete selection.
|
//This effect is watching the autocomplete selection.
|
||||||
//It checks to make sure the token is a valid choice before putting it on the state.
|
//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 {
|
} else {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
setAutocompleteError("");
|
setAutocompleteError("");
|
||||||
|
if (nft) {
|
||||||
|
onChange(autocompleteHolder);
|
||||||
|
return;
|
||||||
|
}
|
||||||
isWormholev1(provider, autocompleteHolder.mintKey).then(
|
isWormholev1(provider, autocompleteHolder.mintKey).then(
|
||||||
(result) => {
|
(result) => {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
|
@ -143,7 +199,7 @@ export default function EthereumSourceTokenSelector(
|
||||||
cancelled = true;
|
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
|
//This effect watches the advancedModeString, and checks that the selected asset is valid before putting
|
||||||
// it on the state.
|
// it on the state.
|
||||||
|
@ -162,68 +218,111 @@ export default function EthereumSourceTokenSelector(
|
||||||
!cancelled && setAdvancedModeError("");
|
!cancelled && setAdvancedModeError("");
|
||||||
!cancelled && setAdvancedModeSymbol("");
|
!cancelled && setAdvancedModeSymbol("");
|
||||||
try {
|
try {
|
||||||
//Validate that the token is not a wormhole v1 asset
|
if (nft) {
|
||||||
const isWormholePromise = isWormholev1(
|
getEthereumNFT(advancedModeHolderString, provider)
|
||||||
provider,
|
.then((token) => {
|
||||||
advancedModeHolderString
|
isNFT(token)
|
||||||
).then(
|
.then((result) => {
|
||||||
(result) => {
|
if (result) {
|
||||||
if (result && !cancelled) {
|
ethNFTToNFTParsedTokenAccount(
|
||||||
setAdvancedModeError(
|
token,
|
||||||
"Wormhole v1 assets are not eligible for transfer."
|
advancedModeHolderTokenId,
|
||||||
);
|
signerAddress
|
||||||
setAdvancedModeLoading(false);
|
)
|
||||||
return Promise.reject();
|
.then((parsedTokenAccount) => {
|
||||||
} else {
|
!cancelled && onChange(parsedTokenAccount);
|
||||||
return Promise.resolve();
|
!cancelled && setAdvancedModeLoading(false);
|
||||||
}
|
})
|
||||||
},
|
.catch((error) => {
|
||||||
(error) => {
|
!cancelled &&
|
||||||
!cancelled &&
|
setAdvancedModeError(
|
||||||
setAdvancedModeError(
|
"Failed to find the specified tokenId"
|
||||||
"Warning: please verify if this is a Wormhole v1 token address. V1 tokens should not be transferred with this bridge"
|
);
|
||||||
);
|
!cancelled && setAdvancedModeLoading(false);
|
||||||
!cancelled && setAdvancedModeLoading(false);
|
});
|
||||||
return Promise.resolve(); //Don't allow an error here to tank the workflow
|
} else {
|
||||||
}
|
!cancelled &&
|
||||||
);
|
setAdvancedModeError(
|
||||||
|
"This token does not support ERC-721"
|
||||||
//Then fetch the asset's information & transform to a parsed token account
|
);
|
||||||
isWormholePromise.then(() =>
|
!cancelled && setAdvancedModeLoading(false);
|
||||||
getEthereumToken(advancedModeHolderString, provider).then(
|
}
|
||||||
(token) => {
|
})
|
||||||
ethTokenToParsedTokenAccount(token, signerAddress).then(
|
.catch((error) => {
|
||||||
(parsedTokenAccount) => {
|
|
||||||
!cancelled && onChange(parsedTokenAccount);
|
|
||||||
!cancelled && setAdvancedModeLoading(false);
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
//These errors can maybe be consolidated
|
|
||||||
!cancelled &&
|
!cancelled &&
|
||||||
setAdvancedModeError(
|
setAdvancedModeError("This token does not support ERC-721");
|
||||||
"Failed to find the specified address"
|
|
||||||
);
|
|
||||||
!cancelled && setAdvancedModeLoading(false);
|
!cancelled && setAdvancedModeLoading(false);
|
||||||
}
|
});
|
||||||
);
|
})
|
||||||
|
.catch((error) => {
|
||||||
//Also attempt to store off the symbol
|
!cancelled &&
|
||||||
token.symbol().then(
|
setAdvancedModeError("This token does not support ERC-721");
|
||||||
(result) => {
|
!cancelled && setAdvancedModeLoading(false);
|
||||||
!cancelled && setAdvancedModeSymbol(result);
|
});
|
||||||
},
|
} else {
|
||||||
(error) => {
|
//Validate that the token is not a wormhole v1 asset
|
||||||
!cancelled &&
|
const isWormholePromise = isWormholev1(
|
||||||
setAdvancedModeError(
|
provider,
|
||||||
"Failed to find the specified address"
|
advancedModeHolderString
|
||||||
);
|
).then(
|
||||||
!cancelled && setAdvancedModeLoading(false);
|
(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) {
|
} catch (e) {
|
||||||
!cancelled &&
|
!cancelled &&
|
||||||
setAdvancedModeError("Failed to find the specified address");
|
setAdvancedModeError("Failed to find the specified address");
|
||||||
|
@ -239,11 +338,14 @@ export default function EthereumSourceTokenSelector(
|
||||||
provider,
|
provider,
|
||||||
signerAddress,
|
signerAddress,
|
||||||
onChange,
|
onChange,
|
||||||
|
nft,
|
||||||
|
advancedModeHolderTokenId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
onChange(null);
|
onChange(null);
|
||||||
setAdvancedModeHolderString("");
|
setAdvancedModeHolderString("");
|
||||||
|
setAdvancedModeHolderTokenId("");
|
||||||
}, [onChange]);
|
}, [onChange]);
|
||||||
|
|
||||||
const handleOnChange = useCallback(
|
const handleOnChange = useCallback(
|
||||||
|
@ -251,6 +353,11 @@ export default function EthereumSourceTokenSelector(
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleTokenIdOnChange = useCallback(
|
||||||
|
(event) => setAdvancedModeHolderTokenId(event.target.value),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const getSymbol = (account: ParsedTokenAccount | null) => {
|
const getSymbol = (account: ParsedTokenAccount | null) => {
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return undefined;
|
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 = () => {
|
const toggleAdvancedMode = () => {
|
||||||
setAdvancedModeHolderString("");
|
setAdvancedModeHolderString("");
|
||||||
setAdvancedModeError("");
|
setAdvancedModeError("");
|
||||||
|
@ -294,29 +413,45 @@ export default function EthereumSourceTokenSelector(
|
||||||
blurOnSelect
|
blurOnSelect
|
||||||
clearOnBlur
|
clearOnBlur
|
||||||
fullWidth={false}
|
fullWidth={false}
|
||||||
filterOptions={filterConfig}
|
filterOptions={nft ? filterConfigNFT : filterConfig}
|
||||||
value={autocompleteHolder}
|
value={autocompleteHolder}
|
||||||
onChange={(event, newValue) => {
|
onChange={(event, newValue) => {
|
||||||
handleAutocompleteChange(newValue);
|
handleAutocompleteChange(newValue);
|
||||||
}}
|
}}
|
||||||
disabled={disabled}
|
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 || []}
|
options={tokenAccounts?.data || []}
|
||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField {...params} label="Token Account" variant="outlined" />
|
<TextField {...params} label="Token Account" variant="outlined" />
|
||||||
)}
|
)}
|
||||||
renderOption={(option) => {
|
renderOption={(option) => {
|
||||||
return renderAccount(
|
return nft
|
||||||
option,
|
? renderNFTAccount(
|
||||||
covalent?.data?.find((x) => x.contract_address === option.mintKey),
|
option,
|
||||||
classes
|
covalent?.data?.find(
|
||||||
);
|
(x) => x.contract_address === option.mintKey
|
||||||
|
),
|
||||||
|
classes
|
||||||
|
)
|
||||||
|
: renderAccount(
|
||||||
|
option,
|
||||||
|
covalent?.data?.find(
|
||||||
|
(x) => x.contract_address === option.mintKey
|
||||||
|
),
|
||||||
|
classes
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
getOptionLabel={(option) => {
|
getOptionLabel={(option) => {
|
||||||
const symbol = getSymbol(option);
|
const symbol = getSymbol(option);
|
||||||
return `${symbol ? symbol : "Unknown"} (Address: ${shortenAddress(
|
return `${symbol ? symbol : "Unknown"} ${
|
||||||
option.mintKey
|
nft && option.name ? option.name : ""
|
||||||
)})`;
|
} (Address: ${shortenAddress(option.mintKey)}${
|
||||||
|
nft ? `, ID: ${option.tokenId}` : ""
|
||||||
|
})`;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{autocompleteError && (
|
{autocompleteError && (
|
||||||
|
@ -335,7 +470,11 @@ export default function EthereumSourceTokenSelector(
|
||||||
|
|
||||||
const content = value ? (
|
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}>
|
<OffsetButton onClick={handleClick} disabled={disabled}>
|
||||||
Clear
|
Clear
|
||||||
</OffsetButton>
|
</OffsetButton>
|
||||||
|
@ -345,8 +484,6 @@ export default function EthereumSourceTokenSelector(
|
||||||
<Typography color="error">{advancedModeError}</Typography>
|
<Typography color="error">{advancedModeError}</Typography>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
) : isLoading ? (
|
|
||||||
<CircularProgress />
|
|
||||||
) : advancedMode ? (
|
) : advancedMode ? (
|
||||||
<>
|
<>
|
||||||
<TextField
|
<TextField
|
||||||
|
@ -362,7 +499,21 @@ export default function EthereumSourceTokenSelector(
|
||||||
helperText={advancedModeError === "" ? undefined : advancedModeError}
|
helperText={advancedModeError === "" ? undefined : advancedModeError}
|
||||||
disabled={disabled || advancedModeLoading}
|
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
|
autoComplete
|
||||||
);
|
);
|
||||||
|
@ -370,7 +521,7 @@ export default function EthereumSourceTokenSelector(
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{content}
|
{content}
|
||||||
{!value && !isLoading && advancedModeToggleButton}
|
{!value && advancedModeToggleButton}
|
||||||
</React.Fragment>
|
</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;
|
metaplexData: any; //DataWrapper<(Metadata | undefined)[]> | undefined | null;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
mintAccounts: DataWrapper<Map<String, string | null>> | undefined;
|
mintAccounts: DataWrapper<Map<String, string | null>> | undefined;
|
||||||
|
nft?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SolanaSourceTokenSelector(
|
export default function SolanaSourceTokenSelector(
|
||||||
props: SolanaSourceTokenSelectorProps
|
props: SolanaSourceTokenSelectorProps
|
||||||
) {
|
) {
|
||||||
const { value, onChange, disabled } = props;
|
const { value, onChange, disabled, nft } = props;
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
|
||||||
const memoizedTokenMap: Map<String, TokenInfo> = useMemo(() => {
|
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.
|
//TODO, do a better check which likely involves supply or checking masterEdition.
|
||||||
const isNFT =
|
const isNFT =
|
||||||
x.decimals === 0 && memoizedMetaplex.get(x.mintKey)?.data?.uri;
|
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(() => {
|
const isOptionDisabled = useMemo(() => {
|
||||||
return (value: ParsedTokenAccount) => isWormholev1(value.mintKey);
|
return (value: ParsedTokenAccount) => isWormholev1(value.mintKey);
|
||||||
|
@ -220,7 +221,11 @@ export default function SolanaSourceTokenSelector(
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
options={filteredOptions}
|
options={filteredOptions}
|
||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField {...params} label="Token Account" variant="outlined" />
|
<TextField
|
||||||
|
{...params}
|
||||||
|
label={nft ? "NFT Account" : "Token Account"}
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
renderOption={(option) => {
|
renderOption={(option) => {
|
||||||
return renderAccount(
|
return renderAccount(
|
||||||
|
|
|
@ -10,32 +10,50 @@ import { useDispatch, useSelector } from "react-redux";
|
||||||
import useGetSourceParsedTokens from "../../hooks/useGetSourceParsedTokenAccounts";
|
import useGetSourceParsedTokens from "../../hooks/useGetSourceParsedTokenAccounts";
|
||||||
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
||||||
import {
|
import {
|
||||||
|
selectNFTSourceChain,
|
||||||
|
selectNFTSourceParsedTokenAccount,
|
||||||
selectTransferSourceChain,
|
selectTransferSourceChain,
|
||||||
selectTransferSourceParsedTokenAccount,
|
selectTransferSourceParsedTokenAccount,
|
||||||
} from "../../store/selectors";
|
} from "../../store/selectors";
|
||||||
import {
|
import {
|
||||||
ParsedTokenAccount,
|
ParsedTokenAccount,
|
||||||
setSourceParsedTokenAccount,
|
setSourceParsedTokenAccount as setTransferSourceParsedTokenAccount,
|
||||||
setSourceWalletAddress,
|
setSourceWalletAddress as setTransferSourceWalletAddress,
|
||||||
} from "../../store/transferSlice";
|
} from "../../store/transferSlice";
|
||||||
|
import {
|
||||||
|
setSourceParsedTokenAccount as setNFTSourceParsedTokenAccount,
|
||||||
|
setSourceWalletAddress as setNFTSourceWalletAddress,
|
||||||
|
} from "../../store/nftSlice";
|
||||||
import EthereumSourceTokenSelector from "./EthereumSourceTokenSelector";
|
import EthereumSourceTokenSelector from "./EthereumSourceTokenSelector";
|
||||||
import SolanaSourceTokenSelector from "./SolanaSourceTokenSelector";
|
import SolanaSourceTokenSelector from "./SolanaSourceTokenSelector";
|
||||||
import TerraSourceTokenSelector from "./TerraSourceTokenSelector";
|
import TerraSourceTokenSelector from "./TerraSourceTokenSelector";
|
||||||
|
|
||||||
type TokenSelectorProps = {
|
type TokenSelectorProps = {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
nft?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TokenSelector = (props: TokenSelectorProps) => {
|
export const TokenSelector = (props: TokenSelectorProps) => {
|
||||||
const { disabled } = props;
|
const { disabled, nft } = props;
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const lookupChain = useSelector(selectTransferSourceChain);
|
const lookupChain = useSelector(
|
||||||
|
nft ? selectNFTSourceChain : selectTransferSourceChain
|
||||||
|
);
|
||||||
const sourceParsedTokenAccount = useSelector(
|
const sourceParsedTokenAccount = useSelector(
|
||||||
selectTransferSourceParsedTokenAccount
|
nft
|
||||||
|
? selectNFTSourceParsedTokenAccount
|
||||||
|
: selectTransferSourceParsedTokenAccount
|
||||||
);
|
);
|
||||||
const walletIsReady = useIsWalletReady(lookupChain);
|
const walletIsReady = useIsWalletReady(lookupChain);
|
||||||
|
|
||||||
|
const setSourceParsedTokenAccount = nft
|
||||||
|
? setNFTSourceParsedTokenAccount
|
||||||
|
: setTransferSourceParsedTokenAccount;
|
||||||
|
const setSourceWalletAddress = nft
|
||||||
|
? setNFTSourceWalletAddress
|
||||||
|
: setTransferSourceWalletAddress;
|
||||||
|
|
||||||
const handleOnChange = useCallback(
|
const handleOnChange = useCallback(
|
||||||
(newTokenAccount: ParsedTokenAccount | null) => {
|
(newTokenAccount: ParsedTokenAccount | null) => {
|
||||||
if (!newTokenAccount) {
|
if (!newTokenAccount) {
|
||||||
|
@ -46,10 +64,15 @@ export const TokenSelector = (props: TokenSelectorProps) => {
|
||||||
dispatch(setSourceWalletAddress(walletIsReady.walletAddress));
|
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
|
//This is only for errors so bad that we shouldn't even mount the component
|
||||||
const fatalError =
|
const fatalError =
|
||||||
|
@ -68,6 +91,7 @@ export const TokenSelector = (props: TokenSelectorProps) => {
|
||||||
solanaTokenMap={maps?.tokenMap}
|
solanaTokenMap={maps?.tokenMap}
|
||||||
metaplexData={maps?.metaplex}
|
metaplexData={maps?.metaplex}
|
||||||
mintAccounts={maps?.mintAccounts}
|
mintAccounts={maps?.mintAccounts}
|
||||||
|
nft={nft}
|
||||||
/>
|
/>
|
||||||
) : lookupChain === CHAIN_ID_ETH ? (
|
) : lookupChain === CHAIN_ID_ETH ? (
|
||||||
<EthereumSourceTokenSelector
|
<EthereumSourceTokenSelector
|
||||||
|
@ -76,6 +100,7 @@ export const TokenSelector = (props: TokenSelectorProps) => {
|
||||||
onChange={handleOnChange}
|
onChange={handleOnChange}
|
||||||
covalent={maps?.covalent || undefined}
|
covalent={maps?.covalent || undefined}
|
||||||
tokenAccounts={maps?.tokenAccounts} //TODO standardize
|
tokenAccounts={maps?.tokenAccounts} //TODO standardize
|
||||||
|
nft={nft}
|
||||||
/>
|
/>
|
||||||
) : lookupChain === CHAIN_ID_TERRA ? (
|
) : lookupChain === CHAIN_ID_TERRA ? (
|
||||||
<TerraSourceTokenSelector
|
<TerraSourceTokenSelector
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { CHAINS_BY_ID } from "../../utils/consts";
|
||||||
import ButtonWithLoader from "../ButtonWithLoader";
|
import ButtonWithLoader from "../ButtonWithLoader";
|
||||||
import KeyAndBalance from "../KeyAndBalance";
|
import KeyAndBalance from "../KeyAndBalance";
|
||||||
import StepDescription from "../StepDescription";
|
import StepDescription from "../StepDescription";
|
||||||
import TransferProgress from "./TransferProgress";
|
import TransferProgress from "../TransferProgress";
|
||||||
|
|
||||||
function Send() {
|
function Send() {
|
||||||
const { handleClick, disabled, showLoader } = useHandleTransfer();
|
const { handleClick, disabled, showLoader } = useHandleTransfer();
|
||||||
|
|
|
@ -3,13 +3,13 @@ import { LinearProgress, makeStyles, Typography } from "@material-ui/core";
|
||||||
import { Connection } from "@solana/web3.js";
|
import { Connection } from "@solana/web3.js";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||||
import {
|
import {
|
||||||
selectTransferIsSendComplete,
|
selectTransferIsSendComplete,
|
||||||
selectTransferSourceChain,
|
selectTransferSourceChain,
|
||||||
selectTransferTransferTx,
|
selectTransferTransferTx,
|
||||||
} from "../../store/selectors";
|
} from "../store/selectors";
|
||||||
import { CHAINS_BY_ID, SOLANA_HOST } from "../../utils/consts";
|
import { CHAINS_BY_ID, SOLANA_HOST } from "../utils/consts";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
root: {
|
root: {
|
|
@ -1,29 +1,73 @@
|
||||||
import {
|
import {
|
||||||
|
ChainId,
|
||||||
CHAIN_ID_ETH,
|
CHAIN_ID_ETH,
|
||||||
CHAIN_ID_SOLANA,
|
CHAIN_ID_SOLANA,
|
||||||
CHAIN_ID_TERRA,
|
CHAIN_ID_TERRA,
|
||||||
|
getOriginalAssetEth,
|
||||||
|
getOriginalAssetSol,
|
||||||
|
getOriginalAssetTerra,
|
||||||
|
WormholeWrappedInfo,
|
||||||
} from "@certusone/wormhole-sdk";
|
} 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 { useEffect } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||||
import {
|
import {
|
||||||
|
selectNFTSourceAsset,
|
||||||
|
selectNFTSourceChain,
|
||||||
|
selectNFTSourceParsedTokenAccount,
|
||||||
selectTransferSourceAsset,
|
selectTransferSourceAsset,
|
||||||
selectTransferSourceChain,
|
selectTransferSourceChain,
|
||||||
} from "../store/selectors";
|
} 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 {
|
import {
|
||||||
getOriginalAssetEth,
|
ETH_NFT_BRIDGE_ADDRESS,
|
||||||
getOriginalAssetSol,
|
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||||
getOriginalAssetTerra,
|
SOLANA_HOST,
|
||||||
} from "../utils/getOriginalAsset";
|
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
|
// 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
|
// tokens. Wrapped tokens are tokens that are non-native, I.E, are locked up on
|
||||||
// a different chain than this one.
|
// a different chain than this one.
|
||||||
function useCheckIfWormholeWrapped() {
|
function useCheckIfWormholeWrapped(nft?: boolean) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const sourceChain = useSelector(selectTransferSourceChain);
|
const sourceChain = useSelector(
|
||||||
const sourceAsset = useSelector(selectTransferSourceAsset);
|
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();
|
const { provider } = useEthereumProvider();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// TODO: loading state, error state
|
// TODO: loading state, error state
|
||||||
|
@ -31,14 +75,42 @@ function useCheckIfWormholeWrapped() {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
(async () => {
|
(async () => {
|
||||||
if (sourceChain === CHAIN_ID_ETH && provider && sourceAsset) {
|
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) {
|
if (!cancelled) {
|
||||||
dispatch(setSourceWormholeWrappedInfo(wrappedInfo));
|
dispatch(setSourceWormholeWrappedInfo(wrappedInfo));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (sourceChain === CHAIN_ID_SOLANA && sourceAsset) {
|
if (sourceChain === CHAIN_ID_SOLANA && sourceAsset) {
|
||||||
try {
|
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) {
|
if (!cancelled) {
|
||||||
dispatch(setSourceWormholeWrappedInfo(wrappedInfo));
|
dispatch(setSourceWormholeWrappedInfo(wrappedInfo));
|
||||||
}
|
}
|
||||||
|
@ -46,7 +118,10 @@ function useCheckIfWormholeWrapped() {
|
||||||
}
|
}
|
||||||
if (sourceChain === CHAIN_ID_TERRA && sourceAsset) {
|
if (sourceChain === CHAIN_ID_TERRA && sourceAsset) {
|
||||||
try {
|
try {
|
||||||
const wrappedInfo = await getOriginalAssetTerra(sourceAsset);
|
const lcd = new LCDClient(TERRA_HOST);
|
||||||
|
const wrappedInfo = makeStateSafe(
|
||||||
|
await getOriginalAssetTerra(lcd, sourceAsset)
|
||||||
|
);
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
dispatch(setSourceWormholeWrappedInfo(wrappedInfo));
|
dispatch(setSourceWormholeWrappedInfo(wrappedInfo));
|
||||||
}
|
}
|
||||||
|
@ -56,7 +131,15 @@ function useCheckIfWormholeWrapped() {
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [dispatch, sourceChain, sourceAsset, provider]);
|
}, [
|
||||||
|
dispatch,
|
||||||
|
sourceChain,
|
||||||
|
sourceAsset,
|
||||||
|
provider,
|
||||||
|
nft,
|
||||||
|
setSourceWormholeWrappedInfo,
|
||||||
|
tokenId,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useCheckIfWormholeWrapped;
|
export default useCheckIfWormholeWrapped;
|
||||||
|
|
|
@ -2,32 +2,62 @@ import {
|
||||||
CHAIN_ID_ETH,
|
CHAIN_ID_ETH,
|
||||||
CHAIN_ID_SOLANA,
|
CHAIN_ID_SOLANA,
|
||||||
CHAIN_ID_TERRA,
|
CHAIN_ID_TERRA,
|
||||||
|
getForeignAssetEth,
|
||||||
|
getForeignAssetSolana,
|
||||||
|
getForeignAssetTerra,
|
||||||
} from "@certusone/wormhole-sdk";
|
} 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 { useEffect } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||||
|
import { setTargetAsset as setNFTTargetAsset } from "../store/nftSlice";
|
||||||
import {
|
import {
|
||||||
|
selectNFTIsSourceAssetWormholeWrapped,
|
||||||
|
selectNFTOriginAsset,
|
||||||
|
selectNFTOriginChain,
|
||||||
|
selectNFTOriginTokenId,
|
||||||
|
selectNFTTargetChain,
|
||||||
selectTransferIsSourceAssetWormholeWrapped,
|
selectTransferIsSourceAssetWormholeWrapped,
|
||||||
selectTransferOriginAsset,
|
selectTransferOriginAsset,
|
||||||
selectTransferOriginChain,
|
selectTransferOriginChain,
|
||||||
selectTransferTargetChain,
|
selectTransferTargetChain,
|
||||||
} from "../store/selectors";
|
} from "../store/selectors";
|
||||||
import { setTargetAsset } from "../store/transferSlice";
|
import { setTargetAsset as setTransferTargetAsset } from "../store/transferSlice";
|
||||||
import { hexToNativeString } from "../utils/array";
|
import { hexToNativeString, hexToUint8Array } from "../utils/array";
|
||||||
import {
|
import {
|
||||||
getForeignAssetEth,
|
ETH_NFT_BRIDGE_ADDRESS,
|
||||||
getForeignAssetSol,
|
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||||
getForeignAssetTerra,
|
SOLANA_HOST,
|
||||||
} from "../utils/getForeignAsset";
|
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 dispatch = useDispatch();
|
||||||
const isSourceAssetWormholeWrapped = useSelector(
|
const isSourceAssetWormholeWrapped = useSelector(
|
||||||
selectTransferIsSourceAssetWormholeWrapped
|
nft
|
||||||
|
? selectNFTIsSourceAssetWormholeWrapped
|
||||||
|
: selectTransferIsSourceAssetWormholeWrapped
|
||||||
);
|
);
|
||||||
const originChain = useSelector(selectTransferOriginChain);
|
const originChain = useSelector(
|
||||||
const originAsset = useSelector(selectTransferOriginAsset);
|
nft ? selectNFTOriginChain : selectTransferOriginChain
|
||||||
const targetChain = useSelector(selectTransferTargetChain);
|
);
|
||||||
|
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();
|
const { provider } = useEthereumProvider();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSourceAssetWormholeWrapped && originChain === targetChain) {
|
if (isSourceAssetWormholeWrapped && originChain === targetChain) {
|
||||||
|
@ -44,36 +74,74 @@ function useFetchTargetAsset() {
|
||||||
originChain &&
|
originChain &&
|
||||||
originAsset
|
originAsset
|
||||||
) {
|
) {
|
||||||
const asset = await getForeignAssetEth(
|
|
||||||
provider,
|
|
||||||
originChain,
|
|
||||||
originAsset
|
|
||||||
);
|
|
||||||
if (!cancelled) {
|
|
||||||
dispatch(setTargetAsset(asset));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (targetChain === CHAIN_ID_SOLANA && originChain && originAsset) {
|
|
||||||
try {
|
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) {
|
if (!cancelled) {
|
||||||
dispatch(setTargetAsset(asset));
|
dispatch(setTargetAsset(asset));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
// TODO: warning for this
|
// 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) {
|
if (targetChain === CHAIN_ID_TERRA && originChain && originAsset) {
|
||||||
try {
|
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) {
|
if (!cancelled) {
|
||||||
dispatch(setTargetAsset(asset));
|
dispatch(setTargetAsset(asset));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
// TODO: warning for this
|
// TODO: warning for this
|
||||||
|
dispatch(setTargetAsset(null));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,6 +156,9 @@ function useFetchTargetAsset() {
|
||||||
originAsset,
|
originAsset,
|
||||||
targetChain,
|
targetChain,
|
||||||
provider,
|
provider,
|
||||||
|
nft,
|
||||||
|
setTargetAsset,
|
||||||
|
tokenId,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,18 @@ import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||||
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
||||||
import { DataWrapper } from "../store/helpers";
|
import { DataWrapper } from "../store/helpers";
|
||||||
import {
|
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,
|
selectSolanaTokenMap,
|
||||||
selectSourceWalletAddress,
|
selectSourceWalletAddress,
|
||||||
selectTerraTokenMap,
|
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 = {
|
export type TerraTokenMetadata = {
|
||||||
protocol: string;
|
protocol: string;
|
||||||
symbol: 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 = {
|
export type CovalentData = {
|
||||||
contract_decimals: number;
|
contract_decimals: number;
|
||||||
contract_ticker_symbol: string;
|
contract_ticker_symbol: string;
|
||||||
|
@ -128,12 +196,28 @@ export type CovalentData = {
|
||||||
balance: string;
|
balance: string;
|
||||||
quote: number | undefined;
|
quote: number | undefined;
|
||||||
quote_rate: 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 (
|
const getEthereumAccountsCovalent = async (
|
||||||
walletAddress: string
|
walletAddress: string,
|
||||||
|
nft?: boolean
|
||||||
): Promise<CovalentData[]> => {
|
): Promise<CovalentData[]> => {
|
||||||
const url = COVALENT_GET_TOKENS_URL(CHAIN_ID_ETH, walletAddress);
|
const url = COVALENT_GET_TOKENS_URL(CHAIN_ID_ETH, walletAddress, nft);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const output = [] as CovalentData[];
|
const output = [] as CovalentData[];
|
||||||
|
@ -144,11 +228,13 @@ const getEthereumAccountsCovalent = async (
|
||||||
for (const item of tokens) {
|
for (const item of tokens) {
|
||||||
// TODO: filter?
|
// TODO: filter?
|
||||||
if (
|
if (
|
||||||
item.contract_decimals &&
|
item.contract_decimals !== undefined &&
|
||||||
item.contract_ticker_symbol &&
|
item.contract_ticker_symbol &&
|
||||||
item.contract_address &&
|
item.contract_address &&
|
||||||
item.balance &&
|
item.balance &&
|
||||||
item.supports_erc?.includes("erc20")
|
(nft
|
||||||
|
? item.supports_erc?.includes("erc721")
|
||||||
|
: item.supports_erc?.includes("erc20"))
|
||||||
) {
|
) {
|
||||||
output.push({ ...item } as CovalentData);
|
output.push({ ...item } as CovalentData);
|
||||||
}
|
}
|
||||||
|
@ -199,10 +285,13 @@ const getMetaplexData = async (mintAddresses: string[]) => {
|
||||||
|
|
||||||
const getSolanaParsedTokenAccounts = (
|
const getSolanaParsedTokenAccounts = (
|
||||||
walletAddress: string,
|
walletAddress: string,
|
||||||
dispatch: Dispatch
|
dispatch: Dispatch,
|
||||||
|
nft: boolean
|
||||||
) => {
|
) => {
|
||||||
const connection = new Connection(SOLANA_HOST, "finalized");
|
const connection = new Connection(SOLANA_HOST, "finalized");
|
||||||
dispatch(fetchSourceParsedTokenAccounts());
|
dispatch(
|
||||||
|
nft ? fetchSourceParsedTokenAccountsNFT() : fetchSourceParsedTokenAccounts()
|
||||||
|
);
|
||||||
return connection
|
return connection
|
||||||
.getParsedTokenAccountsByOwner(new PublicKey(walletAddress), {
|
.getParsedTokenAccountsByOwner(new PublicKey(walletAddress), {
|
||||||
programId: new PublicKey(TOKEN_PROGRAM_ID),
|
programId: new PublicKey(TOKEN_PROGRAM_ID),
|
||||||
|
@ -212,11 +301,17 @@ const getSolanaParsedTokenAccounts = (
|
||||||
const mappedItems = result.value.map((item) =>
|
const mappedItems = result.value.map((item) =>
|
||||||
createParsedTokenAccountFromInfo(item.pubkey, item.account)
|
createParsedTokenAccountFromInfo(item.pubkey, item.account)
|
||||||
);
|
);
|
||||||
dispatch(receiveSourceParsedTokenAccounts(mappedItems));
|
dispatch(
|
||||||
|
nft
|
||||||
|
? receiveSourceParsedTokenAccountsNFT(mappedItems)
|
||||||
|
: receiveSourceParsedTokenAccounts(mappedItems)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
dispatch(
|
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
|
* 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.
|
* 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 dispatch = useDispatch();
|
||||||
|
|
||||||
const tokenAccounts = useSelector(selectTransferSourceParsedTokenAccounts);
|
const tokenAccounts = useSelector(
|
||||||
|
nft
|
||||||
|
? selectNFTSourceParsedTokenAccounts
|
||||||
|
: selectTransferSourceParsedTokenAccounts
|
||||||
|
);
|
||||||
const solanaTokenMap = useSelector(selectSolanaTokenMap);
|
const solanaTokenMap = useSelector(selectSolanaTokenMap);
|
||||||
const terraTokenMap = useSelector(selectTerraTokenMap);
|
const terraTokenMap = useSelector(selectTerraTokenMap);
|
||||||
|
|
||||||
const lookupChain = useSelector(selectTransferSourceChain);
|
const lookupChain = useSelector(
|
||||||
|
nft ? selectNFTSourceChain : selectTransferSourceChain
|
||||||
|
);
|
||||||
const solanaWallet = useSolanaWallet();
|
const solanaWallet = useSolanaWallet();
|
||||||
const solPK = solanaWallet?.publicKey;
|
const solPK = solanaWallet?.publicKey;
|
||||||
//const terraWallet = useConnectedWallet(); //TODO
|
//const terraWallet = useConnectedWallet(); //TODO
|
||||||
|
@ -270,7 +371,9 @@ function useGetAvailableTokens() {
|
||||||
string | undefined
|
string | undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
|
|
||||||
const selectedSourceWalletAddress = useSelector(selectSourceWalletAddress);
|
const selectedSourceWalletAddress = useSelector(
|
||||||
|
nft ? selectNFTSourceWalletAddress : selectSourceWalletAddress
|
||||||
|
);
|
||||||
const currentSourceWalletAddress: string | undefined =
|
const currentSourceWalletAddress: string | undefined =
|
||||||
lookupChain === CHAIN_ID_ETH
|
lookupChain === CHAIN_ID_ETH
|
||||||
? signerAddress
|
? signerAddress
|
||||||
|
@ -286,14 +389,26 @@ function useGetAvailableTokens() {
|
||||||
currentSourceWalletAddress !== undefined &&
|
currentSourceWalletAddress !== undefined &&
|
||||||
currentSourceWalletAddress !== selectedSourceWalletAddress
|
currentSourceWalletAddress !== selectedSourceWalletAddress
|
||||||
) {
|
) {
|
||||||
dispatch(setSourceWalletAddress(undefined));
|
dispatch(
|
||||||
dispatch(setSourceParsedTokenAccount(undefined));
|
nft
|
||||||
dispatch(setSourceParsedTokenAccounts(undefined));
|
? setSourceWalletAddressNFT(undefined)
|
||||||
dispatch(setAmount(""));
|
: setSourceWalletAddress(undefined)
|
||||||
|
);
|
||||||
|
dispatch(
|
||||||
|
nft
|
||||||
|
? setSourceParsedTokenAccountNFT(undefined)
|
||||||
|
: setSourceParsedTokenAccount(undefined)
|
||||||
|
);
|
||||||
|
dispatch(
|
||||||
|
nft
|
||||||
|
? setSourceParsedTokenAccountsNFT(undefined)
|
||||||
|
: setSourceParsedTokenAccounts(undefined)
|
||||||
|
);
|
||||||
|
!nft && dispatch(setAmount(""));
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
}
|
}
|
||||||
}, [selectedSourceWalletAddress, currentSourceWalletAddress, dispatch]);
|
}, [selectedSourceWalletAddress, currentSourceWalletAddress, dispatch, nft]);
|
||||||
|
|
||||||
// Solana metaplex load
|
// Solana metaplex load
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -333,7 +448,7 @@ function useGetAvailableTokens() {
|
||||||
if (
|
if (
|
||||||
!(tokenAccounts.data || tokenAccounts.isFetching || tokenAccounts.error)
|
!(tokenAccounts.data || tokenAccounts.isFetching || tokenAccounts.error)
|
||||||
) {
|
) {
|
||||||
getSolanaParsedTokenAccounts(solPK.toString(), dispatch);
|
getSolanaParsedTokenAccounts(solPK.toString(), dispatch, nft);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!(
|
!(
|
||||||
|
@ -354,6 +469,7 @@ function useGetAvailableTokens() {
|
||||||
solPK,
|
solPK,
|
||||||
tokenAccounts,
|
tokenAccounts,
|
||||||
solanaTokenMap,
|
solanaTokenMap,
|
||||||
|
nft,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
//Solana Mint Accounts lookup
|
//Solana Mint Accounts lookup
|
||||||
|
@ -406,6 +522,8 @@ function useGetAvailableTokens() {
|
||||||
//Ethereum accounts load
|
//Ethereum accounts load
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
//const testWallet = "0xf60c2ea62edbfe808163751dd0d8693dcb30019c";
|
//const testWallet = "0xf60c2ea62edbfe808163751dd0d8693dcb30019c";
|
||||||
|
// const nftTestWallet1 = "0x3f304c6721f35ff9af00fd32650c8e0a982180ab";
|
||||||
|
// const nftTestWallet2 = "0x98ed231428088eb440e8edb5cc8d66dcf913b86e";
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
const walletAddress = signerAddress;
|
const walletAddress = signerAddress;
|
||||||
if (!walletAddress || lookupChain !== CHAIN_ID_ETH) {
|
if (!walletAddress || lookupChain !== CHAIN_ID_ETH) {
|
||||||
|
@ -413,27 +531,53 @@ function useGetAvailableTokens() {
|
||||||
}
|
}
|
||||||
//TODO less cancel
|
//TODO less cancel
|
||||||
!cancelled && setCovalentLoading(true);
|
!cancelled && setCovalentLoading(true);
|
||||||
!cancelled && dispatch(fetchSourceParsedTokenAccounts());
|
!cancelled &&
|
||||||
getEthereumAccountsCovalent(walletAddress).then(
|
dispatch(
|
||||||
|
nft
|
||||||
|
? fetchSourceParsedTokenAccountsNFT()
|
||||||
|
: fetchSourceParsedTokenAccounts()
|
||||||
|
);
|
||||||
|
getEthereumAccountsCovalent(walletAddress, nft).then(
|
||||||
(accounts) => {
|
(accounts) => {
|
||||||
!cancelled && setCovalentLoading(false);
|
!cancelled && setCovalentLoading(false);
|
||||||
!cancelled && setCovalentError(undefined);
|
!cancelled && setCovalentError(undefined);
|
||||||
!cancelled && setCovalent(accounts);
|
!cancelled && setCovalent(accounts);
|
||||||
!cancelled &&
|
!cancelled &&
|
||||||
dispatch(
|
dispatch(
|
||||||
receiveSourceParsedTokenAccounts(
|
nft
|
||||||
accounts.map((x) =>
|
? receiveSourceParsedTokenAccountsNFT(
|
||||||
createParsedTokenAccountFromCovalent(walletAddress, x)
|
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 &&
|
!cancelled &&
|
||||||
dispatch(
|
dispatch(
|
||||||
errorSourceParsedTokenAccounts(
|
nft
|
||||||
"Cannot load your Ethereum tokens at the moment."
|
? errorSourceParsedTokenAccountsNFT(
|
||||||
)
|
"Cannot load your Ethereum NFTs at the moment."
|
||||||
|
)
|
||||||
|
: errorSourceParsedTokenAccounts(
|
||||||
|
"Cannot load your Ethereum tokens at the moment."
|
||||||
|
)
|
||||||
);
|
);
|
||||||
!cancelled &&
|
!cancelled &&
|
||||||
setCovalentError("Cannot load your Ethereum tokens at the moment.");
|
setCovalentError("Cannot load your Ethereum tokens at the moment.");
|
||||||
|
@ -444,7 +588,7 @@ function useGetAvailableTokens() {
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [lookupChain, provider, signerAddress, dispatch]);
|
}, [lookupChain, provider, signerAddress, dispatch, nft]);
|
||||||
|
|
||||||
//Terra accounts load
|
//Terra accounts load
|
||||||
//At present, we don't have any mechanism for doing this.
|
//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 { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||||
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
||||||
import {
|
import {
|
||||||
|
selectNFTTargetAsset,
|
||||||
|
selectNFTTargetChain,
|
||||||
selectTransferTargetAsset,
|
selectTransferTargetAsset,
|
||||||
selectTransferTargetChain,
|
selectTransferTargetChain,
|
||||||
selectTransferTargetParsedTokenAccount,
|
selectTransferTargetParsedTokenAccount,
|
||||||
} from "../store/selectors";
|
} 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";
|
import { uint8ArrayToHex } from "../utils/array";
|
||||||
|
|
||||||
function useSyncTargetAddress(shouldFire: boolean) {
|
function useSyncTargetAddress(shouldFire: boolean, nft?: boolean) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const targetChain = useSelector(selectTransferTargetChain);
|
const targetChain = useSelector(
|
||||||
|
nft ? selectNFTTargetChain : selectTransferTargetChain
|
||||||
|
);
|
||||||
const { signerAddress } = useEthereumProvider();
|
const { signerAddress } = useEthereumProvider();
|
||||||
const solanaWallet = useSolanaWallet();
|
const solanaWallet = useSolanaWallet();
|
||||||
const solPK = solanaWallet?.publicKey;
|
const solPK = solanaWallet?.publicKey;
|
||||||
const targetAsset = useSelector(selectTransferTargetAsset);
|
const targetAsset = useSelector(
|
||||||
|
nft ? selectNFTTargetAsset : selectTransferTargetAsset
|
||||||
|
);
|
||||||
const targetParsedTokenAccount = useSelector(
|
const targetParsedTokenAccount = useSelector(
|
||||||
selectTransferTargetParsedTokenAccount
|
selectTransferTargetParsedTokenAccount
|
||||||
);
|
);
|
||||||
const targetTokenAccountPublicKey = targetParsedTokenAccount?.publicKey;
|
const targetTokenAccountPublicKey = targetParsedTokenAccount?.publicKey;
|
||||||
const terraWallet = useConnectedWallet();
|
const terraWallet = useConnectedWallet();
|
||||||
|
const setTargetAddressHex = nft
|
||||||
|
? setNFTTargetAddressHex
|
||||||
|
: setTransferTargetAddressHex;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (shouldFire) {
|
if (shouldFire) {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
@ -47,7 +57,11 @@ function useSyncTargetAddress(shouldFire: boolean) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// TODO: have the user explicitly select an account on solana
|
// 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
|
// use the target's TokenAccount if it exists
|
||||||
dispatch(
|
dispatch(
|
||||||
setTargetAddressHex(
|
setTargetAddressHex(
|
||||||
|
@ -74,7 +88,11 @@ function useSyncTargetAddress(shouldFire: boolean) {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {
|
||||||
|
if (!cancelled) {
|
||||||
|
dispatch(setTargetAddressHex(undefined));
|
||||||
|
}
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
} else if (
|
} else if (
|
||||||
targetChain === CHAIN_ID_TERRA &&
|
targetChain === CHAIN_ID_TERRA &&
|
||||||
|
@ -104,6 +122,8 @@ function useSyncTargetAddress(shouldFire: boolean) {
|
||||||
targetAsset,
|
targetAsset,
|
||||||
targetTokenAccountPublicKey,
|
targetTokenAccountPublicKey,
|
||||||
terraWallet,
|
terraWallet,
|
||||||
|
nft,
|
||||||
|
setTargetAddressHex,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import { configureStore } from "@reduxjs/toolkit";
|
import { configureStore } from "@reduxjs/toolkit";
|
||||||
import attestReducer from "./attestSlice";
|
import attestReducer from "./attestSlice";
|
||||||
|
import nftReducer from "./nftSlice";
|
||||||
import transferReducer from "./transferSlice";
|
import transferReducer from "./transferSlice";
|
||||||
import tokenReducer from "./tokenSlice";
|
import tokenReducer from "./tokenSlice";
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
attest: attestReducer,
|
attest: attestReducer,
|
||||||
|
nft: nftReducer,
|
||||||
transfer: transferReducer,
|
transfer: transferReducer,
|
||||||
tokens: tokenReducer,
|
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) =>
|
export const selectAttestShouldLockFields = (state: RootState) =>
|
||||||
selectAttestIsSending(state) || selectAttestIsSendComplete(state);
|
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
|
* Transfer
|
||||||
*/
|
*/
|
||||||
|
@ -100,7 +207,7 @@ export const selectTransferSourceError = (
|
||||||
}
|
}
|
||||||
if (state.transfer.sourceParsedTokenAccount.decimals === 0) {
|
if (state.transfer.sourceParsedTokenAccount.decimals === 0) {
|
||||||
// TODO: more advanced NFT check - also check supply and uri
|
// TODO: more advanced NFT check - also check supply and uri
|
||||||
return "NFTs are not currently supported";
|
return "For NFTs, use the NFT flow";
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// these may trigger error: fractional component exceeds decimals
|
// these may trigger error: fractional component exceeds decimals
|
||||||
|
|
|
@ -4,7 +4,7 @@ import {
|
||||||
CHAIN_ID_SOLANA,
|
CHAIN_ID_SOLANA,
|
||||||
} from "@certusone/wormhole-sdk";
|
} from "@certusone/wormhole-sdk";
|
||||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
import { StateSafeWormholeWrappedInfo } from "../utils/getOriginalAsset";
|
import { StateSafeWormholeWrappedInfo } from "../hooks/useCheckIfWormholeWrapped";
|
||||||
import {
|
import {
|
||||||
DataWrapper,
|
DataWrapper,
|
||||||
errorDataWrapper,
|
errorDataWrapper,
|
||||||
|
|
|
@ -30,10 +30,10 @@ export const CHAINS =
|
||||||
id: CHAIN_ID_SOLANA,
|
id: CHAIN_ID_SOLANA,
|
||||||
name: "Solana",
|
name: "Solana",
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
id: CHAIN_ID_TERRA,
|
// id: CHAIN_ID_TERRA,
|
||||||
name: "Terra",
|
// name: "Terra",
|
||||||
},
|
// },
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
|
@ -89,6 +89,11 @@ export const ETH_BRIDGE_ADDRESS = getAddress(
|
||||||
? "0x44F3e7c20850B3B5f3031114726A9240911D912a"
|
? "0x44F3e7c20850B3B5f3031114726A9240911D912a"
|
||||||
: "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550"
|
: "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550"
|
||||||
);
|
);
|
||||||
|
export const ETH_NFT_BRIDGE_ADDRESS = getAddress(
|
||||||
|
CLUSTER === "testnet"
|
||||||
|
? "0x26b4afb60d6c903165150c6f0aa14f8016be4aec" // TODO: test address
|
||||||
|
: "0x26b4afb60d6c903165150c6f0aa14f8016be4aec"
|
||||||
|
);
|
||||||
export const ETH_TOKEN_BRIDGE_ADDRESS = getAddress(
|
export const ETH_TOKEN_BRIDGE_ADDRESS = getAddress(
|
||||||
CLUSTER === "testnet"
|
CLUSTER === "testnet"
|
||||||
? "0xa6CDAddA6e4B6704705b065E01E52e2486c0FBf6"
|
? "0xa6CDAddA6e4B6704705b065E01E52e2486c0FBf6"
|
||||||
|
@ -102,6 +107,10 @@ export const SOL_BRIDGE_ADDRESS =
|
||||||
CLUSTER === "testnet"
|
CLUSTER === "testnet"
|
||||||
? "Brdguy7BmNB4qwEbcqqMbyV5CyJd2sxQNUn6NEpMSsUb"
|
? "Brdguy7BmNB4qwEbcqqMbyV5CyJd2sxQNUn6NEpMSsUb"
|
||||||
: "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o";
|
: "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o";
|
||||||
|
export const SOL_NFT_BRIDGE_ADDRESS =
|
||||||
|
CLUSTER === "testnet"
|
||||||
|
? "NFTWqJR8YnRVqPDvTJrYuLrQDitTG5AScqbeghi4zSA" // TODO: test address
|
||||||
|
: "NFTWqJR8YnRVqPDvTJrYuLrQDitTG5AScqbeghi4zSA";
|
||||||
export const SOL_TOKEN_BRIDGE_ADDRESS =
|
export const SOL_TOKEN_BRIDGE_ADDRESS =
|
||||||
CLUSTER === "testnet"
|
CLUSTER === "testnet"
|
||||||
? "A4Us8EhCC76XdGAN17L4KpRNEK423nMivVHZzZqFqqBg"
|
? "A4Us8EhCC76XdGAN17L4KpRNEK423nMivVHZzZqFqqBg"
|
||||||
|
@ -119,14 +128,17 @@ export const COVALENT_API_KEY = process.env.REACT_APP_COVALENT_API_KEY
|
||||||
|
|
||||||
export const COVALENT_GET_TOKENS_URL = (
|
export const COVALENT_GET_TOKENS_URL = (
|
||||||
chainId: ChainId,
|
chainId: ChainId,
|
||||||
walletAddress: string
|
walletAddress: string,
|
||||||
|
nft?: boolean
|
||||||
) => {
|
) => {
|
||||||
let chainNum = "";
|
let chainNum = "";
|
||||||
if (chainId === CHAIN_ID_ETH) {
|
if (chainId === CHAIN_ID_ETH) {
|
||||||
chainNum = COVALENT_ETHEREUM_MAINNET;
|
chainNum = COVALENT_ETHEREUM_MAINNET;
|
||||||
}
|
}
|
||||||
|
// 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}`;
|
return `https://api.covalenthq.com/v1/${chainNum}/address/${walletAddress}/balances_v2/?key=${COVALENT_API_KEY}${
|
||||||
|
nft ? "&nft=true" : ""
|
||||||
|
}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const COVALENT_ETHEREUM_MAINNET = "1";
|
export const COVALENT_ETHEREUM_MAINNET = "1";
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
import {
|
import {
|
||||||
|
NFTImplementation,
|
||||||
|
NFTImplementation__factory,
|
||||||
TokenImplementation,
|
TokenImplementation,
|
||||||
TokenImplementation__factory,
|
TokenImplementation__factory,
|
||||||
} from "@certusone/wormhole-sdk";
|
} from "@certusone/wormhole-sdk";
|
||||||
import { ethers } from "ethers";
|
import { ethers } from "ethers";
|
||||||
import { formatUnits } from "ethers/lib/utils";
|
import { arrayify, formatUnits } from "ethers/lib/utils";
|
||||||
import { createParsedTokenAccount } from "../hooks/useGetSourceParsedTokenAccounts";
|
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.
|
//This is a valuable intermediate step to the parsed token account, as the token has metadata information on it.
|
||||||
export async function getEthereumToken(
|
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) {
|
export function isValidEthereumAddress(address: string) {
|
||||||
return ethers.utils.isAddress(address);
|
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": {
|
"devDependencies": {
|
||||||
"@chainsafe/truffle-plugin-abigen": "0.0.1",
|
"@chainsafe/truffle-plugin-abigen": "0.0.1",
|
||||||
"@openzeppelin/cli": "^2.8.2",
|
"@openzeppelin/cli": "^2.8.2",
|
||||||
"@openzeppelin/contracts": "^4.1.0",
|
"@openzeppelin/contracts": "^4.3.1",
|
||||||
"@openzeppelin/test-environment": "^0.1.6",
|
"@openzeppelin/test-environment": "^0.1.6",
|
||||||
"@openzeppelin/test-helpers": "^0.5.9",
|
"@openzeppelin/test-helpers": "^0.5.9",
|
||||||
"@truffle/hdwallet-provider": "^1.2.0",
|
"@truffle/hdwallet-provider": "^1.2.0",
|
||||||
|
@ -4095,9 +4095,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@openzeppelin/contracts": {
|
"node_modules/@openzeppelin/contracts": {
|
||||||
"version": "4.1.0",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.3.1.tgz",
|
||||||
"integrity": "sha512-TihZitscnaHNcZgXGj9zDLDyCqjziytB4tMCwXq0XimfWkAjBYyk5/pOsDbbwcavhlc79HhpTEpQcrMnPVa1mw==",
|
"integrity": "sha512-QjgbPPlmDK2clK1hzjw2ROfY8KA5q+PfhDUUxZFEBCZP9fi6d5FuNoh/Uq0oCTMEKPmue69vhX2jcl0N/tFKGw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@openzeppelin/fuzzy-solidity-import-parser": {
|
"node_modules/@openzeppelin/fuzzy-solidity-import-parser": {
|
||||||
|
@ -43868,9 +43868,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@openzeppelin/contracts": {
|
"@openzeppelin/contracts": {
|
||||||
"version": "4.1.0",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.3.1.tgz",
|
||||||
"integrity": "sha512-TihZitscnaHNcZgXGj9zDLDyCqjziytB4tMCwXq0XimfWkAjBYyk5/pOsDbbwcavhlc79HhpTEpQcrMnPVa1mw==",
|
"integrity": "sha512-QjgbPPlmDK2clK1hzjw2ROfY8KA5q+PfhDUUxZFEBCZP9fi6d5FuNoh/Uq0oCTMEKPmue69vhX2jcl0N/tFKGw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@openzeppelin/fuzzy-solidity-import-parser": {
|
"@openzeppelin/fuzzy-solidity-import-parser": {
|
||||||
|
@ -50815,6 +50815,7 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
"bitcore-lib": "^8.25.10",
|
||||||
"unorm": "^1.4.1"
|
"unorm": "^1.4.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@chainsafe/truffle-plugin-abigen": "0.0.1",
|
"@chainsafe/truffle-plugin-abigen": "0.0.1",
|
||||||
"@openzeppelin/cli": "^2.8.2",
|
"@openzeppelin/cli": "^2.8.2",
|
||||||
"@openzeppelin/contracts": "^4.1.0",
|
"@openzeppelin/contracts": "^4.3.1",
|
||||||
"@openzeppelin/test-environment": "^0.1.6",
|
"@openzeppelin/test-environment": "^0.1.6",
|
||||||
"@openzeppelin/test-helpers": "^0.5.9",
|
"@openzeppelin/test-helpers": "^0.5.9",
|
||||||
"@truffle/hdwallet-provider": "^1.2.0",
|
"@truffle/hdwallet-provider": "^1.2.0",
|
||||||
|
|
|
@ -1,35 +1,42 @@
|
||||||
// run this script with truffle exec
|
// run this script with truffle exec
|
||||||
|
|
||||||
const ERC20 = artifacts.require("ERC20PresetMinterPauser")
|
const ERC20 = artifacts.require("ERC20PresetMinterPauser");
|
||||||
const ERC721 = artifacts.require("ERC721PresetMinterPauserAutoId")
|
const ERC721 = artifacts.require("ERC721PresetMinterPauserAutoId");
|
||||||
|
|
||||||
module.exports = async function (callback) {
|
module.exports = async function(callback) {
|
||||||
try {
|
try {
|
||||||
const accounts = await web3.eth.getAccounts();
|
const accounts = await web3.eth.getAccounts();
|
||||||
|
|
||||||
// deploy token contract
|
// deploy token contract
|
||||||
const tokenAddress = (await ERC20.new("Ethereum Test Token", "TKN")).address;
|
const tokenAddress = (await ERC20.new("Ethereum Test Token", "TKN"))
|
||||||
const token = new web3.eth.Contract(ERC20.abi, tokenAddress);
|
.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
|
// mint 1000 units
|
||||||
await token.methods.mint(accounts[0], "1000000000000000000000").send({
|
await token.methods.mint(accounts[0], "1000000000000000000000").send({
|
||||||
from: accounts[0],
|
from: accounts[0],
|
||||||
gas: 1000000
|
gas: 1000000,
|
||||||
});
|
});
|
||||||
|
|
||||||
const nftAddress = (await ERC721.new("Not an APE", "APE", "https://cloudflare-ipfs.com/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/3287")).address;
|
const nftAddress = (
|
||||||
const nft = new web3.eth.Contract(ERC721.abi, nftAddress);
|
await ERC721.new(
|
||||||
await nft.methods.mint(accounts[0]).send({
|
"Not an APE",
|
||||||
from: accounts[0],
|
"APE",
|
||||||
gas: 1000000
|
"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();
|
callback();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
callback(e);
|
callback(e);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
|
@ -7,6 +7,14 @@ fs.copyFileSync(
|
||||||
"src/solana/core/bridge_bg.wasm.d.ts",
|
"src/solana/core/bridge_bg.wasm.d.ts",
|
||||||
"lib/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(
|
fs.copyFileSync(
|
||||||
"src/solana/token/token_bridge_bg.wasm",
|
"src/solana/token/token_bridge_bg.wasm",
|
||||||
"lib/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 { ethers } from "ethers";
|
||||||
import { Bridge__factory } from "../ethers-contracts";
|
import { Bridge__factory } from "../ethers-contracts";
|
||||||
import { ChainId } from "../utils";
|
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
|
* 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 {
|
try {
|
||||||
return await tokenBridge.wrappedAsset(originChain, originAsset);
|
return await tokenBridge.wrappedAsset(originChain, originAsset);
|
||||||
} catch (e) {
|
} 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
|
* Returns a foreign asset address on Solana for a provided native chain and asset address
|
||||||
* @param connection
|
|
||||||
* @param tokenBridgeAddress
|
* @param tokenBridgeAddress
|
||||||
* @param originChain
|
* @param originChain
|
||||||
* @param originAsset zero pad to 32 bytes
|
* @param originAsset zero pad to 32 bytes
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export async function getForeignAssetSol(
|
export async function getForeignAssetSol(
|
||||||
connection: Connection,
|
|
||||||
tokenBridgeAddress: string,
|
tokenBridgeAddress: string,
|
||||||
originChain: ChainId,
|
originChain: ChainId,
|
||||||
originAsset: Uint8Array
|
originAsset: Uint8Array,
|
||||||
|
tokenId: Uint8Array
|
||||||
) {
|
) {
|
||||||
const { wrapped_address } = await import("../solana/nft/nft_bridge");
|
const { wrapped_address } = await import("../solana/nft/nft_bridge");
|
||||||
const wrappedAddress = wrapped_address(
|
const wrappedAddress = wrapped_address(
|
||||||
tokenBridgeAddress,
|
tokenBridgeAddress,
|
||||||
originAsset,
|
originAsset,
|
||||||
originChain
|
originChain,
|
||||||
|
tokenId
|
||||||
);
|
);
|
||||||
const wrappedAddressPK = new PublicKey(wrappedAddress);
|
const wrappedAddressPK = new PublicKey(wrappedAddress);
|
||||||
const wrappedAssetAccountInfo = await connection.getAccountInfo(
|
// we don't require NFT accounts to exist, so don't check them.
|
||||||
wrappedAddressPK
|
return wrappedAddressPK.toString();
|
||||||
);
|
|
||||||
return wrappedAssetAccountInfo ? wrappedAddressPK.toString() : null;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { Connection, PublicKey } from "@solana/web3.js";
|
import { Connection, PublicKey } from "@solana/web3.js";
|
||||||
import { ethers } from "ethers";
|
import { ethers } from "ethers";
|
||||||
import { Bridge__factory } from "../ethers-contracts";
|
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
|
* 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);
|
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
|
* Returns whether or not an asset on Solana is a wormhole wrapped asset
|
||||||
* @param connection
|
* @param connection
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import { Connection, PublicKey } from "@solana/web3.js";
|
import { Connection, PublicKey } from "@solana/web3.js";
|
||||||
import { ethers } from "ethers";
|
import { ethers } from "ethers";
|
||||||
import { arrayify } from "ethers/lib/utils";
|
import { arrayify, zeroPad } from "ethers/lib/utils";
|
||||||
import { TokenImplementation__factory } from "../ethers-contracts";
|
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 { getIsWrappedAssetEth } from "./getIsWrappedAsset";
|
||||||
import { ConnectedWallet as TerraConnectedWallet } from "@terra-money/wallet-provider";
|
|
||||||
|
|
||||||
export interface WormholeWrappedInfo {
|
export interface WormholeWrappedNFTInfo {
|
||||||
isWrapped: boolean;
|
isWrapped: boolean;
|
||||||
chainId: ChainId;
|
chainId: ChainId;
|
||||||
assetAddress: Uint8Array;
|
assetAddress: Uint8Array;
|
||||||
|
tokenId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,8 +22,9 @@ export interface WormholeWrappedInfo {
|
||||||
export async function getOriginalAssetEth(
|
export async function getOriginalAssetEth(
|
||||||
tokenBridgeAddress: string,
|
tokenBridgeAddress: string,
|
||||||
provider: ethers.providers.Web3Provider,
|
provider: ethers.providers.Web3Provider,
|
||||||
wrappedAddress: string
|
wrappedAddress: string,
|
||||||
): Promise<WormholeWrappedInfo> {
|
tokenId: string
|
||||||
|
): Promise<WormholeWrappedNFTInfo> {
|
||||||
const isWrapped = await getIsWrappedAssetEth(
|
const isWrapped = await getIsWrappedAssetEth(
|
||||||
tokenBridgeAddress,
|
tokenBridgeAddress,
|
||||||
provider,
|
provider,
|
||||||
|
@ -36,6 +37,7 @@ export async function getOriginalAssetEth(
|
||||||
);
|
);
|
||||||
const chainId = (await token.chainId()) as ChainId; // origin chain
|
const chainId = (await token.chainId()) as ChainId; // origin chain
|
||||||
const assetAddress = await token.nativeContract(); // origin address
|
const assetAddress = await token.nativeContract(); // origin address
|
||||||
|
// TODO: tokenId
|
||||||
return {
|
return {
|
||||||
isWrapped: true,
|
isWrapped: true,
|
||||||
chainId,
|
chainId,
|
||||||
|
@ -45,19 +47,8 @@ export async function getOriginalAssetEth(
|
||||||
return {
|
return {
|
||||||
isWrapped: false,
|
isWrapped: false,
|
||||||
chainId: CHAIN_ID_ETH,
|
chainId: CHAIN_ID_ETH,
|
||||||
assetAddress: arrayify(wrappedAddress),
|
assetAddress: zeroPad(arrayify(wrappedAddress), 32),
|
||||||
};
|
tokenId,
|
||||||
}
|
|
||||||
|
|
||||||
export async function getOriginalAssetTerra(
|
|
||||||
tokenBridgeAddress: string,
|
|
||||||
wallet: TerraConnectedWallet,
|
|
||||||
wrappedAddress: string
|
|
||||||
): Promise<WormholeWrappedInfo> {
|
|
||||||
return {
|
|
||||||
isWrapped: false,
|
|
||||||
chainId: CHAIN_ID_TERRA,
|
|
||||||
assetAddress: arrayify(""),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,7 +63,7 @@ export async function getOriginalAssetSol(
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
tokenBridgeAddress: string,
|
tokenBridgeAddress: string,
|
||||||
mintAddress: string
|
mintAddress: string
|
||||||
): Promise<WormholeWrappedInfo> {
|
): Promise<WormholeWrappedNFTInfo> {
|
||||||
if (mintAddress) {
|
if (mintAddress) {
|
||||||
// TODO: share some of this with getIsWrappedAssetSol, like a getWrappedMetaAccountAddress or something
|
// TODO: share some of this with getIsWrappedAssetSol, like a getWrappedMetaAccountAddress or something
|
||||||
const { parse_wrapped_meta, wrapped_meta_address } = await import(
|
const { parse_wrapped_meta, wrapped_meta_address } = await import(
|
||||||
|
@ -92,9 +83,17 @@ export async function getOriginalAssetSol(
|
||||||
isWrapped: true,
|
isWrapped: true,
|
||||||
chainId: parsed.chain,
|
chainId: parsed.chain,
|
||||||
assetAddress: parsed.token_address,
|
assetAddress: parsed.token_address,
|
||||||
|
tokenId: parsed.token_id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
isWrapped: false,
|
||||||
|
chainId: CHAIN_ID_SOLANA,
|
||||||
|
assetAddress: new PublicKey(mintAddress).toBytes(),
|
||||||
|
};
|
||||||
|
} catch (e) {}
|
||||||
return {
|
return {
|
||||||
isWrapped: false,
|
isWrapped: false,
|
||||||
chainId: CHAIN_ID_SOLANA,
|
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 { Connection, PublicKey, Transaction } from "@solana/web3.js";
|
||||||
import { ethers } from "ethers";
|
import { ethers } from "ethers";
|
||||||
|
import { CHAIN_ID_SOLANA } from "..";
|
||||||
import { Bridge__factory } from "../ethers-contracts";
|
import { Bridge__factory } from "../ethers-contracts";
|
||||||
import { ixFromRust } from "../solana";
|
import { ixFromRust } from "../solana";
|
||||||
|
|
||||||
|
@ -24,16 +20,13 @@ export async function redeemOnSolana(
|
||||||
bridgeAddress: string,
|
bridgeAddress: string,
|
||||||
tokenBridgeAddress: string,
|
tokenBridgeAddress: string,
|
||||||
payerAddress: string,
|
payerAddress: string,
|
||||||
signedVAA: Uint8Array,
|
signedVAA: Uint8Array
|
||||||
isSolanaNative: boolean,
|
|
||||||
mintAddress?: string // TODO: read the signedVAA and create the account if it doesn't exist
|
|
||||||
) {
|
) {
|
||||||
// TODO: this gets the target account off the vaa, but is there a way to do this via wasm?
|
const { parse_vaa } = await import("../solana/core/bridge");
|
||||||
// also, would this always be safe to do?
|
const parsedVAA = parse_vaa(signedVAA);
|
||||||
// should we rely on this function to create accounts at all?
|
const isSolanaNative =
|
||||||
// const { parse_vaa } = await import("../solana/core/bridge")
|
Buffer.from(new Uint8Array(parsedVAA.payload)).readUInt16BE(65) ===
|
||||||
// const parsedVAA = parse_vaa(signedVAA);
|
CHAIN_ID_SOLANA;
|
||||||
// const targetAddress = new PublicKey(parsedVAA.payload.slice(67, 67 + 32)).toString()
|
|
||||||
const { complete_transfer_wrapped_ix, complete_transfer_native_ix } =
|
const { complete_transfer_wrapped_ix, complete_transfer_native_ix } =
|
||||||
await import("../solana/nft/nft_bridge");
|
await import("../solana/nft/nft_bridge");
|
||||||
const ixs = [];
|
const ixs = [];
|
||||||
|
@ -49,33 +42,6 @@ export async function redeemOnSolana(
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else {
|
} 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(
|
ixs.push(
|
||||||
ixFromRust(
|
ixFromRust(
|
||||||
complete_transfer_wrapped_ix(
|
complete_transfer_wrapped_ix(
|
||||||
|
|
|
@ -41,7 +41,8 @@ export async function transferFromSolana(
|
||||||
targetAddress: Uint8Array,
|
targetAddress: Uint8Array,
|
||||||
targetChain: ChainId,
|
targetChain: ChainId,
|
||||||
originAddress?: Uint8Array,
|
originAddress?: Uint8Array,
|
||||||
originChain?: ChainId
|
originChain?: ChainId,
|
||||||
|
originTokenId?: Uint8Array
|
||||||
) {
|
) {
|
||||||
const nonce = createNonce().readUInt32LE(0);
|
const nonce = createNonce().readUInt32LE(0);
|
||||||
const transferIx = await getBridgeFeeIx(
|
const transferIx = await getBridgeFeeIx(
|
||||||
|
@ -65,8 +66,8 @@ export async function transferFromSolana(
|
||||||
let messageKey = Keypair.generate();
|
let messageKey = Keypair.generate();
|
||||||
const isSolanaNative =
|
const isSolanaNative =
|
||||||
originChain === undefined || originChain === CHAIN_ID_SOLANA;
|
originChain === undefined || originChain === CHAIN_ID_SOLANA;
|
||||||
if (!isSolanaNative && !originAddress) {
|
if (!isSolanaNative && !originAddress && !originTokenId) {
|
||||||
throw new Error("originAddress is required when specifying originChain");
|
throw new Error("originAddress and originTokenId are required when specifying originChain");
|
||||||
}
|
}
|
||||||
const ix = ixFromRust(
|
const ix = ixFromRust(
|
||||||
isSolanaNative
|
isSolanaNative
|
||||||
|
@ -90,6 +91,7 @@ export async function transferFromSolana(
|
||||||
payerAddress,
|
payerAddress,
|
||||||
originChain as number, // checked by isSolanaNative
|
originChain as number, // checked by isSolanaNative
|
||||||
originAddress as Uint8Array, // checked by throw
|
originAddress as Uint8Array, // checked by throw
|
||||||
|
originTokenId as Uint8Array, // checked by throw
|
||||||
nonce,
|
nonce,
|
||||||
targetAddress,
|
targetAddress,
|
||||||
targetChain
|
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 { Connection, PublicKey, Transaction } from "@solana/web3.js";
|
||||||
import { fromUint8Array } from "js-base64";
|
import { MsgExecuteContract } from "@terra-money/terra.js";
|
||||||
import { ethers } from "ethers";
|
import { ethers } from "ethers";
|
||||||
|
import { fromUint8Array } from "js-base64";
|
||||||
import { Bridge__factory } from "../ethers-contracts";
|
import { Bridge__factory } from "../ethers-contracts";
|
||||||
import { ixFromRust } from "../solana";
|
import { ixFromRust } from "../solana";
|
||||||
import { CHAIN_ID_SOLANA } from "../utils";
|
import { CHAIN_ID_SOLANA } from "../utils";
|
||||||
|
|
Loading…
Reference in New Issue