From 754dd375955a27460e46ff01059c4d08252cfba0 Mon Sep 17 00:00:00 2001 From: jhl-alameda <77697590+jhl-alameda@users.noreply.github.com> Date: Mon, 15 Mar 2021 07:27:11 +0800 Subject: [PATCH] Sollet Chrome Extension v0 (#127) * init * ui changes * injection * upd * adding connections page * upd extension * merge * prettier * fix * fix * fix --- .gitignore | 3 + extension/src/background.js | 70 +++++ extension/src/contentscript.js | 24 ++ extension/src/manifest.json | 35 +++ extension/src/script.js | 15 + package.json | 11 +- src/App.js | 37 ++- src/components/BalancesList.js | 179 +++++++----- src/components/ConnectionIcon.js | 11 + src/components/ConnectionsList.js | 153 ++++++++++ src/components/DebugButtons.js | 5 +- src/components/NavigationFrame.js | 131 ++++++++- src/components/instructions/LabelValue.js | 8 +- src/components/instructions/NewOrder.js | 2 +- .../instructions/UnknownInstruction.js | 13 +- src/index.css | 28 ++ src/pages/ConnectionsPage.js | 31 ++ src/pages/PopupPage.js | 273 +++++++++++++----- src/pages/WalletPage.js | 26 +- src/utils/connected-wallets.js | 31 ++ src/utils/page.js | 15 + src/utils/swap/eth.js | 5 +- src/utils/tokens/instructions.js | 1 - src/utils/transactions.js | 12 +- src/utils/utils.ts | 9 + src/utils/wallet-seed.js | 8 +- 26 files changed, 944 insertions(+), 192 deletions(-) create mode 100644 extension/src/background.js create mode 100644 extension/src/contentscript.js create mode 100644 extension/src/manifest.json create mode 100644 extension/src/script.js create mode 100644 src/components/ConnectionIcon.js create mode 100644 src/components/ConnectionsList.js create mode 100644 src/pages/ConnectionsPage.js create mode 100644 src/utils/connected-wallets.js create mode 100644 src/utils/page.js diff --git a/.gitignore b/.gitignore index 5634d36c..d06306fe 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ yarn-error.log* .idea .eslintcache + +# generate with `build:extension` script +extension/build/* \ No newline at end of file diff --git a/extension/src/background.js b/extension/src/background.js new file mode 100644 index 00000000..e8a9d38e --- /dev/null +++ b/extension/src/background.js @@ -0,0 +1,70 @@ +const responseHandlers = new Map(); + +function launchPopup(message, sender, sendResponse) { + const searchParams = new URLSearchParams(); + searchParams.set('origin', sender.origin); + searchParams.set('network', message.data.params.network); + searchParams.set('request', JSON.stringify(message.data)); + + // TODO consolidate popup dimensions + chrome.windows.getLastFocused((focusedWindow) => { + chrome.windows.create({ + url: 'index.html/#' + searchParams.toString(), + type: 'popup', + width: 375, + height: 600, + top: focusedWindow.top, + left: focusedWindow.left + (focusedWindow.width - 375), + setSelfAsOpener: true, + focused: true, + }); + }); + + responseHandlers.set(message.data.id, sendResponse); +} + +function handleConnect(message, sender, sendResponse) { + chrome.storage.local.get('connectedWallets', (result) => { + const connectedWallet = (result.connectedWallets || {})[sender.origin]; + if (!connectedWallet) { + launchPopup(message, sender, sendResponse); + } else { + sendResponse({ + method: 'connected', + params: { + publicKey: connectedWallet.publicKey, + autoApprove: connectedWallet.autoApprove, + }, + id: message.data.id, + }); + } + }); +} + +function handleDisconnect(message, sender, sendResponse) { + chrome.storage.local.get('connectedWallets', (result) => { + delete result.connectedWallets[sender.origin]; + chrome.storage.local.set( + { connectedWallets: result.connectedWallets }, + () => sendResponse({ method: 'disconnected', id: message.data.id }), + ); + }); +} + +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.channel === 'sollet_contentscript_background_channel') { + if (message.data.method === 'connect') { + handleConnect(message, sender, sendResponse); + } else if (message.data.method === 'disconnect') { + handleDisconnect(message, sender, sendResponse); + } else { + launchPopup(message, sender, sendResponse); + } + // keeps response channel open + return true; + } else if (message.channel === 'sollet_extension_background_channel') { + const responseHandler = responseHandlers.get(message.data.id); + responseHandlers.delete(message.data.id); + responseHandler(message.data); + } +}); diff --git a/extension/src/contentscript.js b/extension/src/contentscript.js new file mode 100644 index 00000000..e7c8c769 --- /dev/null +++ b/extension/src/contentscript.js @@ -0,0 +1,24 @@ +const container = document.head || document.documentElement; +const scriptTag = document.createElement('script'); +scriptTag.setAttribute('async', 'false'); +scriptTag.src = chrome.runtime.getURL('script.js'); +container.insertBefore(scriptTag, container.children[0]); +container.removeChild(scriptTag); + +window.addEventListener('sollet_injected_script_message', (event) => { + chrome.runtime.sendMessage( + { + channel: 'sollet_contentscript_background_channel', + data: event.detail, + }, + (response) => { + // Can return null response if window is killed + if (!response) { + return; + } + window.dispatchEvent( + new CustomEvent('sollet_contentscript_message', { detail: response }), + ); + }, + ); +}); diff --git a/extension/src/manifest.json b/extension/src/manifest.json new file mode 100644 index 00000000..225b74df --- /dev/null +++ b/extension/src/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "Sollet", + "description": "Solana SPL Token Wallet", + "version": "0.1", + "browser_action": { + "default_popup": "index.html", + "default_title": "Open the popup" + }, + "manifest_version": 2, + "icons": { + "16": "favicon.ico", + "192": "logo192.png", + "512": "logo512.png" + }, + "background": { + "persistent": false, + "scripts": ["background.js"] + }, + "permissions": [ + "storage" + ], + "content_scripts": [ + { + "matches": ["file://*/*", "http://*/*", "https://*/*"], + "js": [ + "contentscript.js" + ], + "run_at": "document_start", + "all_frames": true + } + ], + "web_accessible_resources": ["script.js"], + "content_security_policy": "script-src 'self' 'sha256-ek+jXksbUr00x+EdLLqiv69t8hATh5rPjHVvVVGA9ms='; object-src 'self'" +} + \ No newline at end of file diff --git a/extension/src/script.js b/extension/src/script.js new file mode 100644 index 00000000..ad618a0d --- /dev/null +++ b/extension/src/script.js @@ -0,0 +1,15 @@ +window.sollet = { + postMessage: (message) => { + const listener = (event) => { + if (event.detail.id === message.id) { + window.removeEventListener('sollet_contentscript_message', listener); + window.postMessage(event.detail); + } + }; + window.addEventListener('sollet_contentscript_message', listener); + + window.dispatchEvent( + new CustomEvent('sollet_injected_script_message', { detail: message }), + ); + }, +}; diff --git a/package.json b/package.json index a200bcfc..6cbf519f 100644 --- a/package.json +++ b/package.json @@ -37,14 +37,21 @@ "predeploy": "git pull --ff-only && yarn && yarn build", "deploy": "gh-pages -d build", "fix": "run-s fix:*", - "fix:prettier": "prettier \"src/**/*.js\" --write", + "fix:prettier": "prettier \"src/**/*.js\" \"extension/src/*.js\" --write", "start": "react-scripts start", "build": "react-scripts build", + "build:extension": "yarn build && cp -a ./build/. ./extension/build/ && yarn build:extension-scripts", + "build:extension-scripts": "cp -a ./extension/src/. ./extension/build/.", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { - "extends": "react-app" + "env": { + "browser": true, + "es6": true, + "webextensions": true + }, + "extends": ["react-app"] }, "jest": { "transformIgnorePatterns": [ diff --git a/src/App.js b/src/App.js index a5a3ea8f..7526e640 100644 --- a/src/App.js +++ b/src/App.js @@ -10,10 +10,14 @@ import NavigationFrame from './components/NavigationFrame'; import { ConnectionProvider } from './utils/connection'; import WalletPage from './pages/WalletPage'; import { useWallet, WalletProvider } from './utils/wallet'; +import { ConnectedWalletsProvider } from './utils/connected-wallets'; import LoadingIndicator from './components/LoadingIndicator'; import { SnackbarProvider } from 'notistack'; import PopupPage from './pages/PopupPage'; import LoginPage from './pages/LoginPage'; +import ConnectionsPage from './pages/ConnectionsPage'; +import { isExtension } from './utils/utils'; +import { PageProvider, usePage } from './utils/page'; export default function App() { // TODO: add toggle for dark mode @@ -25,6 +29,8 @@ export default function App() { type: prefersDarkMode ? 'dark' : 'light', primary: blue, }, + // TODO consolidate popup dimensions + ext: '450', }), [prefersDarkMode], ); @@ -34,6 +40,22 @@ export default function App() { return null; } + let appElement = ( + + }> + + + + ); + + if (isExtension) { + appElement = ( + + {appElement} + + ); + } + return ( }> @@ -41,13 +63,7 @@ export default function App() { - - - }> - - - - + {appElement} @@ -57,11 +73,16 @@ export default function App() { function PageContents() { const wallet = useWallet(); + const [page] = usePage(); if (!wallet) { return ; } if (window.opener) { return ; } - return ; + if (page === 'wallet') { + return ; + } else if (page === 'connections') { + return ; + } } diff --git a/src/components/BalancesList.js b/src/components/BalancesList.js index 5493474b..564c1d12 100644 --- a/src/components/BalancesList.js +++ b/src/components/BalancesList.js @@ -19,7 +19,7 @@ import Link from '@material-ui/core/Link'; import ExpandLess from '@material-ui/icons/ExpandLess'; import ExpandMore from '@material-ui/icons/ExpandMore'; import { makeStyles } from '@material-ui/core/styles'; -import { abbreviateAddress } from '../utils/utils'; +import { abbreviateAddress, useIsExtensionWidth } from '../utils/utils'; import Button from '@material-ui/core/Button'; import SendIcon from '@material-ui/icons/Send'; import ReceiveIcon from '@material-ui/icons/WorkOutline'; @@ -106,6 +106,7 @@ export default function BalancesList() { const [showMergeAccounts, setShowMergeAccounts] = useState(false); const [sortAccounts, setSortAccounts] = useState(SortAccounts.None); const { accounts, setAccountName } = useWalletSelector(); + const isExtensionWidth = useIsExtensionWidth(); // Dummy var to force rerenders on demand. const [, setForceUpdate] = useState(false); const selectedAccount = accounts.find((a) => a.isSelected); @@ -182,12 +183,19 @@ export default function BalancesList() { }); }, [sortedPublicKeys, setUsdValuesCallback]); + const iconSize = isExtensionWidth ? 'small' : 'medium'; + return ( - - {selectedAccount && selectedAccount.name} Balances{' '} + + {selectedAccount && selectedAccount.name} + {isExtensionWidth ? '' : ' Balances'}{' '} {allTokensLoaded && ( <>({numberFormat.format(totalUsdValue.toFixed(2))}) )} @@ -196,23 +204,33 @@ export default function BalancesList() { selectedAccount.name !== 'Main account' && selectedAccount.name !== 'Hardware wallet' && ( - setShowEditAccountNameDialog(true)}> + setShowEditAccountNameDialog(true)} + > )} - setShowMergeAccounts(true)}> + setShowMergeAccounts(true)} + > - setShowAddTokenDialog(true)}> + setShowAddTokenDialog(true)} + > { switch (sortAccounts) { case SortAccounts.None: @@ -234,6 +252,7 @@ export default function BalancesList() { { refreshWalletPublicKeys(wallet); publicKeys.map((publicKey) => @@ -298,6 +317,7 @@ export function BalanceListItem({ publicKey, expandable, setUsdValue }) { const classes = useStyles(); const connection = useConnection(); const [open, setOpen] = useState(false); + const isExtensionWidth = useIsExtensionWidth(); const [, setForceUpdate] = useState(false); // Valid states: // * undefined => loading. @@ -344,6 +364,13 @@ export function BalanceListItem({ publicKey, expandable, setUsdValue }) { } let { amount, decimals, mint, tokenName, tokenSymbol } = balanceInfo; + tokenName = tokenName ?? abbreviateAddress(mint); + let displayName; + if (isExtensionWidth) { + displayName = tokenSymbol ?? tokenName; + } else { + displayName = tokenName + (tokenSymbol ? ` (${tokenSymbol})` : ''); + } // Fetch and cache the associated token address. if (wallet && wallet.publicKey && mint) { @@ -383,7 +410,7 @@ export function BalanceListItem({ publicKey, expandable, setUsdValue }) { return false; })(); - const subtitle = ( + const subtitle = isExtensionWidth ? undefined : (
{isAssociatedToken && (
{balanceFormat.format(amount / Math.pow(10, decimals))}{' '} - {tokenName ?? abbreviateAddress(mint)} - {tokenSymbol ? ` (${tokenSymbol})` : null} + {displayName} } secondary={subtitle} @@ -497,6 +523,7 @@ function BalanceListItemDetails({ publicKey, serumMarkets, balanceInfo }) { balanceInfo.mint?.toBase58(), publicKey.toBase58(), ]); + const isExtensionWidth = useIsExtensionWidth(); if (!balanceInfo) { return ; @@ -514,6 +541,75 @@ function BalanceListItemDetails({ publicKey, serumMarkets, balanceInfo }) { : undefined : undefined; + const additionalInfo = isExtensionWidth ? undefined : ( + <> + + Deposit Address: {publicKey.toBase58()} + + + Token Name: {tokenName ?? 'Unknown'} + + + Token Symbol: {tokenSymbol ?? 'Unknown'} + + {mint ? ( + + Token Address: {mint.toBase58()} + + ) : null} +
+
+ + + View on Solana + + + {market && ( + + + View on Serum + + + )} + {swapInfo && swapInfo.coin.erc20Contract && ( + + + View on Ethereum + + + )} +
+ {exportNeedsDisplay && wallet.allowsExport && ( +
+ + setExportAccDialogOpen(true)}> + Export + + +
+ )} +
+ + ); + return ( <> {wallet.allowsExport && ( @@ -562,70 +658,7 @@ function BalanceListItemDetails({ publicKey, serumMarkets, balanceInfo }) { ) : null}
- - Deposit Address: {publicKey.toBase58()} - - - Token Name: {tokenName ?? 'Unknown'} - - - Token Symbol: {tokenSymbol ?? 'Unknown'} - - {mint ? ( - - Token Address: {mint.toBase58()} - - ) : null} -
-
- - - View on Solana - - - {market && ( - - - View on Serum - - - )} - {swapInfo && swapInfo.coin.erc20Contract && ( - - - View on Ethereum - - - )} -
- {exportNeedsDisplay && wallet.allowsExport && ( -
- - setExportAccDialogOpen(true)}> - Export - - -
- )} -
+ {additionalInfo}
+ + + + ); +} diff --git a/src/components/ConnectionsList.js b/src/components/ConnectionsList.js new file mode 100644 index 00000000..c39fb99e --- /dev/null +++ b/src/components/ConnectionsList.js @@ -0,0 +1,153 @@ +import React, { useState } from 'react'; +import { + AppBar, + Button, + Collapse, + List, + ListItem, + ListItemIcon, + ListItemText, + makeStyles, + Paper, + Toolbar, + Typography, +} from '@material-ui/core'; +import DeleteIcon from '@material-ui/icons/Delete'; +import { DoneAll, ExpandLess, ExpandMore } from '@material-ui/icons'; +import { useConnectedWallets } from '../utils/connected-wallets'; +import { useIsExtensionWidth } from '../utils/utils'; +import { useWalletSelector } from '../utils/wallet'; + +export default function ConnectionsList() { + const isExtensionWidth = useIsExtensionWidth(); + const connectedWallets = useConnectedWallets(); + + return ( + + + + + Connected Dapps + + + + + {Object.entries(connectedWallets).map(([origin, connectedWallet]) => ( + + ))} + + + ); +} + +const ICON_SIZE = 28; +const IMAGE_SIZE = 24; + +const useStyles = makeStyles((theme) => ({ + itemDetails: { + marginLeft: theme.spacing(3), + marginRight: theme.spacing(3), + marginBottom: theme.spacing(2), + }, + buttonContainer: { + display: 'flex', + justifyContent: 'space-evenly', + marginTop: theme.spacing(1), + marginBottom: theme.spacing(1), + }, + listItemIcon: { + backgroundColor: 'white', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: ICON_SIZE, + width: ICON_SIZE, + borderRadius: ICON_SIZE / 2, + }, + listItemImage: { + height: IMAGE_SIZE, + width: IMAGE_SIZE, + borderRadius: IMAGE_SIZE / 2, + }, +})); + +function ConnectionsListItem({ origin, connectedWallet }) { + const classes = useStyles(); + const [open, setOpen] = useState(false); + // TODO better way to get high res icon? + const appleIconUrl = origin + '/apple-touch-icon.png'; + const faviconUrl = origin + '/favicon.ico'; + const [iconUrl, setIconUrl] = useState(appleIconUrl); + const { accounts } = useWalletSelector(); + // TODO better way to do this + const account = accounts.find( + (account) => account.address.toBase58() === connectedWallet.publicKey, + ); + + const setAutoApprove = (autoApprove) => { + chrome.storage.local.get('connectedWallets', (result) => { + result.connectedWallets[origin].autoApprove = autoApprove; + chrome.storage.local.set({ connectedWallets: result.connectedWallets }); + }); + }; + + const disconnectWallet = () => { + chrome.storage.local.get('connectedWallets', (result) => { + delete result.connectedWallets[origin]; + chrome.storage.local.set({ connectedWallets: result.connectedWallets }); + }); + }; + + return ( + <> + setOpen((open) => !open)}> + +
+ setIconUrl(faviconUrl)} + className={classes.listItemImage} + alt={origin} + /> +
+
+
+ +
+ {open ? : } +
+ +
+
+ + +
+
+
+ + ); +} diff --git a/src/components/DebugButtons.js b/src/components/DebugButtons.js index a6f12b04..c4ca62f7 100644 --- a/src/components/DebugButtons.js +++ b/src/components/DebugButtons.js @@ -62,8 +62,9 @@ export default function DebugButtons() { const noSol = amount === 0; const requestAirdropDisabled = endpoint === MAINNET_URL; + const spacing = 24; return ( -
+
Mint Test Token diff --git a/src/components/NavigationFrame.js b/src/components/NavigationFrame.js index 602f62ed..96241942 100644 --- a/src/components/NavigationFrame.js +++ b/src/components/NavigationFrame.js @@ -26,14 +26,26 @@ import AddAccountDialog from './AddAccountDialog'; import DeleteMnemonicDialog from './DeleteMnemonicDialog'; import AddHardwareWalletDialog from './AddHarwareWalletDialog'; import { ExportMnemonicDialog } from './ExportAccountDialog.js'; +import { + isExtension, + isExtensionPopup, + useIsExtensionWidth, +} from '../utils/utils'; +import ConnectionIcon from './ConnectionIcon'; +import { Badge } from '@material-ui/core'; +import { useConnectedWallets } from '../utils/connected-wallets'; +import { usePage } from '../utils/page'; +import { MonetizationOn, OpenInNew } from '@material-ui/icons'; const useStyles = makeStyles((theme) => ({ content: { flexGrow: 1, - paddingTop: theme.spacing(3), paddingBottom: theme.spacing(3), - paddingLeft: theme.spacing(1), - paddingRight: theme.spacing(1), + [theme.breakpoints.up(theme.ext)]: { + paddingTop: theme.spacing(3), + paddingLeft: theme.spacing(1), + paddingRight: theme.spacing(1), + }, }, title: { flexGrow: 1, @@ -44,23 +56,128 @@ const useStyles = makeStyles((theme) => ({ menuItemIcon: { minWidth: 32, }, + badge: { + backgroundColor: theme.palette.success.main, + color: theme.palette.text.main, + height: 16, + width: 16, + }, })); export default function NavigationFrame({ children }) { const classes = useStyles(); + const isExtensionWidth = useIsExtensionWidth(); return ( <> - Solana SPL Token Wallet + {isExtensionWidth ? 'Sollet' : 'Solana SPL Token Wallet'} - - +
{children}
-