bridge_ui: separate attest workflow

Change-Id: If6270d7ce0deb02a48b63b81ba2ef688c5f4af29
This commit is contained in:
Evan Gray 2021-08-10 21:55:06 -04:00 committed by Hendrik Hofstadt
parent f53d180753
commit 1da690aa42
19 changed files with 708 additions and 69 deletions

View File

@ -1,4 +1,5 @@
import { AppBar, makeStyles, Toolbar } from "@material-ui/core"; import { AppBar, makeStyles, Toolbar } from "@material-ui/core";
import Attest from "./components/Attest";
import Transfer from "./components/Transfer"; import Transfer from "./components/Transfer";
import wormholeLogo from "./icons/wormhole.svg"; import wormholeLogo from "./icons/wormhole.svg";
@ -40,6 +41,7 @@ function App() {
</Toolbar> </Toolbar>
</AppBar> </AppBar>
<div className={classes.content}> <div className={classes.content}>
<Attest />
<Transfer /> <Transfer />
</div> </div>
</> </>

View File

@ -0,0 +1,70 @@
import { Button, CircularProgress, makeStyles } from "@material-ui/core";
import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useSolanaWallet } from "../../contexts/SolanaWalletContext";
import useAttestSignedVAA from "../../hooks/useAttestSignedVAA";
import { setIsCreating } from "../../store/attestSlice";
import {
selectAttestIsCreating,
selectAttestTargetChain,
} from "../../store/selectors";
import { CHAIN_ID_SOLANA } from "../../utils/consts";
import createWrappedOn, {
createWrappedOnSolana,
} from "../../utils/createWrappedOn";
const useStyles = makeStyles((theme) => ({
transferButton: {
marginTop: theme.spacing(2),
textTransform: "none",
width: "100%",
},
}));
function Create() {
const dispatch = useDispatch();
const classes = useStyles();
const targetChain = useSelector(selectAttestTargetChain);
const { wallet } = useSolanaWallet();
const solPK = wallet?.publicKey;
const signedVAA = useAttestSignedVAA();
const isCreating = useSelector(selectAttestIsCreating);
const handleCreateClick = useCallback(() => {
if (
targetChain === CHAIN_ID_SOLANA &&
createWrappedOn[targetChain] === createWrappedOnSolana &&
signedVAA
) {
dispatch(setIsCreating(true));
createWrappedOnSolana(wallet, solPK?.toString(), signedVAA);
}
}, [dispatch, targetChain, wallet, solPK, signedVAA]);
return (
<div style={{ position: "relative" }}>
<Button
color="primary"
variant="contained"
className={classes.transferButton}
disabled={isCreating}
onClick={handleCreateClick}
>
Create
</Button>
{isCreating ? (
<CircularProgress
size={24}
color="inherit"
style={{
position: "absolute",
bottom: 0,
left: "50%",
marginLeft: -12,
marginBottom: 6,
}}
/>
) : null}
</div>
);
}
export default Create;

View File

@ -0,0 +1,127 @@
import { Button, CircularProgress, makeStyles } from "@material-ui/core";
import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
import { useSolanaWallet } from "../../contexts/SolanaWalletContext";
import useWrappedAsset from "../../hooks/useWrappedAsset";
import { setIsSending, setSignedVAAHex } from "../../store/attestSlice";
import {
selectAttestIsSendComplete,
selectAttestIsSending,
selectAttestIsTargetComplete,
selectAttestSourceAsset,
selectAttestSourceChain,
selectAttestTargetChain,
} from "../../store/selectors";
import { uint8ArrayToHex } from "../../utils/array";
import attestFrom, {
attestFromEth,
attestFromSolana,
} from "../../utils/attestFrom";
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "../../utils/consts";
const useStyles = makeStyles((theme) => ({
transferButton: {
marginTop: theme.spacing(2),
textTransform: "none",
width: "100%",
},
}));
// TODO: move attest to its own workflow
function Send() {
const classes = useStyles();
const dispatch = useDispatch();
const sourceChain = useSelector(selectAttestSourceChain);
const sourceAsset = useSelector(selectAttestSourceAsset);
const targetChain = useSelector(selectAttestTargetChain);
const isTargetComplete = useSelector(selectAttestIsTargetComplete);
const isSending = useSelector(selectAttestIsSending);
const isSendComplete = useSelector(selectAttestIsSendComplete);
const { provider, signer } = useEthereumProvider();
const { wallet } = useSolanaWallet();
const solPK = wallet?.publicKey;
const {
isLoading: isCheckingWrapped,
// isWrapped,
wrappedAsset,
} = useWrappedAsset(targetChain, sourceChain, sourceAsset, provider);
// TODO: check this and send to separate flow
const isWrapped = true;
console.log(isCheckingWrapped, isWrapped, wrappedAsset);
// TODO: dynamically get "to" wallet
const handleAttestClick = useCallback(() => {
// TODO: more generic way of calling these
if (attestFrom[sourceChain]) {
if (
sourceChain === CHAIN_ID_ETH &&
attestFrom[sourceChain] === attestFromEth
) {
//TODO: just for testing, this should eventually use the store to communicate between steps
(async () => {
dispatch(setIsSending(true));
try {
const vaaBytes = await attestFromEth(provider, signer, sourceAsset);
console.log("bytes in attest", vaaBytes);
vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
} catch (e) {
console.error(e);
dispatch(setIsSending(false));
}
})();
}
if (
sourceChain === CHAIN_ID_SOLANA &&
attestFrom[sourceChain] === attestFromSolana
) {
//TODO: just for testing, this should eventually use the store to communicate between steps
(async () => {
dispatch(setIsSending(true));
try {
const vaaBytes = await attestFromSolana(
wallet,
solPK?.toString(),
sourceAsset
);
console.log("bytes in attest", vaaBytes);
vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
} catch (e) {
console.error(e);
dispatch(setIsSending(false));
}
})();
}
}
}, [dispatch, sourceChain, provider, signer, wallet, solPK, sourceAsset]);
return (
<>
<div style={{ position: "relative" }}>
<Button
color="primary"
variant="contained"
className={classes.transferButton}
onClick={handleAttestClick}
disabled={!isTargetComplete || isSending || isSendComplete}
>
Attest
</Button>
{isSending ? (
<CircularProgress
size={24}
color="inherit"
style={{
position: "absolute",
bottom: 0,
left: "50%",
marginLeft: -12,
marginBottom: 6,
}}
/>
) : null}
</div>
</>
);
}
export default Send;

View File

@ -0,0 +1,85 @@
import { Button, makeStyles, MenuItem, TextField } from "@material-ui/core";
import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
selectAttestIsSourceComplete,
selectAttestShouldLockFields,
selectAttestSourceAsset,
selectAttestSourceChain,
} from "../../store/selectors";
import {
incrementStep,
setSourceAsset,
setSourceChain,
} from "../../store/attestSlice";
import { CHAINS } from "../../utils/consts";
import KeyAndBalance from "../KeyAndBalance";
const useStyles = makeStyles((theme) => ({
transferField: {
marginTop: theme.spacing(5),
},
}));
function Source() {
const classes = useStyles();
const dispatch = useDispatch();
const sourceChain = useSelector(selectAttestSourceChain);
const sourceAsset = useSelector(selectAttestSourceAsset);
const isSourceComplete = useSelector(selectAttestIsSourceComplete);
const shouldLockFields = useSelector(selectAttestShouldLockFields);
const handleSourceChange = useCallback(
(event) => {
dispatch(setSourceChain(event.target.value));
},
[dispatch]
);
const handleAssetChange = useCallback(
(event) => {
dispatch(setSourceAsset(event.target.value));
},
[dispatch]
);
const handleNextClick = useCallback(
(event) => {
dispatch(incrementStep());
},
[dispatch]
);
return (
<>
<TextField
select
fullWidth
value={sourceChain}
onChange={handleSourceChange}
disabled={shouldLockFields}
>
{CHAINS.map(({ id, name }) => (
<MenuItem key={id} value={id}>
{name}
</MenuItem>
))}
</TextField>
<KeyAndBalance chainId={sourceChain} />
<TextField
placeholder="Asset"
fullWidth
className={classes.transferField}
value={sourceAsset}
onChange={handleAssetChange}
disabled={shouldLockFields}
/>
<Button
disabled={!isSourceComplete}
onClick={handleNextClick}
variant="contained"
color="primary"
>
Next
</Button>
</>
);
}
export default Source;

View File

@ -0,0 +1,65 @@
import { Button, MenuItem, TextField } from "@material-ui/core";
import { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
selectAttestIsTargetComplete,
selectAttestShouldLockFields,
selectAttestSourceChain,
selectAttestTargetChain,
} from "../../store/selectors";
import { incrementStep, setTargetChain } from "../../store/attestSlice";
import { CHAINS } from "../../utils/consts";
import KeyAndBalance from "../KeyAndBalance";
function Target() {
const dispatch = useDispatch();
const sourceChain = useSelector(selectAttestSourceChain);
const chains = useMemo(
() => CHAINS.filter((c) => c.id !== sourceChain),
[sourceChain]
);
const targetChain = useSelector(selectAttestTargetChain);
const isTargetComplete = useSelector(selectAttestIsTargetComplete);
const shouldLockFields = useSelector(selectAttestShouldLockFields);
const handleTargetChange = useCallback(
(event) => {
dispatch(setTargetChain(event.target.value));
},
[dispatch]
);
const handleNextClick = useCallback(
(event) => {
dispatch(incrementStep());
},
[dispatch]
);
return (
<>
<TextField
select
fullWidth
value={targetChain}
onChange={handleTargetChange}
disabled={shouldLockFields}
>
{chains.map(({ id, name }) => (
<MenuItem key={id} value={id}>
{name}
</MenuItem>
))}
</TextField>
{/* TODO: determine "to" token address */}
<KeyAndBalance chainId={targetChain} />
<Button
disabled={!isTargetComplete}
onClick={handleNextClick}
variant="contained"
color="primary"
>
Next
</Button>
</>
);
}
export default Target;

View File

@ -0,0 +1,70 @@
import {
Container,
Step,
StepButton,
StepContent,
Stepper,
} from "@material-ui/core";
import { useDispatch, useSelector } from "react-redux";
import useGetBalanceEffect from "../../hooks/useGetBalanceEffect";
import {
selectAttestActiveStep,
selectAttestSignedVAAHex,
} from "../../store/selectors";
import { setStep } from "../../store/attestSlice";
import Create from "./Create";
import Send from "./Send";
import Source from "./Source";
import Target from "./Target";
// TODO: ensure that both wallets are connected to the same known network
function Attest() {
useGetBalanceEffect();
const dispatch = useDispatch();
const activeStep = useSelector(selectAttestActiveStep);
const signedVAAHex = useSelector(selectAttestSignedVAAHex);
return (
<Container maxWidth="md">
<Stepper activeStep={activeStep} orientation="vertical">
<Step>
<StepButton onClick={() => dispatch(setStep(0))}>
Select a source
</StepButton>
<StepContent>
<Source />
</StepContent>
</Step>
<Step>
<StepButton onClick={() => dispatch(setStep(1))}>
Select a target
</StepButton>
<StepContent>
<Target />
</StepContent>
</Step>
<Step>
<StepButton onClick={() => dispatch(setStep(2))}>
Send attestation
</StepButton>
<StepContent>
<Send />
</StepContent>
</Step>
<Step>
<StepButton
onClick={() => dispatch(setStep(3))}
disabled={!signedVAAHex}
>
Create wrapper
</StepButton>
<StepContent>
<Create />
</StepContent>
</Step>
</Stepper>
</Container>
);
}
export default Attest;

View File

@ -3,7 +3,10 @@ import { useCallback } 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 useTransferSignedVAA from "../../hooks/useTransferSignedVAA"; import useTransferSignedVAA from "../../hooks/useTransferSignedVAA";
import { selectIsRedeeming, selectTargetChain } from "../../store/selectors"; import {
selectTransferIsRedeeming,
selectTransferTargetChain,
} from "../../store/selectors";
import { setIsRedeeming } from "../../store/transferSlice"; import { setIsRedeeming } from "../../store/transferSlice";
import { CHAIN_ID_ETH } from "../../utils/consts"; import { CHAIN_ID_ETH } from "../../utils/consts";
import redeemOn, { redeemOnEth } from "../../utils/redeemOn"; import redeemOn, { redeemOnEth } from "../../utils/redeemOn";
@ -19,10 +22,10 @@ const useStyles = makeStyles((theme) => ({
function Redeem() { function Redeem() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const classes = useStyles(); const classes = useStyles();
const targetChain = useSelector(selectTargetChain); const targetChain = useSelector(selectTransferTargetChain);
const { provider, signer } = useEthereumProvider(); const { provider, signer } = useEthereumProvider();
const signedVAA = useTransferSignedVAA(); const signedVAA = useTransferSignedVAA();
const isRedeeming = useSelector(selectIsRedeeming); const isRedeeming = useSelector(selectTransferIsRedeeming);
const handleRedeemClick = useCallback(() => { const handleRedeemClick = useCallback(() => {
if ( if (
targetChain === CHAIN_ID_ETH && targetChain === CHAIN_ID_ETH &&

View File

@ -5,14 +5,14 @@ import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
import { useSolanaWallet } from "../../contexts/SolanaWalletContext"; import { useSolanaWallet } from "../../contexts/SolanaWalletContext";
import useWrappedAsset from "../../hooks/useWrappedAsset"; import useWrappedAsset from "../../hooks/useWrappedAsset";
import { import {
selectAmount, selectTransferAmount,
selectIsSendComplete, selectTransferIsSendComplete,
selectIsSending, selectTransferIsSending,
selectIsTargetComplete, selectTransferIsTargetComplete,
selectSourceAsset, selectTransferSourceAsset,
selectSourceChain, selectTransferSourceChain,
selectSourceParsedTokenAccount, selectTransferSourceParsedTokenAccount,
selectTargetChain, selectTransferTargetChain,
} from "../../store/selectors"; } from "../../store/selectors";
import { setIsSending, setSignedVAAHex } from "../../store/transferSlice"; import { setIsSending, setSignedVAAHex } from "../../store/transferSlice";
import { uint8ArrayToHex } from "../../utils/array"; import { uint8ArrayToHex } from "../../utils/array";
@ -35,17 +35,19 @@ const useStyles = makeStyles((theme) => ({
function Send() { function Send() {
const classes = useStyles(); const classes = useStyles();
const dispatch = useDispatch(); const dispatch = useDispatch();
const sourceChain = useSelector(selectSourceChain); const sourceChain = useSelector(selectTransferSourceChain);
const sourceAsset = useSelector(selectSourceAsset); const sourceAsset = useSelector(selectTransferSourceAsset);
const amount = useSelector(selectAmount); const amount = useSelector(selectTransferAmount);
const targetChain = useSelector(selectTargetChain); const targetChain = useSelector(selectTransferTargetChain);
const isTargetComplete = useSelector(selectIsTargetComplete); const isTargetComplete = useSelector(selectTransferIsTargetComplete);
const isSending = useSelector(selectIsSending); const isSending = useSelector(selectTransferIsSending);
const isSendComplete = useSelector(selectIsSendComplete); const isSendComplete = useSelector(selectTransferIsSendComplete);
const { provider, signer, signerAddress } = useEthereumProvider(); const { provider, signer, signerAddress } = useEthereumProvider();
const { wallet } = useSolanaWallet(); const { wallet } = useSolanaWallet();
const solPK = wallet?.publicKey; const solPK = wallet?.publicKey;
const sourceParsedTokenAccount = useSelector(selectSourceParsedTokenAccount); const sourceParsedTokenAccount = useSelector(
selectTransferSourceParsedTokenAccount
);
const tokenPK = sourceParsedTokenAccount?.publicKey; const tokenPK = sourceParsedTokenAccount?.publicKey;
const decimals = sourceParsedTokenAccount?.decimals; const decimals = sourceParsedTokenAccount?.decimals;
const { const {

View File

@ -2,12 +2,12 @@ import { Button, makeStyles, MenuItem, TextField } from "@material-ui/core";
import { useCallback } from "react"; import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { import {
selectAmount, selectTransferAmount,
selectIsSourceComplete, selectTransferIsSourceComplete,
selectShouldLockFields, selectTransferShouldLockFields,
selectSourceAsset, selectTransferSourceAsset,
selectSourceBalanceString, selectTransferSourceBalanceString,
selectSourceChain, selectTransferSourceChain,
} from "../../store/selectors"; } from "../../store/selectors";
import { import {
incrementStep, incrementStep,
@ -27,12 +27,12 @@ const useStyles = makeStyles((theme) => ({
function Source() { function Source() {
const classes = useStyles(); const classes = useStyles();
const dispatch = useDispatch(); const dispatch = useDispatch();
const sourceChain = useSelector(selectSourceChain); const sourceChain = useSelector(selectTransferSourceChain);
const sourceAsset = useSelector(selectSourceAsset); const sourceAsset = useSelector(selectTransferSourceAsset);
const uiAmountString = useSelector(selectSourceBalanceString); const uiAmountString = useSelector(selectTransferSourceBalanceString);
const amount = useSelector(selectAmount); const amount = useSelector(selectTransferAmount);
const isSourceComplete = useSelector(selectIsSourceComplete); const isSourceComplete = useSelector(selectTransferIsSourceComplete);
const shouldLockFields = useSelector(selectShouldLockFields); const shouldLockFields = useSelector(selectTransferShouldLockFields);
const handleSourceChange = useCallback( const handleSourceChange = useCallback(
(event) => { (event) => {
dispatch(setSourceChain(event.target.value)); dispatch(setSourceChain(event.target.value));

View File

@ -2,10 +2,10 @@ import { Button, MenuItem, TextField } from "@material-ui/core";
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { import {
selectIsTargetComplete, selectTransferIsTargetComplete,
selectShouldLockFields, selectTransferShouldLockFields,
selectSourceChain, selectTransferSourceChain,
selectTargetChain, selectTransferTargetChain,
} from "../../store/selectors"; } from "../../store/selectors";
import { incrementStep, setTargetChain } from "../../store/transferSlice"; import { incrementStep, setTargetChain } from "../../store/transferSlice";
import { CHAINS } from "../../utils/consts"; import { CHAINS } from "../../utils/consts";
@ -13,14 +13,14 @@ import KeyAndBalance from "../KeyAndBalance";
function Target() { function Target() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const sourceChain = useSelector(selectSourceChain); const sourceChain = useSelector(selectTransferSourceChain);
const chains = useMemo( const chains = useMemo(
() => CHAINS.filter((c) => c.id !== sourceChain), () => CHAINS.filter((c) => c.id !== sourceChain),
[sourceChain] [sourceChain]
); );
const targetChain = useSelector(selectTargetChain); const targetChain = useSelector(selectTransferTargetChain);
const isTargetComplete = useSelector(selectIsTargetComplete); const isTargetComplete = useSelector(selectTransferIsTargetComplete);
const shouldLockFields = useSelector(selectShouldLockFields); const shouldLockFields = useSelector(selectTransferShouldLockFields);
const handleTargetChange = useCallback( const handleTargetChange = useCallback(
(event) => { (event) => {
dispatch(setTargetChain(event.target.value)); dispatch(setTargetChain(event.target.value));

View File

@ -7,7 +7,10 @@ import {
} from "@material-ui/core"; } from "@material-ui/core";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import useGetBalanceEffect from "../../hooks/useGetBalanceEffect"; import useGetBalanceEffect from "../../hooks/useGetBalanceEffect";
import { selectActiveStep, selectSignedVAAHex } from "../../store/selectors"; import {
selectTransferActiveStep,
selectTransferSignedVAAHex,
} from "../../store/selectors";
import { setStep } from "../../store/transferSlice"; import { setStep } from "../../store/transferSlice";
import Redeem from "./Redeem"; import Redeem from "./Redeem";
import Send from "./Send"; import Send from "./Send";
@ -22,8 +25,8 @@ import Target from "./Target";
function Transfer() { function Transfer() {
useGetBalanceEffect(); useGetBalanceEffect();
const dispatch = useDispatch(); const dispatch = useDispatch();
const activeStep = useSelector(selectActiveStep); const activeStep = useSelector(selectTransferActiveStep);
const signedVAAHex = useSelector(selectSignedVAAHex); const signedVAAHex = useSelector(selectTransferSignedVAAHex);
return ( return (
<Container maxWidth="md"> <Container maxWidth="md">
<Stepper activeStep={activeStep} orientation="vertical"> <Stepper activeStep={activeStep} orientation="vertical">

View File

@ -0,0 +1,13 @@
import { useMemo } from "react";
import { useSelector } from "react-redux";
import { selectAttestSignedVAAHex } from "../store/selectors";
import { hexToUint8Array } from "../utils/array";
export default function useAttestSignedVAA() {
const signedVAAHex = useSelector(selectAttestSignedVAAHex);
const signedVAA = useMemo(
() => (signedVAAHex ? hexToUint8Array(signedVAAHex) : undefined),
[signedVAAHex]
);
return signedVAA;
}

View File

@ -5,7 +5,10 @@ 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 { TokenImplementation__factory } from "../ethers-contracts"; import { TokenImplementation__factory } from "../ethers-contracts";
import { selectSourceAsset, selectSourceChain } from "../store/selectors"; import {
selectTransferSourceAsset,
selectTransferSourceChain,
} from "../store/selectors";
import { setSourceParsedTokenAccount } from "../store/transferSlice"; import { setSourceParsedTokenAccount } from "../store/transferSlice";
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA, SOLANA_HOST } from "../utils/consts"; import { CHAIN_ID_ETH, CHAIN_ID_SOLANA, SOLANA_HOST } from "../utils/consts";
@ -27,8 +30,8 @@ function createParsedTokenAccount(
function useGetBalanceEffect() { function useGetBalanceEffect() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const sourceChain = useSelector(selectSourceChain); const sourceChain = useSelector(selectTransferSourceChain);
const sourceAsset = useSelector(selectSourceAsset); const sourceAsset = useSelector(selectTransferSourceAsset);
const { wallet } = useSolanaWallet(); const { wallet } = useSolanaWallet();
const solPK = wallet?.publicKey; const solPK = wallet?.publicKey;
const { provider, signerAddress } = useEthereumProvider(); const { provider, signerAddress } = useEthereumProvider();

View File

@ -1,10 +1,10 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { selectSignedVAAHex } from "../store/selectors"; import { selectTransferSignedVAAHex } from "../store/selectors";
import { hexToUint8Array } from "../utils/array"; import { hexToUint8Array } from "../utils/array";
export default function useTransferSignedVAA() { export default function useTransferSignedVAA() {
const signedVAAHex = useSelector(selectSignedVAAHex); const signedVAAHex = useSelector(selectTransferSignedVAAHex);
const signedVAA = useMemo( const signedVAA = useMemo(
() => (signedVAAHex ? hexToUint8Array(signedVAAHex) : undefined), () => (signedVAAHex ? hexToUint8Array(signedVAAHex) : undefined),
[signedVAAHex] [signedVAAHex]

View File

@ -0,0 +1,105 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import {
ChainId,
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
ETH_TEST_TOKEN_ADDRESS,
SOL_TEST_TOKEN_ADDRESS,
} from "../utils/consts";
const LAST_STEP = 3;
type Steps = 0 | 1 | 2 | 3;
export interface AttestState {
activeStep: Steps;
sourceChain: ChainId;
sourceAsset: string;
targetChain: ChainId;
signedVAAHex: string | undefined;
isSending: boolean;
isCreating: boolean;
}
const initialState: AttestState = {
activeStep: 0,
sourceChain: CHAIN_ID_SOLANA,
sourceAsset: SOL_TEST_TOKEN_ADDRESS,
targetChain: CHAIN_ID_ETH,
signedVAAHex: undefined,
isSending: false,
isCreating: false,
};
export const attestSlice = createSlice({
name: "attest",
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;
// TODO: remove or check env - for testing purposes
if (action.payload === CHAIN_ID_ETH) {
state.sourceAsset = ETH_TEST_TOKEN_ADDRESS;
}
if (action.payload === CHAIN_ID_SOLANA) {
state.sourceAsset = SOL_TEST_TOKEN_ADDRESS;
}
if (state.targetChain === action.payload) {
state.targetChain = prevSourceChain;
}
},
setSourceAsset: (state, action: PayloadAction<string>) => {
state.sourceAsset = action.payload;
},
setTargetChain: (state, action: PayloadAction<ChainId>) => {
const prevTargetChain = state.targetChain;
state.targetChain = action.payload;
if (state.sourceChain === action.payload) {
state.sourceChain = prevTargetChain;
state.activeStep = 0;
// TODO: remove or check env - for testing purposes
if (state.targetChain === CHAIN_ID_ETH) {
state.sourceAsset = ETH_TEST_TOKEN_ADDRESS;
}
if (state.targetChain === CHAIN_ID_SOLANA) {
state.sourceAsset = SOL_TEST_TOKEN_ADDRESS;
}
}
},
setSignedVAAHex: (state, action: PayloadAction<string>) => {
state.signedVAAHex = action.payload;
state.isSending = false;
state.activeStep = 3;
},
setIsSending: (state, action: PayloadAction<boolean>) => {
state.isSending = action.payload;
},
setIsCreating: (state, action: PayloadAction<boolean>) => {
state.isCreating = action.payload;
},
},
});
export const {
incrementStep,
decrementStep,
setStep,
setSourceChain,
setSourceAsset,
setTargetChain,
setSignedVAAHex,
setIsSending,
setIsCreating,
} = attestSlice.actions;
export default attestSlice.reducer;

View File

@ -1,8 +1,10 @@
import { configureStore } from "@reduxjs/toolkit"; import { configureStore } from "@reduxjs/toolkit";
import attestReducer from "./attestSlice";
import transferReducer from "./transferSlice"; import transferReducer from "./transferSlice";
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
attest: attestReducer,
transfer: transferReducer, transfer: transferReducer,
}, },
}); });

View File

@ -2,27 +2,64 @@ import { parseUnits } from "ethers/lib/utils";
import { RootState } from "."; import { RootState } from ".";
import { CHAIN_ID_SOLANA } from "../utils/consts"; import { CHAIN_ID_SOLANA } from "../utils/consts";
export const selectActiveStep = (state: RootState) => state.transfer.activeStep; /*
export const selectSourceChain = (state: RootState) => * Attest
*/
export const selectAttestActiveStep = (state: RootState) =>
state.attest.activeStep;
export const selectAttestSourceChain = (state: RootState) =>
state.attest.sourceChain;
export const selectAttestSourceAsset = (state: RootState) =>
state.attest.sourceAsset;
export const selectAttestTargetChain = (state: RootState) =>
state.attest.targetChain;
export const selectAttestSignedVAAHex = (state: RootState) =>
state.attest.signedVAAHex;
export const selectAttestIsSending = (state: RootState) =>
state.attest.isSending;
export const selectAttestIsCreating = (state: RootState) =>
state.attest.isCreating;
// safety checks
// TODO: could make this return a string with a user informative message
export const selectAttestIsSourceComplete = (state: RootState) =>
!!state.attest.sourceChain && !!state.attest.sourceAsset;
// TODO: check wrapped asset exists or is native attest
export const selectAttestIsTargetComplete = (state: RootState) =>
selectAttestIsSourceComplete(state) && !!state.attest.targetChain;
export const selectAttestIsSendComplete = (state: RootState) =>
!!selectAttestSignedVAAHex(state);
export const selectAttestShouldLockFields = (state: RootState) =>
selectAttestIsSending(state) || selectAttestIsSendComplete(state);
/*
* Transfer
*/
export const selectTransferActiveStep = (state: RootState) =>
state.transfer.activeStep;
export const selectTransferSourceChain = (state: RootState) =>
state.transfer.sourceChain; state.transfer.sourceChain;
export const selectSourceAsset = (state: RootState) => export const selectTransferSourceAsset = (state: RootState) =>
state.transfer.sourceAsset; state.transfer.sourceAsset;
export const selectSourceParsedTokenAccount = (state: RootState) => export const selectTransferSourceParsedTokenAccount = (state: RootState) =>
state.transfer.sourceParsedTokenAccount; state.transfer.sourceParsedTokenAccount;
export const selectSourceBalanceString = (state: RootState) => export const selectTransferSourceBalanceString = (state: RootState) =>
state.transfer.sourceParsedTokenAccount?.uiAmountString || ""; state.transfer.sourceParsedTokenAccount?.uiAmountString || "";
export const selectAmount = (state: RootState) => state.transfer.amount; export const selectTransferAmount = (state: RootState) => state.transfer.amount;
export const selectTargetChain = (state: RootState) => export const selectTransferTargetChain = (state: RootState) =>
state.transfer.targetChain; state.transfer.targetChain;
export const selectSignedVAAHex = (state: RootState) => export const selectTransferSignedVAAHex = (state: RootState) =>
state.transfer.signedVAAHex; state.transfer.signedVAAHex;
export const selectIsSending = (state: RootState) => state.transfer.isSending; export const selectTransferIsSending = (state: RootState) =>
export const selectIsRedeeming = (state: RootState) => state.transfer.isSending;
export const selectTransferIsRedeeming = (state: RootState) =>
state.transfer.isRedeeming; state.transfer.isRedeeming;
// safety checks // safety checks
// TODO: could make this return a string with a user informative message // TODO: could make this return a string with a user informative message
export const selectIsSourceComplete = (state: RootState) => export const selectTransferIsSourceComplete = (state: RootState) =>
!!state.transfer.sourceChain && !!state.transfer.sourceChain &&
!!state.transfer.sourceAsset && !!state.transfer.sourceAsset &&
!!state.transfer.sourceParsedTokenAccount && !!state.transfer.sourceParsedTokenAccount &&
@ -41,9 +78,9 @@ export const selectIsSourceComplete = (state: RootState) =>
) )
); );
// TODO: check wrapped asset exists or is native transfer // TODO: check wrapped asset exists or is native transfer
export const selectIsTargetComplete = (state: RootState) => export const selectTransferIsTargetComplete = (state: RootState) =>
selectIsSourceComplete(state) && !!state.transfer.targetChain; selectTransferIsSourceComplete(state) && !!state.transfer.targetChain;
export const selectIsSendComplete = (state: RootState) => export const selectTransferIsSendComplete = (state: RootState) =>
!!selectSignedVAAHex(state); !!selectTransferSignedVAAHex(state);
export const selectShouldLockFields = (state: RootState) => export const selectTransferShouldLockFields = (state: RootState) =>
selectIsSending(state) || selectIsSendComplete(state); selectTransferIsSending(state) || selectTransferIsSendComplete(state);

View File

@ -1,6 +1,7 @@
import Wallet from "@project-serum/sol-wallet-adapter"; import Wallet from "@project-serum/sol-wallet-adapter";
import { import {
Connection, Keypair, Connection,
Keypair,
PublicKey, PublicKey,
SystemProgram, SystemProgram,
Transaction, Transaction,
@ -66,8 +67,7 @@ export async function attestFromEth(
export async function attestFromSolana( export async function attestFromSolana(
wallet: Wallet | undefined, wallet: Wallet | undefined,
payerAddress: string | undefined, //TODO: we may not need this since we have wallet payerAddress: string | undefined, //TODO: we may not need this since we have wallet
mintAddress: string, mintAddress: string
decimals: number
) { ) {
if (!wallet || !wallet.publicKey || !payerAddress) return; if (!wallet || !wallet.publicKey || !payerAddress) return;
const nonceConst = Math.random() * 100000; const nonceConst = Math.random() * 100000;
@ -106,8 +106,8 @@ export async function attestFromSolana(
SOL_BRIDGE_ADDRESS, SOL_BRIDGE_ADDRESS,
payerAddress, payerAddress,
messageKey.publicKey.toString(), messageKey.publicKey.toString(),
mintAddress, // TODO: mint_metadata: what address is this supposed to be? mintAddress,
nonce, nonce
) )
); );
const transaction = new Transaction().add(transferIx, ix); const transaction = new Transaction().add(transferIx, ix);

View File

@ -0,0 +1,52 @@
import Wallet from "@project-serum/sol-wallet-adapter";
import { Connection, PublicKey, Transaction } from "@solana/web3.js";
import { ixFromRust } from "../sdk";
import {
CHAIN_ID_SOLANA,
SOLANA_HOST,
SOL_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
} from "./consts";
export async function createWrappedOnSolana(
wallet: Wallet | undefined,
payerAddress: string | undefined, //TODO: we may not need this since we have wallet
signedVAA: Uint8Array
) {
if (!wallet || !wallet.publicKey || !payerAddress) return;
console.log("creating wrapped");
console.log("PROGRAM:", SOL_TOKEN_BRIDGE_ADDRESS);
console.log("BRIDGE:", SOL_BRIDGE_ADDRESS);
console.log("PAYER:", payerAddress);
console.log("VAA:", signedVAA);
// TODO: share connection in context?
const connection = new Connection(SOLANA_HOST, "confirmed");
const { create_wrapped_ix } = await import("token-bridge");
const ix = ixFromRust(
create_wrapped_ix(
SOL_TOKEN_BRIDGE_ADDRESS,
SOL_BRIDGE_ADDRESS,
payerAddress,
signedVAA
)
);
const transaction = new Transaction().add(ix);
const { blockhash } = await connection.getRecentBlockhash();
transaction.recentBlockhash = blockhash;
transaction.feePayer = new PublicKey(payerAddress);
// Sign transaction, broadcast, and confirm
const signed = await wallet.signTransaction(transaction);
console.log("SIGNED", signed);
const txid = await connection.sendRawTransaction(signed.serialize());
console.log("SENT", txid);
const conf = await connection.confirmTransaction(txid);
console.log("CONFIRMED", conf);
const info = await connection.getTransaction(txid);
console.log("INFO", info);
}
const createWrappedOn = {
[CHAIN_ID_SOLANA]: createWrappedOnSolana,
};
export default createWrappedOn;