WIP dapp integration

This commit is contained in:
Gary Wang 2020-08-04 10:23:13 -07:00
parent fc296cf59e
commit fbf5ad0951
3 changed files with 218 additions and 4 deletions

View File

@ -2,8 +2,8 @@ import React, { Suspense } from 'react';
import CssBaseline from '@material-ui/core/CssBaseline'; import CssBaseline from '@material-ui/core/CssBaseline';
import useMediaQuery from '@material-ui/core/useMediaQuery'; import useMediaQuery from '@material-ui/core/useMediaQuery';
import { import {
unstable_createMuiStrictModeTheme as createMuiTheme,
ThemeProvider, ThemeProvider,
unstable_createMuiStrictModeTheme as createMuiTheme,
} from '@material-ui/core/styles'; } from '@material-ui/core/styles';
import blue from '@material-ui/core/colors/blue'; import blue from '@material-ui/core/colors/blue';
import NavigationFrame from './components/NavigationFrame'; import NavigationFrame from './components/NavigationFrame';
@ -12,8 +12,9 @@ import WalletPage from './WalletPage';
import { WalletProvider } from './utils/wallet'; import { WalletProvider } from './utils/wallet';
import LoadingIndicator from './components/LoadingIndicator'; import LoadingIndicator from './components/LoadingIndicator';
import { SnackbarProvider } from 'notistack'; import { SnackbarProvider } from 'notistack';
import PopupPage from './PopupPage';
function App() { export default function App() {
// TODO: add toggle for dark mode // TODO: add toggle for dark mode
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
const theme = React.useMemo( const theme = React.useMemo(
@ -27,6 +28,11 @@ function App() {
[prefersDarkMode], [prefersDarkMode],
); );
// Disallow rendering inside an iframe to prevent clickjacking.
if (window.self !== window.top) {
return null;
}
return ( return (
<Suspense fallback={<LoadingIndicator />}> <Suspense fallback={<LoadingIndicator />}>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
@ -36,7 +42,7 @@ function App() {
<SnackbarProvider maxSnack={5} autoHideDuration={8000}> <SnackbarProvider maxSnack={5} autoHideDuration={8000}>
<NavigationFrame> <NavigationFrame>
<Suspense fallback={<LoadingIndicator />}> <Suspense fallback={<LoadingIndicator />}>
<WalletPage /> <PageContents />
</Suspense> </Suspense>
</NavigationFrame> </NavigationFrame>
</SnackbarProvider> </SnackbarProvider>
@ -47,4 +53,9 @@ function App() {
); );
} }
export default App; function PageContents() {
if (window.opener) {
return <PopupPage opener={window.opener} />;
}
return <WalletPage />;
}

201
src/PopupPage.js Normal file
View File

@ -0,0 +1,201 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useWallet } from './utils/wallet';
import { Typography } from '@material-ui/core';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import CardActions from '@material-ui/core/CardActions';
import Button from '@material-ui/core/Button';
import ImportExportIcon from '@material-ui/icons/ImportExport';
import { makeStyles } from '@material-ui/core/styles';
import assert from 'assert';
import bs58 from 'bs58';
import nacl from 'tweetnacl';
export default function PopupPage({ opener }) {
const wallet = useWallet();
const origin = useMemo(() => {
let params = new URLSearchParams(window.location.hash.slice(1));
return params.get('origin');
}, []);
const postMessage = useCallback(
(message) => {
opener.postMessage({ jsonrpc: '2.0', ...message }, origin);
},
[opener, origin],
);
const [connectedAccount, setConnectedAccount] = useState(null);
const hasConnectedAccount = !!connectedAccount;
const [requests, setRequests] = useState([]);
// Send a disconnect event if this window is closed, this component is
// unmounted, or setConnectedAccount(null) is called.
useEffect(() => {
if (hasConnectedAccount) {
function unloadHandler() {
postMessage({ method: 'disconnected' });
}
window.addEventListener('beforeunload', unloadHandler);
return () => {
unloadHandler();
window.removeEventListener('beforeunload', unloadHandler);
};
}
}, [hasConnectedAccount, postMessage]);
// Disconnect if the user switches to a different wallet.
useEffect(() => {
if (
connectedAccount &&
!connectedAccount.publicKey.equals(wallet.account.publicKey)
) {
setConnectedAccount(null);
}
}, [connectedAccount, wallet]);
// Push requests from the parent window into a queue.
useEffect(() => {
function messageHandler(e) {
if (e.origin === origin && e.source === window.opener) {
if (e.data.method !== 'signTransaction') {
postMessage({ error: 'Unsupported method', id: e.data.id });
}
setRequests((requests) => [...requests, e.data]);
}
}
window.addEventListener('message', messageHandler);
return () => window.removeEventListener('message', messageHandler);
}, [origin, postMessage]);
// Switch focus to the parent window. This requires that the parent
// runs `window.name = 'parent'` before opening the popup.
function focusParent() {
window.open('', 'parent');
}
if (
!connectedAccount ||
!connectedAccount.publicKey.equals(wallet.account.publicKey)
) {
// Approve the parent page to connect to this wallet.
function connect() {
setConnectedAccount(wallet.account);
postMessage({
method: 'connected',
params: { publicKey: wallet.account.publicKey.toBase58() },
});
focusParent();
}
return <ApproveConnectionForm origin={origin} onApprove={connect} />;
}
if (requests.length > 0) {
const request = requests[0];
assert(request.method === 'signTransaction');
const message = bs58.decode(request.params.message);
function sendSignature() {
setRequests((requests) => requests.slice(1));
postMessage({
result: {
signature: bs58.encode(
nacl.sign.detached(message, wallet.account.secretKey),
),
publicKey: wallet.account.publicKey.toBase58(),
},
id: request.id,
});
if (requests.length === 1) {
focusParent();
}
}
function sendReject() {
setRequests((requests) => requests.slice(1));
postMessage({
error: 'Transaction cancelled',
id: request.id,
});
if (requests.length === 1) {
focusParent();
}
}
return (
<ApproveSignatureForm
origin={origin}
message={message}
onApprove={sendSignature}
onReject={sendReject}
/>
);
}
return (
<Typography>Please keep this window open in the background.</Typography>
);
}
const useStyles = makeStyles((theme) => ({
connection: {
marginTop: theme.spacing(3),
marginBottom: theme.spacing(3),
textAlign: 'center',
},
transaction: {
wordBreak: 'break-all',
},
actions: {
justifyContent: 'space-between',
},
}));
function ApproveConnectionForm({ origin, onApprove }) {
const wallet = useWallet();
const classes = useStyles();
return (
<Card>
<CardContent>
<Typography variant="h6" component="h1" gutterBottom>
Allow this site to access your Solana account?
</Typography>
<div className={classes.connection}>
<Typography>{origin}</Typography>
<ImportExportIcon fontSize="large" />
<Typography>{wallet.account.publicKey.toBase58()}</Typography>
</div>
<Typography>Only connect with sites you trust.</Typography>
</CardContent>
<CardActions className={classes.actions}>
<Button onClick={window.close}>Cancel</Button>
<Button color="primary" onClick={onApprove}>
Connect
</Button>
</CardActions>
</Card>
);
}
function ApproveSignatureForm({ origin, message, onApprove, onReject }) {
const classes = useStyles();
// TODO: decode message
return (
<Card>
<CardContent>
<Typography variant="h6" component="h1" gutterBottom>
{origin} would like to send the following transaction:
</Typography>
<Typography className={classes.transaction}>
{bs58.encode(message)}
</Typography>
</CardContent>
<CardActions className={classes.actions}>
<Button onClick={onReject}>Cancel</Button>
<Button color="primary" onClick={onApprove}>
Approve
</Button>
</CardActions>
</Card>
);
}

View File

@ -22,6 +22,8 @@ const useStyles = makeStyles((theme) => ({
content: { content: {
paddingTop: theme.spacing(3), paddingTop: theme.spacing(3),
paddingBottom: theme.spacing(3), paddingBottom: theme.spacing(3),
paddingLeft: theme.spacing(1),
paddingRight: theme.spacing(1),
}, },
title: { title: {
flexGrow: 1, flexGrow: 1,