From fbf5ad09513752e057b0f05889e0207e29e0352a Mon Sep 17 00:00:00 2001 From: Gary Wang Date: Tue, 4 Aug 2020 10:23:13 -0700 Subject: [PATCH] WIP dapp integration --- src/App.js | 19 ++- src/PopupPage.js | 201 ++++++++++++++++++++++++++++++ src/components/NavigationFrame.js | 2 + 3 files changed, 218 insertions(+), 4 deletions(-) create mode 100644 src/PopupPage.js diff --git a/src/App.js b/src/App.js index 4acc2b9..e12b19f 100644 --- a/src/App.js +++ b/src/App.js @@ -2,8 +2,8 @@ import React, { Suspense } from 'react'; import CssBaseline from '@material-ui/core/CssBaseline'; import useMediaQuery from '@material-ui/core/useMediaQuery'; import { - unstable_createMuiStrictModeTheme as createMuiTheme, ThemeProvider, + unstable_createMuiStrictModeTheme as createMuiTheme, } from '@material-ui/core/styles'; import blue from '@material-ui/core/colors/blue'; import NavigationFrame from './components/NavigationFrame'; @@ -12,8 +12,9 @@ import WalletPage from './WalletPage'; import { WalletProvider } from './utils/wallet'; import LoadingIndicator from './components/LoadingIndicator'; import { SnackbarProvider } from 'notistack'; +import PopupPage from './PopupPage'; -function App() { +export default function App() { // TODO: add toggle for dark mode const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); const theme = React.useMemo( @@ -27,6 +28,11 @@ function App() { [prefersDarkMode], ); + // Disallow rendering inside an iframe to prevent clickjacking. + if (window.self !== window.top) { + return null; + } + return ( }> @@ -36,7 +42,7 @@ function App() { }> - + @@ -47,4 +53,9 @@ function App() { ); } -export default App; +function PageContents() { + if (window.opener) { + return ; + } + return ; +} diff --git a/src/PopupPage.js b/src/PopupPage.js new file mode 100644 index 0000000..8a09988 --- /dev/null +++ b/src/PopupPage.js @@ -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 ; + } + + 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 ( + + ); + } + + return ( + Please keep this window open in the background. + ); +} + +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 ( + + + + Allow this site to access your Solana account? + +
+ {origin} + + {wallet.account.publicKey.toBase58()} +
+ Only connect with sites you trust. +
+ + + + +
+ ); +} + +function ApproveSignatureForm({ origin, message, onApprove, onReject }) { + const classes = useStyles(); + + // TODO: decode message + + return ( + + + + {origin} would like to send the following transaction: + + + {bs58.encode(message)} + + + + + + + + ); +} diff --git a/src/components/NavigationFrame.js b/src/components/NavigationFrame.js index fcbcb8a..d8a6bf0 100644 --- a/src/components/NavigationFrame.js +++ b/src/components/NavigationFrame.js @@ -22,6 +22,8 @@ const useStyles = makeStyles((theme) => ({ content: { paddingTop: theme.spacing(3), paddingBottom: theme.spacing(3), + paddingLeft: theme.spacing(1), + paddingRight: theme.spacing(1), }, title: { flexGrow: 1,