bridge_ui: vaa-based recovery
Change-Id: I8604258b7ba5642eac60eb46393a689e718bd757
This commit is contained in:
parent
82280559bd
commit
98e05e39cb
|
@ -12,6 +12,7 @@
|
|||
"@certusone/wormhole-sdk": "file:..\\sdk\\js",
|
||||
"@material-ui/core": "^4.12.2",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@material-ui/lab": "^4.0.0-alpha.60",
|
||||
"@metamask/detect-provider": "^1.2.0",
|
||||
"@reduxjs/toolkit": "^1.6.1",
|
||||
"@solana/spl-token": "^0.1.6",
|
||||
|
@ -5001,6 +5002,32 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@material-ui/lab": {
|
||||
"version": "4.0.0-alpha.60",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/lab/-/lab-4.0.0-alpha.60.tgz",
|
||||
"integrity": "sha512-fadlYsPJF+0fx2lRuyqAuJj7hAS1tLDdIEEdov5jlrpb5pp4b+mRDUqQTUxi4inRZHS1bEXpU8QWUhO6xX88aA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.4.4",
|
||||
"@material-ui/utils": "^4.11.2",
|
||||
"clsx": "^1.0.4",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-is": "^16.8.0 || ^17.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@material-ui/core": "^4.12.1",
|
||||
"@types/react": "^16.8.6 || ^17.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@material-ui/styles": {
|
||||
"version": "4.11.4",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.4.tgz",
|
||||
|
@ -43348,6 +43375,18 @@
|
|||
"@babel/runtime": "^7.4.4"
|
||||
}
|
||||
},
|
||||
"@material-ui/lab": {
|
||||
"version": "4.0.0-alpha.60",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/lab/-/lab-4.0.0-alpha.60.tgz",
|
||||
"integrity": "sha512-fadlYsPJF+0fx2lRuyqAuJj7hAS1tLDdIEEdov5jlrpb5pp4b+mRDUqQTUxi4inRZHS1bEXpU8QWUhO6xX88aA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.4.4",
|
||||
"@material-ui/utils": "^4.11.2",
|
||||
"clsx": "^1.0.4",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-is": "^16.8.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"@material-ui/styles": {
|
||||
"version": "4.11.4",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.4.tgz",
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
"@certusone/wormhole-sdk": "file:..\\sdk\\js",
|
||||
"@material-ui/core": "^4.12.2",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@material-ui/lab": "^4.0.0-alpha.60",
|
||||
"@metamask/detect-provider": "^1.2.0",
|
||||
"@reduxjs/toolkit": "^1.6.1",
|
||||
"@solana/spl-token": "^0.1.6",
|
||||
|
|
|
@ -1,42 +1,16 @@
|
|||
import { Button, CircularProgress, makeStyles } from "@material-ui/core";
|
||||
import { useHandleCreateWrapped } from "../../hooks/useHandleCreateWrapped";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
transferButton: {
|
||||
marginTop: theme.spacing(2),
|
||||
textTransform: "none",
|
||||
width: "100%",
|
||||
},
|
||||
}));
|
||||
import ButtonWithLoader from "../ButtonWithLoader";
|
||||
|
||||
function Create() {
|
||||
const classes = useStyles();
|
||||
const { handleClick, disabled, showLoader } = useHandleCreateWrapped();
|
||||
return (
|
||||
<div style={{ position: "relative" }}>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
className={classes.transferButton}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
{showLoader ? (
|
||||
<CircularProgress
|
||||
size={24}
|
||||
color="inherit"
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: "50%",
|
||||
marginLeft: -12,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<ButtonWithLoader
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
showLoader={showLoader}
|
||||
>
|
||||
Create
|
||||
</ButtonWithLoader>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,44 +1,16 @@
|
|||
import { Button, CircularProgress, makeStyles } from "@material-ui/core";
|
||||
import { useHandleAttest } from "../../hooks/useHandleAttest";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
transferButton: {
|
||||
marginTop: theme.spacing(2),
|
||||
textTransform: "none",
|
||||
width: "100%",
|
||||
},
|
||||
}));
|
||||
import ButtonWithLoader from "../ButtonWithLoader";
|
||||
|
||||
function Send() {
|
||||
const classes = useStyles();
|
||||
const { handleClick, disabled, showLoader } = useHandleAttest();
|
||||
return (
|
||||
<>
|
||||
<div style={{ position: "relative" }}>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
className={classes.transferButton}
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
Attest
|
||||
</Button>
|
||||
{showLoader ? (
|
||||
<CircularProgress
|
||||
size={24}
|
||||
color="inherit"
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: "50%",
|
||||
marginLeft: -12,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
<ButtonWithLoader
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
showLoader={showLoader}
|
||||
>
|
||||
Attest
|
||||
</ButtonWithLoader>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import { Button, CircularProgress, makeStyles } from "@material-ui/core";
|
||||
import { ReactChild } from "react";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
position: "relative",
|
||||
},
|
||||
button: {
|
||||
marginTop: theme.spacing(2),
|
||||
textTransform: "none",
|
||||
width: "100%",
|
||||
},
|
||||
loader: {
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: "50%",
|
||||
marginLeft: -12,
|
||||
marginBottom: 6,
|
||||
},
|
||||
}));
|
||||
|
||||
export default function ButtonWithLoader({
|
||||
disabled,
|
||||
onClick,
|
||||
showLoader,
|
||||
children,
|
||||
}: {
|
||||
disabled: boolean;
|
||||
onClick: () => void;
|
||||
showLoader: boolean;
|
||||
children: ReactChild;
|
||||
}) {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
className={classes.button}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
{showLoader ? (
|
||||
<CircularProgress
|
||||
size={24}
|
||||
color="inherit"
|
||||
className={classes.loader}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
import { Typography } from "@material-ui/core";
|
||||
import {
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
Token,
|
||||
TOKEN_PROGRAM_ID,
|
||||
} from "@solana/spl-token";
|
||||
import { Connection, PublicKey, Transaction } from "@solana/web3.js";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
||||
import { SOLANA_HOST } from "../utils/consts";
|
||||
import { signSendAndConfirm } from "../utils/solana";
|
||||
import ButtonWithLoader from "./ButtonWithLoader";
|
||||
|
||||
export default function SolanaCreateAssociatedAddress({
|
||||
mintAddress,
|
||||
readableTargetAddress,
|
||||
}: {
|
||||
mintAddress: string;
|
||||
readableTargetAddress: string;
|
||||
}) {
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [associatedAccountExists, setAssociatedAccountExists] = useState(true); // for now, assume it exists until we confirm it doesn't
|
||||
const solanaWallet = useSolanaWallet();
|
||||
const solPK = solanaWallet?.publicKey;
|
||||
useEffect(() => {
|
||||
setAssociatedAccountExists(true);
|
||||
if (!mintAddress || !readableTargetAddress || !solPK) return;
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
// TODO: share connection in context?
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
const mintPublicKey = new PublicKey(mintAddress);
|
||||
const payerPublicKey = new PublicKey(solPK); // currently assumes the wallet is the owner
|
||||
const associatedAddress = await Token.getAssociatedTokenAddress(
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
TOKEN_PROGRAM_ID,
|
||||
mintPublicKey,
|
||||
payerPublicKey
|
||||
);
|
||||
const match = associatedAddress.toString() === readableTargetAddress;
|
||||
if (match) {
|
||||
const associatedAddressInfo = await connection.getAccountInfo(
|
||||
associatedAddress
|
||||
);
|
||||
if (!associatedAddressInfo) {
|
||||
if (!cancelled) {
|
||||
setAssociatedAccountExists(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [mintAddress, readableTargetAddress, solPK]);
|
||||
const handleClick = useCallback(() => {
|
||||
if (
|
||||
associatedAccountExists ||
|
||||
!mintAddress ||
|
||||
!readableTargetAddress ||
|
||||
!solPK
|
||||
)
|
||||
return;
|
||||
(async () => {
|
||||
// TODO: share connection in context?
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
const mintPublicKey = new PublicKey(mintAddress);
|
||||
const payerPublicKey = new PublicKey(solPK); // currently assumes the wallet is the owner
|
||||
const associatedAddress = await Token.getAssociatedTokenAddress(
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
TOKEN_PROGRAM_ID,
|
||||
mintPublicKey,
|
||||
payerPublicKey
|
||||
);
|
||||
const match = associatedAddress.toString() === readableTargetAddress;
|
||||
if (match) {
|
||||
const associatedAddressInfo = await connection.getAccountInfo(
|
||||
associatedAddress
|
||||
);
|
||||
if (!associatedAddressInfo) {
|
||||
setIsCreating(true);
|
||||
const transaction = new Transaction().add(
|
||||
await Token.createAssociatedTokenAccountInstruction(
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
TOKEN_PROGRAM_ID,
|
||||
mintPublicKey,
|
||||
associatedAddress,
|
||||
payerPublicKey, // owner
|
||||
payerPublicKey // payer
|
||||
)
|
||||
);
|
||||
const { blockhash } = await connection.getRecentBlockhash();
|
||||
transaction.recentBlockhash = blockhash;
|
||||
transaction.feePayer = new PublicKey(payerPublicKey);
|
||||
await signSendAndConfirm(solanaWallet, connection, transaction);
|
||||
setIsCreating(false);
|
||||
setAssociatedAccountExists(true);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [
|
||||
associatedAccountExists,
|
||||
mintAddress,
|
||||
solPK,
|
||||
readableTargetAddress,
|
||||
solanaWallet,
|
||||
]);
|
||||
if (associatedAccountExists) return null;
|
||||
return (
|
||||
<>
|
||||
<Typography color="error" variant="body2">
|
||||
This associated token account doesn't exist.
|
||||
</Typography>
|
||||
<ButtonWithLoader
|
||||
disabled={
|
||||
!mintAddress || !readableTargetAddress || !solPK || isCreating
|
||||
}
|
||||
onClick={handleClick}
|
||||
showLoader={isCreating}
|
||||
>
|
||||
Create Associated Token Account
|
||||
</ButtonWithLoader>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,256 @@
|
|||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Divider,
|
||||
Fab,
|
||||
makeStyles,
|
||||
TextField,
|
||||
} from "@material-ui/core";
|
||||
import { Alert } from "@material-ui/lab";
|
||||
import { Restore } from "@material-ui/icons";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
setSignedVAAHex,
|
||||
setStep,
|
||||
setTargetChain,
|
||||
} from "../../store/transferSlice";
|
||||
import { selectTransferSignedVAAHex } from "../../store/selectors";
|
||||
import { hexToNativeString, hexToUint8Array } from "../../utils/array";
|
||||
import { ChainId } from "@certusone/wormhole-sdk";
|
||||
import { BigNumber } from "ethers";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
fab: {
|
||||
position: "fixed",
|
||||
bottom: theme.spacing(2),
|
||||
right: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
|
||||
// 0 u256 amount
|
||||
// 32 [u8; 32] token_address
|
||||
// 64 u16 token_chain
|
||||
// 66 [u8; 32] recipient
|
||||
// 98 u16 recipient_chain
|
||||
// 100 u256 fee
|
||||
|
||||
// TODO: move to wasm / sdk, share with solana
|
||||
const parsePayload = (arr: Buffer) => ({
|
||||
amount: BigNumber.from(arr.slice(1, 1 + 32)).toBigInt(),
|
||||
originAddress: arr.slice(33, 33 + 32).toString("hex"), // TODO: is this origin or source?
|
||||
originChain: arr.readUInt16BE(65) as ChainId, // TODO: is this origin or source?
|
||||
targetAddress: arr.slice(67, 67 + 32).toString("hex"),
|
||||
targetChain: arr.readUInt16BE(99) as ChainId,
|
||||
});
|
||||
|
||||
function RecoveryDialogContent({ onClose }: { onClose: () => void }) {
|
||||
const dispatch = useDispatch();
|
||||
const currentSignedVAA = useSelector(selectTransferSignedVAAHex);
|
||||
const [recoverySignedVAA, setRecoverySignedVAA] = useState(currentSignedVAA);
|
||||
const [recoveryParsedVAA, setRecoveryParsedVAA] = useState<any>(null);
|
||||
const handleSignedVAAChange = useCallback((event) => {
|
||||
setRecoverySignedVAA(event.target.value);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
if (recoverySignedVAA) {
|
||||
(async () => {
|
||||
try {
|
||||
const { parse_vaa } = await import(
|
||||
"@certusone/wormhole-sdk/lib/solana/core/bridge"
|
||||
);
|
||||
const parsedVAA = parse_vaa(hexToUint8Array(recoverySignedVAA));
|
||||
if (!cancelled) {
|
||||
setRecoveryParsedVAA(parsedVAA);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
if (!cancelled) {
|
||||
setRecoveryParsedVAA(null);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [recoverySignedVAA]);
|
||||
const parsedPayload = useMemo(
|
||||
() =>
|
||||
recoveryParsedVAA?.payload
|
||||
? parsePayload(Buffer.from(new Uint8Array(recoveryParsedVAA.payload)))
|
||||
: null,
|
||||
[recoveryParsedVAA]
|
||||
);
|
||||
const parsedPayloadTargetChain = parsedPayload?.targetChain;
|
||||
const enableRecovery = recoverySignedVAA && parsedPayloadTargetChain;
|
||||
const handleRecoverClick = useCallback(() => {
|
||||
if (enableRecovery && recoverySignedVAA && parsedPayloadTargetChain) {
|
||||
// TODO: make recovery reducer
|
||||
dispatch(setSignedVAAHex(recoverySignedVAA));
|
||||
dispatch(setTargetChain(parsedPayloadTargetChain));
|
||||
dispatch(setStep(3));
|
||||
onClose();
|
||||
}
|
||||
}, [
|
||||
dispatch,
|
||||
enableRecovery,
|
||||
recoverySignedVAA,
|
||||
parsedPayloadTargetChain,
|
||||
onClose,
|
||||
]);
|
||||
return (
|
||||
<>
|
||||
<DialogContent>
|
||||
<Alert severity="info">
|
||||
If you have sent your tokens but have not redeemed them, you may paste
|
||||
the signed VAA here to resume from the redeem step.
|
||||
</Alert>
|
||||
<TextField
|
||||
label="Signed VAA (Hex)"
|
||||
value={recoverySignedVAA || ""}
|
||||
onChange={handleSignedVAAChange}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
/>
|
||||
<Box my={4}>
|
||||
<Divider />
|
||||
</Box>
|
||||
<TextField
|
||||
label="Emitter Chain"
|
||||
disabled
|
||||
value={recoveryParsedVAA?.emitter_chain || ""}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
/>
|
||||
<TextField
|
||||
label="Emitter Address"
|
||||
disabled
|
||||
value={
|
||||
(recoveryParsedVAA &&
|
||||
hexToNativeString(
|
||||
recoveryParsedVAA.emitter_address,
|
||||
recoveryParsedVAA.emitter_chain
|
||||
)) ||
|
||||
""
|
||||
}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
/>
|
||||
<TextField
|
||||
label="Sequence"
|
||||
disabled
|
||||
value={recoveryParsedVAA?.sequence || ""}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
/>
|
||||
<TextField
|
||||
label="Timestamp"
|
||||
disabled
|
||||
value={
|
||||
(recoveryParsedVAA &&
|
||||
new Date(recoveryParsedVAA.timestamp * 1000).toLocaleString()) ||
|
||||
""
|
||||
}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
/>
|
||||
<Box my={4}>
|
||||
<Divider />
|
||||
</Box>
|
||||
<TextField
|
||||
label="Origin Chain"
|
||||
disabled
|
||||
value={parsedPayload?.originChain.toString() || ""}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
/>
|
||||
<TextField
|
||||
label="Origin Token Address"
|
||||
disabled
|
||||
value={
|
||||
(parsedPayload &&
|
||||
hexToNativeString(
|
||||
parsedPayload.originAddress,
|
||||
parsedPayload.originChain
|
||||
)) ||
|
||||
""
|
||||
}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
/>
|
||||
<TextField
|
||||
label="Target Chain"
|
||||
disabled
|
||||
value={parsedPayload?.targetChain.toString() || ""}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
/>
|
||||
<TextField
|
||||
label="Target Address"
|
||||
disabled
|
||||
value={
|
||||
(parsedPayload &&
|
||||
hexToNativeString(
|
||||
parsedPayload.targetAddress,
|
||||
parsedPayload.targetChain
|
||||
)) ||
|
||||
""
|
||||
}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
/>
|
||||
<TextField
|
||||
label="Amount"
|
||||
disabled
|
||||
value={parsedPayload?.amount.toString() || ""}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
/>
|
||||
<Box my={4}>
|
||||
<Divider />
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} variant="contained" color="default">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleRecoverClick}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={!enableRecovery}
|
||||
>
|
||||
Recover
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Recovery() {
|
||||
const classes = useStyles();
|
||||
const [open, setOpen] = useState(false);
|
||||
const handleOpenClick = useCallback(() => {
|
||||
setOpen(true);
|
||||
}, []);
|
||||
const handleCloseClick = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
<Fab className={classes.fab} onClick={handleOpenClick}>
|
||||
<Restore />
|
||||
</Fab>
|
||||
<Dialog open={open} onClose={handleCloseClick} maxWidth="md" fullWidth>
|
||||
<DialogTitle>Recovery</DialogTitle>
|
||||
<RecoveryDialogContent onClose={handleCloseClick} />
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,42 +1,23 @@
|
|||
import { Button, CircularProgress, makeStyles } from "@material-ui/core";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useHandleRedeem } from "../../hooks/useHandleRedeem";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
transferButton: {
|
||||
marginTop: theme.spacing(2),
|
||||
textTransform: "none",
|
||||
width: "100%",
|
||||
},
|
||||
}));
|
||||
import { selectTransferTargetChain } from "../../store/selectors";
|
||||
import ButtonWithLoader from "../ButtonWithLoader";
|
||||
import KeyAndBalance from "../KeyAndBalance";
|
||||
|
||||
function Redeem() {
|
||||
const classes = useStyles();
|
||||
const { handleClick, disabled, showLoader } = useHandleRedeem();
|
||||
const targetChain = useSelector(selectTransferTargetChain);
|
||||
return (
|
||||
<div style={{ position: "relative" }}>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
className={classes.transferButton}
|
||||
<>
|
||||
<KeyAndBalance chainId={targetChain} />
|
||||
<ButtonWithLoader
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
showLoader={showLoader}
|
||||
>
|
||||
Redeem
|
||||
</Button>
|
||||
{showLoader ? (
|
||||
<CircularProgress
|
||||
size={24}
|
||||
color="inherit"
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: "50%",
|
||||
marginLeft: -12,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</ButtonWithLoader>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,43 +1,22 @@
|
|||
import { Button, CircularProgress, makeStyles } from "@material-ui/core";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useHandleTransfer } from "../../hooks/useHandleTransfer";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
transferButton: {
|
||||
marginTop: theme.spacing(2),
|
||||
textTransform: "none",
|
||||
width: "100%",
|
||||
},
|
||||
}));
|
||||
import { selectTransferSourceChain } from "../../store/selectors";
|
||||
import ButtonWithLoader from "../ButtonWithLoader";
|
||||
import KeyAndBalance from "../KeyAndBalance";
|
||||
|
||||
function Send() {
|
||||
const classes = useStyles();
|
||||
const { handleClick, disabled, showLoader } = useHandleTransfer();
|
||||
const sourceChain = useSelector(selectTransferSourceChain);
|
||||
return (
|
||||
<>
|
||||
<div style={{ position: "relative" }}>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
className={classes.transferButton}
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
Transfer
|
||||
</Button>
|
||||
{showLoader ? (
|
||||
<CircularProgress
|
||||
size={24}
|
||||
color="inherit"
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: "50%",
|
||||
marginLeft: -12,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<KeyAndBalance chainId={sourceChain} />
|
||||
<ButtonWithLoader
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
showLoader={showLoader}
|
||||
>
|
||||
Transfer
|
||||
</ButtonWithLoader>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Button, makeStyles, MenuItem, TextField } from "@material-ui/core";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
|
||||
import useSyncTargetAddress from "../../hooks/useSyncTargetAddress";
|
||||
import {
|
||||
selectTransferIsTargetComplete,
|
||||
|
@ -15,6 +16,7 @@ import { incrementStep, setTargetChain } from "../../store/transferSlice";
|
|||
import { hexToNativeString } from "../../utils/array";
|
||||
import { CHAINS } from "../../utils/consts";
|
||||
import KeyAndBalance from "../KeyAndBalance";
|
||||
import SolanaCreateAssociatedAddress from "../SolanaCreateAssociatedAddress";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
transferField: {
|
||||
|
@ -74,6 +76,12 @@ function Target() {
|
|||
value={readableTargetAddress}
|
||||
disabled={true}
|
||||
/>
|
||||
{targetChain === CHAIN_ID_SOLANA && targetAsset ? (
|
||||
<SolanaCreateAssociatedAddress
|
||||
mintAddress={targetAsset}
|
||||
readableTargetAddress={readableTargetAddress}
|
||||
/>
|
||||
) : null}
|
||||
<TextField
|
||||
label="Asset"
|
||||
fullWidth
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
selectTransferSignedVAAHex,
|
||||
} from "../../store/selectors";
|
||||
import { setStep } from "../../store/transferSlice";
|
||||
import Recovery from "./Recovery";
|
||||
import Redeem from "./Redeem";
|
||||
import Send from "./Send";
|
||||
import Source from "./Source";
|
||||
|
@ -73,6 +74,7 @@ function Transfer() {
|
|||
</StepContent>
|
||||
</Step>
|
||||
</Stepper>
|
||||
<Recovery />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ export const SolanaWalletProvider: FC = (props) => {
|
|||
// @solana/wallet-adapter-wallets includes all the adapters but supports tree shaking --
|
||||
// Only the wallets you want to instantiate here will be compiled into your application
|
||||
const wallets = useMemo(() => {
|
||||
console.log("running wallets memo again");
|
||||
return [
|
||||
getPhantomWallet(),
|
||||
// getSolflareWallet(),
|
||||
|
|
|
@ -23,9 +23,6 @@ import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
|||
import useTransferSignedVAA from "../hooks/useTransferSignedVAA";
|
||||
import {
|
||||
selectTransferIsRedeeming,
|
||||
selectTransferIsSourceAssetWormholeWrapped,
|
||||
selectTransferOriginChain,
|
||||
selectTransferTargetAsset,
|
||||
selectTransferTargetChain,
|
||||
} from "../store/selectors";
|
||||
import { reset, setIsRedeeming } from "../store/transferSlice";
|
||||
|
@ -61,9 +58,7 @@ async function solana(
|
|||
enqueueSnackbar: any,
|
||||
wallet: WalletContextState,
|
||||
payerAddress: string, //TODO: we may not need this since we have wallet
|
||||
signedVAA: Uint8Array,
|
||||
isSolanaNative: boolean,
|
||||
mintAddress?: string // TODO: read the signedVAA and create the account if it doesn't exist
|
||||
signedVAA: Uint8Array
|
||||
) {
|
||||
dispatch(setIsRedeeming(true));
|
||||
try {
|
||||
|
@ -82,9 +77,7 @@ async function solana(
|
|||
SOL_BRIDGE_ADDRESS,
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
payerAddress,
|
||||
signedVAA,
|
||||
isSolanaNative,
|
||||
mintAddress
|
||||
signedVAA
|
||||
);
|
||||
await signSendAndConfirm(wallet, connection, transaction);
|
||||
dispatch(reset());
|
||||
|
@ -129,12 +122,7 @@ async function terra(
|
|||
export function useHandleRedeem() {
|
||||
const dispatch = useDispatch();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const isSourceAssetWormholeWrapped = useSelector(
|
||||
selectTransferIsSourceAssetWormholeWrapped
|
||||
);
|
||||
const originChain = useSelector(selectTransferOriginChain);
|
||||
const targetChain = useSelector(selectTransferTargetChain);
|
||||
const targetAsset = useSelector(selectTransferTargetAsset);
|
||||
const solanaWallet = useSolanaWallet();
|
||||
const solPK = solanaWallet?.publicKey;
|
||||
const { signer } = useEthereumProvider();
|
||||
|
@ -155,9 +143,7 @@ export function useHandleRedeem() {
|
|||
enqueueSnackbar,
|
||||
solanaWallet,
|
||||
solPK.toString(),
|
||||
signedVAA,
|
||||
!!isSourceAssetWormholeWrapped && originChain === CHAIN_ID_SOLANA,
|
||||
targetAsset || undefined
|
||||
signedVAA
|
||||
);
|
||||
} else if (targetChain === CHAIN_ID_TERRA && !!terraWallet && signedVAA) {
|
||||
terra(dispatch, enqueueSnackbar, terraWallet, signedVAA);
|
||||
|
@ -175,9 +161,6 @@ export function useHandleRedeem() {
|
|||
solanaWallet,
|
||||
solPK,
|
||||
terraWallet,
|
||||
isSourceAssetWormholeWrapped,
|
||||
originChain,
|
||||
targetAsset,
|
||||
]);
|
||||
return useMemo(
|
||||
() => ({
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
import {
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
Token,
|
||||
TOKEN_PROGRAM_ID,
|
||||
} from "@solana/spl-token";
|
||||
import { Connection, PublicKey, Transaction } from "@solana/web3.js";
|
||||
import { ethers } from "ethers";
|
||||
import { Bridge__factory } from "../ethers-contracts";
|
||||
import { ixFromRust } from "../solana";
|
||||
import { CHAIN_ID_SOLANA } from "../utils";
|
||||
|
||||
export async function redeemOnEth(
|
||||
tokenBridgeAddress: string,
|
||||
|
@ -24,16 +20,13 @@ export async function redeemOnSolana(
|
|||
bridgeAddress: string,
|
||||
tokenBridgeAddress: string,
|
||||
payerAddress: string,
|
||||
signedVAA: Uint8Array,
|
||||
isSolanaNative: boolean,
|
||||
mintAddress?: string // TODO: read the signedVAA and create the account if it doesn't exist
|
||||
signedVAA: Uint8Array
|
||||
) {
|
||||
// TODO: this gets the target account off the vaa, but is there a way to do this via wasm?
|
||||
// also, would this always be safe to do?
|
||||
// should we rely on this function to create accounts at all?
|
||||
// const { parse_vaa } = await import("../solana/core/bridge")
|
||||
// const parsedVAA = parse_vaa(signedVAA);
|
||||
// const targetAddress = new PublicKey(parsedVAA.payload.slice(67, 67 + 32)).toString()
|
||||
const { parse_vaa } = await import("../solana/core/bridge");
|
||||
const parsedVAA = parse_vaa(signedVAA);
|
||||
const isSolanaNative =
|
||||
Buffer.from(new Uint8Array(parsedVAA.payload)).readUInt16BE(65) ===
|
||||
CHAIN_ID_SOLANA;
|
||||
const { complete_transfer_wrapped_ix, complete_transfer_native_ix } =
|
||||
await import("../solana/token/token_bridge");
|
||||
const ixs = [];
|
||||
|
@ -49,33 +42,6 @@ export async function redeemOnSolana(
|
|||
)
|
||||
);
|
||||
} else {
|
||||
// TODO: we should always do this, they could buy wrapped somewhere else and transfer it back for the first time, but again, do it based on vaa
|
||||
if (mintAddress) {
|
||||
const mintPublicKey = new PublicKey(mintAddress);
|
||||
// TODO: re: todo above, this should be swapped for the address from the vaa (may not be the same as the payer)
|
||||
const payerPublicKey = new PublicKey(payerAddress);
|
||||
const associatedAddress = await Token.getAssociatedTokenAddress(
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
TOKEN_PROGRAM_ID,
|
||||
mintPublicKey,
|
||||
payerPublicKey
|
||||
);
|
||||
const associatedAddressInfo = await connection.getAccountInfo(
|
||||
associatedAddress
|
||||
);
|
||||
if (!associatedAddressInfo) {
|
||||
ixs.push(
|
||||
await Token.createAssociatedTokenAccountInstruction(
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
TOKEN_PROGRAM_ID,
|
||||
mintPublicKey,
|
||||
associatedAddress,
|
||||
payerPublicKey, // owner
|
||||
payerPublicKey // payer
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
ixs.push(
|
||||
ixFromRust(
|
||||
complete_transfer_wrapped_ix(
|
||||
|
|
Loading…
Reference in New Issue